1 // This file is written in D programming language 2 /** 3 * Daemon implementation for GNU/Linux platform. 4 * 5 * The main symbols you might be interested in: 6 * * $(B sendSignalDynamic) and $(B endSignal) - is easy way to send signals to created daemons 7 * * $(B runDaemon) - forks daemon process and places hooks that are described by $(B Daemon) template 8 * 9 * Copyright: © 2013-2014 Anton Gushcha 10 * License: Subject to the terms of the MIT license, as written in the included LICENSE file. 11 * Authors: NCrashed <ncrashed@gmail.com> 12 */ 13 module daemonize.linux; 14 15 version(linux): 16 17 static if( __VERSION__ < 2070 ) 18 { 19 import std.c.stdlib; 20 import std.c.linux.linux; 21 } 22 else 23 { 24 import core.stdc.stdlib; 25 import core.sys.posix.unistd; 26 import core.sys.posix.signal; 27 } 28 29 import std.conv; 30 import std.exception; 31 import std.file; 32 import std.path; 33 import std.process; 34 import std.stdio; 35 import std..string; 36 37 import core.sys.linux.errno; 38 39 import daemonize.daemon; 40 import daemonize..string; 41 import daemonize.keymap; 42 import daemonize.log; 43 44 /// Returns local pid file that is used when no custom one is specified 45 string defaultPidFile(string daemonName) 46 { 47 return expandTilde(buildPath("~", ".daemonize", daemonName ~ ".pid")); 48 } 49 50 /// Returns local lock file that is used when no custom one is specified 51 string defaultLockFile(string daemonName) 52 { 53 return expandTilde(buildPath("~", ".daemonize", daemonName ~ ".lock")); 54 } 55 56 /// Checks is $(B sig) is actually built-in 57 @nogc @safe bool isNativeSignal(Signal sig) pure nothrow 58 { 59 switch(sig) 60 { 61 case(Signal.Abort): return true; 62 case(Signal.HangUp): return true; 63 case(Signal.Interrupt): return true; 64 case(Signal.Quit): return true; 65 case(Signal.Terminate): return true; 66 default: return false; 67 } 68 } 69 70 /// Checks is $(B sig) is not actually built-in 71 @nogc @safe bool isCustomSignal(Signal sig) pure nothrow 72 { 73 return !isNativeSignal(sig); 74 } 75 76 /** 77 * Main template in the module that actually creates daemon process. 78 * $(B DaemonInfo) is a $(B Daemon) instance that holds name of the daemon 79 * and hooks for numerous $(B Signal)s. 80 * 81 * Daemon is detached from terminal, therefore it needs a preinitialized $(B logger). 82 * 83 * As soon as daemon is ready the function executes $(B main) delegate that returns 84 * application return code. 85 * 86 * Daemon uses pid and lock files. Pid file holds process id for communications with 87 * other applications. If $(B pidFilePath) isn't set, the default path to pid file is 88 * '~/.daemonize/<daemonName>.pid'. Lock file prevents from execution of numerous copies 89 * of daemons. If $(B lockFilePath) isn't set, the default path to lock file is 90 * '~/.daemonize/<daemonName>.lock'. If you want several instances of one daemon, redefine 91 * pid and lock files paths. 92 * 93 * Sometimes lock and pid files are located at `/var/run` directory and needs a root access. 94 * If $(B userId) and $(B groupId) parameters are set, daemon tries to create lock and pid files 95 * and drops root privileges. 96 * 97 * Example: 98 * --------- 99 * 100 * alias daemon = Daemon!( 101 * "DaemonizeExample1", // unique name 102 * 103 * // Setting associative map signal -> callbacks 104 * KeyValueList!( 105 * Composition!(Signal.Terminate, Signal.Quit, Signal.Shutdown, Signal.Stop), (logger, signal) 106 * { 107 * logger.logInfo("Exiting..."); 108 * return false; // returning false will terminate daemon 109 * }, 110 * Signal.HangUp, (logger) 111 * { 112 * logger.logInfo("Hello World!"); 113 * return true; // continue execution 114 * } 115 * ), 116 * 117 * // Main function where your code is 118 * (logger, shouldExit) { 119 * // will stop the daemon in 5 minutes 120 * auto time = Clock.currSystemTick + cast(TickDuration)5.dur!"minutes"; 121 * bool timeout = false; 122 * while(!shouldExit() && time > Clock.currSystemTick) { } 123 * 124 * logger.logInfo("Exiting main function!"); 125 * 126 * return 0; 127 * } 128 * ); 129 * 130 * return buildDaemon!daemon.run(logger); 131 * --------- 132 */ 133 template buildDaemon(alias DaemonInfo) 134 if(isDaemon!DaemonInfo || isDaemonClient!DaemonInfo) 135 { 136 alias daemon = readDaemonInfo!DaemonInfo; 137 138 static if(isDaemon!DaemonInfo) 139 { 140 int run(shared IDaemonLogger logger 141 , string pidFilePath = "", string lockFilePath = "" 142 , int userId = -1, int groupId = -1) 143 { 144 // Local locak file 145 if(lockFilePath == "") 146 { 147 lockFilePath = defaultLockFile(DaemonInfo.daemonName); 148 } 149 150 // Local pid file 151 if(pidFilePath == "") 152 { 153 pidFilePath = defaultPidFile(DaemonInfo.daemonName); 154 } 155 156 savedLogger = logger; 157 savedPidFilePath = pidFilePath; 158 savedLockFilePath = lockFilePath; 159 160 // Handling lockfile if any 161 enforceLockFile(lockFilePath, userId); 162 scope(exit) deleteLockFile(lockFilePath); 163 164 // Saving process ID and session ID 165 pid_t pid, sid; 166 167 // For off the parent process 168 pid = fork(); 169 if(pid < 0) 170 { 171 savedLogger.logError("Failed to start daemon: fork failed"); 172 173 // Deleting fresh lockfile 174 deleteLockFile(lockFilePath); 175 176 terminate(EXIT_FAILURE); 177 } 178 179 // If we got good PID, then we can exit the parent process 180 if(pid > 0) 181 { 182 // handling pidfile if any 183 writePidFile(pidFilePath, pid, userId); 184 185 savedLogger.logInfo(text("Daemon is detached with pid ", pid)); 186 terminate(EXIT_SUCCESS, false); 187 } 188 189 // dropping root privileges 190 dropRootPrivileges(groupId, userId); 191 192 // Change the file mode mask and suppress printing to console 193 umask(0); 194 savedLogger.minOutputLevel(DaemonLogLevel.Muted); 195 196 // Handling of deleting pid file 197 scope(exit) deletePidFile(pidFilePath); 198 199 // Create a new SID for the child process 200 sid = setsid(); 201 if (sid < 0) 202 { 203 deleteLockFile(lockFilePath); 204 deletePidFile(pidFilePath); 205 206 terminate(EXIT_FAILURE); 207 } 208 209 // Close out the standard file descriptors 210 close(0); 211 close(1); 212 close(2); 213 214 void bindSignal(int sig, sighandler_t handler) 215 { 216 enforce(signal(sig, handler) != SIG_ERR, text("Cannot catch signal ", sig)); 217 } 218 219 // Bind native signals 220 // other signals cause application to hang or cause no signal detection 221 // sigusr1 sigusr2 are used by garbage collector 222 bindSignal(SIGABRT, &signal_handler_daemon); 223 bindSignal(SIGTERM, &signal_handler_daemon); 224 bindSignal(SIGQUIT, &signal_handler_daemon); 225 bindSignal(SIGINT, &signal_handler_daemon); 226 bindSignal(SIGQUIT, &signal_handler_daemon); 227 bindSignal(SIGHUP, &signal_handler_daemon); 228 229 assert(daemon.canFitRealtimeSignals, "Cannot fit all custom signals to real-time signals range!"); 230 foreach(signame; daemon.customSignals) 231 { 232 bindSignal(daemon.mapRealTimeSignal(signame), &signal_handler_daemon); 233 } 234 235 int code = EXIT_FAILURE; 236 try code = DaemonInfo.mainFunc(savedLogger, &shouldExitFunc ); 237 catch (Throwable th) 238 { 239 savedLogger.logError(text("Catched unhandled throwable at daemon level at ", th.file, ": ", th.line, " : ", th.msg)); 240 savedLogger.logError("Terminating..."); 241 } 242 finally 243 { 244 deleteLockFile(lockFilePath); 245 deletePidFile(pidFilePath); 246 } 247 248 terminate(code); 249 return 0; 250 } 251 } 252 253 /** 254 * As custom signals are mapped to realtime signals at runtime, it is complicated 255 * to calculate signal number by hands. The function simplifies sending signals 256 * to daemons that were created by the package. 257 * 258 * The $(B DaemonInfo) could be a full description of desired daemon or simplified one 259 * (template ($B DaemonClient). That info is used to remap custom signals to realtime ones. 260 * 261 * $(B daemonName) is passed as runtime parameter to be able read service name at runtime. 262 * $(B signal) is the signal that you want to send. $(B pidFilePath) is optional parameter 263 * that overrides default algorithm of finding pid files (calculated from $(B daemonName) in form 264 * of '~/.daemonize/<daemonName>.pid'). 265 * 266 * See_Also: $(B sendSignal) version of the function that takes daemon name from $(B DaemonInfo). 267 */ 268 void sendSignalDynamic(shared IDaemonLogger logger, string daemonName, Signal signal, string pidFilePath = "") 269 { 270 savedLogger = logger; 271 272 // Try to find at default place 273 if(pidFilePath == "") 274 { 275 pidFilePath = defaultPidFile(daemonName); 276 } 277 278 // Reading file 279 int pid = readPidFile(pidFilePath); 280 281 logger.logInfo(text("Sending signal ", signal, " to daemon ", daemonName, " (pid ", pid, ")")); 282 kill(pid, daemon.mapSignal(signal)); 283 } 284 285 /// ditto 286 void sendSignal(shared IDaemonLogger logger, Signal signal, string pidFilePath = "") 287 { 288 sendSignalDynamic(logger, DaemonInfo.daemonName, signal, pidFilePath); 289 } 290 291 /** 292 * In GNU/Linux daemon doesn't require deinstallation. 293 */ 294 void uninstall() {} 295 296 /** 297 * Saves info about exception into daemon $(B logger) 298 */ 299 static class LoggedException : Exception 300 { 301 @safe nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) 302 { 303 savedLogger.logError(msg); 304 super(msg, file, line, next); 305 } 306 307 @safe nothrow this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__) 308 { 309 savedLogger.logError(msg); 310 super(msg, file, line, next); 311 } 312 } 313 314 private 315 { 316 shared IDaemonLogger savedLogger; 317 string savedPidFilePath; 318 string savedLockFilePath; 319 320 __gshared bool shouldExit; 321 322 bool shouldExitFunc() 323 { 324 return shouldExit; 325 } 326 327 /// Actual signal handler 328 static if(isDaemon!DaemonInfo) extern(C) void signal_handler_daemon(int sig) nothrow 329 { 330 foreach(signal; DaemonInfo.signalMap.keys) 331 { 332 alias handler = DaemonInfo.signalMap.get!signal; 333 334 static if(isComposition!signal) 335 { 336 foreach(subsignal; signal.signals) 337 { 338 if(daemon.mapSignal(subsignal) == sig) 339 { 340 try 341 { 342 static if(__traits(compiles, {handler(savedLogger, subsignal);})) 343 bool res = handler(savedLogger, subsignal); 344 else 345 bool res = handler(savedLogger); 346 347 if(!res) 348 { 349 deleteLockFile(savedLockFilePath); 350 deletePidFile(savedPidFilePath); 351 352 shouldExit = true; 353 //terminate(EXIT_SUCCESS); 354 } 355 else return; 356 357 } catch(Throwable th) 358 { 359 savedLogger.logError(text("Caught at signal ", subsignal," handler: ", th)); 360 } 361 } 362 } 363 } else 364 { 365 if(daemon.mapSignal(signal) == sig) 366 { 367 try 368 { 369 static if(__traits(compiles, handler(savedLogger, signal))) 370 bool res = handler(savedLogger, signal); 371 else 372 bool res = handler(savedLogger); 373 374 if(!res) 375 { 376 deleteLockFile(savedLockFilePath); 377 deletePidFile(savedPidFilePath); 378 379 shouldExit = true; 380 //terminate(EXIT_SUCCESS); 381 } 382 else return; 383 } 384 catch(Throwable th) 385 { 386 savedLogger.logError(text("Caught at signal ", signal," handler: ", th)); 387 } 388 } 389 } 390 } 391 } 392 393 /** 394 * Checks existence of special lock file at $(B path) and prevents from 395 * continuing if there is it. Also changes permissions for the file if 396 * $(B userid) not -1. 397 */ 398 void enforceLockFile(string path, int userid) 399 { 400 if(path.exists) 401 { 402 savedLogger.logError(text("There is another daemon instance running: lock file is '",path,"'")); 403 savedLogger.logInfo("Remove the file if previous instance if daemon has crashed"); 404 terminate(-1); 405 } else 406 { 407 if(!path.dirName.exists) 408 { 409 mkdirRecurse(path.dirName); 410 } 411 auto file = File(path, "w"); 412 file.close(); 413 } 414 415 // if root, change permission on file to be able to remove later 416 if (getuid() == 0 && userid >= 0) 417 { 418 savedLogger.logDebug("Changing permissions for lock file: " ~ path); 419 executeShell(text("chown ", userid," ", path.dirName)); 420 executeShell(text("chown ", userid," ", path)); 421 } 422 } 423 424 /** 425 * Removing lock file while terminating. 426 */ 427 void deleteLockFile(string path) 428 { 429 if(path.exists) 430 { 431 try 432 { 433 path.remove(); 434 } 435 catch(Exception e) 436 { 437 savedLogger.logWarning(text("Failed to remove lock file: ", path)); 438 return; 439 } 440 } 441 } 442 443 /** 444 * Writing down file with process id $(B pid) to $(B path) and changes 445 * permissions to $(B userid) (if not -1 and there is root dropping). 446 */ 447 void writePidFile(string path, int pid, uint userid) 448 { 449 try 450 { 451 if(!path.dirName.exists) 452 { 453 mkdirRecurse(path.dirName); 454 } 455 auto file = File(path, "w"); 456 scope(exit) file.close(); 457 458 file.write(pid); 459 460 // if root, change permission on file to be able to remove later 461 if (getuid() == 0 && userid >= 0) 462 { 463 savedLogger.logDebug("Changing permissions for pid file: " ~ path); 464 executeShell(text("chown ", userid," ", path.dirName)); 465 executeShell(text("chown ", userid," ", path)); 466 } 467 } catch(Exception e) 468 { 469 savedLogger.logWarning(text("Failed to write pid file: ", path)); 470 return; 471 } 472 } 473 474 /// Removing process id file 475 void deletePidFile(string path) 476 { 477 try 478 { 479 path.remove(); 480 } catch(Exception e) 481 { 482 savedLogger.logWarning(text("Failed to remove pid file: ", path)); 483 return; 484 } 485 } 486 487 /** 488 * Dropping root privileges to $(B groupid) and $(B userid). 489 */ 490 void dropRootPrivileges(int groupid, int userid) 491 { 492 if (getuid() == 0) 493 { 494 if(groupid < 0 || userid < 0) 495 { 496 savedLogger.logWarning("Running as root, but doesn't specified groupid and/or userid for" 497 ~ " privileges lowing!"); 498 return; 499 } 500 501 savedLogger.logInfo("Running as root, dropping privileges..."); 502 // process is running as root, drop privileges 503 if (setgid(groupid) != 0) 504 { 505 savedLogger.logError(text("setgid: Unable to drop group privileges: ", strerror(errno).fromStringz)); 506 assert(false); 507 } 508 if (setuid(userid) != 0) 509 { 510 savedLogger.logError(text("setuid: Unable to drop user privileges: ", strerror(errno).fromStringz)); 511 assert(false); 512 } 513 } 514 } 515 516 /// Terminating application with cleanup 517 void terminate(int code, bool isDaemon = true) nothrow 518 { 519 if(isDaemon) 520 { 521 savedLogger.logInfo("Daemon is terminating with code: " ~ to!string(code)); 522 savedLogger.finalize(); 523 524 gc_term(); 525 try { 526 _d_critical_term(); 527 _d_monitor_staticdtor(); 528 } catch (Exception ex) { 529 } 530 } 531 532 exit(code); 533 } 534 535 /// Tries to read a number from $(B filename) 536 int readPidFile(string filename) 537 { 538 std.stdio.writeln(filename); 539 if(!filename.exists) 540 throw new LoggedException("Cannot find pid file at '" ~ filename ~ "'!"); 541 542 auto file = File(filename, "r"); 543 return file.readln.to!int; 544 } 545 } 546 } 547 private 548 { 549 // https://issues.dlang.org/show_bug.cgi?id=13282 550 extern (C) nothrow 551 { 552 int __libc_current_sigrtmin(); 553 int __libc_current_sigrtmax(); 554 } 555 extern (C) nothrow 556 { 557 // These are for control of termination 558 // druntime rt.critical_ 559 void _d_critical_term(); 560 // druntime rt.monitor_ 561 void _d_monitor_staticdtor(); 562 563 void gc_term(); 564 565 alias int pid_t; 566 567 // daemon functions 568 pid_t fork(); 569 int umask(int); 570 int setsid(); 571 int close(int fd); 572 573 // Signal trapping in Linux 574 alias void function(int) sighandler_t; 575 sighandler_t signal(int signum, sighandler_t handler); 576 char* strerror(int errnum) pure; 577 } 578 579 /// Handles utilities for signal mapping from local representation to GNU/Linux one 580 template readDaemonInfo(alias DaemonInfo) 581 if(isDaemon!DaemonInfo || isDaemonClient!DaemonInfo) 582 { 583 template extractCustomSignals(T...) 584 { 585 static if(T.length < 2) alias extractCustomSignals = T[0]; 586 else static if(isComposition!(T[1])) alias extractCustomSignals = StrictExpressionList!(T[0].expand, staticFilter!(isCustomSignal, T[1].signals)); 587 else static if(isCustomSignal(T[1])) alias extractCustomSignals = StrictExpressionList!(T[0].expand, T[1]); 588 else alias extractCustomSignals = T[0]; 589 } 590 591 template extractNativeSignals(T...) 592 { 593 static if(T.length < 2) alias extractNativeSignals = T[0]; 594 else static if(isComposition!(T[1])) alias extractNativeSignals = StrictExpressionList!(T[0].expand, staticFilter!(isNativeSignal, T[1].signals)); 595 else static if(isNativeSignal(T[1])) alias extractNativeSignals = StrictExpressionList!(T[0].expand, T[1]); 596 else alias extractNativeSignals = T[0]; 597 } 598 599 static if(isDaemon!DaemonInfo) 600 { 601 alias customSignals = staticFold!(extractCustomSignals, StrictExpressionList!(), DaemonInfo.signalMap.keys).expand; //pragma(msg, [customSignals]); 602 alias nativeSignals = staticFold!(extractNativeSignals, StrictExpressionList!(), DaemonInfo.signalMap.keys).expand; //pragma(msg, [nativeSignals]); 603 } else 604 { 605 alias customSignals = staticFold!(extractCustomSignals, StrictExpressionList!(), DaemonInfo.signals).expand; //pragma(msg, [customSignals]); 606 alias nativeSignals = staticFold!(extractNativeSignals, StrictExpressionList!(), DaemonInfo.signals).expand; //pragma(msg, [nativeSignals]); 607 } 608 609 /** 610 * Checks if all not native signals can be binded 611 * to real-time signals. 612 */ 613 bool canFitRealtimeSignals() 614 { 615 return customSignals.length <= __libc_current_sigrtmax - __libc_current_sigrtmin; 616 } 617 618 /// Converts platform independent signal to native 619 @safe int mapSignal(Signal sig) nothrow 620 { 621 switch(sig) 622 { 623 case(Signal.Abort): return SIGABRT; 624 case(Signal.HangUp): return SIGHUP; 625 case(Signal.Interrupt): return SIGINT; 626 case(Signal.Quit): return SIGQUIT; 627 case(Signal.Terminate): return SIGTERM; 628 default: return mapRealTimeSignal(sig); 629 } 630 } 631 632 /// Converting custom signal to real-time signal 633 @trusted int mapRealTimeSignal(Signal sig) nothrow 634 { 635 assert(!isNativeSignal(sig)); 636 637 int counter = 0; 638 foreach(key; customSignals) 639 { 640 if(sig == key) return counter + __libc_current_sigrtmin; 641 else counter++; 642 } 643 644 assert(false, "Parameter signal not in daemon description!"); 645 } 646 } 647 }