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 }