Passed
Push — master ( 032384...6ce998 )
by Fabio
05:25
created

TProcessHelper::fork()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 22
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 17
nc 9
nop 1
dl 0
loc 22
rs 8.8333
c 0
b 0
f 0
1
<?php
2
/**
3
 * TProcessHelper class file
4
 *
5
 * @author Brad Anderson <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 */
9
10
namespace Prado\Util\Helpers;
11
12
use Prado\Exceptions\TNotSupportedException;
13
use Prado\Prado;
0 ignored issues
show
Bug introduced by
The type Prado\Prado was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use Prado\TComponent;
15
use Prado\TEventParameter;
16
use Prado\Util\Behaviors\TCaptureForkLog;
17
use Prado\Util\TSignalsDispatcher;
0 ignored issues
show
Bug introduced by
The type Prado\Util\TSignalsDispatcher was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
19
/**
20
 * TProcessHelper class.
21
 *
22
 * This class handles process related functions.
23
 *
24
 * {@see self::isSystemWindows()} is used to determine if the PHP system is Windows
25
 * or not.
26
 *
27
 * {@see self::isForkable()} can be used if the system supports forking of the current
28
 * process.  {@see self::fork()} is used to fork the current process, where supported.
29
 * When forking, `fxPrepareForFork` {@see self::FX_PREPARE_FOR_FORK} is raised before
30
 * forking and `fxRestoreAfterFork` {@see self::FX_RESTORE_AFTER_FORK} is raised after
31
 * forking.  When $captureForkLog (fork parameter) is true, a {@see \Prado\Util\Behaviors\TCaptureForkLog}
32
 * behavior is attached to the {@see \Prado\TApplication} object.  All forked child
33
 * processes ensure the {@see \Prado\Util\TSignalsDispatcher} behavior is attached
34
 * to the TApplication object to allow for graceful termination on exiting signals.
35
 *
36
 * When filtering commands for popen and proc_open, {@see self::filterCommand()} will
37
 * replace '@php' with PHP_BINARY and wrap Windows commands with double quotes.
38
 * Individual arguments can be properly shell escaped with {@see self::escapeShellArg()}.
39
 *
40
 * Linux Process signals can be sent with {@see self::sendSignal()} to the current
41
 * pid or child pid.  To kill a child pid, call {@see self::kill()}.  {@see self::isRunning}
42
 * can determine if a child process is still running.
43
 *
44
 * System Process priority can be retrieved and set with {@see self::getProcessPriority()}
45
 * and {@see self::setProcessPriority()}, respectively.
46
 *
47
 * @author Brad Anderson <[email protected]>
48
 * @since 4.2.3
49
 */
