1 /* 2 * winrc/win_svc.c - windows services API implementation for unbound 3 * 4 * Copyright (c) 2009, NLnet Labs. All rights reserved. 5 * 6 * This software is open source. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 12 * Redistributions of source code must retain the above copyright notice, 13 * this list of conditions and the following disclaimer. 14 * 15 * Redistributions in binary form must reproduce the above copyright notice, 16 * this list of conditions and the following disclaimer in the documentation 17 * and/or other materials provided with the distribution. 18 * 19 * Neither the name of the NLNET LABS nor the names of its contributors may 20 * be used to endorse or promote products derived from this software without 21 * specific prior written permission. 22 * 23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 29 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 */ 35 36 /** 37 * \file 38 * 39 * This file contains functions to integrate with the windows services API. 40 * This means it handles the commandline switches to install and remove 41 * the service (via CreateService and DeleteService), it handles 42 * the ServiceMain() main service entry point when started as a service, 43 * and it handles the Handler[_ex]() to process requests to the service 44 * (such as start and stop and status). 45 */ 46 #include "config.h" 47 #include "winrc/win_svc.h" 48 #include "winrc/w_inst.h" 49 #include "daemon/daemon.h" 50 #include "daemon/worker.h" 51 #include "daemon/remote.h" 52 #include "util/config_file.h" 53 #include "util/netevent.h" 54 #include "util/ub_event.h" 55 #include "util/net_help.h" 56 57 /** global service status */ 58 static SERVICE_STATUS service_status; 59 /** global service status handle */ 60 static SERVICE_STATUS_HANDLE service_status_handle; 61 /** global service stop event */ 62 static WSAEVENT service_stop_event = NULL; 63 /** event struct for stop callbacks */ 64 static struct ub_event* service_stop_ev = NULL; 65 /** if stop even means shutdown or restart */ 66 static int service_stop_shutdown = 0; 67 /** config file to open. global communication to service_main() */ 68 static char* service_cfgfile = CONFIGFILE; 69 /** commandline verbosity. global communication to service_main() */ 70 static int service_cmdline_verbose = 0; 71 /** the cron callback */ 72 static struct comm_timer* service_cron = NULL; 73 /** the cron thread */ 74 static ub_thread_type cron_thread = NULL; 75 /** if cron has already done its quick check */ 76 static int cron_was_quick = 0; 77 78 /** 79 * Report current service status to service control manager 80 * @param state: current state 81 * @param exitcode: error code (when stopped) 82 * @param wait: pending operation estimated time in milliseconds. 83 */ 84 static void report_status(DWORD state, DWORD exitcode, DWORD wait) 85 { 86 static DWORD checkpoint = 1; 87 service_status.dwCurrentState = state; 88 service_status.dwWin32ExitCode = exitcode; 89 service_status.dwWaitHint = wait; 90 if(state == SERVICE_START_PENDING) 91 service_status.dwControlsAccepted = 0; 92 else service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP; 93 if(state == SERVICE_RUNNING || state == SERVICE_STOPPED) 94 service_status.dwCheckPoint = 0; 95 else service_status.dwCheckPoint = checkpoint++; 96 SetServiceStatus(service_status_handle, &service_status); 97 } 98 99 /** 100 * Service control handler. Called by serviceControlManager when a control 101 * code is sent to the service (with ControlService). 102 * @param ctrl: control code 103 */ 104 static void 105 hdlr(DWORD ctrl) 106 { 107 if(ctrl == SERVICE_CONTROL_STOP) { 108 report_status(SERVICE_STOP_PENDING, NO_ERROR, 0); 109 service_stop_shutdown = 1; 110 /* send signal to stop */ 111 if(!WSASetEvent(service_stop_event)) 112 log_err("Could not WSASetEvent: %s", 113 wsa_strerror(WSAGetLastError())); 114 return; 115 } else { 116 /* ctrl == SERVICE_CONTROL_INTERROGATE or whatever */ 117 /* update status */ 118 report_status(service_status.dwCurrentState, NO_ERROR, 0); 119 } 120 } 121 122 /** 123 * report event to system event log 124 * For use during startup and shutdown. 125 * @param str: the error 126 */ 127 static void 128 reportev(const char* str) 129 { 130 char b[256]; 131 char e[256]; 132 HANDLE* s; 133 LPCTSTR msg = b; 134 /* print quickly to keep GetLastError value */ 135 wsvc_err2str(e, sizeof(e), str, GetLastError()); 136 snprintf(b, sizeof(b), "%s: %s", SERVICE_NAME, e); 137 s = RegisterEventSource(NULL, SERVICE_NAME); 138 if(!s) return; 139 ReportEvent(s, /* event log */ 140 EVENTLOG_ERROR_TYPE, /* event type */ 141 0, /* event category */ 142 MSG_GENERIC_ERR, /* event ID (from gen_msg.mc) */ 143 NULL, /* user security context */ 144 1, /* numstrings */ 145 0, /* binary size */ 146 &msg, /* strings */ 147 NULL); /* binary data */ 148 DeregisterEventSource(s); 149 } 150 151 /** 152 * Obtain registry string (if it exists). 153 * @param key: key string 154 * @param name: name of value to fetch. 155 * @return malloced string with the result or NULL if it did not 156 * exist on an error (logged) was encountered. 157 */ 158 static char* 159 lookup_reg_str(const char* key, const char* name) 160 { 161 HKEY hk = NULL; 162 DWORD type = 0; 163 BYTE buf[1024]; 164 DWORD len = (DWORD)sizeof(buf); 165 LONG ret; 166 char* result = NULL; 167 ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key, 0, KEY_READ, &hk); 168 if(ret == ERROR_FILE_NOT_FOUND) 169 return NULL; /* key does not exist */ 170 else if(ret != ERROR_SUCCESS) { 171 reportev("RegOpenKeyEx failed"); 172 return NULL; 173 } 174 ret = RegQueryValueEx(hk, (LPCTSTR)name, 0, &type, buf, &len); 175 if(RegCloseKey(hk)) 176 reportev("RegCloseKey"); 177 if(ret == ERROR_FILE_NOT_FOUND) 178 return NULL; /* name does not exist */ 179 else if(ret != ERROR_SUCCESS) { 180 reportev("RegQueryValueEx failed"); 181 return NULL; 182 } 183 if(type == REG_SZ || type == REG_MULTI_SZ || type == REG_EXPAND_SZ) { 184 buf[sizeof(buf)-1] = 0; 185 buf[sizeof(buf)-2] = 0; /* for multi_sz */ 186 result = strdup((char*)buf); 187 if(!result) reportev("out of memory"); 188 } 189 return result; 190 } 191 192 /** 193 * Obtain registry integer (if it exists). 194 * @param key: key string 195 * @param name: name of value to fetch. 196 * @return integer value (if it exists), or 0 on error. 197 */ 198 static int 199 lookup_reg_int(const char* key, const char* name) 200 { 201 HKEY hk = NULL; 202 DWORD type = 0; 203 BYTE buf[1024]; 204 DWORD len = (DWORD)sizeof(buf); 205 LONG ret; 206 int result = 0; 207 ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key, 0, KEY_READ, &hk); 208 if(ret == ERROR_FILE_NOT_FOUND) 209 return 0; /* key does not exist */ 210 else if(ret != ERROR_SUCCESS) { 211 reportev("RegOpenKeyEx failed"); 212 return 0; 213 } 214 ret = RegQueryValueEx(hk, (LPCTSTR)name, 0, &type, buf, &len); 215 if(RegCloseKey(hk)) 216 reportev("RegCloseKey"); 217 if(ret == ERROR_FILE_NOT_FOUND) 218 return 0; /* name does not exist */ 219 else if(ret != ERROR_SUCCESS) { 220 reportev("RegQueryValueEx failed"); 221 return 0; 222 } 223 if(type == REG_SZ || type == REG_MULTI_SZ || type == REG_EXPAND_SZ) { 224 buf[sizeof(buf)-1] = 0; 225 buf[sizeof(buf)-2] = 0; /* for multi_sz */ 226 result = atoi((char*)buf); 227 } else if(type == REG_DWORD) { 228 DWORD r; 229 memmove(&r, buf, sizeof(r)); 230 result = r; 231 } 232 return result; 233 } 234 235 /** wait for unbound-anchor process to finish */ 236 static void 237 waitforubanchor(PROCESS_INFORMATION* pinfo) 238 { 239 /* we have 5 seconds scheduled for it, usually it will be very fast, 240 * with only a UDP message or two (100 msec or so), but the https 241 * connections could take some time */ 242 DWORD count = 7900; 243 DWORD ret = WAIT_TIMEOUT; 244 /* decrease timer every 1/10 second, we are still starting up */ 245 while(ret == WAIT_TIMEOUT) { 246 ret = WaitForSingleObject(pinfo->hProcess, 100); 247 if(count > 4000) count -= 100; 248 else count--; /* go slow, it is taking long */ 249 if(count > 3000) 250 report_status(SERVICE_START_PENDING, NO_ERROR, count); 251 } 252 verbose(VERB_ALGO, "unbound-anchor done"); 253 if(ret != WAIT_OBJECT_0) { 254 return; /* did not end successfully */ 255 } 256 if(!GetExitCodeProcess(pinfo->hProcess, &ret)) { 257 log_err("GetExitCodeProcess failed"); 258 return; 259 } 260 verbose(VERB_ALGO, "unbound-anchor exit code is %d", (int)ret); 261 if(ret != 0) { 262 log_info("The root trust anchor has been updated."); 263 } 264 } 265 266 267 /** 268 * Perform root anchor update if so configured, by calling that process 269 */ 270 static void 271 call_root_update(void) 272 { 273 char* rootanchor; 274 rootanchor = lookup_reg_str("Software\\Unbound", "RootAnchor"); 275 if(rootanchor && strlen(rootanchor)>0) { 276 STARTUPINFO sinfo; 277 PROCESS_INFORMATION pinfo; 278 memset(&pinfo, 0, sizeof(pinfo)); 279 memset(&sinfo, 0, sizeof(sinfo)); 280 sinfo.cb = sizeof(sinfo); 281 verbose(VERB_ALGO, "rootanchor: %s", rootanchor); 282 report_status(SERVICE_START_PENDING, NO_ERROR, 8000); 283 if(!CreateProcess(NULL, rootanchor, NULL, NULL, 0, 284 CREATE_NO_WINDOW, NULL, NULL, &sinfo, &pinfo)) 285 log_err("CreateProcess error for unbound-anchor.exe"); 286 else { 287 waitforubanchor(&pinfo); 288 CloseHandle(pinfo.hProcess); 289 CloseHandle(pinfo.hThread); 290 } 291 } 292 free(rootanchor); 293 } 294 295 /** 296 * Init service. Keeps calling status pending to tell service control 297 * manager that this process is not hanging. 298 * @param r: restart, true on restart 299 * @param d: daemon returned here. 300 * @param c: config file returned here. 301 * @return false if failed. 302 */ 303 static int 304 service_init(int r, struct daemon** d, struct config_file** c) 305 { 306 struct config_file* cfg = NULL; 307 struct daemon* daemon = NULL; 308 309 if(!service_cfgfile) { 310 char* newf = lookup_reg_str("Software\\Unbound", "ConfigFile"); 311 if(newf) service_cfgfile = newf; 312 else service_cfgfile = strdup(CONFIGFILE); 313 if(!service_cfgfile) fatal_exit("out of memory"); 314 } 315 316 /* create daemon */ 317 if(r) daemon = *d; 318 else daemon = daemon_init(); 319 if(!daemon) return 0; 320 if(!r) report_status(SERVICE_START_PENDING, NO_ERROR, 2800); 321 322 /* read config */ 323 cfg = config_create(); 324 if(!cfg) return 0; 325 if(!config_read(cfg, service_cfgfile, daemon->chroot)) { 326 if(errno != ENOENT) { 327 log_err("error in config file"); 328 return 0; 329 } 330 log_warn("could not open config file, using defaults"); 331 } 332 if(!r) report_status(SERVICE_START_PENDING, NO_ERROR, 2600); 333 334 verbose(VERB_QUERY, "winservice - apply settings"); 335 /* apply settings and init */ 336 verbosity = cfg->verbosity + service_cmdline_verbose; 337 w_config_adjust_directory(cfg); 338 if(cfg->directory && cfg->directory[0]) { 339 char* dir = cfg->directory; 340 if(chdir(dir)) { 341 log_err("could not chdir to %s: %s", 342 dir, strerror(errno)); 343 if(errno != ENOENT) 344 return 0; 345 log_warn("could not change directory - continuing"); 346 } else 347 verbose(VERB_QUERY, "chdir to %s", dir); 348 } 349 log_init(cfg->logfile, cfg->use_syslog, cfg->chrootdir); 350 if(!r) report_status(SERVICE_START_PENDING, NO_ERROR, 2400); 351 verbose(VERB_QUERY, "winservice - apply cfg"); 352 daemon_apply_cfg(daemon, cfg); 353 354 if(!r) report_status(SERVICE_START_PENDING, NO_ERROR, 2300); 355 if(!(daemon->rc = daemon_remote_create(cfg))) { 356 log_err("could not set up remote-control"); 357 daemon_delete(daemon); 358 config_delete(cfg); 359 return 0; 360 } 361 if(cfg->ssl_service_key && cfg->ssl_service_key[0]) { 362 if(!(daemon->listen_sslctx = listen_sslctx_create( 363 cfg->ssl_service_key, cfg->ssl_service_pem, NULL))) 364 fatal_exit("could not set up listen SSL_CTX"); 365 } 366 if(!(daemon->connect_sslctx = connect_sslctx_create(NULL, NULL, 367 cfg->tls_cert_bundle, cfg->tls_win_cert))) 368 fatal_exit("could not set up connect SSL_CTX"); 369 370 /* open ports */ 371 /* keep reporting that we are busy starting */ 372 if(!r) report_status(SERVICE_START_PENDING, NO_ERROR, 2200); 373 verbose(VERB_QUERY, "winservice - open ports"); 374 if(!daemon_open_shared_ports(daemon)) return 0; 375 verbose(VERB_QUERY, "winservice - ports opened"); 376 if(!r) report_status(SERVICE_START_PENDING, NO_ERROR, 2000); 377 378 *d = daemon; 379 *c = cfg; 380 return 1; 381 } 382 383 /** 384 * Deinit the service 385 */ 386 static void 387 service_deinit(struct daemon* daemon, struct config_file* cfg) 388 { 389 daemon_cleanup(daemon); 390 config_delete(cfg); 391 daemon_delete(daemon); 392 } 393 394 #ifdef DOXYGEN 395 #define ATTR_UNUSED(x) x 396 #endif 397 /** 398 * The main function for the service. 399 * Called by the services API when starting unbound on windows in background. 400 * Arguments could have been present in the string 'path'. 401 * @param argc: nr args 402 * @param argv: arg text. 403 */ 404 static void 405 service_main(DWORD ATTR_UNUSED(argc), LPTSTR* ATTR_UNUSED(argv)) 406 { 407 struct config_file* cfg = NULL; 408 struct daemon* daemon = NULL; 409 410 service_status_handle = RegisterServiceCtrlHandler(SERVICE_NAME, 411 (LPHANDLER_FUNCTION)hdlr); 412 if(!service_status_handle) { 413 reportev("Could not RegisterServiceCtrlHandler"); 414 return; 415 } 416 417 service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS; 418 service_status.dwServiceSpecificExitCode = 0; 419 420 /* see if we have root anchor update enabled */ 421 call_root_update(); 422 423 /* we are now starting up */ 424 report_status(SERVICE_START_PENDING, NO_ERROR, 3000); 425 if(!service_init(0, &daemon, &cfg)) { 426 reportev("Could not service_init"); 427 report_status(SERVICE_STOPPED, NO_ERROR, 0); 428 return; 429 } 430 431 /* event that gets signalled when we want to quit; it 432 * should get registered in the worker-0 waiting loop. */ 433 service_stop_event = WSACreateEvent(); 434 if(service_stop_event == WSA_INVALID_EVENT) { 435 log_err("WSACreateEvent: %s", wsa_strerror(WSAGetLastError())); 436 reportev("Could not WSACreateEvent"); 437 report_status(SERVICE_STOPPED, NO_ERROR, 0); 438 return; 439 } 440 if(!WSAResetEvent(service_stop_event)) { 441 log_err("WSAResetEvent: %s", wsa_strerror(WSAGetLastError())); 442 } 443 444 /* SetServiceStatus SERVICE_RUNNING;*/ 445 report_status(SERVICE_RUNNING, NO_ERROR, 0); 446 verbose(VERB_QUERY, "winservice - init complete"); 447 448 /* daemon performs work */ 449 while(!service_stop_shutdown) { 450 daemon_fork(daemon); 451 if(!service_stop_shutdown) { 452 daemon_cleanup(daemon); 453 config_delete(cfg); cfg=NULL; 454 if(!service_init(1, &daemon, &cfg)) { 455 reportev("Could not service_init"); 456 report_status(SERVICE_STOPPED, NO_ERROR, 0); 457 return; 458 } 459 } 460 } 461 462 /* exit */ 463 verbose(VERB_ALGO, "winservice - cleanup."); 464 report_status(SERVICE_STOP_PENDING, NO_ERROR, 0); 465 if(service_stop_event) (void)WSACloseEvent(service_stop_event); 466 service_deinit(daemon, cfg); 467 free(service_cfgfile); 468 verbose(VERB_QUERY, "winservice - full stop"); 469 report_status(SERVICE_STOPPED, NO_ERROR, 0); 470 } 471 472 /** start the service */ 473 static void 474 service_start(const char* cfgfile, int v, int c) 475 { 476 SERVICE_TABLE_ENTRY myservices[2] = { 477 {SERVICE_NAME, (LPSERVICE_MAIN_FUNCTION)service_main}, 478 {NULL, NULL} }; 479 verbosity=v; 480 if(verbosity >= VERB_QUERY) { 481 /* log to file about start sequence */ 482 fclose(fopen("C:\\unbound.log", "w")); 483 log_init("C:\\unbound.log", 0, 0); 484 verbose(VERB_QUERY, "open logfile"); 485 } else log_init(0, 1, 0); /* otherwise, use Application log */ 486 if(c) { 487 service_cfgfile = strdup(cfgfile); 488 if(!service_cfgfile) fatal_exit("out of memory"); 489 } else service_cfgfile = NULL; 490 service_cmdline_verbose = v; 491 /* this call returns when service has stopped. */ 492 if(!StartServiceCtrlDispatcher(myservices)) { 493 reportev("Could not StartServiceCtrlDispatcher"); 494 } 495 } 496 497 void 498 wsvc_command_option(const char* wopt, const char* cfgfile, int v, int c) 499 { 500 if(strcmp(wopt, "install") == 0) 501 wsvc_install(stdout, NULL); 502 else if(strcmp(wopt, "remove") == 0) 503 wsvc_remove(stdout); 504 else if(strcmp(wopt, "service") == 0) 505 service_start(cfgfile, v, c); 506 else if(strcmp(wopt, "start") == 0) 507 wsvc_rc_start(stdout); 508 else if(strcmp(wopt, "stop") == 0) 509 wsvc_rc_stop(stdout); 510 else fatal_exit("unknown option: %s", wopt); 511 exit(0); 512 } 513 514 void 515 worker_win_stop_cb(int ATTR_UNUSED(fd), short ATTR_UNUSED(ev), void* arg) 516 { 517 struct worker* worker = (struct worker*)arg; 518 verbose(VERB_QUERY, "caught stop signal (wsaevent)"); 519 worker->need_to_exit = 1; 520 comm_base_exit(worker->base); 521 } 522 523 /** wait for cron process to finish */ 524 static void 525 waitforit(PROCESS_INFORMATION* pinfo) 526 { 527 DWORD ret = WaitForSingleObject(pinfo->hProcess, INFINITE); 528 verbose(VERB_ALGO, "cronaction done"); 529 if(ret != WAIT_OBJECT_0) { 530 return; /* did not end successfully */ 531 } 532 if(!GetExitCodeProcess(pinfo->hProcess, &ret)) { 533 log_err("GetExitCodeProcess failed"); 534 return; 535 } 536 verbose(VERB_ALGO, "exit code is %d", (int)ret); 537 if(ret != 1) { 538 if(!WSASetEvent(service_stop_event)) 539 log_err("Could not WSASetEvent: %s", 540 wsa_strerror(WSAGetLastError())); 541 } 542 } 543 544 /** Do the cron action and wait for result exit value */ 545 static void* 546 win_do_cron(void* ATTR_UNUSED(arg)) 547 { 548 int mynum=65; 549 char* cronaction; 550 log_thread_set(&mynum); 551 cronaction = lookup_reg_str("Software\\Unbound", "CronAction"); 552 if(cronaction && strlen(cronaction)>0) { 553 STARTUPINFO sinfo; 554 PROCESS_INFORMATION pinfo; 555 memset(&pinfo, 0, sizeof(pinfo)); 556 memset(&sinfo, 0, sizeof(sinfo)); 557 sinfo.cb = sizeof(sinfo); 558 verbose(VERB_ALGO, "cronaction: %s", cronaction); 559 if(!CreateProcess(NULL, cronaction, NULL, NULL, 0, 560 CREATE_NO_WINDOW, NULL, NULL, &sinfo, &pinfo)) 561 log_err("CreateProcess error"); 562 else { 563 waitforit(&pinfo); 564 CloseHandle(pinfo.hProcess); 565 CloseHandle(pinfo.hThread); 566 } 567 } 568 free(cronaction); 569 /* stop self */ 570 CloseHandle(cron_thread); 571 cron_thread = NULL; 572 return NULL; 573 } 574 575 /** Set the timer for cron for the next wake up */ 576 static void 577 set_cron_timer(void) 578 { 579 struct timeval tv; 580 int crontime; 581 if(cron_was_quick == 0) { 582 cron_was_quick = 1; 583 crontime = 3600; /* first update some time after boot */ 584 } else { 585 crontime = lookup_reg_int("Software\\Unbound", "CronTime"); 586 if(crontime == 0) crontime = 60*60*24; /* 24 hours */ 587 } 588 memset(&tv, 0, sizeof(tv)); 589 tv.tv_sec = (time_t)crontime; 590 comm_timer_set(service_cron, &tv); 591 } 592 593 void 594 wsvc_cron_cb(void* arg) 595 { 596 struct worker* worker = (struct worker*)arg; 597 /* perform cronned operation */ 598 verbose(VERB_ALGO, "cron timer callback"); 599 if(cron_thread == NULL) { 600 /* create new thread to do it */ 601 ub_thread_create(&cron_thread, win_do_cron, worker); 602 } 603 /* reschedule */ 604 set_cron_timer(); 605 } 606 607 void wsvc_setup_worker(struct worker* worker) 608 { 609 /* if not started with -w service, do nothing */ 610 if(!service_stop_event) 611 return; 612 if(!(service_stop_ev = ub_winsock_register_wsaevent( 613 comm_base_internal(worker->base), service_stop_event, 614 &worker_win_stop_cb, worker))) { 615 fatal_exit("could not register wsaevent"); 616 return; 617 } 618 if(!service_cron) { 619 service_cron = comm_timer_create(worker->base, 620 wsvc_cron_cb, worker); 621 if(!service_cron) 622 fatal_exit("could not create cron timer"); 623 set_cron_timer(); 624 } 625 } 626 627 void wsvc_desetup_worker(struct worker* ATTR_UNUSED(worker)) 628 { 629 comm_timer_delete(service_cron); 630 service_cron = NULL; 631 } 632