1/* mswindows.c -- Windows-specific support 2 Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 3 2005, 2006, 2007, 2008, 2009 Free Software Foundation, Inc. 4 5This file is part of GNU Wget. 6 7GNU Wget is free software; you can redistribute it and/or modify 8it under the terms of the GNU General Public License as published by 9the Free Software Foundation; either version 3 of the License, or 10(at your option) any later version. 11 12GNU Wget is distributed in the hope that it will be useful, 13but WITHOUT ANY WARRANTY; without even the implied warranty of 14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15GNU General Public License for more details. 16 17You should have received a copy of the GNU General Public License 18along with Wget. If not, see <http://www.gnu.org/licenses/>. 19 20Additional permission under GNU GPL version 3 section 7 21 22If you modify this program, or any covered work, by linking or 23combining it with the OpenSSL project's OpenSSL library (or a 24modified version of that library), containing parts covered by the 25terms of the OpenSSL or SSLeay licenses, the Free Software Foundation 26grants you additional permission to convey the resulting work. 27Corresponding Source for a non-source form of such a combination 28shall include the source code for the parts of OpenSSL used as well 29as that of the covered work. */ 30 31#define INHIBIT_WRAP /* avoid wrapping of socket, bind, ... */ 32 33#include "wget.h" 34 35#include <stdio.h> 36#include <stdlib.h> 37#include <string.h> 38#include <errno.h> 39#include <math.h> 40 41 42#include "utils.h" 43#include "url.h" 44 45#ifndef ES_SYSTEM_REQUIRED 46#define ES_SYSTEM_REQUIRED 0x00000001 47#endif 48 49#ifndef ES_CONTINUOUS 50#define ES_CONTINUOUS 0x80000000 51#endif 52 53 54/* Defined in log.c. */ 55void log_request_redirect_output (const char *); 56 57/* Windows version of xsleep in utils.c. */ 58 59void 60xsleep (double seconds) 61{ 62#ifdef HAVE_USLEEP 63 if (seconds > 1000) 64 { 65 /* Explained in utils.c. */ 66 sleep (seconds); 67 seconds -= (long) seconds; 68 } 69 usleep (seconds * 1000000); 70#else /* not HAVE_USLEEP */ 71 SleepEx ((DWORD) (seconds * 1000 + .5), FALSE); 72#endif /* not HAVE_USLEEP */ 73} 74 75void 76windows_main (char **exec_name) 77{ 78 char *p; 79 80 /* Remove .EXE from filename if it has one. */ 81 *exec_name = xstrdup (*exec_name); 82 p = strrchr (*exec_name, '.'); 83 if (p) 84 *p = '\0'; 85} 86 87static void 88ws_cleanup (void) 89{ 90 xfree ((char*)exec_name); 91 WSACleanup (); 92} 93 94#if defined(CTRLBREAK_BACKGND) || defined(CTRLC_BACKGND) 95static void 96ws_hangup (const char *reason) 97{ 98 fprintf (stderr, _("Continuing in background.\n")); 99 log_request_redirect_output (reason); 100 101 /* Detach process from the current console. Under Windows 9x, if we 102 were launched from a 16-bit process (which is usually the case; 103 command.com is 16-bit) the parent process should resume right away. 104 Under NT or if launched from a 32-process under 9x, this is a futile 105 gesture as the parent will wait for us to terminate before resuming. */ 106 FreeConsole (); 107} 108#endif 109 110/* Construct the name for a named section (a.k.a. `file mapping') object. 111 The returned string is dynamically allocated and needs to be xfree()'d. */ 112static char * 113make_section_name (DWORD pid) 114{ 115 return aprintf ("gnu_wget_fake_fork_%lu", pid); 116} 117 118/* This structure is used to hold all the data that is exchanged between 119 parent and child. */ 120struct fake_fork_info 121{ 122 HANDLE event; 123 bool logfile_changed; 124 char lfilename[MAX_PATH + 1]; 125}; 126 127/* Determines if we are the child and if so performs the child logic. 128 Return values: 129 < 0 error 130 0 parent 131 > 0 child 132*/ 133static int 134fake_fork_child (void) 135{ 136 HANDLE section, event; 137 struct fake_fork_info *info; 138 char *name; 139 140 name = make_section_name (GetCurrentProcessId ()); 141 section = OpenFileMapping (FILE_MAP_WRITE, FALSE, name); 142 xfree (name); 143 /* It seems that Windows 9x and NT set last-error inconsistently when 144 OpenFileMapping() fails; so we assume it failed because the section 145 object does not exist. */ 146 if (!section) 147 return 0; /* We are the parent. */ 148 149 info = MapViewOfFile (section, FILE_MAP_WRITE, 0, 0, 0); 150 if (!info) 151 { 152 CloseHandle (section); 153 return -1; 154 } 155 156 event = info->event; 157 158 info->logfile_changed = false; 159 if (!opt.lfilename && (!opt.quiet || opt.server_response)) 160 { 161 /* See utils:fork_to_background for explanation. */ 162 FILE *new_log_fp = unique_create (DEFAULT_LOGFILE, false, &opt.lfilename); 163 if (new_log_fp) 164 { 165 info->logfile_changed = true; 166 strncpy (info->lfilename, opt.lfilename, sizeof (info->lfilename)); 167 info->lfilename[sizeof (info->lfilename) - 1] = '\0'; 168 fclose (new_log_fp); 169 } 170 } 171 172 UnmapViewOfFile (info); 173 CloseHandle (section); 174 175 /* Inform the parent that we've done our part. */ 176 if (!SetEvent (event)) 177 return -1; 178 179 CloseHandle (event); 180 return 1; /* We are the child. */ 181} 182 183/* Windows doesn't support the fork() call; so we fake it by invoking 184 another copy of Wget with the same arguments with which we were 185 invoked. The child copy of Wget should perform the same initialization 186 sequence as the parent; so we should have two processes that are 187 essentially identical. We create a specially named section object that 188 allows the child to distinguish itself from the parent and is used to 189 exchange information between the two processes. We use an event object 190 for synchronization. */ 191static void 192fake_fork (void) 193{ 194 char exe[MAX_PATH + 1]; 195 DWORD exe_len, le; 196 SECURITY_ATTRIBUTES sa; 197 HANDLE section, event, h[2]; 198 STARTUPINFO si; 199 PROCESS_INFORMATION pi; 200 struct fake_fork_info *info; 201 char *name; 202 BOOL rv; 203 204 section = pi.hProcess = pi.hThread = NULL; 205 206 /* Get the fully qualified name of our executable. This is more reliable 207 than using argv[0]. */ 208 exe_len = GetModuleFileName (GetModuleHandle (NULL), exe, sizeof (exe)); 209 if (!exe_len || (exe_len >= sizeof (exe))) 210 return; 211 212 sa.nLength = sizeof (sa); 213 sa.lpSecurityDescriptor = NULL; 214 sa.bInheritHandle = TRUE; 215 216 /* Create an anonymous inheritable event object that starts out 217 non-signaled. */ 218 event = CreateEvent (&sa, FALSE, FALSE, NULL); 219 if (!event) 220 return; 221 222 /* Create the child process detached form the current console and in a 223 suspended state. */ 224 xzero (si); 225 si.cb = sizeof (si); 226 rv = CreateProcess (exe, GetCommandLine (), NULL, NULL, TRUE, 227 CREATE_SUSPENDED | DETACHED_PROCESS, 228 NULL, NULL, &si, &pi); 229 if (!rv) 230 goto cleanup; 231 232 /* Create a named section object with a name based on the process id of 233 the child. */ 234 name = make_section_name (pi.dwProcessId); 235 section = 236 CreateFileMapping (INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 237 sizeof (struct fake_fork_info), name); 238 le = GetLastError(); 239 xfree (name); 240 /* Fail if the section object already exists (should not happen). */ 241 if (!section || (le == ERROR_ALREADY_EXISTS)) 242 { 243 rv = FALSE; 244 goto cleanup; 245 } 246 247 /* Copy the event handle into the section object. */ 248 info = MapViewOfFile (section, FILE_MAP_WRITE, 0, 0, 0); 249 if (!info) 250 { 251 rv = FALSE; 252 goto cleanup; 253 } 254 255 info->event = event; 256 257 UnmapViewOfFile (info); 258 259 /* Start the child process. */ 260 rv = ResumeThread (pi.hThread); 261 if (!rv) 262 { 263 TerminateProcess (pi.hProcess, (DWORD) -1); 264 goto cleanup; 265 } 266 267 /* Wait for the child to signal to us that it has done its part. If it 268 terminates before signaling us it's an error. */ 269 270 h[0] = event; 271 h[1] = pi.hProcess; 272 rv = WAIT_OBJECT_0 == WaitForMultipleObjects (2, h, FALSE, 5 * 60 * 1000); 273 if (!rv) 274 goto cleanup; 275 276 info = MapViewOfFile (section, FILE_MAP_READ, 0, 0, 0); 277 if (!info) 278 { 279 rv = FALSE; 280 goto cleanup; 281 } 282 283 /* Ensure string is properly terminated. */ 284 if (info->logfile_changed && 285 !memchr (info->lfilename, '\0', sizeof (info->lfilename))) 286 { 287 rv = FALSE; 288 goto cleanup; 289 } 290 291 printf (_("Continuing in background, pid %lu.\n"), pi.dwProcessId); 292 if (info->logfile_changed) 293 printf (_("Output will be written to %s.\n"), quote (info->lfilename)); 294 295 UnmapViewOfFile (info); 296 297cleanup: 298 299 if (event) 300 CloseHandle (event); 301 if (section) 302 CloseHandle (section); 303 if (pi.hThread) 304 CloseHandle (pi.hThread); 305 if (pi.hProcess) 306 CloseHandle (pi.hProcess); 307 308 /* We're the parent. If all is well, terminate. */ 309 if (rv) 310 exit (0); 311 312 /* We failed, return. */ 313} 314 315/* This is the corresponding Windows implementation of the 316 fork_to_background() function in utils.c. */ 317void 318fork_to_background (void) 319{ 320 int rv; 321 322 rv = fake_fork_child (); 323 if (rv < 0) 324 { 325 fprintf (stderr, "fake_fork_child() failed\n"); 326 abort (); 327 } 328 else if (rv == 0) 329 { 330 /* We're the parent. */ 331 fake_fork (); 332 /* If fake_fork() returns, it failed. */ 333 fprintf (stderr, "fake_fork() failed\n"); 334 abort (); 335 } 336 /* If we get here, we're the child. */ 337} 338 339static BOOL WINAPI 340ws_handler (DWORD dwEvent) 341{ 342 switch (dwEvent) 343 { 344#ifdef CTRLC_BACKGND 345 case CTRL_C_EVENT: 346 ws_hangup ("CTRL+C"); 347 return TRUE; 348#endif 349#ifdef CTRLBREAK_BACKGND 350 case CTRL_BREAK_EVENT: 351 ws_hangup ("CTRL+Break"); 352 return TRUE; 353#endif 354 default: 355 return FALSE; 356 } 357} 358 359static char *title_buf = NULL; 360static char *curr_url = NULL; 361static int old_percentage = -1; 362 363/* Updates the console title with the URL of the current file being 364 transferred. */ 365void 366ws_changetitle (const char *url) 367{ 368 xfree_null (title_buf); 369 xfree_null (curr_url); 370 title_buf = xmalloc (strlen (url) + 20); 371 curr_url = xstrdup (url); 372 old_percentage = -1; 373 sprintf (title_buf, "Wget %s", curr_url); 374 SetConsoleTitle (title_buf); 375} 376 377/* Updates the console title with the percentage of the current file 378 transferred. */ 379void 380ws_percenttitle (double percentage_float) 381{ 382 int percentage; 383 384 if (!title_buf || !curr_url) 385 return; 386 387 percentage = (int) percentage_float; 388 389 /* Clamp percentage value. */ 390 if (percentage < 0) 391 percentage = 0; 392 if (percentage > 100) 393 percentage = 100; 394 395 /* Only update the title when the percentage has changed. */ 396 if (percentage == old_percentage) 397 return; 398 399 old_percentage = percentage; 400 401 sprintf (title_buf, "Wget [%d%%] %s", percentage, curr_url); 402 SetConsoleTitle (title_buf); 403} 404 405/* Returns a pointer to the fully qualified name of the directory that 406 contains the Wget binary (wget.exe). The returned path does not have a 407 trailing path separator. Returns NULL on failure. */ 408char * 409ws_mypath (void) 410{ 411 static char *wspathsave = NULL; 412 413 if (!wspathsave) 414 { 415 char buf[MAX_PATH + 1]; 416 char *p; 417 DWORD len; 418 419 len = GetModuleFileName (GetModuleHandle (NULL), buf, sizeof (buf)); 420 if (!len || (len >= sizeof (buf))) 421 return NULL; 422 423 p = strrchr (buf, PATH_SEPARATOR); 424 if (!p) 425 return NULL; 426 427 *p = '\0'; 428 wspathsave = xstrdup (buf); 429 } 430 431 return wspathsave; 432} 433 434/* Prevent Windows entering sleep/hibernation-mode while Wget is doing 435 a lengthy transfer. Windows does not, by default, consider network 436 activity in console-programs as activity! Works on Win-98/ME/2K 437 and up. */ 438static void 439set_sleep_mode (void) 440{ 441 typedef DWORD (WINAPI *func_t) (DWORD); 442 func_t set_exec_state; 443 444 set_exec_state = 445 (func_t) GetProcAddress (GetModuleHandle ("KERNEL32.DLL"), 446 "SetThreadExecutionState"); 447 448 if (set_exec_state) 449 set_exec_state (ES_SYSTEM_REQUIRED | ES_CONTINUOUS); 450} 451 452/* Perform Windows specific initialization. */ 453void 454ws_startup (void) 455{ 456 WSADATA data; 457 WORD requested = MAKEWORD (1, 1); 458 int err = WSAStartup (requested, &data); 459 if (err != 0) 460 { 461 fprintf (stderr, _("%s: Couldn't find usable socket driver.\n"), 462 exec_name); 463 exit (1); 464 } 465 466 if (data.wVersion < requested) 467 { 468 fprintf (stderr, _("%s: Couldn't find usable socket driver.\n"), 469 exec_name); 470 WSACleanup (); 471 exit (1); 472 } 473 474 atexit (ws_cleanup); 475 set_sleep_mode (); 476 SetConsoleCtrlHandler (ws_handler, TRUE); 477} 478 479/* run_with_timeout Windows implementation. */ 480 481/* Stack size 0 uses default thread stack-size (reserve+commit). 482 Determined by what's in the PE header. */ 483#define THREAD_STACK_SIZE 0 484 485struct thread_data 486{ 487 void (*fun) (void *); 488 void *arg; 489 DWORD ws_error; 490}; 491 492/* The callback that runs FUN(ARG) in a separate thread. This 493 function exists for two reasons: a) to not require FUN to be 494 declared WINAPI/__stdcall[1], and b) to retrieve Winsock errors, 495 which are per-thread. The latter is useful when FUN calls Winsock 496 functions, which is how run_with_timeout is used in Wget. 497 498 [1] MSVC can use __fastcall globally (cl /Gr) and on Watcom this is 499 the default (wcc386 -3r). */ 500 501static DWORD WINAPI 502thread_helper (void *arg) 503{ 504 struct thread_data *td = (struct thread_data *) arg; 505 506 /* Initialize Winsock error to what it was in the parent. That way 507 the subsequent call to WSAGetLastError will return the same value 508 if td->fun doesn't change Winsock error state. */ 509 WSASetLastError (td->ws_error); 510 511 td->fun (td->arg); 512 513 /* Return Winsock error to the caller, in case FUN ran Winsock 514 code. */ 515 td->ws_error = WSAGetLastError (); 516 return 0; 517} 518 519/* Call FUN(ARG), but don't allow it to run for more than TIMEOUT 520 seconds. Returns true if the function was interrupted with a 521 timeout, false otherwise. 522 523 This works by running FUN in a separate thread and terminating the 524 thread if it doesn't finish in the specified time. */ 525 526bool 527run_with_timeout (double seconds, void (*fun) (void *), void *arg) 528{ 529 HANDLE thread_hnd; 530 struct thread_data thread_arg; 531 DWORD thread_id; 532 bool rc; 533 534 DEBUGP (("seconds %.2f, ", seconds)); 535 536 if (seconds == 0) 537 { 538 blocking_fallback: 539 fun (arg); 540 return false; 541 } 542 543 thread_arg.fun = fun; 544 thread_arg.arg = arg; 545 thread_arg.ws_error = WSAGetLastError (); 546 thread_hnd = CreateThread (NULL, THREAD_STACK_SIZE, thread_helper, 547 &thread_arg, 0, &thread_id); 548 if (!thread_hnd) 549 { 550 DEBUGP (("CreateThread() failed; [%#lx]\n", 551 (unsigned long) GetLastError ())); 552 goto blocking_fallback; 553 } 554 555 if (WaitForSingleObject (thread_hnd, (DWORD)(1000 * seconds)) 556 == WAIT_OBJECT_0) 557 { 558 /* Propagate error state (which is per-thread) to this thread, 559 so the caller can inspect it. */ 560 WSASetLastError (thread_arg.ws_error); 561 DEBUGP (("Winsock error: %d\n", WSAGetLastError ())); 562 rc = false; 563 } 564 else 565 { 566 TerminateThread (thread_hnd, 1); 567 rc = true; 568 } 569 570 CloseHandle (thread_hnd); /* Clear-up after TerminateThread(). */ 571 thread_hnd = NULL; 572 return rc; 573} 574 575/* Wget expects network calls such as connect, recv, send, etc., to set 576 errno on failure. To achieve that, Winsock calls are wrapped with code 577 that, in case of error, sets errno to the value of WSAGetLastError(). 578 In addition, we provide a wrapper around strerror, which recognizes 579 Winsock errors and prints the appropriate error message. */ 580 581/* Define a macro that creates a function definition that wraps FUN into 582 a function that sets errno the way the rest of the code expects. */ 583 584#define WRAP(fun, decl, call) int wrapped_##fun decl { \ 585 int retval = fun call; \ 586 if (retval < 0) \ 587 errno = WSAGetLastError (); \ 588 return retval; \ 589} 590 591WRAP (socket, (int domain, int type, int protocol), (domain, type, protocol)) 592WRAP (bind, (int s, struct sockaddr *a, int alen), (s, a, alen)) 593WRAP (connect, (int s, const struct sockaddr *a, int alen), (s, a, alen)) 594WRAP (listen, (int s, int backlog), (s, backlog)) 595WRAP (accept, (int s, struct sockaddr *a, int *alen), (s, a, alen)) 596WRAP (recv, (int s, void *buf, int len, int flags), (s, buf, len, flags)) 597WRAP (send, (int s, const void *buf, int len, int flags), (s, buf, len, flags)) 598WRAP (select, (int n, fd_set *r, fd_set *w, fd_set *e, const struct timeval *tm), 599 (n, r, w, e, tm)) 600WRAP (getsockname, (int s, struct sockaddr *n, int *nlen), (s, n, nlen)) 601WRAP (getpeername, (int s, struct sockaddr *n, int *nlen), (s, n, nlen)) 602WRAP (setsockopt, (int s, int level, int opt, const void *val, int len), 603 (s, level, opt, val, len)) 604WRAP (closesocket, (int s), (s)) 605 606/* Return the text of the error message for Winsock error WSERR. */ 607 608static const char * 609get_winsock_error (int wserr) 610{ 611 switch (wserr) { 612 case WSAEINTR: return "Interrupted system call"; 613 case WSAEBADF: return "Bad file number"; 614 case WSAEACCES: return "Permission denied"; 615 case WSAEFAULT: return "Bad address"; 616 case WSAEINVAL: return "Invalid argument"; 617 case WSAEMFILE: return "Too many open files"; 618 case WSAEWOULDBLOCK: return "Resource temporarily unavailable"; 619 case WSAEINPROGRESS: return "Operation now in progress"; 620 case WSAEALREADY: return "Operation already in progress"; 621 case WSAENOTSOCK: return "Socket operation on nonsocket"; 622 case WSAEDESTADDRREQ: return "Destination address required"; 623 case WSAEMSGSIZE: return "Message too long"; 624 case WSAEPROTOTYPE: return "Protocol wrong type for socket"; 625 case WSAENOPROTOOPT: return "Bad protocol option"; 626 case WSAEPROTONOSUPPORT: return "Protocol not supported"; 627 case WSAESOCKTNOSUPPORT: return "Socket type not supported"; 628 case WSAEOPNOTSUPP: return "Operation not supported"; 629 case WSAEPFNOSUPPORT: return "Protocol family not supported"; 630 case WSAEAFNOSUPPORT: return "Address family not supported by protocol family"; 631 case WSAEADDRINUSE: return "Address already in use"; 632 case WSAEADDRNOTAVAIL: return "Cannot assign requested address"; 633 case WSAENETDOWN: return "Network is down"; 634 case WSAENETUNREACH: return "Network is unreachable"; 635 case WSAENETRESET: return "Network dropped connection on reset"; 636 case WSAECONNABORTED: return "Software caused connection abort"; 637 case WSAECONNRESET: return "Connection reset by peer"; 638 case WSAENOBUFS: return "No buffer space available"; 639 case WSAEISCONN: return "Socket is already connected"; 640 case WSAENOTCONN: return "Socket is not connected"; 641 case WSAESHUTDOWN: return "Cannot send after socket shutdown"; 642 case WSAETOOMANYREFS: return "Too many references"; 643 case WSAETIMEDOUT: return "Connection timed out"; 644 case WSAECONNREFUSED: return "Connection refused"; 645 case WSAELOOP: return "Too many levels of symbolic links"; 646 case WSAENAMETOOLONG: return "File name too long"; 647 case WSAEHOSTDOWN: return "Host is down"; 648 case WSAEHOSTUNREACH: return "No route to host"; 649 case WSAENOTEMPTY: return "Not empty"; 650 case WSAEPROCLIM: return "Too many processes"; 651 case WSAEUSERS: return "Too many users"; 652 case WSAEDQUOT: return "Bad quota"; 653 case WSAESTALE: return "Something is stale"; 654 case WSAEREMOTE: return "Remote error"; 655 case WSAEDISCON: return "Disconnected"; 656 657 /* Extended Winsock errors */ 658 case WSASYSNOTREADY: return "Winsock library is not ready"; 659 case WSANOTINITIALISED: return "Winsock library not initalised"; 660 case WSAVERNOTSUPPORTED: return "Winsock version not supported"; 661 662 case WSAHOST_NOT_FOUND: return "Host not found"; 663 case WSATRY_AGAIN: return "Host not found, try again"; 664 case WSANO_RECOVERY: return "Unrecoverable error in call to nameserver"; 665 case WSANO_DATA: return "No data record of requested type"; 666 667 default: 668 return NULL; 669 } 670} 671 672/* Return the error message corresponding to ERR. This is different 673 from Windows libc strerror() in that it handles Winsock errors 674 correctly. */ 675 676const char * 677windows_strerror (int err) 678{ 679 const char *p; 680 if (err >= 0 && err < sys_nerr) 681 return strerror (err); 682 else if ((p = get_winsock_error (err)) != NULL) 683 return p; 684 else 685 { 686 static char buf[32]; 687 snprintf (buf, sizeof (buf), "Unknown error %d (%#x)", err, err); 688 return buf; 689 } 690} 691 692#ifdef ENABLE_IPV6 693/* An inet_ntop implementation that uses WSAAddressToString. 694 Prototype complies with POSIX 1003.1-2004. This is only used under 695 IPv6 because Wget prints IPv4 addresses using inet_ntoa. */ 696 697const char * 698inet_ntop (int af, const void *src, char *dst, socklen_t cnt) 699{ 700 /* struct sockaddr can't accomodate struct sockaddr_in6. */ 701 union { 702 struct sockaddr_in6 sin6; 703 struct sockaddr_in sin; 704 } sa; 705 DWORD dstlen = cnt; 706 size_t srcsize; 707 708 xzero (sa); 709 switch (af) 710 { 711 case AF_INET: 712 sa.sin.sin_family = AF_INET; 713 sa.sin.sin_addr = *(struct in_addr *) src; 714 srcsize = sizeof (sa.sin); 715 break; 716 case AF_INET6: 717 sa.sin6.sin6_family = AF_INET6; 718 sa.sin6.sin6_addr = *(struct in6_addr *) src; 719 srcsize = sizeof (sa.sin6); 720 break; 721 default: 722 abort (); 723 } 724 725 if (WSAAddressToString ((struct sockaddr *) &sa, srcsize, NULL, dst, &dstlen) != 0) 726 { 727 errno = WSAGetLastError(); 728 return NULL; 729 } 730 return (const char *) dst; 731} 732#endif 733