PocketMine-MP 5.15.1 git-5ef247620a7c6301a849b54e5ef1009217729fc8
ConsoleReaderChildProcessDaemon.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\console;
25
28use Symfony\Component\Filesystem\Path;
29use function base64_encode;
30use function fgets;
31use function fopen;
32use function preg_replace;
33use function proc_close;
34use function proc_open;
35use function proc_terminate;
36use function sprintf;
37use function stream_select;
38use function stream_socket_accept;
39use function stream_socket_get_name;
40use function stream_socket_server;
41use function stream_socket_shutdown;
42use function trim;
43use const PHP_BINARY;
44use const STREAM_SHUT_RDWR;
45
61 private \PrefixedLogger $logger;
63 private $subprocess;
65 private $socket;
66
67 public function __construct(
68 \Logger $logger
69 ){
70 $this->logger = new \PrefixedLogger($logger, "Console Reader Daemon");
71 $this->prepareSubprocess();
72 }
73
74 private function prepareSubprocess() : void{
75 $server = stream_socket_server("tcp://127.0.0.1:0");
76 if($server === false){
77 throw new \RuntimeException("Failed to open console reader socket server");
78 }
79 $address = Utils::assumeNotFalse(stream_socket_get_name($server, false), "stream_socket_get_name() shouldn't return false here");
80
81 //Windows sucks, and likes to corrupt UTF-8 file paths when they travel to the subprocess, so we base64 encode
82 //the path to avoid the problem. This is an abysmally shitty hack, but here we are :(
83 $sub = Utils::assumeNotFalse(proc_open(
84 [PHP_BINARY, '-dopcache.enable_cli=0', '-r', sprintf('require base64_decode("%s", true);', base64_encode(Path::join(__DIR__, 'ConsoleReaderChildProcess.php'))), $address],
85 [
86 2 => fopen("php://stderr", "w"),
87 ],
88 $pipes
89 ), "Something has gone horribly wrong");
90
91 $client = stream_socket_accept($server, 15);
92 if($client === false){
93 throw new AssumptionFailedError("stream_socket_accept() returned false");
94 }
95 stream_socket_shutdown($server, STREAM_SHUT_RDWR);
96
97 $this->subprocess = $sub;
98 $this->socket = $client;
99 }
100
101 private function shutdownSubprocess() : void{
102 //we have no way to signal to the subprocess to shut down gracefully; besides, Windows sucks, and the subprocess
103 //gets stuck in a blocking fgets() read because stream_select() is a hunk of junk (hence the separate process in
104 //the first place).
105 proc_terminate($this->subprocess);
106 proc_close($this->subprocess);
107 stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
108 }
109
110 public function readLine() : ?string{
111 $r = [$this->socket];
112 $w = null;
113 $e = null;
114 if(stream_select($r, $w, $e, 0, 0) === 1){
115 $command = fgets($this->socket);
116 if($command === false){
117 $this->logger->debug("Lost connection to subprocess, restarting (maybe the child process was killed from outside?)");
118 $this->shutdownSubprocess();
119 $this->prepareSubprocess();
120 return null;
121 }
122
123 $command = preg_replace("#\\x1b\\x5b([^\\x1b]*\\x7e|[\\x40-\\x50])#", "", trim($command)) ?? throw new AssumptionFailedError("This regex is assumed to be valid");
124 $command = preg_replace('/[[:cntrl:]]/', '', $command) ?? throw new AssumptionFailedError("This regex is assumed to be valid");
125
126 return $command !== "" ? $command : null;
127 }
128
129 return null;
130 }
131
132 public function quit() : void{
133 $this->shutdownSubprocess();
134 }
135}
static assumeNotFalse(mixed $value, \Closure|string $context="This should never be false")
Definition: Utils.php:623