1: <?php declare(strict_types=1);
2:
3: namespace Salient\Utility;
4:
5: use Salient\Core\Facade\Err;
6: use LogicException;
7: use RuntimeException;
8:
9: /**
10: * Work with the runtime environment
11: *
12: * @api
13: */
14: final class Sys extends AbstractUtility
15: {
16: /**
17: * Get the configured memory_limit, in bytes
18: */
19: public static function getMemoryLimit(): int
20: {
21: return Get::bytes((string) ini_get('memory_limit'));
22: }
23:
24: /**
25: * Get the current memory usage of the script as a percentage of the
26: * memory_limit
27: */
28: public static function getMemoryUsagePercent(): float
29: {
30: $limit = self::getMemoryLimit();
31:
32: return $limit <= 0
33: ? 0
34: : (memory_get_usage() * 100 / $limit);
35: }
36:
37: /**
38: * Get user and system CPU times for the current run, in microseconds
39: *
40: * @return array{int,int} `[<user_time>, <system_time>]`
41: */
42: public static function getCpuUsage(): array
43: {
44: $usage = getrusage();
45:
46: if ($usage === false) {
47: // @codeCoverageIgnoreStart
48: return [0, 0];
49: // @codeCoverageIgnoreEnd
50: }
51:
52: $user_s = $usage['ru_utime.tv_sec'] ?? 0;
53: $user_us = $usage['ru_utime.tv_usec'] ?? 0;
54: $sys_s = $usage['ru_stime.tv_sec'] ?? 0;
55: $sys_us = $usage['ru_stime.tv_usec'] ?? 0;
56:
57: return [
58: $user_s * 1000000 + $user_us,
59: $sys_s * 1000000 + $sys_us,
60: ];
61: }
62:
63: /**
64: * Get the filename used to run the script
65: *
66: * Use `$parentDir` to get the running script's path relative to a parent
67: * directory.
68: *
69: * @throws LogicException if the running script is not in `$parentDir`.
70: */
71: public static function getProgramName(?string $parentDir = null): string
72: {
73: $filename = $_SERVER['SCRIPT_FILENAME'];
74:
75: if ($parentDir === null) {
76: return $filename;
77: }
78:
79: $filename = File::getRelativePath($filename, $parentDir);
80: if ($filename === null) {
81: throw new LogicException(sprintf(
82: "'%s' is not in '%s'",
83: $_SERVER['SCRIPT_FILENAME'],
84: $parentDir,
85: ));
86: }
87:
88: return $filename;
89: }
90:
91: /**
92: * Get the basename of the file used to run the script
93: *
94: * @param string ...$suffixes Removed from the end of the filename.
95: */
96: public static function getProgramBasename(string ...$suffixes): string
97: {
98: $basename = basename($_SERVER['SCRIPT_FILENAME']);
99:
100: if (!$suffixes) {
101: return $basename;
102: }
103:
104: foreach ($suffixes as $suffix) {
105: $length = strlen($suffix);
106: if (substr($basename, -$length) === $suffix) {
107: return substr($basename, 0, -$length);
108: }
109: }
110:
111: return $basename;
112: }
113:
114: /**
115: * Get the directory PHP uses for temporary file storage by default
116: *
117: * @throws RuntimeException if the path returned by
118: * {@see sys_get_temp_dir()} is not a writable directory.
119: */
120: public static function getTempDir(): string
121: {
122: $tempDir = sys_get_temp_dir();
123: $dir = @realpath($tempDir);
124: if ($dir === false || !is_dir($dir) || !is_writable($dir)) {
125: // @codeCoverageIgnoreStart
126: throw new RuntimeException(
127: sprintf('Not a writable directory: %s', $tempDir),
128: );
129: // @codeCoverageIgnoreEnd
130: }
131: return $dir;
132: }
133:
134: /**
135: * Get the user ID or username of the current user
136: *
137: * @return int|string
138: */
139: public static function getUserId()
140: {
141: if (function_exists('posix_geteuid')) {
142: return posix_geteuid();
143: }
144:
145: $user = Env::getNullable('USERNAME', null);
146: if ($user !== null) {
147: return $user;
148: }
149:
150: // @codeCoverageIgnoreStart
151: $user = Env::getNullable('USER', null);
152: if ($user !== null) {
153: return $user;
154: }
155:
156: throw new RuntimeException('Unable to identify user');
157: // @codeCoverageIgnoreEnd
158: }
159:
160: /**
161: * Check if a process with the given process ID is running
162: */
163: public static function isProcessRunning(int $pid): bool
164: {
165: if (!self::isWindows()) {
166: return posix_kill($pid, 0);
167: }
168:
169: $command = sprintf('tasklist /fo csv /nh /fi "PID eq %d"', $pid);
170: $stream = File::openPipe($command, 'r');
171: $csv = File::getCsv($stream);
172: if (File::closePipe($stream, $command) !== 0) {
173: // @codeCoverageIgnoreStart
174: throw new RuntimeException(
175: sprintf('Command failed: %s', $command)
176: );
177: // @codeCoverageIgnoreEnd
178: }
179:
180: return count($csv) === 1
181: && isset($csv[0][1])
182: && $csv[0][1] === (string) $pid;
183: }
184:
185: /**
186: * Get a command string with arguments escaped for this platform's shell
187: *
188: * Don't use this method to prepare commands for {@see proc_open()}. Its
189: * quoting behaviour on Windows is unstable.
190: *
191: * @param string[] $args
192: */
193: public static function escapeCommand(array $args): string
194: {
195: $windows = self::isWindows();
196:
197: foreach ($args as &$arg) {
198: $arg = $windows
199: ? self::escapeCmdArg($arg)
200: : self::escapeShellArg($arg);
201: }
202:
203: return implode(' ', $args);
204: }
205:
206: /**
207: * Escape an argument for POSIX-compatible shells
208: */
209: private static function escapeShellArg(string $arg): string
210: {
211: if ($arg === '' || Regex::match('/[^a-z0-9+.\/@_-]/i', $arg)) {
212: return "'" . str_replace("'", "'\''", $arg) . "'";
213: }
214:
215: return $arg;
216: }
217:
218: /**
219: * Escape an argument for cmd.exe on Windows
220: *
221: * Derived from `Composer\Util\ProcessExecutor::escapeArgument()`, which
222: * credits <https://github.com/johnstevenson/winbox-args>.
223: */
224: private static function escapeCmdArg(string $arg): string
225: {
226: $arg = Regex::replace('/(\\\\*)"/', '$1$1\"', $arg, -1, $quoteCount);
227: $quote = $arg === '' || strpbrk($arg, " \t,") !== false;
228: $meta = $quoteCount > 0 || Regex::match('/%[^%]+%|![^!]+!/', $arg);
229:
230: if (!$meta && !$quote) {
231: $quote = strpbrk($arg, '^&|<>()') !== false;
232: }
233:
234: if ($quote) {
235: $arg = '"' . Regex::replace('/(\\\\*)$/', '$1$1', $arg) . '"';
236: }
237:
238: if ($meta) {
239: $arg = Regex::replace('/["^&|<>()%!]/', '^$0', $arg);
240: }
241:
242: return $arg;
243: }
244:
245: /**
246: * True if the script is running on Windows
247: */
248: public static function isWindows(): bool
249: {
250: return \PHP_OS_FAMILY === 'Windows';
251: }
252:
253: /**
254: * Make a clean exit from the running script on SIGTERM, SIGINT or SIGHUP
255: *
256: * @return bool `false` if signal handlers can't be installed on this
257: * platform, otherwise `true`.
258: */
259: public static function handleExitSignals(): bool
260: {
261: if (!function_exists('pcntl_async_signals')) {
262: return false;
263: }
264:
265: $handler = static function (int $signal): void {
266: // @codeCoverageIgnoreStart
267: $status = 128 + $signal;
268: if (
269: class_exists(Err::class)
270: && Err::isLoaded()
271: && Err::isRegistered()
272: ) {
273: Err::handleExitSignal($status);
274: }
275: exit($status);
276: // @codeCoverageIgnoreEnd
277: };
278:
279: pcntl_async_signals(true);
280:
281: return pcntl_signal(\SIGTERM, $handler)
282: && pcntl_signal(\SIGINT, $handler)
283: && pcntl_signal(\SIGHUP, $handler);
284: }
285: }
286: