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 }