1 /**
2  * PID file and lock
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <ae@cy.md>
12  */
13 
14 module ae.sys.pidfile;
15 
16 import std.conv : text;
17 import std.exception;
18 import std.file : thisExePath, tempDir;
19 import std.path : baseName, buildPath;
20 import std.process : thisProcessID;
21 import std.stdio : File;
22 
23 import ae.sys.process : getCurrentUser;
24 
25 version (Posix) import core.sys.posix.unistd : geteuid;
26 
27 static File pidFile; /// The PID file. Kept open and locked while the program is running.
28 
29 /// Create and lock a PID file.
30 /// If the PID file exists and is locked, an exception is thrown.
31 /// To use, simply call this function at the top of `main` to prevent multiple instances.
32 void createPidFile(
33 	string name = defaultPidFileName(),
34 	string path = defaultPidFilePath())
35 {
36 	auto fullPath = buildPath(path, name);
37 	assert(!pidFile.isOpen(), "A PID file has already been created for this program / process");
38 	pidFile.open(fullPath, "w+b");
39 	scope(failure) pidFile.close();
40 	enforce(pidFile.tryLock(), "Failed to acquire lock on PID file " ~ fullPath ~ ". Is another instance running?");
41 	pidFile.write(thisProcessID);
42 	pidFile.flush();
43 }
44 
45 /// The default `name` argument for `createPidFile`.
46 /// Returns a file name containing the username and program name.
47 string defaultPidFileName()
48 {
49 	return text(getCurrentUser(), "-", thisExePath.baseName, ".pid");
50 }
51 
52 /// The default `path` argument for `createPidFile`.
53 /// Returns "/var/run" if running as root on POSIX,
54 /// or the temporary directory otherwise.
55 string defaultPidFilePath()
56 {
57 	version (Posix)
58 	{
59 		if (geteuid() == 0)
60 			return "/var/run";
61 		else
62 			return tempDir; // /var/run and /var/lock are usually not writable for non-root processes
63 	}
64 	version (Windows)
65 		return tempDir;
66 }
67 
68 unittest
69 {
70 	createPidFile();
71 
72 	auto realPidFile = pidFile;
73 	pidFile = File.init;
74 
75 	static void runForked(void delegate() code)
76 	{
77 		version (Posix)
78 		{
79 			// Since locks are per-process, we cannot test lock failures within
80 			// the same process. fork() is used to create a second process.
81 			import core.sys.posix.sys.wait : wait;
82 			import core.sys.posix.unistd : fork, _exit;
83 			int child, status;
84 			if ((child = fork()) == 0)
85 			{
86 				code();
87 				_exit(0);
88 			}
89 			else
90 			{
91 				assert(wait(&status) != -1);
92 				assert(status == 0, "Fork crashed");
93 			}
94 		}
95 		else
96 			code();
97 	}
98 
99 	runForked({
100 		assertThrown!Exception(createPidFile);
101 		assert(!pidFile.isOpen);
102 	});
103 	
104 	realPidFile.close();
105 
106 	runForked({
107 		createPidFile();
108 	});
109 }