1/*
2 * tclXselect.c
3 *
4 * Select command.  This is the generic code associated with the select system
5 * call.  It relies on the Unix style select, which operates on bit sets of
6 * file numbers.  Platform specific code is called to translate channels into
7 * file numbers, but all operations are generic.  On Win32, this only works
8 * on sockets.  Ideally, it would push more code into the platform specific
9 * modules and work on more file types.  However, right now, I don't see a
10 * good way to do this on Win32.
11 *-----------------------------------------------------------------------------
12 * Copyright 1991-1999 Karl Lehenbauer and Mark Diekhans.
13 *
14 * Permission to use, copy, modify, and distribute this software and its
15 * documentation for any purpose and without fee is hereby granted, provided
16 * that the above copyright notice appear in all copies.  Karl Lehenbauer and
17 * Mark Diekhans make no representations about the suitability of this
18 * software for any purpose.  It is provided "as is" without express or
19 * implied warranty.
20 *-----------------------------------------------------------------------------
21 * $Id: tclXselect.c,v 1.7 2005/07/27 22:31:15 hobbs Exp $
22 *-----------------------------------------------------------------------------
23 */
24
25#ifndef NO_SELECT
26
27#include "tclExtdInt.h"
28
29/*
30 * A few systems (A/UX 2.0) have select but no macros, define em in this case.
31 */
32#ifndef FD_SET
33#   define FD_SET(fd,fdset)     (fdset)->fds_bits[0] |= (1<<(fd))
34#   define FD_CLR(fd,fdset)     (fdset)->fds_bits[0] &= ~(1<<(fd))
35#   define FD_ZERO(fdset)       (fdset)->fds_bits[0] = 0
36#   define FD_ISSET(fd,fdset)   (((fdset)->fds_bits[0]) & (1<<(fd)))
37#endif
38
39/*
40 * Data kept about a file channel.
41 */
42typedef struct {
43    Tcl_Obj     *channelIdObj;
44    Tcl_Channel  channel;
45#ifdef WIN32
46    /*
47     * XXX Not strictly correct, according to TclX's usage of fds, but we
48     * XXX expect noone to really being using select hardcore on Windows
49     */
50    unsigned int readFd;
51    unsigned int writeFd;
52#else
53    int readFd;
54    int writeFd;
55#endif
56} channelData_t;
57
58/*
59 * Prototypes of internal functions.
60 */
61static int
62ParseSelectFileList _ANSI_ARGS_((Tcl_Interp     *interp,
63                                 int             chanAccess,
64                                 Tcl_Obj        *handleList,
65                                 fd_set         *fileSetPtr,
66                                 channelData_t **channelListPtr,
67                                 int            *maxFileIdPtr));
68
69static int
70FindPendingData _ANSI_ARGS_((int            fileDescCnt,
71                             channelData_t *channelList,
72                             fd_set        *fileDescSetPtr));
73
74static Tcl_Obj *
75ReturnSelectedFileList _ANSI_ARGS_((fd_set        *fileDescSetPtr,
76                                    int            fileDescCnt,
77                                    channelData_t *channelListPtr));
78
79static int
80TclX_SelectObjCmd _ANSI_ARGS_((ClientData clientData,
81                               Tcl_Interp *interp,
82                               int objc,
83                               Tcl_Obj *CONST objv[]));
84
85
86/*-----------------------------------------------------------------------------
87 * ParseSelectFileList --
88 *
89 *   Parse a list of file handles for select.
90 *
91 * Parameters:
92 *   o interp - Error messages are returned in the result.
93 *   o chanAccess - TCL_READABLE for read direction, TCL_WRITABLE for write
94 *     direction or both for both files.
95 *   o handleList (I) - The list of file handles to parse, may be empty.
96 *   o fileSetPtr - The select fd_set for the parsed handles is
97 *     filled in.
98 *   o channelListPtr - A pointer to a dynamically allocated list of
99 *     the channels that are in the set.  If the list is empty, NULL is
100 *     returned.
101 *   o maxFileIdPtr (I/O) - If a file id greater than the current value is
102 *     encountered, it will be set to that file id.
103 * Returns:
104 *   The number of files in the list, or -1 if an error occured.
105 * FIX: Should really pass in access and only get channels that are
106 * requested.
107 *-----------------------------------------------------------------------------
108 */
109static int
110ParseSelectFileList (interp, chanAccess, handleList, fileSetPtr,
111                     channelListPtr, maxFileIdPtr)
112    Tcl_Interp    *interp;
113    int            chanAccess;
114    Tcl_Obj       *handleList;
115    fd_set        *fileSetPtr;
116    channelData_t **channelListPtr;
117    int           *maxFileIdPtr;
118{
119    int handleCnt, idx;
120    Tcl_Obj **handleObjv;
121    channelData_t *channelList;
122
123    /*
124     * Optimize empty list handling.
125     */
126    if (TclX_IsNullObj (handleList)) {
127        *channelListPtr = NULL;
128        return 0;
129    }
130
131    if (Tcl_ListObjGetElements (interp, handleList, &handleCnt,
132                                &handleObjv) != TCL_OK) {
133        return -1;
134    }
135
136    /*
137     * Handle case of an empty list.
138     */
139    if (handleCnt == 0) {
140        *channelListPtr = NULL;
141        return 0;
142    }
143
144    channelList =
145        (channelData_t*) ckalloc (sizeof (channelData_t) * handleCnt);
146
147    for (idx = 0; idx < handleCnt; idx++) {
148        channelList [idx].channelIdObj = handleObjv [idx];
149        channelList [idx].channel =
150            TclX_GetOpenChannelObj (interp,
151                                    handleObjv [idx],
152                                    chanAccess);
153        if (channelList [idx].channel == NULL)
154            goto errorExit;
155
156        if (chanAccess & TCL_READABLE) {
157            if (TclXOSGetSelectFnum (interp, channelList [idx].channel,
158			TCL_READABLE,
159			&channelList [idx].readFd) != TCL_OK)
160                goto errorExit;
161            FD_SET (channelList [idx].readFd, fileSetPtr);
162            if ((int)channelList [idx].readFd > *maxFileIdPtr)
163                *maxFileIdPtr = (int)channelList [idx].readFd;
164        } else {
165            channelList [idx].readFd = -1;
166        }
167
168        if (chanAccess & TCL_WRITABLE) {
169            if (TclXOSGetSelectFnum (interp, channelList [idx].channel,
170			TCL_WRITABLE,
171			&channelList [idx].writeFd) != TCL_OK)
172                goto errorExit;
173            FD_SET (channelList [idx].writeFd, fileSetPtr);
174            if ((int)channelList [idx].writeFd > *maxFileIdPtr)
175                *maxFileIdPtr = (int)channelList [idx].writeFd;
176        } else {
177            channelList [idx].writeFd = -1;
178        }
179    }
180
181    *channelListPtr = channelList;
182    return handleCnt;
183
184  errorExit:
185    ckfree ((char *) channelList);
186    return -1;
187
188}
189
190/*-----------------------------------------------------------------------------
191 * FindPendingData --
192 *
193 *   Scan a list of read files to determine if any of them have data pending
194 * in their buffers.
195 *
196 * Parameters:
197 *   o fileDescCnt (I) - Number of descriptors in the list.
198 *   o channelListPtr (I) - A pointer to a list of the channel data for
199 *     the channels to check.
200 *   o fileDescSetPtr (I) - A select fd_set with will have a bit set for
201 *     every file that has data pending it its buffer.
202 * Returns:
203 *   TRUE if any where found that had pending data, FALSE if none were found.
204 *-----------------------------------------------------------------------------
205 */
206static int
207FindPendingData (fileDescCnt, channelList, fileDescSetPtr)
208    int            fileDescCnt;
209    channelData_t *channelList;
210    fd_set        *fileDescSetPtr;
211{
212    int idx, found = FALSE;
213
214    FD_ZERO (fileDescSetPtr);
215
216    for (idx = 0; idx < fileDescCnt; idx++) {
217        if (Tcl_InputBuffered (channelList [idx].channel)) {
218            FD_SET (channelList [idx].readFd, fileDescSetPtr);
219            found = TRUE;
220        }
221    }
222    return found;
223}
224
225/*-----------------------------------------------------------------------------
226 * ReturnSelectedFileList --
227 *
228 *   Take the resulting file descriptor sets from a select, and the
229 *   list of file descritpors and build up a list of Tcl file handles.
230 *
231 * Parameters:
232 *   o fileDescSetPtr (I) - The select fd_set.
233 *   o fileDescCnt (I) - Number of descriptors in the list.
234 *   o channelListPtr (I) - A pointer to a list of the FILE pointers for
235 *     files that are in the set.
236 * Returns:
237 *   List of file handles.
238 *-----------------------------------------------------------------------------
239 */
240static Tcl_Obj *
241ReturnSelectedFileList (fileDescSetPtr, fileDescCnt, channelList)
242    fd_set        *fileDescSetPtr;
243    int            fileDescCnt;
244    channelData_t *channelList;
245{
246    int idx, handleCnt;
247    Tcl_Obj *fileHandleList = Tcl_NewListObj (0, NULL);
248
249    handleCnt = 0;
250    for (idx = 0; idx < fileDescCnt; idx++) {
251        if (((channelList [idx].readFd >= 0) &&
252             FD_ISSET (channelList [idx].readFd, fileDescSetPtr)) ||
253            ((channelList [idx].writeFd >= 0) &&
254             FD_ISSET (channelList [idx].writeFd, fileDescSetPtr))) {
255            Tcl_ListObjAppendElement (NULL, fileHandleList,
256                                      channelList [idx].channelIdObj);
257            handleCnt++;
258        }
259    }
260
261    return fileHandleList;
262}
263
264/*-----------------------------------------------------------------------------
265 * TclX_SelectObjCmd --
266 *  Implements the select TCL command:
267 *      select readhandles ?writehandles? ?excepthandles? ?timeout?
268 *
269 *  This command is extra smart in the fact that it checks for read data
270 * pending in the stdio buffer first before doing a select.
271 *
272 * Results:
273 *     A list in the form:
274 *        {readhandles writehandles excepthandles}
275 *     or {} it the timeout expired.
276 *-----------------------------------------------------------------------------
277 */
278static int
279TclX_SelectObjCmd (clientData, interp, objc, objv)
280    ClientData   clientData;
281    Tcl_Interp  *interp;
282    int          objc;
283    Tcl_Obj     *CONST objv[];
284{
285    static int chanAccess [] = {TCL_READABLE, TCL_WRITABLE, 0};
286    int idx;
287    fd_set fdSets [3], readPendingFDSet;
288    int descCnts [3];
289    channelData_t *descLists [3];
290    Tcl_Obj *handleSetList [3];
291    int numSelected, maxFileId = 0, pending;
292    int result = TCL_ERROR;
293    struct timeval  timeoutRec;
294    struct timeval *timeoutRecPtr;
295
296    if (objc < 2) {
297        return TclX_WrongArgs (interp, objv [0],
298                      " readFileIds ?writeFileIds? ?exceptFileIds? ?timeout?");
299    }
300
301    /*
302     * Initialize. 0 == read, 1 == write and 2 == exception.
303     */
304    for (idx = 0; idx < 3; idx++) {
305        FD_ZERO (&fdSets [idx]);
306        descCnts [idx] = 0;
307        descLists [idx] = NULL;
308    }
309
310    /*
311     * Parse the file handles and set everything up for the select call.
312     */
313    for (idx = 0; (idx < 3) && (idx < objc - 1); idx++) {
314        descCnts [idx] = ParseSelectFileList (interp,
315                                              chanAccess [idx],
316                                              objv [idx + 1],
317                                              &fdSets [idx],
318                                              &descLists [idx],
319                                              &maxFileId);
320        if (descCnts [idx] < 0)
321            goto exitPoint;
322    }
323
324    /*
325     * Get the time out.  Zero is different that not specified.
326     */
327    timeoutRecPtr = NULL;
328    if ((objc > 4) && !TclX_IsNullObj (objv [4])) {
329        double  timeout, seconds, microseconds;
330
331        if (Tcl_GetDoubleFromObj (interp, objv [4], &timeout) != TCL_OK)
332            goto exitPoint;
333        if (timeout < 0.0) {
334            TclX_AppendObjResult (interp, "timeout must be greater than ",
335                                  "or equal to zero", (char *) NULL);
336            goto exitPoint;
337        }
338        seconds = floor (timeout);
339        microseconds = (timeout - seconds) * 1000000.0;
340        timeoutRec.tv_sec = (long) seconds;
341        timeoutRec.tv_usec = (long) microseconds;
342        timeoutRecPtr = &timeoutRec;
343    }
344
345    /*
346     * Check if any data is pending in the read buffers.  If there is,
347     * then do the select, but don't block in it.
348     */
349    pending = FindPendingData (descCnts [0], descLists [0], &readPendingFDSet);
350    if (pending) {
351        timeoutRec.tv_sec = 0;
352        timeoutRec.tv_usec = 0;
353        timeoutRecPtr = &timeoutRec;
354    }
355
356    /*
357     * All set, do the select.
358     */
359    numSelected = select (maxFileId + 1,
360                          &fdSets [0], &fdSets [1], &fdSets [2],
361                          timeoutRecPtr);
362    if (numSelected < 0) {
363        TclX_AppendObjResult (interp, "select error: ",
364                              Tcl_PosixError (interp), (char *) NULL);
365        goto exitPoint;
366    }
367
368    /*
369     * If there is read data pending in the buffers, force the bits to be set
370     * in the read fdSet.
371     */
372    if (pending) {
373        for (idx = 0; idx < descCnts [0]; idx++) {
374            if (FD_ISSET (descLists [0] [idx].readFd, &readPendingFDSet))
375                FD_SET (descLists [0] [idx].readFd, &(fdSets [0]));
376        }
377    }
378
379    /*
380     * Return the result, either a 3 element list, or leave the result
381     * empty if the timeout occured.
382     */
383    if (numSelected > 0 || pending) {
384        for (idx = 0; idx < 3; idx++) {
385            handleSetList [idx] =
386                ReturnSelectedFileList (&fdSets [idx],
387                                        descCnts [idx],
388                                        descLists [idx]);
389        }
390        Tcl_SetObjResult (interp, Tcl_NewListObj (3, handleSetList));
391    }
392
393    result = TCL_OK;
394
395  exitPoint:
396    for (idx = 0; idx < 3; idx++) {
397        if (descLists [idx] != NULL)
398            ckfree ((char *) descLists [idx]);
399    }
400    return result;
401}
402#else /* NO_SELECT */
403/*-----------------------------------------------------------------------------
404 * TclX_SelectCmd --
405 *     Dummy select command that returns an error for systems that don't
406 *     have select.
407 *-----------------------------------------------------------------------------
408 */
409static int
410TclX_SelectObjCmd (clientData, interp, objc, objv)
411    ClientData   clientData;
412    Tcl_Interp  *interp;
413    int          objc;
414    Tcl_Obj     *CONST objv[];
415{
416    Tcl_AppendResult(interp, Tcl_GetString(objv[0]),
417	    " is not available on this OS", (char *) NULL);
418    return TCL_ERROR;
419}
420#endif /* NO_SELECT */
421
422
423/*-----------------------------------------------------------------------------
424 * TclX_SelectInit --
425 *     Initialize the select command.
426 *-----------------------------------------------------------------------------
427 */
428void
429TclX_SelectInit (interp)
430    Tcl_Interp *interp;
431{
432    Tcl_CreateObjCommand (interp,
433                          "select",
434                          TclX_SelectObjCmd,
435                          (ClientData) NULL,
436                          (Tcl_CmdDeleteProc*) NULL);
437}
438
439