Passed
Pull Request — master (#986)
by
unknown
05:01
created

TProcessHelper::isSurroundedBy()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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