ares_event_configchg.c (19584B)
1 /* MIT License 2 * 3 * Copyright (c) 2024 Brad House 4 * 5 * Permission is hereby granted, free of charge, to any person obtaining a copy 6 * of this software and associated documentation files (the "Software"), to deal 7 * in the Software without restriction, including without limitation the rights 8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 * copies of the Software, and to permit persons to whom the Software is 10 * furnished to do so, subject to the following conditions: 11 * 12 * The above copyright notice and this permission notice (including the next 13 * paragraph) shall be included in all copies or substantial portions of the 14 * Software. 15 * 16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 * SOFTWARE. 23 * 24 * SPDX-License-Identifier: MIT 25 */ 26 #include "ares_private.h" 27 #include "ares_event.h" 28 29 #if defined(__ANDROID__) && defined(CARES_THREADS) 30 31 ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg, 32 ares_event_thread_t *e) 33 { 34 (void)configchg; 35 (void)e; 36 /* No ability */ 37 return ARES_ENOTIMP; 38 } 39 40 void ares_event_configchg_destroy(ares_event_configchg_t *configchg) 41 { 42 /* No-op */ 43 (void)configchg; 44 } 45 46 #elif defined(__linux__) && defined(CARES_THREADS) 47 48 # include <sys/inotify.h> 49 50 struct ares_event_configchg { 51 int inotify_fd; 52 ares_event_thread_t *e; 53 }; 54 55 void ares_event_configchg_destroy(ares_event_configchg_t *configchg) 56 { 57 if (configchg == NULL) { 58 return; /* LCOV_EXCL_LINE: DefensiveCoding */ 59 } 60 61 /* Tell event system to stop monitoring for changes. This will cause the 62 * cleanup to be called */ 63 ares_event_update(NULL, configchg->e, ARES_EVENT_FLAG_NONE, NULL, 64 configchg->inotify_fd, NULL, NULL, NULL); 65 } 66 67 static void ares_event_configchg_free(void *data) 68 { 69 ares_event_configchg_t *configchg = data; 70 if (configchg == NULL) { 71 return; /* LCOV_EXCL_LINE: DefensiveCoding */ 72 } 73 74 if (configchg->inotify_fd >= 0) { 75 close(configchg->inotify_fd); 76 configchg->inotify_fd = -1; 77 } 78 79 ares_free(configchg); 80 } 81 82 static void ares_event_configchg_cb(ares_event_thread_t *e, ares_socket_t fd, 83 void *data, ares_event_flags_t flags) 84 { 85 const ares_event_configchg_t *configchg = data; 86 87 /* Some systems cannot read integer variables if they are not 88 * properly aligned. On other systems, incorrect alignment may 89 * decrease performance. Hence, the buffer used for reading from 90 * the inotify file descriptor should have the same alignment as 91 * struct inotify_event. */ 92 unsigned char buf[4096] 93 __attribute__((aligned(__alignof__(struct inotify_event)))); 94 const struct inotify_event *event; 95 ssize_t len; 96 ares_bool_t triggered = ARES_FALSE; 97 98 (void)fd; 99 (void)flags; 100 101 while (1) { 102 const unsigned char *ptr; 103 104 len = read(configchg->inotify_fd, buf, sizeof(buf)); 105 if (len <= 0) { 106 break; 107 } 108 109 /* Loop over all events in the buffer. Says kernel will check the buffer 110 * size provided, so I assume it won't ever return partial events. */ 111 for (ptr = buf; ptr < buf + len; 112 ptr += sizeof(struct inotify_event) + event->len) { 113 event = (const struct inotify_event *)((const void *)ptr); 114 115 if (event->len == 0 || ares_strlen(event->name) == 0) { 116 continue; 117 } 118 119 if (ares_strcaseeq(event->name, "resolv.conf") || 120 ares_strcaseeq(event->name, "nsswitch.conf")) { 121 triggered = ARES_TRUE; 122 } 123 } 124 } 125 126 /* Only process after all events are read. No need to process more often as 127 * we don't want to reload the config back to back */ 128 if (triggered) { 129 ares_reinit(e->channel); 130 } 131 } 132 133 ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg, 134 ares_event_thread_t *e) 135 { 136 ares_status_t status = ARES_SUCCESS; 137 ares_event_configchg_t *c; 138 139 (void)e; 140 141 /* Not used by this implementation */ 142 *configchg = NULL; 143 144 c = ares_malloc_zero(sizeof(*c)); 145 if (c == NULL) { 146 return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ 147 } 148 149 c->e = e; 150 c->inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); 151 if (c->inotify_fd == -1) { 152 status = ARES_ESERVFAIL; /* LCOV_EXCL_LINE: UntestablePath */ 153 goto done; /* LCOV_EXCL_LINE: UntestablePath */ 154 } 155 156 /* We need to monitor /etc/resolv.conf, /etc/nsswitch.conf */ 157 if (inotify_add_watch(c->inotify_fd, "/etc", 158 IN_CREATE | IN_MODIFY | IN_MOVED_TO | IN_ONLYDIR) == 159 -1) { 160 status = ARES_ESERVFAIL; /* LCOV_EXCL_LINE: UntestablePath */ 161 goto done; /* LCOV_EXCL_LINE: UntestablePath */ 162 } 163 164 status = 165 ares_event_update(NULL, e, ARES_EVENT_FLAG_READ, ares_event_configchg_cb, 166 c->inotify_fd, c, ares_event_configchg_free, NULL); 167 168 done: 169 if (status != ARES_SUCCESS) { 170 ares_event_configchg_free(c); 171 } else { 172 *configchg = c; 173 } 174 return status; 175 } 176 177 #elif defined(USE_WINSOCK) && defined(CARES_THREADS) 178 179 # include <winsock2.h> 180 # include <iphlpapi.h> 181 # include <stdio.h> 182 # include <windows.h> 183 184 struct ares_event_configchg { 185 HANDLE ifchg_hnd; 186 HKEY regip4; 187 HANDLE regip4_event; 188 HANDLE regip4_wait; 189 HKEY regip6; 190 HANDLE regip6_event; 191 HANDLE regip6_wait; 192 ares_event_thread_t *e; 193 }; 194 195 void ares_event_configchg_destroy(ares_event_configchg_t *configchg) 196 { 197 if (configchg == NULL) { 198 return; 199 } 200 201 # ifdef HAVE_NOTIFYIPINTERFACECHANGE 202 if (configchg->ifchg_hnd != NULL) { 203 CancelMibChangeNotify2(configchg->ifchg_hnd); 204 configchg->ifchg_hnd = NULL; 205 } 206 # endif 207 208 # ifdef HAVE_REGISTERWAITFORSINGLEOBJECT 209 if (configchg->regip4_wait != NULL) { 210 UnregisterWait(configchg->regip4_wait); 211 configchg->regip4_wait = NULL; 212 } 213 214 if (configchg->regip6_wait != NULL) { 215 UnregisterWait(configchg->regip6_wait); 216 configchg->regip6_wait = NULL; 217 } 218 219 if (configchg->regip4 != NULL) { 220 RegCloseKey(configchg->regip4); 221 configchg->regip4 = NULL; 222 } 223 224 if (configchg->regip6 != NULL) { 225 RegCloseKey(configchg->regip6); 226 configchg->regip6 = NULL; 227 } 228 229 if (configchg->regip4_event != NULL) { 230 CloseHandle(configchg->regip4_event); 231 configchg->regip4_event = NULL; 232 } 233 234 if (configchg->regip6_event != NULL) { 235 CloseHandle(configchg->regip6_event); 236 configchg->regip6_event = NULL; 237 } 238 # endif 239 240 ares_free(configchg); 241 } 242 243 244 # ifdef HAVE_NOTIFYIPINTERFACECHANGE 245 static void NETIOAPI_API_ 246 ares_event_configchg_ip_cb(PVOID CallerContext, PMIB_IPINTERFACE_ROW Row, 247 MIB_NOTIFICATION_TYPE NotificationType) 248 { 249 ares_event_configchg_t *configchg = CallerContext; 250 (void)Row; 251 (void)NotificationType; 252 ares_reinit(configchg->e->channel); 253 } 254 # endif 255 256 static ares_bool_t 257 ares_event_configchg_regnotify(ares_event_configchg_t *configchg) 258 { 259 # ifdef HAVE_REGISTERWAITFORSINGLEOBJECT 260 # if defined(__WATCOMC__) && !defined(REG_NOTIFY_THREAD_AGNOSTIC) 261 # define REG_NOTIFY_THREAD_AGNOSTIC 0x10000000L 262 # endif 263 DWORD flags = REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET | 264 REG_NOTIFY_THREAD_AGNOSTIC; 265 266 if (RegNotifyChangeKeyValue(configchg->regip4, TRUE, flags, 267 configchg->regip4_event, TRUE) != ERROR_SUCCESS) { 268 return ARES_FALSE; 269 } 270 271 if (RegNotifyChangeKeyValue(configchg->regip6, TRUE, flags, 272 configchg->regip6_event, TRUE) != ERROR_SUCCESS) { 273 return ARES_FALSE; 274 } 275 # else 276 (void)configchg; 277 # endif 278 return ARES_TRUE; 279 } 280 281 static VOID CALLBACK ares_event_configchg_reg_cb(PVOID lpParameter, 282 BOOLEAN TimerOrWaitFired) 283 { 284 ares_event_configchg_t *configchg = lpParameter; 285 (void)TimerOrWaitFired; 286 287 ares_reinit(configchg->e->channel); 288 289 /* Re-arm, as its single-shot. However, we don't know which one needs to 290 * be re-armed, so we just do both */ 291 ares_event_configchg_regnotify(configchg); 292 } 293 294 ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg, 295 ares_event_thread_t *e) 296 { 297 ares_status_t status = ARES_SUCCESS; 298 ares_event_configchg_t *c = NULL; 299 300 c = ares_malloc_zero(sizeof(**configchg)); 301 if (c == NULL) { 302 return ARES_ENOMEM; 303 } 304 305 c->e = e; 306 307 # ifdef HAVE_NOTIFYIPINTERFACECHANGE 308 /* NOTE: If a user goes into the control panel and changes the network 309 * adapter DNS addresses manually, this will NOT trigger a notification. 310 * We've also tried listening on NotifyUnicastIpAddressChange(), but 311 * that didn't get triggered either. 312 */ 313 if (NotifyIpInterfaceChange(AF_UNSPEC, ares_event_configchg_ip_cb, c, FALSE, 314 &c->ifchg_hnd) != NO_ERROR) { 315 status = ARES_ESERVFAIL; 316 goto done; 317 } 318 # endif 319 320 # ifdef HAVE_REGISTERWAITFORSINGLEOBJECT 321 /* Monitor HKLM\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces 322 * and HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces 323 * for changes via RegNotifyChangeKeyValue() */ 324 if (RegOpenKeyExW( 325 HKEY_LOCAL_MACHINE, 326 L"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces", 327 0, KEY_NOTIFY, &c->regip4) != ERROR_SUCCESS) { 328 status = ARES_ESERVFAIL; 329 goto done; 330 } 331 332 if (RegOpenKeyExW( 333 HKEY_LOCAL_MACHINE, 334 L"SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters\\Interfaces", 335 0, KEY_NOTIFY, &c->regip6) != ERROR_SUCCESS) { 336 status = ARES_ESERVFAIL; 337 goto done; 338 } 339 340 c->regip4_event = CreateEvent(NULL, TRUE, FALSE, NULL); 341 if (c->regip4_event == NULL) { 342 status = ARES_ESERVFAIL; 343 goto done; 344 } 345 346 c->regip6_event = CreateEvent(NULL, TRUE, FALSE, NULL); 347 if (c->regip6_event == NULL) { 348 status = ARES_ESERVFAIL; 349 goto done; 350 } 351 352 if (!RegisterWaitForSingleObject(&c->regip4_wait, c->regip4_event, 353 ares_event_configchg_reg_cb, c, INFINITE, 354 WT_EXECUTEDEFAULT)) { 355 status = ARES_ESERVFAIL; 356 goto done; 357 } 358 359 if (!RegisterWaitForSingleObject(&c->regip6_wait, c->regip6_event, 360 ares_event_configchg_reg_cb, c, INFINITE, 361 WT_EXECUTEDEFAULT)) { 362 status = ARES_ESERVFAIL; 363 goto done; 364 } 365 # endif 366 367 if (!ares_event_configchg_regnotify(c)) { 368 status = ARES_ESERVFAIL; 369 goto done; 370 } 371 372 done: 373 if (status != ARES_SUCCESS) { 374 ares_event_configchg_destroy(c); 375 } else { 376 *configchg = c; 377 } 378 379 return status; 380 } 381 382 #elif defined(__APPLE__) && defined(CARES_THREADS) 383 384 # include <sys/types.h> 385 # include <unistd.h> 386 # include <notify.h> 387 # include <dlfcn.h> 388 # include <fcntl.h> 389 390 struct ares_event_configchg { 391 int fd; 392 int token; 393 }; 394 395 void ares_event_configchg_destroy(ares_event_configchg_t *configchg) 396 { 397 (void)configchg; 398 399 /* Cleanup happens automatically */ 400 } 401 402 static void ares_event_configchg_free(void *data) 403 { 404 ares_event_configchg_t *configchg = data; 405 if (configchg == NULL) { 406 return; 407 } 408 409 if (configchg->fd >= 0) { 410 notify_cancel(configchg->token); 411 /* automatically closes fd */ 412 configchg->fd = -1; 413 } 414 415 ares_free(configchg); 416 } 417 418 static void ares_event_configchg_cb(ares_event_thread_t *e, ares_socket_t fd, 419 void *data, ares_event_flags_t flags) 420 { 421 ares_event_configchg_t *configchg = data; 422 ares_bool_t triggered = ARES_FALSE; 423 424 (void)fd; 425 (void)flags; 426 427 while (1) { 428 int t = 0; 429 ssize_t len; 430 431 len = read(configchg->fd, &t, sizeof(t)); 432 433 if (len < (ssize_t)sizeof(t)) { 434 break; 435 } 436 437 /* Token is read in network byte order (yeah, docs don't mention this) */ 438 t = (int)ntohl(t); 439 440 if (t != configchg->token) { 441 continue; 442 } 443 444 triggered = ARES_TRUE; 445 } 446 447 /* Only process after all events are read. No need to process more often as 448 * we don't want to reload the config back to back */ 449 if (triggered) { 450 ares_reinit(e->channel); 451 } 452 } 453 454 ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg, 455 ares_event_thread_t *e) 456 { 457 ares_status_t status = ARES_SUCCESS; 458 void *handle = NULL; 459 const char *(*pdns_configuration_notify_key)(void) = NULL; 460 const char *notify_key = NULL; 461 int flags; 462 size_t i; 463 const char *searchlibs[] = { 464 "/usr/lib/libSystem.dylib", 465 "/System/Library/Frameworks/SystemConfiguration.framework/" 466 "SystemConfiguration", 467 NULL 468 }; 469 470 *configchg = ares_malloc_zero(sizeof(**configchg)); 471 if (*configchg == NULL) { 472 return ARES_ENOMEM; 473 } 474 475 /* Load symbol as it isn't normally public */ 476 for (i = 0; searchlibs[i] != NULL; i++) { 477 handle = dlopen(searchlibs[i], RTLD_LAZY); 478 if (handle == NULL) { 479 /* Fail, loop! */ 480 continue; 481 } 482 483 pdns_configuration_notify_key = 484 (const char *(*)(void))dlsym(handle, "dns_configuration_notify_key"); 485 if (pdns_configuration_notify_key != NULL) { 486 break; 487 } 488 489 /* Fail, loop! */ 490 dlclose(handle); 491 handle = NULL; 492 } 493 494 if (pdns_configuration_notify_key == NULL) { 495 status = ARES_ESERVFAIL; 496 goto done; 497 } 498 499 notify_key = pdns_configuration_notify_key(); 500 if (notify_key == NULL) { 501 status = ARES_ESERVFAIL; 502 goto done; 503 } 504 505 if (notify_register_file_descriptor(notify_key, &(*configchg)->fd, 0, 506 &(*configchg)->token) != 507 NOTIFY_STATUS_OK) { 508 status = ARES_ESERVFAIL; 509 goto done; 510 } 511 512 /* Set file descriptor to non-blocking */ 513 flags = fcntl((*configchg)->fd, F_GETFL, 0); 514 fcntl((*configchg)->fd, F_SETFL, flags | O_NONBLOCK); 515 516 /* Register file descriptor with event subsystem */ 517 status = ares_event_update(NULL, e, ARES_EVENT_FLAG_READ, 518 ares_event_configchg_cb, (*configchg)->fd, 519 *configchg, ares_event_configchg_free, NULL); 520 521 done: 522 if (status != ARES_SUCCESS) { 523 ares_event_configchg_free(*configchg); 524 *configchg = NULL; 525 } 526 527 if (handle) { 528 dlclose(handle); 529 } 530 531 return status; 532 } 533 534 #elif defined(HAVE_STAT) && !defined(_WIN32) && defined(CARES_THREADS) 535 # ifdef HAVE_SYS_TYPES_H 536 # include <sys/types.h> 537 # endif 538 # ifdef HAVE_SYS_STAT_H 539 # include <sys/stat.h> 540 # endif 541 542 typedef struct { 543 size_t size; 544 time_t mtime; 545 } fileinfo_t; 546 547 struct ares_event_configchg { 548 ares_bool_t isup; 549 ares_thread_t *thread; 550 ares_htable_strvp_t *filestat; 551 ares_thread_mutex_t *lock; 552 ares_thread_cond_t *wake; 553 const char *resolvconf_path; 554 ares_event_thread_t *e; 555 }; 556 557 static ares_status_t config_change_check(ares_htable_strvp_t *filestat, 558 const char *resolvconf_path) 559 { 560 size_t i; 561 const char *configfiles[16]; 562 ares_bool_t changed = ARES_FALSE; 563 size_t cnt = 0; 564 565 memset(configfiles, 0, sizeof(configfiles)); 566 567 configfiles[cnt++] = resolvconf_path; 568 configfiles[cnt++] = "/etc/nsswitch.conf"; 569 #ifdef _AIX 570 configfiles[cnt++] = "/etc/netsvc.conf"; 571 #endif 572 #ifdef __osf /* Tru64 */ 573 configfiles[cnt++] = "/etc/svc.conf"; 574 #endif 575 #ifdef __QNX__ 576 configfiles[cnt++] = "/etc/net.cfg"; 577 #endif 578 configfiles[cnt++] = NULL; 579 580 for (i = 0; configfiles[i] != NULL; i++) { 581 fileinfo_t *fi = ares_htable_strvp_get_direct(filestat, configfiles[i]); 582 struct stat st; 583 584 if (stat(configfiles[i], &st) == 0) { 585 if (fi == NULL) { 586 fi = ares_malloc_zero(sizeof(*fi)); 587 if (fi == NULL) { 588 return ARES_ENOMEM; 589 } 590 if (!ares_htable_strvp_insert(filestat, configfiles[i], fi)) { 591 ares_free(fi); 592 return ARES_ENOMEM; 593 } 594 } 595 if (fi->size != (size_t)st.st_size || fi->mtime != (time_t)st.st_mtime) { 596 changed = ARES_TRUE; 597 } 598 fi->size = (size_t)st.st_size; 599 fi->mtime = (time_t)st.st_mtime; 600 } else if (fi != NULL) { 601 /* File no longer exists, remove */ 602 ares_htable_strvp_remove(filestat, configfiles[i]); 603 changed = ARES_TRUE; 604 } 605 } 606 607 if (changed) { 608 return ARES_SUCCESS; 609 } 610 return ARES_ENOTFOUND; 611 } 612 613 static void *ares_event_configchg_thread(void *arg) 614 { 615 ares_event_configchg_t *c = arg; 616 617 ares_thread_mutex_lock(c->lock); 618 while (c->isup) { 619 ares_status_t status; 620 621 if (ares_thread_cond_timedwait(c->wake, c->lock, 30000) != ARES_ETIMEOUT) { 622 continue; 623 } 624 625 /* make sure status didn't change even though we got a timeout */ 626 if (!c->isup) { 627 break; 628 } 629 630 status = config_change_check(c->filestat, c->resolvconf_path); 631 if (status == ARES_SUCCESS) { 632 ares_reinit(c->e->channel); 633 } 634 } 635 636 ares_thread_mutex_unlock(c->lock); 637 return NULL; 638 } 639 640 ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg, 641 ares_event_thread_t *e) 642 { 643 ares_status_t status = ARES_SUCCESS; 644 ares_event_configchg_t *c = NULL; 645 646 *configchg = NULL; 647 648 c = ares_malloc_zero(sizeof(*c)); 649 if (c == NULL) { 650 status = ARES_ENOMEM; 651 goto done; 652 } 653 654 c->e = e; 655 656 c->filestat = ares_htable_strvp_create(ares_free); 657 if (c->filestat == NULL) { 658 status = ARES_ENOMEM; 659 goto done; 660 } 661 662 c->wake = ares_thread_cond_create(); 663 if (c->wake == NULL) { 664 status = ARES_ENOMEM; 665 goto done; 666 } 667 668 c->lock = ares_thread_mutex_create(); 669 if (c->lock == NULL) { 670 status = ARES_ENOMEM; 671 goto done; 672 } 673 674 c->resolvconf_path = c->e->channel->resolvconf_path; 675 if (c->resolvconf_path == NULL) { 676 c->resolvconf_path = PATH_RESOLV_CONF; 677 } 678 679 status = config_change_check(c->filestat, c->resolvconf_path); 680 if (status == ARES_ENOMEM) { 681 goto done; 682 } 683 684 c->isup = ARES_TRUE; 685 status = ares_thread_create(&c->thread, ares_event_configchg_thread, c); 686 687 done: 688 if (status != ARES_SUCCESS) { 689 ares_event_configchg_destroy(c); 690 } else { 691 *configchg = c; 692 } 693 return status; 694 } 695 696 void ares_event_configchg_destroy(ares_event_configchg_t *configchg) 697 { 698 if (configchg == NULL) { 699 return; 700 } 701 702 if (configchg->lock) { 703 ares_thread_mutex_lock(configchg->lock); 704 } 705 706 configchg->isup = ARES_FALSE; 707 if (configchg->wake) { 708 ares_thread_cond_signal(configchg->wake); 709 } 710 711 if (configchg->lock) { 712 ares_thread_mutex_unlock(configchg->lock); 713 } 714 715 if (configchg->thread) { 716 void *rv = NULL; 717 ares_thread_join(configchg->thread, &rv); 718 } 719 720 ares_thread_mutex_destroy(configchg->lock); 721 ares_thread_cond_destroy(configchg->wake); 722 ares_htable_strvp_destroy(configchg->filestat); 723 ares_free(configchg); 724 } 725 726 #else 727 728 ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg, 729 ares_event_thread_t *e) 730 { 731 (void)configchg; 732 (void)e; 733 /* No ability */ 734 return ARES_ENOTIMP; 735 } 736 737 void ares_event_configchg_destroy(ares_event_configchg_t *configchg) 738 { 739 /* No-op */ 740 (void)configchg; 741 } 742 743 #endif