OSDN Git Service

8a36dcbe4079d33730f5b9bbb5bf876f8949d96a
[ffftp/ffftp.git] / putty / WINDOWS / WINSFTP.C
1 /*\r
2  * winsftp.c: the Windows-specific parts of PSFTP and PSCP.\r
3  */\r
4 \r
5 #include <assert.h>\r
6 \r
7 #include "putty.h"\r
8 #include "psftp.h"\r
9 #include "ssh.h"\r
10 #include "int64.h"\r
11 \r
12 char *get_ttymode(void *frontend, const char *mode) { return NULL; }\r
13 \r
14 int get_userpass_input(prompts_t *p, unsigned char *in, int inlen)\r
15 {\r
16     int ret;\r
17     ret = cmdline_get_passwd_input(p, in, inlen);\r
18     if (ret == -1)\r
19         ret = console_get_userpass_input(p, in, inlen);\r
20     return ret;\r
21 }\r
22 \r
23 void platform_get_x11_auth(struct X11Display *display, const Config *cfg)\r
24 {\r
25     /* Do nothing, therefore no auth. */\r
26 }\r
27 const int platform_uses_x11_unix_by_default = TRUE;\r
28 \r
29 /* ----------------------------------------------------------------------\r
30  * File access abstraction.\r
31  */\r
32 \r
33 /*\r
34  * Set local current directory. Returns NULL on success, or else an\r
35  * error message which must be freed after printing.\r
36  */\r
37 char *psftp_lcd(char *dir)\r
38 {\r
39     char *ret = NULL;\r
40 \r
41     if (!SetCurrentDirectory(dir)) {\r
42         LPVOID message;\r
43         int i;\r
44         FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |\r
45                       FORMAT_MESSAGE_FROM_SYSTEM |\r
46                       FORMAT_MESSAGE_IGNORE_INSERTS,\r
47                       NULL, GetLastError(),\r
48                       MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),\r
49                       (LPTSTR)&message, 0, NULL);\r
50         i = strcspn((char *)message, "\n");\r
51         ret = dupprintf("%.*s", i, (LPCTSTR)message);\r
52         LocalFree(message);\r
53     }\r
54 \r
55     return ret;\r
56 }\r
57 \r
58 /*\r
59  * Get local current directory. Returns a string which must be\r
60  * freed.\r
61  */\r
62 char *psftp_getcwd(void)\r
63 {\r
64     char *ret = snewn(256, char);\r
65     int len = GetCurrentDirectory(256, ret);\r
66     if (len > 256)\r
67         ret = sresize(ret, len, char);\r
68     GetCurrentDirectory(len, ret);\r
69     return ret;\r
70 }\r
71 \r
72 #define TIME_POSIX_TO_WIN(t, ft) do { \\r
73     ULARGE_INTEGER uli; \\r
74     uli.QuadPart = ((ULONGLONG)(t) + 11644473600ull) * 10000000ull; \\r
75     (ft).dwLowDateTime  = uli.LowPart; \\r
76     (ft).dwHighDateTime = uli.HighPart; \\r
77 } while(0)\r
78 #define TIME_WIN_TO_POSIX(ft, t) do { \\r
79     ULARGE_INTEGER uli; \\r
80     uli.LowPart  = (ft).dwLowDateTime; \\r
81     uli.HighPart = (ft).dwHighDateTime; \\r
82     uli.QuadPart = uli.QuadPart / 10000000ull - 11644473600ull; \\r
83     (t) = (unsigned long) uli.QuadPart; \\r
84 } while(0)\r
85 \r
86 struct RFile {\r
87     HANDLE h;\r
88 };\r
89 \r
90 RFile *open_existing_file(char *name, uint64 *size,\r
91                           unsigned long *mtime, unsigned long *atime)\r
92 {\r
93     HANDLE h;\r
94     RFile *ret;\r
95 \r
96     h = CreateFile(name, GENERIC_READ, FILE_SHARE_READ, NULL,\r
97                    OPEN_EXISTING, 0, 0);\r
98     if (h == INVALID_HANDLE_VALUE)\r
99         return NULL;\r
100 \r
101     ret = snew(RFile);\r
102     ret->h = h;\r
103 \r
104     if (size)\r
105         size->lo=GetFileSize(h, &(size->hi));\r
106 \r
107     if (mtime || atime) {\r
108         FILETIME actime, wrtime;\r
109         GetFileTime(h, NULL, &actime, &wrtime);\r
110         if (atime)\r
111             TIME_WIN_TO_POSIX(actime, *atime);\r
112         if (mtime)\r
113             TIME_WIN_TO_POSIX(wrtime, *mtime);\r
114     }\r
115 \r
116     return ret;\r
117 }\r
118 \r
119 int read_from_file(RFile *f, void *buffer, int length)\r
120 {\r
121     int ret;\r
122     DWORD read;\r
123     ret = ReadFile(f->h, buffer, length, &read, NULL);\r
124     if (!ret)\r
125         return -1;                     /* error */\r
126     else\r
127         return read;\r
128 }\r
129 \r
130 void close_rfile(RFile *f)\r
131 {\r
132     CloseHandle(f->h);\r
133     sfree(f);\r
134 }\r
135 \r
136 struct WFile {\r
137     HANDLE h;\r
138 };\r
139 \r
140 WFile *open_new_file(char *name)\r
141 {\r
142     HANDLE h;\r
143     WFile *ret;\r
144 \r
145     h = CreateFile(name, GENERIC_WRITE, 0, NULL,\r
146                    CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);\r
147     if (h == INVALID_HANDLE_VALUE)\r
148         return NULL;\r
149 \r
150     ret = snew(WFile);\r
151     ret->h = h;\r
152 \r
153     return ret;\r
154 }\r
155 \r
156 WFile *open_existing_wfile(char *name, uint64 *size)\r
157 {\r
158     HANDLE h;\r
159     WFile *ret;\r
160 \r
161     h = CreateFile(name, GENERIC_WRITE, FILE_SHARE_READ, NULL,\r
162                    OPEN_EXISTING, 0, 0);\r
163     if (h == INVALID_HANDLE_VALUE)\r
164         return NULL;\r
165 \r
166     ret = snew(WFile);\r
167     ret->h = h;\r
168 \r
169     if (size)\r
170         size->lo=GetFileSize(h, &(size->hi));\r
171 \r
172     return ret;\r
173 }\r
174 \r
175 int write_to_file(WFile *f, void *buffer, int length)\r
176 {\r
177     int ret;\r
178     DWORD written;\r
179     ret = WriteFile(f->h, buffer, length, &written, NULL);\r
180     if (!ret)\r
181         return -1;                     /* error */\r
182     else\r
183         return written;\r
184 }\r
185 \r
186 void set_file_times(WFile *f, unsigned long mtime, unsigned long atime)\r
187 {\r
188     FILETIME actime, wrtime;\r
189     TIME_POSIX_TO_WIN(atime, actime);\r
190     TIME_POSIX_TO_WIN(mtime, wrtime);\r
191     SetFileTime(f->h, NULL, &actime, &wrtime);\r
192 }\r
193 \r
194 void close_wfile(WFile *f)\r
195 {\r
196     CloseHandle(f->h);\r
197     sfree(f);\r
198 }\r
199 \r
200 /* Seek offset bytes through file, from whence, where whence is\r
201    FROM_START, FROM_CURRENT, or FROM_END */\r
202 int seek_file(WFile *f, uint64 offset, int whence)\r
203 {\r
204     DWORD movemethod;\r
205 \r
206     switch (whence) {\r
207     case FROM_START:\r
208         movemethod = FILE_BEGIN;\r
209         break;\r
210     case FROM_CURRENT:\r
211         movemethod = FILE_CURRENT;\r
212         break;\r
213     case FROM_END:\r
214         movemethod = FILE_END;\r
215         break;\r
216     default:\r
217         return -1;\r
218     }\r
219 \r
220     SetFilePointer(f->h, offset.lo, &(offset.hi), movemethod);\r
221     \r
222     if (GetLastError() != NO_ERROR)\r
223         return -1;\r
224     else \r
225         return 0;\r
226 }\r
227 \r
228 uint64 get_file_posn(WFile *f)\r
229 {\r
230     uint64 ret;\r
231 \r
232     ret.hi = 0L;\r
233     ret.lo = SetFilePointer(f->h, 0L, &(ret.hi), FILE_CURRENT);\r
234 \r
235     return ret;\r
236 }\r
237 \r
238 int file_type(char *name)\r
239 {\r
240     DWORD attr;\r
241     attr = GetFileAttributes(name);\r
242     /* We know of no `weird' files under Windows. */\r
243     if (attr == (DWORD)-1)\r
244         return FILE_TYPE_NONEXISTENT;\r
245     else if (attr & FILE_ATTRIBUTE_DIRECTORY)\r
246         return FILE_TYPE_DIRECTORY;\r
247     else\r
248         return FILE_TYPE_FILE;\r
249 }\r
250 \r
251 struct DirHandle {\r
252     HANDLE h;\r
253     char *name;\r
254 };\r
255 \r
256 DirHandle *open_directory(char *name)\r
257 {\r
258     HANDLE h;\r
259     WIN32_FIND_DATA fdat;\r
260     char *findfile;\r
261     DirHandle *ret;\r
262 \r
263     /* Enumerate files in dir `foo'. */\r
264     findfile = dupcat(name, "/*", NULL);\r
265     h = FindFirstFile(findfile, &fdat);\r
266     if (h == INVALID_HANDLE_VALUE)\r
267         return NULL;\r
268     sfree(findfile);\r
269 \r
270     ret = snew(DirHandle);\r
271     ret->h = h;\r
272     ret->name = dupstr(fdat.cFileName);\r
273     return ret;\r
274 }\r
275 \r
276 char *read_filename(DirHandle *dir)\r
277 {\r
278     do {\r
279 \r
280         if (!dir->name) {\r
281             WIN32_FIND_DATA fdat;\r
282             int ok = FindNextFile(dir->h, &fdat);\r
283             if (!ok)\r
284                 return NULL;\r
285             else\r
286                 dir->name = dupstr(fdat.cFileName);\r
287         }\r
288 \r
289         assert(dir->name);\r
290         if (dir->name[0] == '.' &&\r
291             (dir->name[1] == '\0' ||\r
292              (dir->name[1] == '.' && dir->name[2] == '\0'))) {\r
293             sfree(dir->name);\r
294             dir->name = NULL;\r
295         }\r
296 \r
297     } while (!dir->name);\r
298 \r
299     if (dir->name) {\r
300         char *ret = dir->name;\r
301         dir->name = NULL;\r
302         return ret;\r
303     } else\r
304         return NULL;\r
305 }\r
306 \r
307 void close_directory(DirHandle *dir)\r
308 {\r
309     FindClose(dir->h);\r
310     if (dir->name)\r
311         sfree(dir->name);\r
312     sfree(dir);\r
313 }\r
314 \r
315 int test_wildcard(char *name, int cmdline)\r
316 {\r
317     HANDLE fh;\r
318     WIN32_FIND_DATA fdat;\r
319 \r
320     /* First see if the exact name exists. */\r
321     if (GetFileAttributes(name) != (DWORD)-1)\r
322         return WCTYPE_FILENAME;\r
323 \r
324     /* Otherwise see if a wildcard match finds anything. */\r
325     fh = FindFirstFile(name, &fdat);\r
326     if (fh == INVALID_HANDLE_VALUE)\r
327         return WCTYPE_NONEXISTENT;\r
328 \r
329     FindClose(fh);\r
330     return WCTYPE_WILDCARD;\r
331 }\r
332 \r
333 struct WildcardMatcher {\r
334     HANDLE h;\r
335     char *name;\r
336     char *srcpath;\r
337 };\r
338 \r
339 /*\r
340  * Return a pointer to the portion of str that comes after the last\r
341  * slash (or backslash or colon, if `local' is TRUE).\r
342  */\r
343 static char *stripslashes(char *str, int local)\r
344 {\r
345     char *p;\r
346 \r
347     if (local) {\r
348         p = strchr(str, ':');\r
349         if (p) str = p+1;\r
350     }\r
351 \r
352     p = strrchr(str, '/');\r
353     if (p) str = p+1;\r
354 \r
355     if (local) {\r
356         p = strrchr(str, '\\');\r
357         if (p) str = p+1;\r
358     }\r
359 \r
360     return str;\r
361 }\r
362 \r
363 WildcardMatcher *begin_wildcard_matching(char *name)\r
364 {\r
365     HANDLE h;\r
366     WIN32_FIND_DATA fdat;\r
367     WildcardMatcher *ret;\r
368     char *last;\r
369 \r
370     h = FindFirstFile(name, &fdat);\r
371     if (h == INVALID_HANDLE_VALUE)\r
372         return NULL;\r
373 \r
374     ret = snew(WildcardMatcher);\r
375     ret->h = h;\r
376     ret->srcpath = dupstr(name);\r
377     last = stripslashes(ret->srcpath, 1);\r
378     *last = '\0';\r
379     if (fdat.cFileName[0] == '.' &&\r
380         (fdat.cFileName[1] == '\0' ||\r
381          (fdat.cFileName[1] == '.' && fdat.cFileName[2] == '\0')))\r
382         ret->name = NULL;\r
383     else\r
384         ret->name = dupcat(ret->srcpath, fdat.cFileName, NULL);\r
385 \r
386     return ret;\r
387 }\r
388 \r
389 char *wildcard_get_filename(WildcardMatcher *dir)\r
390 {\r
391     while (!dir->name) {\r
392         WIN32_FIND_DATA fdat;\r
393         int ok = FindNextFile(dir->h, &fdat);\r
394 \r
395         if (!ok)\r
396             return NULL;\r
397 \r
398         if (fdat.cFileName[0] == '.' &&\r
399             (fdat.cFileName[1] == '\0' ||\r
400              (fdat.cFileName[1] == '.' && fdat.cFileName[2] == '\0')))\r
401             dir->name = NULL;\r
402         else\r
403             dir->name = dupcat(dir->srcpath, fdat.cFileName, NULL);\r
404     }\r
405 \r
406     if (dir->name) {\r
407         char *ret = dir->name;\r
408         dir->name = NULL;\r
409         return ret;\r
410     } else\r
411         return NULL;\r
412 }\r
413 \r
414 void finish_wildcard_matching(WildcardMatcher *dir)\r
415 {\r
416     FindClose(dir->h);\r
417     if (dir->name)\r
418         sfree(dir->name);\r
419     sfree(dir->srcpath);\r
420     sfree(dir);\r
421 }\r
422 \r
423 int vet_filename(char *name)\r
424 {\r
425     if (strchr(name, '/') || strchr(name, '\\') || strchr(name, ':'))\r
426         return FALSE;\r
427 \r
428     if (!name[strspn(name, ".")])      /* entirely composed of dots */\r
429         return FALSE;\r
430 \r
431     return TRUE;\r
432 }\r
433 \r
434 int create_directory(char *name)\r
435 {\r
436     return CreateDirectory(name, NULL) != 0;\r
437 }\r
438 \r
439 char *dir_file_cat(char *dir, char *file)\r
440 {\r
441     return dupcat(dir, "\\", file, NULL);\r
442 }\r
443 \r
444 /* ----------------------------------------------------------------------\r
445  * Platform-specific network handling.\r
446  */\r
447 \r
448 /*\r
449  * Be told what socket we're supposed to be using.\r
450  */\r
451 static SOCKET sftp_ssh_socket = INVALID_SOCKET;\r
452 static HANDLE netevent = INVALID_HANDLE_VALUE;\r
453 char *do_select(SOCKET skt, int startup)\r
454 {\r
455     int events;\r
456     if (startup)\r
457         sftp_ssh_socket = skt;\r
458     else\r
459         sftp_ssh_socket = INVALID_SOCKET;\r
460 \r
461     if (p_WSAEventSelect) {\r
462         if (startup) {\r
463             events = (FD_CONNECT | FD_READ | FD_WRITE |\r
464                       FD_OOB | FD_CLOSE | FD_ACCEPT);\r
465             netevent = CreateEvent(NULL, FALSE, FALSE, NULL);\r
466         } else {\r
467             events = 0;\r
468         }\r
469         if (p_WSAEventSelect(skt, netevent, events) == SOCKET_ERROR) {\r
470             switch (p_WSAGetLastError()) {\r
471               case WSAENETDOWN:\r
472                 return "Network is down";\r
473               default:\r
474                 return "WSAEventSelect(): unknown error";\r
475             }\r
476         }\r
477     }\r
478     return NULL;\r
479 }\r
480 extern int select_result(WPARAM, LPARAM);\r
481 \r
482 int do_eventsel_loop(HANDLE other_event)\r
483 {\r
484     int n, nhandles, nallhandles, netindex, otherindex;\r
485     long next, ticks;\r
486     HANDLE *handles;\r
487     SOCKET *sklist;\r
488     int skcount;\r
489     long now = GETTICKCOUNT();\r
490 \r
491     if (run_timers(now, &next)) {\r
492         ticks = next - GETTICKCOUNT();\r
493         if (ticks < 0) ticks = 0;  /* just in case */\r
494     } else {\r
495         ticks = INFINITE;\r
496     }\r
497 \r
498     handles = handle_get_events(&nhandles);\r
499     handles = sresize(handles, nhandles+2, HANDLE);\r
500     nallhandles = nhandles;\r
501 \r
502     if (netevent != INVALID_HANDLE_VALUE)\r
503         handles[netindex = nallhandles++] = netevent;\r
504     else\r
505         netindex = -1;\r
506     if (other_event != INVALID_HANDLE_VALUE)\r
507         handles[otherindex = nallhandles++] = other_event;\r
508     else\r
509         otherindex = -1;\r
510 \r
511     n = WaitForMultipleObjects(nallhandles, handles, FALSE, ticks);\r
512 \r
513     if ((unsigned)(n - WAIT_OBJECT_0) < (unsigned)nhandles) {\r
514         handle_got_event(handles[n - WAIT_OBJECT_0]);\r
515     } else if (netindex >= 0 && n == WAIT_OBJECT_0 + netindex) {\r
516         WSANETWORKEVENTS things;\r
517         SOCKET socket;\r
518         extern SOCKET first_socket(int *), next_socket(int *);\r
519         extern int select_result(WPARAM, LPARAM);\r
520         int i, socketstate;\r
521 \r
522         /*\r
523          * We must not call select_result() for any socket\r
524          * until we have finished enumerating within the\r
525          * tree. This is because select_result() may close\r
526          * the socket and modify the tree.\r
527          */\r
528         /* Count the active sockets. */\r
529         i = 0;\r
530         for (socket = first_socket(&socketstate);\r
531              socket != INVALID_SOCKET;\r
532              socket = next_socket(&socketstate)) i++;\r
533 \r
534         /* Expand the buffer if necessary. */\r
535         sklist = snewn(i, SOCKET);\r
536 \r
537         /* Retrieve the sockets into sklist. */\r
538         skcount = 0;\r
539         for (socket = first_socket(&socketstate);\r
540              socket != INVALID_SOCKET;\r
541              socket = next_socket(&socketstate)) {\r
542             sklist[skcount++] = socket;\r
543         }\r
544 \r
545         /* Now we're done enumerating; go through the list. */\r
546         for (i = 0; i < skcount; i++) {\r
547             WPARAM wp;\r
548             socket = sklist[i];\r
549             wp = (WPARAM) socket;\r
550             if (!p_WSAEnumNetworkEvents(socket, NULL, &things)) {\r
551                 static const struct { int bit, mask; } eventtypes[] = {\r
552                     {FD_CONNECT_BIT, FD_CONNECT},\r
553                     {FD_READ_BIT, FD_READ},\r
554                     {FD_CLOSE_BIT, FD_CLOSE},\r
555                     {FD_OOB_BIT, FD_OOB},\r
556                     {FD_WRITE_BIT, FD_WRITE},\r
557                     {FD_ACCEPT_BIT, FD_ACCEPT},\r
558                 };\r
559                 int e;\r
560 \r
561                 noise_ultralight(socket);\r
562                 noise_ultralight(things.lNetworkEvents);\r
563 \r
564                 for (e = 0; e < lenof(eventtypes); e++)\r
565                     if (things.lNetworkEvents & eventtypes[e].mask) {\r
566                         LPARAM lp;\r
567                         int err = things.iErrorCode[eventtypes[e].bit];\r
568                         lp = WSAMAKESELECTREPLY(eventtypes[e].mask, err);\r
569                         select_result(wp, lp);\r
570                     }\r
571             }\r
572         }\r
573 \r
574         sfree(sklist);\r
575     }\r
576 \r
577     sfree(handles);\r
578 \r
579     if (n == WAIT_TIMEOUT) {\r
580         now = next;\r
581     } else {\r
582         now = GETTICKCOUNT();\r
583     }\r
584 \r
585     if (otherindex >= 0 && n == WAIT_OBJECT_0 + otherindex)\r
586         return 1;\r
587 \r
588     return 0;\r
589 }\r
590 \r
591 /*\r
592  * Wait for some network data and process it.\r
593  *\r
594  * We have two variants of this function. One uses select() so that\r
595  * it's compatible with WinSock 1. The other uses WSAEventSelect\r
596  * and MsgWaitForMultipleObjects, so that we can consistently use\r
597  * WSAEventSelect throughout; this enables us to also implement\r
598  * ssh_sftp_get_cmdline() using a parallel mechanism.\r
599  */\r
600 int ssh_sftp_loop_iteration(void)\r
601 {\r
602     if (p_WSAEventSelect == NULL) {\r
603         fd_set readfds;\r
604         int ret;\r
605         long now = GETTICKCOUNT();\r
606 \r
607         if (sftp_ssh_socket == INVALID_SOCKET)\r
608             return -1;                 /* doom */\r
609 \r
610         if (socket_writable(sftp_ssh_socket))\r
611             select_result((WPARAM) sftp_ssh_socket, (LPARAM) FD_WRITE);\r
612 \r
613         do {\r
614             long next, ticks;\r
615             struct timeval tv, *ptv;\r
616 \r
617             if (run_timers(now, &next)) {\r
618                 ticks = next - GETTICKCOUNT();\r
619                 if (ticks <= 0)\r
620                     ticks = 1;         /* just in case */\r
621                 tv.tv_sec = ticks / 1000;\r
622                 tv.tv_usec = ticks % 1000 * 1000;\r
623                 ptv = &tv;\r
624             } else {\r
625                 ptv = NULL;\r
626             }\r
627 \r
628             FD_ZERO(&readfds);\r
629             FD_SET(sftp_ssh_socket, &readfds);\r
630             ret = p_select(1, &readfds, NULL, NULL, ptv);\r
631 \r
632             if (ret < 0)\r
633                 return -1;                     /* doom */\r
634             else if (ret == 0)\r
635                 now = next;\r
636             else\r
637                 now = GETTICKCOUNT();\r
638 \r
639         } while (ret == 0);\r
640 \r
641         select_result((WPARAM) sftp_ssh_socket, (LPARAM) FD_READ);\r
642 \r
643         return 0;\r
644     } else {\r
645         return do_eventsel_loop(INVALID_HANDLE_VALUE);\r
646     }\r
647 }\r
648 \r
649 /*\r
650  * Read a command line from standard input.\r
651  * \r
652  * In the presence of WinSock 2, we can use WSAEventSelect to\r
653  * mediate between the socket and stdin, meaning we can send\r
654  * keepalives and respond to server events even while waiting at\r
655  * the PSFTP command prompt. Without WS2, we fall back to a simple\r
656  * fgets.\r
657  */\r
658 struct command_read_ctx {\r
659     HANDLE event;\r
660     char *line;\r
661 };\r
662 \r
663 static DWORD WINAPI command_read_thread(void *param)\r
664 {\r
665     struct command_read_ctx *ctx = (struct command_read_ctx *) param;\r
666 \r
667     ctx->line = fgetline(stdin);\r
668 \r
669     SetEvent(ctx->event);\r
670 \r
671     return 0;\r
672 }\r
673 \r
674 char *ssh_sftp_get_cmdline(char *prompt, int no_fds_ok)\r
675 {\r
676     int ret;\r
677     struct command_read_ctx actx, *ctx = &actx;\r
678     DWORD threadid;\r
679 \r
680     fputs(prompt, stdout);\r
681     fflush(stdout);\r
682 \r
683     if ((sftp_ssh_socket == INVALID_SOCKET && no_fds_ok) ||\r
684         p_WSAEventSelect == NULL) {\r
685         return fgetline(stdin);        /* very simple */\r
686     }\r
687 \r
688     /*\r
689      * Create a second thread to read from stdin. Process network\r
690      * and timing events until it terminates.\r
691      */\r
692     ctx->event = CreateEvent(NULL, FALSE, FALSE, NULL);\r
693     ctx->line = NULL;\r
694 \r
695     if (!CreateThread(NULL, 0, command_read_thread,\r
696                       ctx, 0, &threadid)) {\r
697         fprintf(stderr, "Unable to create command input thread\n");\r
698         cleanup_exit(1);\r
699     }\r
700 \r
701     do {\r
702         ret = do_eventsel_loop(ctx->event);\r
703 \r
704         /* Error return can only occur if netevent==NULL, and it ain't. */\r
705         assert(ret >= 0);\r
706     } while (ret == 0);\r
707 \r
708     return ctx->line;\r
709 }\r
710 \r
711 /* ----------------------------------------------------------------------\r
712  * Main program. Parse arguments etc.\r
713  */\r
714 int main(int argc, char *argv[])\r
715 {\r
716     int ret;\r
717 \r
718     ret = psftp_main(argc, argv);\r
719 \r
720     return ret;\r
721 }\r