50
class TProcessHelper
51
{
52
	/** @var string When running a Pipe or Process, this is replaced with PHP_BINARY */
53
	public const PHP_COMMAND = "@php";
54
55
	/** @var string The global event prior to forking a process. */
56
	public const FX_PREPARE_FOR_FORK = 'fxPrepareForFork';
57
58
	/** @var string The global event after forking a process. */
59
	public const FX_RESTORE_AFTER_FORK = 'fxRestoreAfterFork';
60
61
	/**
62
	 * Linux priority is -20 (for "real time") to 19 (for idle).
63
	 * The WINDOWS_*_PRIORITY is what the windows priority would map into the PRADO
64
	 * and linux priority numbering.  Windows will only have these priorities.
65
	 */
66
	public const WINDOWS_IDLE_PRIORITY = 19;
67
	public const WINDOWS_BELOW_NORMAL_PRIORITY = 8;
68
	public const WINDOWS_NORMAL_PRIORITY = 0;
69
	public const WINDOWS_ABOVE_NORMAL_PRIORITY = -5;
70
	public const WINDOWS_HIGH_PRIORITY = -10;
71
	public const WINDOWS_REALTIME_PRIORITY = -17;
72
73
	/**
74
	 * Checks if the system that PHP is run on is Windows.
75
	 * @return bool Is the system Windows.
76
	 */
77
	public static function isSystemWindows(): bool
78
	{
79
		static $isWindows = null;
80
		if ($isWindows === null) {
81
			$isWindows = strncasecmp(php_uname('s'), 'win', 3) === 0;
82
		}
83
		return $isWindows;
84
	}
85
86
	/**
87
	 * Checks if the system that PHP is run on is MacOS (Darwin).
88
	 * @return bool Is the system MacOS (Darwin).
89
	 */
90
	public static function isSystemMacOS(): bool
91
	{
92
		static $isDarwin = null;
93
		if ($isDarwin === null) {
94
			$isDarwin = strncasecmp(php_uname('s'), 'darwin', 6) === 0;
95
		}
96
		return $isDarwin;
97
	}
98
99
	/**
100
	 * Checks if the system that PHP is run on is Linux.
101
	 * @return bool Is the system Linux.
102
	 */
103
	public static function isSystemLinux(): bool
104
	{
105
		static $isLinux = null;
106
		if ($isLinux === null) {
107
			$isLinux = strncasecmp(php_uname('s'), 'linux', 5) === 0;
108
		}
109
		return $isLinux;
110
	}
111
112
	/**
113
	 * @return bool Can PHP fork the process.
114
	 */
115
	public static function isForkable(): bool
116
	{
117
		return function_exists('pcntl_fork');
118
	}
119
120
	/**
121
	 * This forks the current process.  When specified, it will install a {@see \Prado\Util\Behaviors\TCaptureForkLog}.
122
	 * Before forking, `fxPrepareForFork` is raised and after forking `fxRestoreAfterFork` is raised.
123
	 *
124
	 * `fxPrepareForFork` handlers should return null or an array of data that it will
125
	 * receive in `fxRestoreAfterFork`.
126
	 * ```php
127
	 *	public function fxPrepareForFork ($sender, $param) {
128
	 *		return ['mydata' => 'value'];
129
	 *	}
130
	 *
131
	 *	public function fxRestoreAfterFork ($sender, $param) {
132
	 *		$param['mydata'] === 'value';
133
	 *		$param['pid'];
134
	 *	}
135
	 * ```
136
	 * @param bool $captureForkLog Installs {@see \Prado\Util\Behaviors\TCaptureForkLog} behavior on the application
137
	 *  so the fork log is stored by the forking process.  Default false.
138
	 * @throws TNotSupportedException When PHP Forking `pcntl_fork` is not supporting
139
	 * @return int The Child Process ID.  For children processes, they receive 0.  Failure is -1.
140
	 */
141
	public static function fork(bool $captureForkLog = false): int
142
	{
143
		if (!static::isForkable()) {
144
			throw new TNotSupportedException('processhelper_no_forking');
145
		}
146
		$app = Prado::getApplication();
147
		if ($captureForkLog && !$app->asa(TCaptureForkLog::class)) {
148
			$app->attachBehavior(TCaptureForkLog::BEHAVIOR_NAME, TCaptureForkLog::class);
149
		}
150
		$responses = $app->raiseEvent(static::FX_PREPARE_FOR_FORK, $app, null);
151
		$restore = array_merge(...$responses);
152
		$restore['pid'] = $pid = pcntl_fork();
153
		$app->raiseEvent(static::FX_RESTORE_AFTER_FORK, $app, $restore);
154
		if ($pid > 0) {
155
			Prado::info("Fork child: $pid", static::class);
156
		} elseif ($pid === 0) {
157
			Prado::info("Executing child fork", static::class);
158
			TSignalsDispatcher::singleton();
159
		} elseif ($pid === -1) {
160
			Prado::notice("failed fork", static::class);
161
		}
162
		return $pid;
163
	}
164
165
	/**
166
	 * If the exitCode is an exit code, returns the exit Status.
167
	 * @param int $exitCode
168
	 * @return int The exit Status
169
	 */
170
	public static function exitStatus(int $exitCode): int
171
	{
172
		if (function_exists('pcntl_wifexited') && pcntl_wifexited($exitCode)) {
173
			$exitCode = pcntl_wexitstatus($exitCode);
174
		}
175
		return $exitCode;
176
	}
177
178
	/**
179
	 * Filters a {@see popen} or {@see proc_open} command.
180
	 * The string "@php" is replaced by {@see PHP_BINARY} and in Windows the string
181
	 * is surrounded by double quotes.
182
	 *
183
	 * @param mixed $command
184
	 */
185
	public static function filterCommand($command)
186
	{
187
		$command = str_replace(static::PHP_COMMAND, PHP_BINARY, $command);
188
189
		if (TProcessHelper::isSystemWindows()) {
190
			if (is_string($command)) {
191
				$command = '"' . $command . '"';  //Windows, better command support
192
			}
193
		}
194
		return $command;
195
	}
196
197
	/**
198
	 * Sends a process signal on posix or linux systems.
199
	 * @param int $signal The signal to be sent.
200
	 * @param ?int $pid The process to send the signal, default null for the current
201
	 *   process.
202
	 * @throws TNotSupportedException When running on Windows.
203
	 */
204
	public static function sendSignal(int $signal, ?int $pid = null): bool
205
	{
206
		if (static::isSystemWindows()) {
207
			throw new TNotSupportedException('processhelper_no_signals');
208
		}
209
		if ($pid === null) {
210
			$pid = getmypid();
211
		}
212
		if (function_exists("posix_kill")) {
213
			return posix_kill($pid, $signal);
214
		}
215
		exec("/usr/bin/kill -s $signal $pid 2>&1", $output, $return_code);
216
		return !$return_code;
217
	}
218
219
	/**
220
	 * Kills a process.
221
	 * @param int $pid The PID to kill.
222
	 * @return bool Was the signal successfully sent.
223
	 */
224
	public static function kill(int $pid): bool
225
	{
226
		if (static::isSystemWindows()) {
227
			return shell_exec("taskkill /F /PID $pid") !== null;
228
		}
229
		return static::sendSignal(SIGKILL, $pid);
230
	}
231
232
	/**
233
	 * @param int $pid The Process ID to check if it is running.
234
	 * @return bool Is the PID running.
235
	 */
236
	public static function isRunning(int $pid): bool
237
	{
238
		if (static::isSystemWindows()) {
239
			$out = [];
240
			exec("TASKLIST /FO LIST /FI \"PID eq $pid\"", $out);
241
			return count($out) > 1;
242
		}
243
244
		return static::sendSignal(0, $pid);
245
	}
246
247
	/**
248
	 * @param ?int $pid The process id to get the priority of, default null for current
249
	 *   process.
250
	 * @return ?int The priority of the process.
251
	 */
252
	public static function getProcessPriority(?int $pid = null): ?int
253
	{
254
		if ($pid === null) {
255
			$pid = getmypid();
256
		}
257
		if (static::isSystemWindows()) {
258
			$output = shell_exec("wmic process where ProcessId={$pid} get priority");
259
			preg_match('/^\s*Priority\s*\r?\n\s*(\d+)/m', $output, $matches);
260
			if (isset($matches[1])) {
261
				$priorityValues = [ // Map Windows Priority Numbers to Linux style Numbers
262
					TProcessWindowsPriority::Idle => static::WINDOWS_IDLE_PRIORITY,
263
					TProcessWindowsPriority::BelowNormal => static::WINDOWS_BELOW_NORMAL_PRIORITY,
264
					TProcessWindowsPriority::Normal => static::WINDOWS_NORMAL_PRIORITY,
265
					TProcessWindowsPriority::AboveNormal => static::WINDOWS_ABOVE_NORMAL_PRIORITY,
266
					TProcessWindowsPriority::HighPriority => static::WINDOWS_HIGH_PRIORITY,
267
					TProcessWindowsPriority::Realtime => static::WINDOWS_REALTIME_PRIORITY,
268
				];
269
				return $priorityValues[$matches[1]] ?? null;
270
			} else {
271
				return null;
272
			}
273
		} else {
274
			if (strlen($priority = trim(shell_exec('exec ps -o nice= -p ' . $pid)))) {
275
				return (int) $priority;
276
			}
277
			return null;
278
		}
279
	}
280
281
	/**
282
	 * In linux systems, the priority can only go up (and have less priority).
283
	 * @param int $priority The priority of the PID.
284
	 * @param ?int $pid The PID to change the priority, default null for current process.
285
	 * @return bool Was successful.
286
	 */
287
	public static function setProcessPriority(int $priority, ?int $pid = null): bool
288
	{
289
		if ($pid === null) {
290
			$pid = getmypid();
291
		}
292
		if (static::isSystemWindows()) {
293
			$priorityValues = [ // The priority cap to windows text priority.
294
				-15 => TProcessWindowsPriorityName::Realtime,
295
				-10 => TProcessWindowsPriorityName::HighPriority,
296
				-5 => TProcessWindowsPriorityName::AboveNormal,
297
				4 => TProcessWindowsPriorityName::Normal,
298
				9 => TProcessWindowsPriorityName::BelowNormal,
299
				PHP_INT_MAX => TProcessWindowsPriorityName::Idle,
300
			];
301
			foreach($priorityValues as $keyPriority => $priorityName) {
302
				if ($priority <= $keyPriority) {
303
					break;
304
				}
305
			}
306
			$command = "wmic process where ProcessId={$pid} CALL setpriority \"$priorityName\"";
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $priorityName seems to be defined by a foreach iteration on line 301. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
307
			$result = shell_exec($command);
308
			if (strpos($result, 'successful') !== false) {
309
				return true;
310
			}
311
			if (!preg_match('/ReturnValue\s*=\s*(\d+);/m', $result, $matches)) {
312
				return false;
313
			}
314
			return $matches[1] === 0;
315
		} else {
316
			if (static::isSystemMacOS()) {
317
				if (($pp = static::getProcessPriority($pid)) === null) {
318
					return false;
319
				}
320
321
				$priority -= $pp;
322
			}
323
324
			$result = shell_exec("exec renice -n $priority -p $pid");
325
326
			// On MacOS, working properly consists of returning nothing.
327
			//	only errors return "renice: 40812: setpriority: Permission denied" (when lowering priority without permission)
328
			//		(Lowering the priority is increasing its importance)
329
			// On the github linux test system it return "3539 (process ID) old priority 0, new priority 8"
330
			//	for an error, it return: "renice: failed to set priority for 3612 (process ID): Permission denied"
331
			if (is_string($result) && str_contains($result, 'denied')) {
332
				return false;
333
			}
334
335
			return true;
336
		}
337
	}
338
339
	/**
340
	 * Escapes a string to be used as a shell argument.
341
	 * @param string $argument
342
	 * @return string
343
	 */
344
	public static function escapeShellArg(string $argument): string
345
	{
346
		// Fix for PHP bug #43784 escapeshellarg removes % from given string
347
		// Fix for PHP bug #49446 escapeshellarg doesn't work on Windows
348
		// @see https://bugs.php.net/bug.php?id=43784
349
		// @see https://bugs.php.net/bug.php?id=49446
350
		if (static::isSystemWindows()) {
351
			if ($argument === '') {
352
				return '""';
353
			}
354
355
			$escapedArgument = '';
356
			$addQuote = false;
357
358
			foreach (preg_split('/(")/', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) {
359
				if ($part === '"') {
360
					$escapedArgument .= '\\"';
361
				} elseif (static::isSurroundedBy($part, '%')) {
362
					// environment variables
363
					$escapedArgument .= '^%"' . substr($part, 1, -1) . '"^%';
364
				} else {
365
					// escape trailing backslash
366
					if (str_ends_with($part, '\\')) {
367
						$part .= '\\';
368
					}
369
					$addQuote = true;
370
					$escapedArgument .= $part;
371
				}
372
			}
373
374
			if ($addQuote) {
0 ignored issues
show
introduced by
The condition $addQuote is always false.
Loading history...
375
				$escapedArgument = '"' . $escapedArgument . '"';
376
			}
377
378
			return $escapedArgument;
379
		}
380
381
		return "'" . str_replace("'", "'\\''", $argument) . "'";
382
	}
383
384
	/**
385
	 * Is the string surrounded by the prefix and reversed in appendix.
386
	 * @param string $string
387
	 * @param string $prefix
388
	 * @return bool Is the string surrounded by the string
389
	 */
390
	public static function isSurroundedBy(string $string, string $prefix): bool
391
	{
392
		$len = strlen($prefix);
393
		return strlen($string) >= 2 * $len && str_starts_with($string, $prefix) && str_ends_with($string, strrev($prefix));
394
	}
395
}
396