2017-07-25 22:11:02 +02:00
|
|
|
<?php
|
2018-11-06 03:57:36 +01:00
|
|
|
namespace Psalm\Internal\Fork;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2019-07-05 22:24:00 +02:00
|
|
|
use function array_fill_keys;
|
|
|
|
use function array_keys;
|
|
|
|
use function array_pop;
|
|
|
|
use function array_values;
|
|
|
|
use function base64_decode;
|
|
|
|
use function base64_encode;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function count;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function error_log;
|
|
|
|
use function explode;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function extension_loaded;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function fclose;
|
|
|
|
use function feof;
|
|
|
|
use function fread;
|
|
|
|
use function fwrite;
|
|
|
|
use function gettype;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function ini_get;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function intval;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function pcntl_fork;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function pcntl_waitpid;
|
|
|
|
use function pcntl_wexitstatus;
|
|
|
|
use function pcntl_wifsignaled;
|
|
|
|
use function pcntl_wtermsig;
|
|
|
|
use const PHP_EOL;
|
|
|
|
use const PHP_VERSION;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function posix_get_last_error;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function posix_kill;
|
|
|
|
use function posix_strerror;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function serialize;
|
2019-07-05 22:24:00 +02:00
|
|
|
use const SIGALRM;
|
|
|
|
use const STREAM_IPPROTO_IP;
|
|
|
|
use const STREAM_PF_UNIX;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function stream_select;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function stream_set_blocking;
|
|
|
|
use const STREAM_SOCK_STREAM;
|
|
|
|
use function stream_socket_pair;
|
|
|
|
use function strlen;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function strpos;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function substr;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function unserialize;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function usleep;
|
|
|
|
use function version_compare;
|
2019-06-26 22:52:29 +02:00
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
/**
|
|
|
|
* Adapted with relatively few changes from
|
|
|
|
* https://github.com/etsy/phan/blob/1ccbe7a43a6151ca7c0759d6c53e2c3686994e53/src/Phan/ForkPool.php
|
|
|
|
*
|
|
|
|
* Authors: https://github.com/morria, https://github.com/TysonAndre
|
|
|
|
*
|
|
|
|
* Fork off to n-processes and divide up tasks between
|
|
|
|
* each process.
|
|
|
|
*/
|
|
|
|
class Pool
|
|
|
|
{
|
|
|
|
const EXIT_SUCCESS = 1;
|
|
|
|
const EXIT_FAILURE = 0;
|
|
|
|
|
|
|
|
/** @var int[] */
|
|
|
|
private $child_pid_list = [];
|
|
|
|
|
|
|
|
/** @var resource[] */
|
|
|
|
private $read_streams = [];
|
|
|
|
|
|
|
|
/** @var bool */
|
|
|
|
private $did_have_error = false;
|
|
|
|
|
2019-09-22 07:03:42 +02:00
|
|
|
/** @var ?\Closure(mixed): void */
|
2019-05-30 16:30:41 +02:00
|
|
|
private $task_done_closure;
|
|
|
|
|
2019-06-27 16:51:13 +02:00
|
|
|
public const MAC_PCRE_MESSAGE = 'Mac users: pcre.jit is set to 1 in your PHP config.' . PHP_EOL
|
|
|
|
. 'The pcre jit is known to cause segfaults in PHP 7.3 on Macs, and Psalm' . PHP_EOL
|
|
|
|
. 'will not execute in threaded mode to avoid indecipherable errors.' . PHP_EOL
|
|
|
|
. 'Consider adding pcre.jit=0 to your PHP config.' . PHP_EOL
|
|
|
|
. 'Relevant info: https://bugs.php.net/bug.php?id=77260';
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
/**
|
2019-06-16 00:24:43 +02:00
|
|
|
* @param array<int, array<int, mixed>> $process_task_data_iterator
|
2017-07-25 22:11:02 +02:00
|
|
|
* An array of task data items to be divided up among the
|
|
|
|
* workers. The size of this is the number of forked processes.
|
|
|
|
* @param \Closure $startup_closure
|
|
|
|
* A closure to execute upon starting a child
|
2019-06-16 00:24:43 +02:00
|
|
|
* @param \Closure(int, mixed):mixed $task_closure
|
2017-07-25 22:11:02 +02:00
|
|
|
* A method to execute on each task data.
|
|
|
|
* This closure must return an array (to be gathered).
|
2019-06-16 00:24:43 +02:00
|
|
|
* @param \Closure():mixed $shutdown_closure
|
2017-07-25 22:11:02 +02:00
|
|
|
* A closure to execute upon shutting down a child
|
2019-06-16 00:24:43 +02:00
|
|
|
* @param ?\Closure(mixed $data):void $task_done_closure
|
2019-05-30 16:30:41 +02:00
|
|
|
* A closure to execute when a task is done
|
2017-07-25 22:11:02 +02:00
|
|
|
*
|
|
|
|
* @psalm-suppress MixedAssignment
|
|
|
|
*/
|
|
|
|
public function __construct(
|
|
|
|
array $process_task_data_iterator,
|
|
|
|
\Closure $startup_closure,
|
|
|
|
\Closure $task_closure,
|
2019-05-30 16:30:41 +02:00
|
|
|
\Closure $shutdown_closure,
|
|
|
|
?\Closure $task_done_closure = null
|
2017-07-25 22:11:02 +02:00
|
|
|
) {
|
|
|
|
$pool_size = count($process_task_data_iterator);
|
2019-05-30 16:30:41 +02:00
|
|
|
$this->task_done_closure = $task_done_closure;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2017-12-01 01:00:09 +01:00
|
|
|
\assert(
|
|
|
|
$pool_size > 1,
|
|
|
|
'The pool size must be >= 2 to use the fork pool.'
|
|
|
|
);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2019-01-10 06:14:40 +01:00
|
|
|
if (!extension_loaded('pcntl')) {
|
2019-07-05 22:24:00 +02:00
|
|
|
echo
|
2019-01-10 06:14:40 +01:00
|
|
|
'The pcntl extension must be loaded in order for Psalm to be able to use multiple processes.'
|
2019-07-05 22:24:00 +02:00
|
|
|
. PHP_EOL;
|
2019-06-17 22:42:47 +02:00
|
|
|
exit(1);
|
2019-01-10 06:14:40 +01:00
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2019-05-08 17:42:30 +02:00
|
|
|
if (ini_get('pcre.jit') === '1'
|
|
|
|
&& \PHP_OS === 'Darwin'
|
|
|
|
&& version_compare(PHP_VERSION, '7.3.0') >= 0
|
2019-11-30 14:26:26 +01:00
|
|
|
&& version_compare(PHP_VERSION, '7.4.0') < 0
|
2019-05-08 17:42:30 +02:00
|
|
|
) {
|
|
|
|
die(
|
2019-06-27 16:51:13 +02:00
|
|
|
self::MAC_PCRE_MESSAGE . PHP_EOL
|
2019-05-08 17:42:30 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
// We'll keep track of if this is the parent process
|
|
|
|
// so that we can tell who will be doing the waiting
|
|
|
|
$is_parent = false;
|
|
|
|
|
|
|
|
$sockets = [];
|
|
|
|
|
|
|
|
// Fork as many times as requested to get the given
|
|
|
|
// pool size
|
|
|
|
for ($proc_id = 0; $proc_id < $pool_size; ++$proc_id) {
|
|
|
|
// Create an IPC socket pair.
|
|
|
|
$sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
|
|
|
|
if (!$sockets) {
|
|
|
|
error_log('unable to create stream socket pair');
|
|
|
|
exit(self::EXIT_FAILURE);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fork
|
|
|
|
if (($pid = pcntl_fork()) < 0) {
|
|
|
|
error_log(posix_strerror(posix_get_last_error()));
|
|
|
|
exit(self::EXIT_FAILURE);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parent
|
|
|
|
if ($pid > 0) {
|
|
|
|
$is_parent = true;
|
|
|
|
$this->child_pid_list[] = $pid;
|
|
|
|
$this->read_streams[] = self::streamForParent($sockets);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Child
|
|
|
|
if ($pid === 0) {
|
|
|
|
$is_parent = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we're the parent, return
|
|
|
|
if ($is_parent) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the write stream for the child.
|
|
|
|
$write_stream = self::streamForChild($sockets);
|
|
|
|
|
|
|
|
// Execute anything the children wanted to execute upon
|
|
|
|
// starting up
|
|
|
|
$startup_closure();
|
|
|
|
|
|
|
|
// Get the work for this process
|
|
|
|
$task_data_iterator = array_values($process_task_data_iterator)[$proc_id];
|
2019-05-30 22:54:15 +02:00
|
|
|
|
|
|
|
$task_done_buffer = '';
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
foreach ($task_data_iterator as $i => $task_data) {
|
2019-06-16 00:36:45 +02:00
|
|
|
$task_result = $task_closure($i, $task_data);
|
2019-05-30 22:54:15 +02:00
|
|
|
$task_done_message = new ForkTaskDoneMessage($task_result);
|
2019-07-03 22:58:27 +02:00
|
|
|
$serialized_message = $task_done_buffer . base64_encode(serialize($task_done_message)) . "\n";
|
2019-05-30 22:54:15 +02:00
|
|
|
|
2019-05-31 18:14:53 +02:00
|
|
|
if (strlen($serialized_message) > 200) {
|
|
|
|
$bytes_written = @fwrite($write_stream, $serialized_message);
|
|
|
|
|
|
|
|
if (strlen($serialized_message) !== $bytes_written) {
|
|
|
|
$task_done_buffer = substr($serialized_message, $bytes_written);
|
|
|
|
} else {
|
|
|
|
$task_done_buffer = '';
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$task_done_buffer = $serialized_message;
|
2019-05-30 22:54:15 +02:00
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Execute each child's shutdown closure before
|
|
|
|
// exiting the process
|
|
|
|
$results = $shutdown_closure();
|
|
|
|
|
|
|
|
// Serialize this child's produced results and send them to the parent.
|
2019-05-30 22:54:15 +02:00
|
|
|
$process_done_message = new ForkProcessDoneMessage($results ?: []);
|
2019-07-03 22:58:27 +02:00
|
|
|
$serialized_message = $task_done_buffer . base64_encode(serialize($process_done_message)) . "\n";
|
2019-05-30 22:54:15 +02:00
|
|
|
|
|
|
|
$bytes_to_write = strlen($serialized_message);
|
|
|
|
$bytes_written = 0;
|
|
|
|
|
|
|
|
while ($bytes_written < $bytes_to_write) {
|
2019-08-11 23:13:43 +02:00
|
|
|
// attempt to write the remaining unsent part
|
2019-05-30 22:54:15 +02:00
|
|
|
$bytes_written += @fwrite($write_stream, substr($serialized_message, $bytes_written));
|
|
|
|
|
|
|
|
if ($bytes_written < $bytes_to_write) {
|
|
|
|
// wait a bit
|
|
|
|
usleep(500000);
|
|
|
|
}
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
fclose($write_stream);
|
|
|
|
|
|
|
|
// Children exit after completing their work
|
|
|
|
exit(self::EXIT_SUCCESS);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prepare the socket pair to be used in a parent process and
|
|
|
|
* return the stream the parent will use to read results.
|
|
|
|
*
|
|
|
|
* @param resource[] $sockets the socket pair for IPC
|
|
|
|
*
|
|
|
|
* @return resource
|
|
|
|
*/
|
|
|
|
private static function streamForParent(array $sockets)
|
|
|
|
{
|
|
|
|
list($for_read, $for_write) = $sockets;
|
|
|
|
|
|
|
|
// The parent will not use the write channel, so it
|
|
|
|
// must be closed to prevent deadlock.
|
|
|
|
fclose($for_write);
|
|
|
|
|
|
|
|
// stream_select will be used to read multiple streams, so these
|
|
|
|
// must be set to non-blocking mode.
|
|
|
|
if (!stream_set_blocking($for_read, false)) {
|
|
|
|
error_log('unable to set read stream to non-blocking');
|
|
|
|
exit(self::EXIT_FAILURE);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $for_read;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prepare the socket pair to be used in a child process and return
|
|
|
|
* the stream the child will use to write results.
|
|
|
|
*
|
|
|
|
* @param resource[] $sockets the socket pair for IPC
|
|
|
|
*
|
|
|
|
* @return resource
|
|
|
|
*/
|
|
|
|
private static function streamForChild(array $sockets)
|
|
|
|
{
|
|
|
|
list($for_read, $for_write) = $sockets;
|
|
|
|
|
|
|
|
// The while will not use the read channel, so it must
|
|
|
|
// be closed to prevent deadlock.
|
|
|
|
fclose($for_read);
|
|
|
|
|
|
|
|
return $for_write;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Read the results that each child process has serialized on their write streams.
|
|
|
|
* The results are returned in an array, one for each worker. The order of the results
|
|
|
|
* is not maintained.
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*
|
|
|
|
* @psalm-suppress MixedAssignment
|
|
|
|
*/
|
|
|
|
private function readResultsFromChildren()
|
|
|
|
{
|
|
|
|
// Create an array of all active streams, indexed by
|
|
|
|
// resource id.
|
|
|
|
$streams = [];
|
|
|
|
foreach ($this->read_streams as $stream) {
|
|
|
|
$streams[intval($stream)] = $stream;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create an array for the content received on each stream,
|
|
|
|
// indexed by resource id.
|
|
|
|
$content = array_fill_keys(array_keys($streams), '');
|
|
|
|
|
2019-05-30 22:54:15 +02:00
|
|
|
$terminationMessages = [];
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
// Read the data off of all the stream.
|
|
|
|
while (count($streams) > 0) {
|
|
|
|
$needs_read = array_values($streams);
|
|
|
|
$needs_write = null;
|
|
|
|
$needs_except = null;
|
|
|
|
|
|
|
|
// Wait for data on at least one stream.
|
|
|
|
$num = stream_select($needs_read, $needs_write, $needs_except, null /* no timeout */);
|
|
|
|
if ($num === false) {
|
|
|
|
error_log('unable to select on read stream');
|
|
|
|
exit(self::EXIT_FAILURE);
|
|
|
|
}
|
|
|
|
|
|
|
|
// For each stream that was ready, read the content.
|
|
|
|
foreach ($needs_read as $file) {
|
|
|
|
$buffer = fread($file, 1024);
|
2019-05-30 16:30:41 +02:00
|
|
|
if ($buffer !== false) {
|
2017-07-25 22:11:02 +02:00
|
|
|
$content[intval($file)] .= $buffer;
|
|
|
|
}
|
|
|
|
|
2019-07-03 22:58:27 +02:00
|
|
|
if (strpos($buffer, "\n") !== false) {
|
|
|
|
$serialized_messages = explode("\n", $content[intval($file)]);
|
2019-05-30 22:54:15 +02:00
|
|
|
$content[intval($file)] = array_pop($serialized_messages);
|
|
|
|
|
|
|
|
foreach ($serialized_messages as $serialized_message) {
|
2019-07-05 22:24:00 +02:00
|
|
|
$message = unserialize(base64_decode($serialized_message, true));
|
2019-05-30 22:54:15 +02:00
|
|
|
|
|
|
|
if ($message instanceof ForkProcessDoneMessage) {
|
|
|
|
$terminationMessages[] = $message->data;
|
|
|
|
} elseif ($message instanceof ForkTaskDoneMessage) {
|
|
|
|
if ($this->task_done_closure !== null) {
|
|
|
|
($this->task_done_closure)($message->data);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
error_log('Child should return ForkMessage - response type=' . gettype($message));
|
|
|
|
$this->did_have_error = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
// If the stream has closed, stop trying to select on it.
|
|
|
|
if (feof($file)) {
|
2019-05-30 22:54:15 +02:00
|
|
|
if ($content[intval($file)] !== '') {
|
|
|
|
error_log('Child did not send full message before closing the connection');
|
|
|
|
$this->did_have_error = true;
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
fclose($file);
|
|
|
|
unset($streams[intval($file)]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-30 22:54:15 +02:00
|
|
|
return array_values($terminationMessages);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Wait for all child processes to complete
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
2019-03-22 07:14:29 +01:00
|
|
|
public function wait(): array
|
2017-07-25 22:11:02 +02:00
|
|
|
{
|
|
|
|
// Read all the streams from child processes into an array.
|
|
|
|
$content = $this->readResultsFromChildren();
|
|
|
|
|
|
|
|
// Wait for all children to return
|
|
|
|
foreach ($this->child_pid_list as $child_pid) {
|
2019-03-22 07:14:29 +01:00
|
|
|
$process_lookup = posix_kill($child_pid, 0);
|
2018-10-17 21:53:54 +02:00
|
|
|
|
|
|
|
$status = 0;
|
|
|
|
|
|
|
|
if ($process_lookup) {
|
2018-11-18 22:05:36 +01:00
|
|
|
/**
|
|
|
|
* @psalm-suppress UndefinedConstant - does not exist on windows
|
|
|
|
* @psalm-suppress MixedArgument
|
|
|
|
*/
|
2018-10-17 21:53:54 +02:00
|
|
|
posix_kill($child_pid, SIGALRM);
|
|
|
|
|
|
|
|
if (pcntl_waitpid($child_pid, $status) < 0) {
|
|
|
|
error_log(posix_strerror(posix_get_last_error()));
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check to see if the child died a graceful death
|
|
|
|
if (pcntl_wifsignaled($status)) {
|
|
|
|
$return_code = pcntl_wexitstatus($status);
|
|
|
|
$term_sig = pcntl_wtermsig($status);
|
2018-10-15 23:40:42 +02:00
|
|
|
|
2018-11-18 22:05:36 +01:00
|
|
|
/**
|
|
|
|
* @psalm-suppress UndefinedConstant - does not exist on windows
|
|
|
|
*/
|
2018-10-15 23:40:42 +02:00
|
|
|
if ($term_sig !== SIGALRM) {
|
|
|
|
$this->did_have_error = true;
|
|
|
|
error_log("Child terminated with return code $return_code and signal $term_sig");
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $content;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if this had an error, e.g. due to memory limits or due to a child process crashing.
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function didHaveError()
|
|
|
|
{
|
|
|
|
return $this->did_have_error;
|
|
|
|
}
|
|
|
|
}
|