Commit | Line | Data |
---|---|---|
0f07afe1 CF |
1 | /* wince-stub.c -- debugging stub for a Windows CE device |
2 | ||
3 | Copyright 1999, 2000 Free Software Foundation, Inc. | |
4 | Contributed by Cygnus Solutions, A Red Hat Company. | |
5 | ||
6 | This file is part of GDB. | |
7 | ||
8 | This program is free software; you can redistribute it and/or modify | |
9 | it under the terms of the GNU General Public License as published by | |
10 | the Free Software Foundation; either version 2 of the License, or | |
11 | (at your option) any later version. | |
12 | ||
13 | This program is distributed in the hope that it will be useful, | |
14 | but WITHOUT ANY WARRANTY; without eve nthe implied warranty of | |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 | GNU General Public License for more details. | |
17 | ||
18 | You should have received a copy of the GNU General Public License | |
19 | along with this program; if not, write to the Free Software | |
20 | Foundation, Inc., 59 Temple Place - Suite 330, | |
21 | Boston, MA 02111-1307, USA. | |
22 | */ | |
23 | ||
24 | /* by Christopher Faylor (cgf@cygnus.com) */ | |
25 | ||
26 | #include <stdarg.h> | |
27 | #include <windows.h> | |
28 | #include <winsock.h> | |
29 | #include "wince-stub.h" | |
30 | ||
31 | #define MALLOC(n) (void *) LocalAlloc (LMEM_MOVEABLE, (UINT)(n)) | |
32 | #define REALLOC(s, n) (void *) LocalReAlloc ((HLOCAL)(s), (UINT)(n), LMEM_MOVEABLE) | |
33 | ||
34 | static int skip_next_id = 0; /* Don't read next API code from socket */ | |
35 | ||
36 | /* v-style interface for handling varying argyment list error messages. | |
37 | Displays the error message in a dialog box and exits when user clicks | |
38 | on OK. */ | |
39 | static void | |
40 | vstub_error (LPCWSTR fmt, va_list args) | |
41 | { | |
42 | WCHAR buf[4096]; | |
43 | wvsprintfW (buf, fmt, args); | |
44 | ||
45 | MessageBoxW (NULL, buf, L"GDB", MB_ICONERROR); | |
46 | WSACleanup (); | |
47 | ExitThread (1); | |
48 | } | |
49 | ||
50 | /* The standard way to display an error message and exit. */ | |
51 | static void | |
52 | stub_error (LPCWSTR fmt, ...) | |
53 | { | |
54 | va_list args; | |
55 | va_start (args, fmt); | |
56 | vstub_error (fmt, args); | |
57 | } | |
58 | ||
59 | /* Standard "oh well" can't communicate error. Someday this might attempt | |
60 | synchronization. */ | |
61 | static void | |
62 | attempt_resync (LPCWSTR huh, int s) | |
63 | { | |
64 | stub_error (L"lost synchronization with host attempting %s. Error %d", huh, WSAGetLastError ()); | |
65 | } | |
66 | ||
67 | /* Read arbitrary stuff from a socket. */ | |
68 | static int | |
69 | sockread (LPCWSTR huh, int s, void *str, size_t n) | |
70 | { | |
71 | for (;;) | |
72 | { | |
73 | if (recv (s, str, n, 0) == (int) n) | |
74 | return n; | |
75 | attempt_resync (huh, s); | |
76 | } | |
77 | } | |
78 | ||
79 | /* Write arbitrary stuff to a socket. */ | |
80 | static int | |
81 | sockwrite (LPCWSTR huh, int s, const void *str, size_t n) | |
82 | { | |
83 | for (;;) | |
84 | { | |
85 | if (send (s, str, n, 0) == (int) n) | |
86 | return n; | |
87 | attempt_resync (huh, s); | |
88 | } | |
89 | } | |
90 | ||
91 | /* Allocate a limited pool of memory, reallocating over unused | |
92 | buffers. This assumes that there will never be more than four | |
93 | "buffers" required which, so far, is a safe assumption. */ | |
94 | static LPVOID | |
95 | mempool (gdb_wince_len len) | |
96 | { | |
97 | static int n = -1; | |
98 | static LPWSTR outs[4] = {NULL /*, NULL, etc. */}; | |
99 | ||
100 | if (++n >= (sizeof (outs) / sizeof (outs[0]))) | |
101 | n = 0; | |
102 | ||
103 | /* Allocate space for the converted string, reusing any previously allocated | |
104 | space, if applicable. */ | |
105 | if (outs[n]) | |
106 | outs[n] = (LPWSTR) REALLOC (outs[n], len); | |
107 | else | |
108 | outs[n] = (LPWSTR) MALLOC (len); | |
109 | ||
110 | return outs[n]; | |
111 | } | |
112 | ||
113 | /* Get a an ID (possibly) and a DWORD from the host gdb. | |
114 | Don't bother with the id if the main loop has already | |
115 | read it. */ | |
116 | static DWORD | |
117 | getdword (LPCWSTR huh, int s, gdb_wince_id what_this) | |
118 | { | |
119 | DWORD n; | |
120 | gdb_wince_id what; | |
121 | ||
122 | if (skip_next_id) | |
123 | skip_next_id = 0; | |
124 | else | |
125 | do | |
126 | if (sockread (huh, s, &what, sizeof (what)) != sizeof (what)) | |
127 | stub_error (L"error getting record type from host - %s.", huh); | |
128 | while (what_this != what); | |
129 | ||
130 | if (sockread (huh, s, &n, sizeof (n)) != sizeof (n)) | |
131 | stub_error (L"error getting %s from host.", huh); | |
132 | ||
133 | return n; | |
134 | } | |
135 | ||
136 | /* Get a an ID (possibly) and a WORD from the host gdb. | |
137 | Don't bother with the id if the main loop has already | |
138 | read it. */ | |
139 | static WORD | |
140 | getword (LPCWSTR huh, int s, gdb_wince_id what_this) | |
141 | { | |
142 | WORD n; | |
143 | gdb_wince_id what; | |
144 | ||
145 | if (skip_next_id) | |
146 | skip_next_id = 0; | |
147 | else | |
148 | do | |
149 | if (sockread (huh, s, &what, sizeof (what)) != sizeof (what)) | |
150 | stub_error (L"error getting record type from host - %s.", huh); | |
151 | while (what_this != what); | |
152 | ||
153 | if (sockread (huh, s, &n, sizeof (n)) != sizeof (n)) | |
154 | stub_error (L"error getting %s from host.", huh); | |
155 | ||
156 | return n; | |
157 | } | |
158 | ||
159 | /* Handy defines for getting various types of values. */ | |
160 | #define gethandle(huh, s, what) (HANDLE) getdword ((huh), (s), (what)) | |
161 | #define getpvoid(huh, s, what) (LPVOID) getdword ((huh), (s), (what)) | |
162 | #define getlen(huh, s, what) (gdb_wince_len) getword ((huh), (s), (what)) | |
163 | ||
164 | /* Get an arbitrary block of memory from the gdb host. This comes in | |
165 | two chunks an id/dword representing the length and the stream of memory | |
166 | itself. Returns a pointer, allocated via mempool, to a memory buffer. */ | |
167 | static LPWSTR | |
168 | getmemory (LPCWSTR huh, int s, gdb_wince_id what, gdb_wince_len *inlen) | |
169 | { | |
170 | LPVOID p; | |
171 | gdb_wince_len dummy; | |
172 | ||
173 | if (!inlen) | |
174 | inlen = &dummy; | |
175 | ||
176 | *inlen = getlen (huh, s, what); | |
177 | ||
178 | p = mempool (*inlen); /* FIXME: check for error */ | |
179 | ||
180 | if ((gdb_wince_len) sockread (huh, s, p, *inlen) != *inlen) | |
181 | stub_error (L"error getting string from host."); | |
182 | ||
183 | return p; | |
184 | } | |
185 | ||
186 | /* Output an id/dword to the host */ | |
187 | static void | |
188 | putdword (LPCWSTR huh, int s, gdb_wince_id what, DWORD n) | |
189 | { | |
190 | if (sockwrite (huh, s, &what, sizeof (what)) != sizeof (what)) | |
191 | stub_error (L"error writing record id for %s to host.", huh); | |
192 | if (sockwrite (huh, s, &n, sizeof (n)) != sizeof (n)) | |
193 | stub_error (L"error writing %s to host.", huh); | |
194 | } | |
195 | ||
196 | /* Output an id/word to the host */ | |
197 | static void | |
198 | putword (LPCWSTR huh, int s, gdb_wince_id what, WORD n) | |
199 | { | |
200 | if (sockwrite (huh, s, &what, sizeof (what)) != sizeof (what)) | |
201 | stub_error (L"error writing record id for %s to host.", huh); | |
202 | if (sockwrite (huh, s, &n, sizeof (n)) != sizeof (n)) | |
203 | stub_error (L"error writing %s to host.", huh); | |
204 | } | |
205 | ||
206 | /* Convenience define for outputting a "gdb_wince_len" type. */ | |
207 | #define putlen(huh, s, what, n) putword ((huh), (s), (what), (gdb_wince_len) (n)) | |
208 | ||
209 | /* Put an arbitrary block of memory to the gdb host. This comes in | |
210 | two chunks an id/dword representing the length and the stream of memory | |
211 | itself. */ | |
212 | static void | |
213 | putmemory (LPCWSTR huh, int s, gdb_wince_id what, const void *mem, gdb_wince_len len) | |
214 | { | |
215 | putlen (huh, s, what, len); | |
216 | if (((short) len > 0) && (gdb_wince_len) sockwrite (huh, s, mem, len) != len) | |
217 | stub_error (L"error writing memory to host."); | |
218 | } | |
219 | ||
220 | /* Output the result of an operation to the host. If res != 0, sends a block of | |
221 | memory starting at mem of len bytes. If res == 0, sends -GetLastError () and | |
222 | avoids sending the mem. */ | |
223 | static void | |
224 | putresult (LPCWSTR huh, gdb_wince_result res, int s, gdb_wince_id what, const void *mem, gdb_wince_len len) | |
225 | { | |
226 | if (!res) | |
227 | len = -(int) GetLastError (); | |
228 | putmemory (huh, s, what, mem, len); | |
229 | } | |
230 | ||
231 | static HANDLE curproc; /* Currently unused, but nice for debugging */ | |
232 | ||
233 | /* Emulate CreateProcess. Returns &pi if no error. */ | |
234 | static void | |
235 | create_process (int s) | |
236 | { | |
237 | LPWSTR exec_file = getmemory (L"CreateProcess exec_file", s, GDB_CREATEPROCESS, NULL); | |
238 | LPWSTR args = getmemory (L"CreateProcess args", s, GDB_CREATEPROCESS, NULL); | |
239 | DWORD flags = getdword (L"CreateProcess flags", s, GDB_CREATEPROCESS); | |
240 | PROCESS_INFORMATION pi; | |
241 | gdb_wince_result res; | |
242 | ||
243 | res = CreateProcessW (exec_file, | |
244 | args, /* command line */ | |
245 | NULL, /* Security */ | |
246 | NULL, /* thread */ | |
247 | FALSE, /* inherit handles */ | |
248 | flags, /* start flags */ | |
249 | NULL, | |
250 | NULL, /* current directory */ | |
251 | NULL, | |
252 | &pi); | |
253 | putresult (L"CreateProcess", res, s, GDB_CREATEPROCESS, &pi, sizeof (pi)); | |
254 | curproc = pi.hProcess; | |
255 | } | |
256 | ||
257 | /* Emulate TerminateProcess. Returns return value of TerminateProcess if | |
258 | no error. | |
259 | *** NOTE: For some unknown reason, TerminateProcess seems to always return | |
260 | an ACCESS_DENIED (on Windows CE???) error. So, force a TRUE value for now. */ | |
261 | static void | |
262 | terminate_process (int s) | |
263 | { | |
264 | gdb_wince_result res; | |
265 | HANDLE h = gethandle (L"TerminateProcess handle", s, GDB_TERMINATEPROCESS); | |
266 | ||
267 | res = TerminateProcess (h, 0) || 1; /* Doesn't seem to work on SH so default to TRUE */ | |
268 | putresult (L"Terminate process result", res, s, GDB_TERMINATEPROCESS, | |
269 | &res, sizeof (res)); | |
270 | } | |
271 | ||
272 | /* Emulate WaitForDebugEvent. Returns the debug event on success. */ | |
273 | static void | |
274 | wait_for_debug_event (int s) | |
275 | { | |
276 | DWORD ms = getdword (L"WaitForDebugEvent ms", s, GDB_WAITFORDEBUGEVENT); | |
277 | gdb_wince_result res; | |
278 | DEBUG_EVENT ev; | |
279 | ||
280 | res = WaitForDebugEvent (&ev, ms); | |
281 | putresult (L"WaitForDebugEvent event", res, s, GDB_WAITFORDEBUGEVENT, | |
282 | &ev, sizeof (ev)); | |
283 | } | |
284 | ||
285 | /* Emulate GetThreadContext. Returns CONTEXT structure on success. */ | |
286 | static void | |
287 | get_thread_context (int s) | |
288 | { | |
289 | CONTEXT c; | |
290 | HANDLE h = gethandle (L"GetThreadContext handle", s, GDB_GETTHREADCONTEXT); | |
291 | gdb_wince_result res; | |
292 | ||
293 | memset (&c, 0, sizeof (c)); | |
294 | c.ContextFlags = getdword (L"GetThreadContext handle", s, GDB_GETTHREADCONTEXT); | |
295 | ||
296 | res = (gdb_wince_result) GetThreadContext (h, &c); | |
297 | putresult (L"GetThreadContext data", res, s, GDB_GETTHREADCONTEXT, | |
298 | &c, sizeof (c)); | |
299 | } | |
300 | ||
301 | /* Emulate GetThreadContext. Returns success of SetThreadContext. */ | |
302 | static void | |
303 | set_thread_context (int s) | |
304 | { | |
305 | gdb_wince_result res; | |
306 | HANDLE h = gethandle (L"SetThreadContext handle", s, GDB_SETTHREADCONTEXT); | |
307 | LPCONTEXT pc = (LPCONTEXT) getmemory (L"SetThreadContext context", s, | |
308 | GDB_SETTHREADCONTEXT, NULL); | |
309 | ||
310 | res = SetThreadContext (h, pc); | |
311 | putresult (L"SetThreadContext result", res, s, GDB_SETTHREADCONTEXT, | |
312 | &res, sizeof (res)); | |
313 | } | |
314 | ||
315 | /* Emulate ReadProcessMemory. Returns memory read on success. */ | |
316 | static void | |
317 | read_process_memory (int s) | |
318 | { | |
319 | HANDLE h = gethandle (L"ReadProcessMemory handle", s, GDB_READPROCESSMEMORY); | |
320 | LPVOID p = getpvoid (L"ReadProcessMemory base", s, GDB_READPROCESSMEMORY); | |
321 | gdb_wince_len len = getlen (L"ReadProcessMemory size", s, GDB_READPROCESSMEMORY); | |
322 | LPVOID buf = mempool ((gdb_wince_len) len); | |
323 | DWORD outlen; | |
324 | gdb_wince_result res; | |
325 | ||
326 | outlen = 0; | |
327 | res = (gdb_wince_result) ReadProcessMemory (h, p, buf, len, &outlen); | |
328 | putresult (L"ReadProcessMemory data", res, s, GDB_READPROCESSMEMORY, | |
329 | buf, (gdb_wince_len) outlen); | |
330 | } | |
331 | ||
332 | /* Emulate WriteProcessMemory. Returns WriteProcessMemory success. */ | |
333 | static void | |
334 | write_process_memory (int s) | |
335 | { | |
336 | HANDLE h = gethandle (L"WriteProcessMemory handle", s, GDB_WRITEPROCESSMEMORY); | |
337 | LPVOID p = getpvoid (L"WriteProcessMemory base", s, GDB_WRITEPROCESSMEMORY); | |
338 | gdb_wince_len len; | |
339 | LPVOID buf = getmemory (L"WriteProcessMemory buf", s, GDB_WRITEPROCESSMEMORY, &len); | |
340 | DWORD outlen; | |
341 | gdb_wince_result res; | |
342 | ||
343 | outlen = 0; | |
344 | res = WriteProcessMemory (h, p, buf, (DWORD) len, &outlen); | |
345 | putresult (L"WriteProcessMemory data", res, s, GDB_WRITEPROCESSMEMORY, | |
346 | (gdb_wince_len *) & outlen, sizeof (gdb_wince_len)); | |
347 | } | |
348 | ||
349 | /* Return non-zero to gdb host if given thread is alive. */ | |
350 | static void | |
351 | thread_alive (int s) | |
352 | { | |
353 | HANDLE h = gethandle (L"ThreadAlive handle", s, GDB_THREADALIVE); | |
354 | gdb_wince_result res; | |
355 | ||
356 | res = WaitForSingleObject (h, 0) == WAIT_OBJECT_0 ? 1 : 0; | |
357 | putresult (L"WriteProcessMemory data", res, s, GDB_THREADALIVE, | |
358 | &res, sizeof (res)); | |
359 | } | |
360 | ||
361 | /* Emulate SuspendThread. Returns value returned from SuspendThread. */ | |
362 | static void | |
363 | suspend_thread (int s) | |
364 | { | |
365 | DWORD res; | |
366 | HANDLE h = gethandle (L"SuspendThread handle", s, GDB_SUSPENDTHREAD); | |
367 | res = SuspendThread (h); | |
368 | putdword (L"SuspendThread result", s, GDB_SUSPENDTHREAD, res); | |
369 | } | |
370 | ||
371 | /* Emulate ResumeThread. Returns value returned from ResumeThread. */ | |
372 | static void | |
373 | resume_thread (int s) | |
374 | { | |
375 | DWORD res; | |
376 | HANDLE h = gethandle (L"ResumeThread handle", s, GDB_RESUMETHREAD); | |
377 | res = ResumeThread (h); | |
378 | putdword (L"ResumeThread result", s, GDB_RESUMETHREAD, res); | |
379 | } | |
380 | ||
381 | /* Emulate ContinueDebugEvent. Returns ContinueDebugEvent success. */ | |
382 | static void | |
383 | continue_debug_event (int s) | |
384 | { | |
385 | gdb_wince_result res; | |
386 | DWORD pid = getdword (L"ContinueDebugEvent pid", s, GDB_CONTINUEDEBUGEVENT); | |
387 | DWORD tid = getdword (L"ContinueDebugEvent tid", s, GDB_CONTINUEDEBUGEVENT); | |
388 | DWORD status = getdword (L"ContinueDebugEvent status", s, GDB_CONTINUEDEBUGEVENT); | |
389 | res = (gdb_wince_result) ContinueDebugEvent (pid, tid, status); | |
390 | putresult (L"ContinueDebugEvent result", res, s, GDB_CONTINUEDEBUGEVENT, &res, sizeof (res)); | |
391 | } | |
392 | ||
393 | /* Emulate CloseHandle. Returns CloseHandle success. */ | |
394 | static void | |
395 | close_handle (int s) | |
396 | { | |
397 | gdb_wince_result res; | |
398 | HANDLE h = gethandle (L"CloseHandle handle", s, GDB_CLOSEHANDLE); | |
399 | res = (gdb_wince_result) CloseHandle (h); | |
400 | putresult (L"CloseHandle result", res, s, GDB_CLOSEHANDLE, &res, sizeof (res)); | |
401 | } | |
402 | ||
403 | /* Handle single step instruction */ | |
404 | static void | |
405 | single_step (int s) | |
406 | { | |
407 | } | |
408 | ||
409 | /* Main loop for reading requests from gdb host on the socket. */ | |
410 | static void | |
411 | dispatch (int s) | |
412 | { | |
413 | gdb_wince_id id; | |
414 | ||
415 | /* Continue reading from socket until receive a GDB_STOPSUB. */ | |
416 | while (sockread (L"Dispatch", s, &id, sizeof (id)) > 0) | |
417 | { | |
418 | skip_next_id = 1; | |
419 | switch (id) | |
420 | { | |
421 | case GDB_CREATEPROCESS: | |
422 | create_process (s); | |
423 | break; | |
424 | case GDB_TERMINATEPROCESS: | |
425 | terminate_process (s); | |
426 | break; | |
427 | case GDB_WAITFORDEBUGEVENT: | |
428 | wait_for_debug_event (s); | |
429 | break; | |
430 | case GDB_GETTHREADCONTEXT: | |
431 | get_thread_context (s); | |
432 | break; | |
433 | case GDB_SETTHREADCONTEXT: | |
434 | set_thread_context (s); | |
435 | break; | |
436 | case GDB_READPROCESSMEMORY: | |
437 | read_process_memory (s); | |
438 | break; | |
439 | case GDB_WRITEPROCESSMEMORY: | |
440 | write_process_memory (s); | |
441 | break; | |
442 | case GDB_THREADALIVE: | |
443 | thread_alive (s); | |
444 | break; | |
445 | case GDB_SUSPENDTHREAD: | |
446 | suspend_thread (s); | |
447 | break; | |
448 | case GDB_RESUMETHREAD: | |
449 | resume_thread (s); | |
450 | break; | |
451 | case GDB_CONTINUEDEBUGEVENT: | |
452 | continue_debug_event (s); | |
453 | break; | |
454 | case GDB_CLOSEHANDLE: | |
455 | close_handle (s); | |
456 | break; | |
457 | case GDB_STOPSTUB: | |
458 | terminate_process (s); | |
459 | return; | |
460 | case GDB_SINGLESTEP: | |
461 | single_step (s); | |
462 | return; | |
463 | default: | |
464 | { | |
465 | WCHAR buf[80]; | |
466 | wsprintfW (buf, L"Invalid command id received: %d", id); | |
467 | MessageBoxW (NULL, buf, L"GDB", MB_ICONERROR); | |
468 | skip_next_id = 0; | |
469 | } | |
470 | } | |
471 | } | |
472 | } | |
473 | ||
474 | /* The Windows Main entry point */ | |
475 | int WINAPI | |
476 | WinMain (HINSTANCE hi, HINSTANCE hp, LPWSTR cmd, int show) | |
477 | { | |
478 | struct hostent *h; | |
479 | int s; | |
480 | struct WSAData wd; | |
481 | struct sockaddr_in sin; | |
482 | int tmp; | |
483 | LPWSTR whost; | |
484 | char host[80]; | |
485 | ||
486 | whost = wcschr (cmd, L' '); /* Look for argument. */ | |
0f07afe1 CF |
487 | |
488 | /* If no host is specified, just use default */ | |
489 | if (whost) | |
490 | { | |
491 | /* Eat any spaces. */ | |
492 | while (*whost == L' ' || *whost == L'\t') | |
493 | whost++; | |
494 | ||
495 | wcstombs (host, whost, 80); /* Convert from UNICODE to ascii */ | |
496 | } | |
497 | ||
498 | MessageBoxW (NULL, whost, L"GDB", MB_ICONERROR); | |
499 | ||
500 | /* Winsock initialization. */ | |
501 | if (WSAStartup (MAKEWORD (1, 1), &wd)) | |
502 | stub_error (L"Couldn't initialize WINSOCK."); | |
503 | ||
504 | /* If whost was specified, first try it. If it was not specified or the | |
505 | host lookup failed, try the Windows CE magic ppp_peer lookup. ppp_peer | |
506 | is supposed to be the Windows host sitting on the other end of the | |
507 | serial cable. */ | |
508 | if (whost && *whost && (h = gethostbyname (host)) != NULL) | |
509 | /* nothing to do */ ; | |
510 | else if ((h = gethostbyname ("ppp_peer")) == NULL) | |
511 | stub_error (L"Couldn't get IP address of host system. Error %d", WSAGetLastError ()); | |
512 | ||
513 | /* Get a socket. */ | |
514 | if ((s = socket (AF_INET, SOCK_STREAM, 0)) < 0) | |
515 | stub_error (L"Couldn't connect to host system. Error %d", WSAGetLastError ()); | |
516 | ||
517 | /* Allow rapid reuse of the port. */ | |
518 | tmp = 1; | |
519 | setsockopt (s, SOL_SOCKET, SO_REUSEADDR, (char *) &tmp, sizeof (tmp)); | |
520 | ||
521 | /* Set up the information for connecting to the host gdb process. */ | |
522 | memset (&sin, 0, sizeof (sin)); | |
523 | sin.sin_family = h->h_addrtype; | |
524 | memcpy (&sin.sin_addr, h->h_addr, h->h_length); | |
525 | sin.sin_port = htons (7000); /* FIXME: This should be configurable */ | |
526 | ||
527 | /* Connect to host */ | |
528 | if (connect (s, (struct sockaddr *) &sin, sizeof (sin)) < 0) | |
529 | stub_error (L"Couldn't connect to host gdb."); | |
530 | ||
531 | /* Read from socket until told to exit. */ | |
532 | dispatch (s); | |
533 | WSACleanup (); | |
534 | return 0; | |
535 | } |