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