1 // Written in D programming language
2 /**
3 *    Boilerplate code to start linux daemon. Other platforms will
4 *    fallback to terminal mode.
5 *
6 *    See_Also: terminal
7 *    Copyright: © 2014 DSoftOut
8 *    License: Subject to the terms of the MIT license, as written in the included LICENSE file.
9 *    Authors: NCrashed <ncrashed@gmail.com>
10 */
11 module daemon;
12 
13 import std.c.stdlib;
14 import std.stdio;
15 import std.conv;
16 import std.exception;
17 import std.file;
18 import std.process;
19 import std.path;
20 version (linux) 
21 {
22     import std.c.linux.linux;
23     import core.sys.linux.errno;
24 }
25 import dlogg.log;
26 import util;
27 
28 private 
29 {
30     void delegate() savedListener, savedTermListener;
31     void delegate() savedRotateListener;
32     
33     shared ILogger savedLogger;
34     
35     string savedPidFile;
36     string savedLockFile;
37     
38     extern(C)
39     {
40         /// These are for control of termination
41         //void _STD_monitor_staticdtor();
42         //void _STD_critical_term();
43         void gc_term();
44         
45         version (linux) 
46         {
47             alias int pid_t;
48     
49             // daemon functions
50             pid_t fork();
51             int umask(int);
52             int setsid();
53             int close(int fd);
54     
55             // Signal trapping in Linux
56             alias void function(int) sighandler_t;
57             sighandler_t signal(int signum, sighandler_t handler);
58             int __libc_current_sigrtmin();
59             char* strerror(int errnum);
60             
61             void signal_handler_daemon(int sig)
62             {
63                 if(sig == SIGABRT || sig == SIGTERM || sig == SIGQUIT || sig == SIGINT || sig == SIGQUIT)
64                 {
65                     savedLogger.logInfo(text("Signal ", to!string(sig), " caught..."));
66                     savedTermListener();
67                     deletePidFile(savedPidFile);
68                     deleteLockFile(savedLockFile);
69                     terminate(EXIT_SUCCESS);
70                 } else if(sig == SIGHUP)
71                 {
72                     savedLogger.logInfo(text("Signal ", to!string(sig), " caught..."));
73                     savedListener();
74                 } else if(sig == SIGROTATE)
75                 {
76                     savedLogger.logInfo(text("User signal ", sig, " is caught!"));
77                     savedRotateListener();
78                 }
79             }
80         }
81     }
82     version(linux)
83     {
84         private immutable int SIGROTATE;
85         static this()
86         {
87             SIGROTATE = __libc_current_sigrtmin + 10;
88         }
89     
90     
91         void enforceLockFile(string path, int userid)
92         {
93             if(path.exists)
94             {
95                 savedLogger.logError(text("There is another pgator instance running: lock file is '",path,"'"));
96                 savedLogger.logInfo("Remove the file if previous instance if pgator has crashed");
97                 terminate(-1);
98             } else
99             {
100                 if(!path.dirName.exists)
101                 {
102                     mkdirRecurse(path.dirName);
103                 }
104                 auto file = File(path, "w");
105                 file.close();
106             }
107             
108             // if root, change permission on file to be able to remove later
109             if (getuid() == 0 && userid >= 0) 
110             {
111                 savedLogger.logDebug("Changing permissions for lock file: ", path);
112                 executeShell(text("chown ", userid," ", path.dirName));
113                 executeShell(text("chown ", userid," ", path));
114             }
115         }
116         
117         void deleteLockFile(string path)
118         {
119             if(path.exists)
120             {
121                 scope(failure)
122                 {
123                     savedLogger.logWarning(text("Failed to remove lock file: ", path));
124                     return;
125                 }
126                 path.remove();
127             }
128         }
129         
130         void writePidFile(string path, int pid, uint userid)
131         {
132             scope(failure)
133             {
134                 savedLogger.logWarning(text("Failed to write pid file: ", path));
135                 return;
136             }
137             
138             if(!path.dirName.exists)
139             {
140                 mkdirRecurse(path.dirName);
141             }
142             auto file = File(path, "w");
143             scope(exit) file.close();
144             
145             file.write(pid);
146             
147             // if root, change permission on file to be able to remove later
148             if (getuid() == 0 && userid >= 0) 
149             {
150                 savedLogger.logDebug("Changing permissions for pid file: ", path);
151                 executeShell(text("chown ", userid," ", path.dirName));
152                 executeShell(text("chown ", userid," ", path));
153             }
154         }
155         
156         void deletePidFile(string path)
157         {
158             scope(failure)
159             {
160                 savedLogger.logWarning(text("Failed to remove pid file: ", path));
161                 return;
162             }
163             path.remove();
164         }
165         
166         void dropRootPrivileges(int groupid, int userid)
167         {
168             if (getuid() == 0) 
169             {
170                 if(groupid < 0 || userid < 0)
171                 {
172                     savedLogger.logWarning("Running as root, but doesn't specified groupid and/or userid for"
173                         " privileges lowing!");
174                     return;
175                 }
176                 
177                 savedLogger.logInfo("Running as root, dropping privileges...");
178                 // process is running as root, drop privileges 
179                 if (setgid(groupid) != 0)
180                 {
181                     savedLogger.logError(text("setgid: Unable to drop group privileges: ", strerror(errno).fromStringz));
182                     assert(false);
183                 }
184                 if (setuid(userid) != 0)
185                 {
186                     savedLogger.logError(text("setuid: Unable to drop user privileges: ", strerror(errno).fromStringz));
187                     assert(false);
188                 }
189             }
190         }
191     }
192 }
193 
194 private void terminate(int code) 
195 {
196     savedLogger.logError("Daemon is terminating with code: " ~ to!string(code));
197     savedLogger.finalize();
198     
199     gc_term();
200     version (linux) 
201     {
202         //_STD_critical_term();
203         //_STD_monitor_staticdtor();
204     }
205 
206     exit(code);
207 }
208 
209 /**
210 *   Forks daemon process with $(B progMain) main function and passes $(B args) to it. If daemon 
211 *   catches SIGHUP signal, $(B listener) delegate is called. If daemon catches SIGQUIT, SIGABRT,
212 *   or any other terminating sygnal, the termListener is called.
213 *
214 *   If application receives "real-time" signal $(B SIGROTATE) defined as SIGRTMIN+10, then $(B rotateListener) is called
215 *   to handle 'logrotate' utility.
216 * 
217 *   Daemon writes log message into provided $(B logger) and will close it while exiting.
218 *
219 *   File $(B pidfile) is used to write down PID of detached daemon. This file is usefull for interfacing with external 
220 *   tools thus it is only way to know daemon PID (for this moment). This file is removed while exiting.
221 *
222 *   File $(B lockfile) is used to track multiple daemon instances. If this file is exists, the another daemon is running
223 *   and pgator should exit immediately. 
224 *
225 *   $(B groupid) and $(B userid) are used to low privileges with run as root. 
226 */
227 int runDaemon(shared ILogger logger, int delegate(string[]) progMain, string[] args, void delegate() listener
228         , void delegate() termListener, void delegate() rotateListener, string pidfile, string lockfile
229         , int groupid = -1, int userid = -1)
230 {
231     savedLogger = logger;
232     savedPidFile = pidfile;
233     savedLockFile = lockfile;
234     
235     // Daemonize under Linux
236     version (linux) 
237     {
238         // Handling lockfile
239         enforceLockFile(lockfile, userid);
240         scope(failure) deleteLockFile(lockfile);
241     
242         // Our process ID and Session ID
243         pid_t pid, sid;
244 
245         // Fork off the parent process
246         pid = fork();
247         if (pid < 0) 
248         {
249             savedLogger.logError("Failed to start daemon: fork failed");
250             deleteLockFile(lockfile);
251             exit(EXIT_FAILURE);
252         }
253         // If we got a good PID, then we can exit the parent process.
254         if (pid > 0) 
255         {
256             savedLogger.logInfo(text("Daemon detached with pid ", pid));
257             
258             // handling pidfile
259             writePidFile(pidfile, pid, userid);
260             
261             exit(EXIT_SUCCESS);
262         }
263         
264         // dropping root
265         dropRootPrivileges(groupid, userid);
266         
267         // Change the file mode mask
268         umask(0);
269         savedLogger.minOutputLevel(LoggingLevel.Muted);
270     } else
271     {
272         savedLogger.minOutputLevel(LoggingLevel.Notice);
273         savedLogger.logError("Daemon mode isn't supported for this platform!");
274     }
275 
276     version (linux) 
277     {
278         scope(failure) deletePidFile(pidfile);
279         
280         // Create a new SID for the child process
281         sid = setsid();
282         if (sid < 0) terminate(EXIT_FAILURE);
283 
284         // Close out the standard file descriptors
285         close(0);
286         close(1);
287         close(2);
288 
289         void bindSignal(int sig, sighandler_t handler)
290         {
291             enforce(signal(sig, handler) != SIG_ERR, text("Cannot catch signal ", sig));
292         }
293         
294         savedTermListener = termListener;
295         bindSignal(SIGABRT, &signal_handler_daemon);
296         bindSignal(SIGTERM, &signal_handler_daemon);
297         bindSignal(SIGQUIT, &signal_handler_daemon);
298         bindSignal(SIGINT, &signal_handler_daemon);
299         bindSignal(SIGQUIT, &signal_handler_daemon);
300         
301         savedListener = listener;
302         bindSignal(SIGHUP, &signal_handler_daemon);
303         
304         savedRotateListener = rotateListener;
305         bindSignal(SIGROTATE, &signal_handler_daemon);
306     }
307 
308     savedLogger.logInfo("Server is starting in daemon mode...");
309     int code = EXIT_FAILURE;
310     try 
311     {
312         code = progMain(args);
313     } catch (Exception ex) 
314     {
315         savedLogger.logError(text("Catched unhandled exception in daemon level: ", ex.msg));
316         savedLogger.logError("Terminating...");
317     } finally 
318     {
319         version(linux) 
320         {
321             deletePidFile(pidfile);
322             deleteLockFile(lockfile);
323         }
324         terminate(code);
325     }
326 
327     return 0;
328 }