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 }