1 /** Various utilities related to processes
2   **/
3 module theprocess.utils;
4 
5 private import std.format;
6 private import std.process;
7 private import std.file;
8 private import std.stdio;
9 private import std.exception;
10 private import std.string: join;
11 private import std.typecons;
12 
13 
14 private import thepath;
15 
16 version(Posix) private import core.sys.posix.sys.types: uid_t, gid_t;
17 
18 
19 /** Resolve program name according to system path
20   *
21   * Params:
22   *     program = name of program to find
23   * Returns:
24   *     Nullable!Path to program.
25   **/
26 @safe Nullable!Path resolveProgram(in string program) {
27     import std.path: pathSeparator;
28     import std.array: split;
29     foreach(sys_path; environment["PATH"].split(pathSeparator)) {
30         auto sys_program_path = Path(sys_path).join(program);
31         if (!sys_program_path.exists)
32             continue;
33 
34         // TODO: check with lstat if link is not broken
35         version(Posix) {
36             import core.sys.posix.sys.stat: S_IXUSR, S_IXGRP, S_IXOTH;
37             if (!(sys_program_path.getAttributes() & (S_IXUSR | S_IXGRP | S_IXOTH)))
38                 continue;
39         }
40 
41         return sys_program_path.nullable;
42     }
43     return Nullable!Path.init;
44 }
45 
46 
47 ///
48 version(Posix) unittest {
49     import unit_threaded.assertions;
50 
51     resolveProgram("sh").isNull.shouldBeFalse;
52 
53     version(OSX)
54         resolveProgram("sh").get.toString.shouldEqual("/bin/sh");
55     else
56         resolveProgram("sh").get.toString.shouldEqual("/usr/bin/sh");
57 
58     resolveProgram("unexisting_program").isNull.shouldBeTrue;
59 }
60 
61 
62 /** Check whether a process with the given PID is currently running.
63   *
64   * On Posix this uses kill(pid, 0): no signal is sent, but the kernel
65   * validates whether the target process exists and the caller has permission
66   * to signal it.  ESRCH ("no such process") is the only errno value that
67   * conclusively means the process is gone.
68   *
69   * On Windows this opens a query handle via OpenProcess and reads the exit
70   * code with GetExitCodeProcess.  If the handle cannot be opened the
71   * function returns false (process absent or inaccessible).
72   *
73   * Params:
74   *     pid = OS-level process identifier as a raw integer.
75   *
76   * Returns:
77   *     true if the process appears to be running, false otherwise.
78   **/
79 @trusted bool isProcessRunning(int pid) nothrow {
80     version(Posix) {
81         import core.sys.posix.signal : kill;
82         import core.stdc.errno : errno, ESRCH;
83 
84         if (kill(pid, 0) == 0) return true;
85         return errno != ESRCH;
86     } else version(Windows) {
87         import core.sys.windows.winbase : OpenProcess, CloseHandle, GetExitCodeProcess, STILL_ACTIVE;
88         import core.sys.windows.winnt : PROCESS_QUERY_INFORMATION, DWORD;
89 
90         auto handle = OpenProcess(PROCESS_QUERY_INFORMATION, false, cast(DWORD) pid);
91         if (handle is null) return false;
92         scope(exit) CloseHandle(handle);
93         DWORD code;
94         if (!GetExitCodeProcess(handle, &code)) return false;
95         return code == STILL_ACTIVE;
96     } else {
97         static assert(false, "isProcessRunning is not implemented for this platform");
98     }
99 }
100 
101 /** Check whether a process is currently running.
102   *
103   * Overload accepting a $(D std.process.Pid) directly.
104   *
105   * Params:
106   *     pid = Pid handle returned by spawnProcess or similar.
107   *
108   * Returns:
109   *     true if the process appears to be running, false otherwise.
110   **/
111 @trusted bool isProcessRunning(Pid pid) nothrow {
112     return isProcessRunning(pid.processID);
113 }
114 
115 
116 /// isProcessRunning returns true for a live process and false after it exits
117 unittest {
118     import unit_threaded.assertions;
119     import std.process : spawnProcess, kill, wait;
120 
121     version(Posix) {
122         auto pid = spawnProcess(["sleep", "10"]);
123     } else version(Windows) {
124         auto pid = spawnProcess(["cmd", "/c", "timeout", "/t", "10", "/nobreak"]);
125     }
126     int rawPid = pid.processID;
127 
128     isProcessRunning(rawPid).shouldBeTrue;
129     isProcessRunning(pid).shouldBeTrue;
130 
131     pid.kill();
132     pid.wait();
133 
134     isProcessRunning(rawPid).shouldBeFalse;
135     isProcessRunning(pid).shouldBeFalse;
136 }
137 
138 
139 /** D-friendly representation of a system user (passwd entry).
140   *
141   * Obtained via $(LREF getSystemUser) or $(LREF getCurrentUser).
142   **/
143 version(Posix) struct SystemUser {
144     string name;     /// Login name
145     uid_t  uid;      /// User ID
146     gid_t  gid;      /// Primary group ID
147     string homeDir;  /// Home directory path
148     string shell;    /// Login shell path
149 }
150 
151 
152 /** Look up a system user by name.
153   *
154   * Params:
155   *     username = login name to look up
156   * Returns:
157   *     Nullable!SystemUser — null if no such user exists.
158   * Throws:
159   *     Exception on unexpected errors from getpwnam_r.
160   **/
161 version(Posix) @trusted Nullable!SystemUser getSystemUser(in string username) {
162     import core.sys.posix.pwd: getpwnam_r, passwd;
163     import std.string: toStringz, fromStringz;
164     import core.stdc.errno: ENOENT, ESRCH, EBADF, EPERM;
165     import core.stdc.string: strerror;
166 
167     passwd pwd;
168     passwd* result;
169     size_t bufsize = 16384;
170     char[] buf = new char[bufsize];
171 
172     int s = getpwnam_r(username.toStringz, &pwd, &buf[0], bufsize, &result);
173     if (s == ENOENT || s == ESRCH || s == EBADF || s == EPERM || result is null)
174         return Nullable!SystemUser.init;
175 
176     if (s != 0)
177         throw new Exception(
178             "Got error on attempt to get user %s: %s"
179             .format(username, strerror(s).fromStringz));
180 
181     return SystemUser(
182         pwd.pw_name.fromStringz.idup,
183         pwd.pw_uid,
184         pwd.pw_gid,
185         pwd.pw_dir.fromStringz.idup,
186         pwd.pw_shell.fromStringz.idup,
187     ).nullable;
188 }
189 
190 
191 ///
192 version(Posix) unittest {
193     import unit_threaded.assertions;
194 
195     auto root = getSystemUser("root");
196     root.isNull.shouldBeFalse;
197     root.get.name.shouldEqual("root");
198     root.get.uid.shouldEqual(0);
199 
200     getSystemUser("this_user_definitely_does_not_exist_xyzzy").isNull.shouldBeTrue;
201 }
202 
203 
204 /** Return the SystemUser entry for the current effective user.
205   *
206   * Returns:
207   *     SystemUser for the calling process's effective UID.
208   * Throws:
209   *     Exception if the entry cannot be found or an error occurs.
210   **/
211 version(Posix) @trusted SystemUser getCurrentUser() {
212     import core.sys.posix.pwd: getpwuid_r, passwd;
213     import core.sys.posix.unistd: geteuid;
214     import std.string: fromStringz;
215     import core.stdc.string: strerror;
216 
217     passwd pwd;
218     passwd* result;
219     size_t bufsize = 16384;
220     char[] buf = new char[bufsize];
221 
222     int s = getpwuid_r(geteuid(), &pwd, &buf[0], bufsize, &result);
223     if (s != 0)
224         throw new Exception(
225             "Got error on attempt to get current user: %s"
226             .format(strerror(s).fromStringz));
227     if (result is null)
228         throw new Exception("Current user not found in password database");
229 
230     return SystemUser(
231         pwd.pw_name.fromStringz.idup,
232         pwd.pw_uid,
233         pwd.pw_gid,
234         pwd.pw_dir.fromStringz.idup,
235         pwd.pw_shell.fromStringz.idup,
236     );
237 }
238 
239 
240 ///
241 version(Posix) unittest {
242     import unit_threaded.assertions;
243     import core.sys.posix.unistd: geteuid;
244 
245     auto user = getCurrentUser();
246     user.uid.shouldEqual(geteuid());
247 }
248 
249 
250 /** Check whether username matches the current effective user.
251   *
252   * Compares the uid of the named user against the process's effective UID,
253   * so it works correctly in setuid scenarios.
254   *
255   * Params:
256   *     username = login name to compare against
257   * Returns:
258   *     true if the user exists and their uid equals geteuid().
259   **/
260 version(Posix) @trusted bool isCurrentUser(in string username) {
261     import core.sys.posix.unistd: geteuid;
262 
263     auto u = getSystemUser(username);
264     return !u.isNull && u.get.uid == geteuid();
265 }
266 
267 
268 ///
269 version(Posix) unittest {
270     import unit_threaded.assertions;
271 
272     isCurrentUser(getCurrentUser().name).shouldBeTrue;
273     isCurrentUser("this_user_definitely_does_not_exist_xyzzy").shouldBeFalse;
274 }
275 
276 
277 /** Check if system user with specified name exists
278   *
279   * Params:
280   *     username = name of user to check if exists
281   * Returns:
282   *     True if such user exists, otherwise false.
283   **/
284 version(Posix) @trusted bool systemUserExists(in string username) {
285     return !getSystemUser(username).isNull;
286 }
287 
288 
289 ///
290 version(Posix) unittest {
291     import unit_threaded.assertions;
292 
293     systemUserExists("root").shouldBeTrue;
294     systemUserExists("this_user_definitely_does_not_exist_xyzzy").shouldBeFalse;
295 }