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 }