PocketMine-MP 5.15.1 git-5ef247620a7c6301a849b54e5ef1009217729fc8
CrashDump.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\crash;
25
26use Composer\InstalledVersions;
38use Symfony\Component\Filesystem\Path;
39use function array_map;
40use function base64_encode;
41use function error_get_last;
42use function file;
43use function file_exists;
44use function file_get_contents;
45use function get_loaded_extensions;
46use function json_encode;
47use function ksort;
48use function max;
49use function mb_scrub;
50use function mb_strtoupper;
51use function microtime;
52use function ob_end_clean;
53use function ob_get_contents;
54use function ob_start;
55use function php_uname;
56use function phpinfo;
57use function phpversion;
58use function preg_replace;
59use function sprintf;
60use function str_split;
61use function str_starts_with;
62use function strpos;
63use function substr;
64use function zend_version;
65use function zlib_encode;
66use const E_COMPILE_ERROR;
67use const E_CORE_ERROR;
68use const E_ERROR;
69use const E_PARSE;
70use const E_RECOVERABLE_ERROR;
71use const E_USER_ERROR;
72use const FILE_IGNORE_NEW_LINES;
73use const JSON_THROW_ON_ERROR;
74use const JSON_UNESCAPED_SLASHES;
75use const PHP_OS;
76use const PHP_VERSION;
77use const SORT_STRING;
78use const ZLIB_ENCODING_DEFLATE;
79
81
88 private const FORMAT_VERSION = 4;
89
90 public const PLUGIN_INVOLVEMENT_NONE = "none";
91 public const PLUGIN_INVOLVEMENT_DIRECT = "direct";
92 public const PLUGIN_INVOLVEMENT_INDIRECT = "indirect";
93
94 public const FATAL_ERROR_MASK =
95 E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR;
96
97 private CrashDumpData $data;
98 private string $encodedData;
99
100 public function __construct(
101 private Server $server,
102 private ?PluginManager $pluginManager
103 ){
104 $now = microtime(true);
105
106 $this->data = new CrashDumpData();
107 $this->data->format_version = self::FORMAT_VERSION;
108 $this->data->time = $now;
109 $this->data->uptime = $now - $this->server->getStartTime();
110
111 $this->baseCrash();
112 $this->generalData();
113 $this->pluginsData();
114
115 $this->extraData();
116
117 $json = json_encode($this->data, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
118 $this->encodedData = Utils::assumeNotFalse(zlib_encode($json, ZLIB_ENCODING_DEFLATE, 9), "ZLIB compression failed");
119 }
120
121 public function getEncodedData() : string{
122 return $this->encodedData;
123 }
124
125 public function getData() : CrashDumpData{
126 return $this->data;
127 }
128
129 public function encodeData(CrashDumpRenderer $renderer) : void{
130 $renderer->addLine();
131 $renderer->addLine("----------------------REPORT THE DATA BELOW THIS LINE-----------------------");
132 $renderer->addLine();
133 $renderer->addLine("===BEGIN CRASH DUMP===");
134 foreach(str_split(base64_encode($this->encodedData), 76) as $line){
135 $renderer->addLine($line);
136 }
137 $renderer->addLine("===END CRASH DUMP===");
138 }
139
140 private function pluginsData() : void{
141 if($this->pluginManager !== null){
142 $plugins = $this->pluginManager->getPlugins();
143 ksort($plugins, SORT_STRING);
144 foreach($plugins as $p){
145 $d = $p->getDescription();
146 $this->data->plugins[$d->getName()] = new CrashDumpDataPluginEntry(
147 name: $d->getName(),
148 version: $d->getVersion(),
149 authors: $d->getAuthors(),
150 api: $d->getCompatibleApis(),
151 enabled: $p->isEnabled(),
152 depends: $d->getDepend(),
153 softDepends: $d->getSoftDepend(),
154 main: $d->getMain(),
155 load: mb_strtoupper($d->getOrder()->name),
156 website: $d->getWebsite()
157 );
158 }
159 }
160 }
161
162 private function extraData() : void{
163 global $argv;
164
165 if($this->server->getConfigGroup()->getPropertyBool(YmlServerProperties::AUTO_REPORT_SEND_SETTINGS, true)){
166 $this->data->parameters = (array) $argv;
167 if(($serverDotProperties = @file_get_contents(Path::join($this->server->getDataPath(), "server.properties"))) !== false){
168 $this->data->serverDotProperties = preg_replace("#^rcon\\.password=(.*)$#m", "rcon.password=******", $serverDotProperties) ?? throw new AssumptionFailedError("Pattern is valid");
169 }
170 if(($pocketmineDotYml = @file_get_contents(Path::join($this->server->getDataPath(), "pocketmine.yml"))) !== false){
171 $this->data->pocketmineDotYml = $pocketmineDotYml;
172 }
173 }
174 $extensions = [];
175 foreach(get_loaded_extensions() as $ext){
176 $version = phpversion($ext);
177 $extensions[$ext] = $version !== false ? $version : "**UNKNOWN**";
178 }
179 $this->data->extensions = $extensions;
180
181 $this->data->jit_mode = Utils::getOpcacheJitMode();
182
183 if($this->server->getConfigGroup()->getPropertyBool(YmlServerProperties::AUTO_REPORT_SEND_PHPINFO, true)){
184 ob_start();
185 phpinfo();
186 $this->data->phpinfo = ob_get_contents(); // @phpstan-ignore-line
187 ob_end_clean();
188 }
189 }
190
191 private function baseCrash() : void{
192 global $lastExceptionError, $lastError;
193
194 if(isset($lastExceptionError)){
195 $error = $lastExceptionError;
196 }else{
197 $error = error_get_last();
198 if($error === null || ($error["type"] & self::FATAL_ERROR_MASK) === 0){
199 throw new \RuntimeException("Crash error information missing - did something use exit()?");
200 }
201 $error["trace"] = Utils::printableTrace(Utils::currentTrace(3)); //Skipping CrashDump->baseCrash, CrashDump->construct, Server->crashDump
202 $error["fullFile"] = $error["file"];
203 $error["file"] = Filesystem::cleanPath($error["file"]);
204 try{
205 $error["type"] = ErrorTypeToStringMap::get($error["type"]);
206 }catch(\InvalidArgumentException $e){
207 //pass
208 }
209 if(($pos = strpos($error["message"], "\n")) !== false){
210 $error["message"] = substr($error["message"], 0, $pos);
211 }
212 $error["thread"] = "Main";
213 }
214 $error["message"] = mb_scrub($error["message"], 'UTF-8');
215
216 if(isset($lastError)){
217 $this->data->lastError = $lastError;
218 $this->data->lastError["message"] = mb_scrub($this->data->lastError["message"], 'UTF-8');
219 $this->data->lastError["trace"] = array_map(array: $lastError["trace"], callback: fn(ThreadCrashInfoFrame $frame) => $frame->getPrintableFrame());
220 }
221
222 $this->data->error = $error;
223 unset($this->data->error["fullFile"]);
224 unset($this->data->error["trace"]);
225
226 $this->data->plugin_involvement = self::PLUGIN_INVOLVEMENT_NONE;
227 if(!$this->determinePluginFromFile($error["fullFile"], true)){ //fatal errors won't leave any stack trace
228 foreach($error["trace"] as $frame){
229 $frameFile = $frame->getFile();
230 if($frameFile === null){
231 continue; //PHP core
232 }
233 if($this->determinePluginFromFile($frameFile, false)){
234 break;
235 }
236 }
237 }
238
239 if($this->server->getConfigGroup()->getPropertyBool(YmlServerProperties::AUTO_REPORT_SEND_CODE, true) && file_exists($error["fullFile"])){
240 $file = @file($error["fullFile"], FILE_IGNORE_NEW_LINES);
241 if($file !== false){
242 for($l = max(0, $error["line"] - 10); $l < $error["line"] + 10 && isset($file[$l]); ++$l){
243 $this->data->code[$l + 1] = $file[$l];
244 }
245 }
246 }
247
248 $this->data->trace = array_map(array: $error["trace"], callback: fn(ThreadCrashInfoFrame $frame) => $frame->getPrintableFrame());
249 $this->data->thread = $error["thread"];
250 }
251
252 private function determinePluginFromFile(string $filePath, bool $crashFrame) : bool{
253 $frameCleanPath = Filesystem::cleanPath($filePath);
254 if(!str_starts_with($frameCleanPath, Filesystem::CLEAN_PATH_SRC_PREFIX)){
255 if($crashFrame){
256 $this->data->plugin_involvement = self::PLUGIN_INVOLVEMENT_DIRECT;
257 }else{
258 $this->data->plugin_involvement = self::PLUGIN_INVOLVEMENT_INDIRECT;
259 }
260
261 if(file_exists($filePath)){
262 $reflection = new \ReflectionClass(PluginBase::class);
263 $file = $reflection->getProperty("file");
264 foreach($this->server->getPluginManager()->getPlugins() as $plugin){
265 $filePath = Filesystem::cleanPath($file->getValue($plugin));
266 if(str_starts_with($frameCleanPath, $filePath)){
267 $this->data->plugin = $plugin->getName();
268 break;
269 }
270 }
271 }
272 return true;
273 }
274 return false;
275 }
276
277 private function generalData() : void{
278 $composerLibraries = [];
279 foreach(InstalledVersions::getInstalledPackages() as $package){
280 $composerLibraries[$package] = sprintf(
281 "%s@%s",
282 InstalledVersions::getPrettyVersion($package) ?? "unknown",
283 InstalledVersions::getReference($package) ?? "unknown"
284 );
285 }
286
287 $this->data->general = new CrashDumpDataGeneral(
288 name: $this->server->getName(),
289 base_version: VersionInfo::BASE_VERSION,
290 build: VersionInfo::BUILD_NUMBER(),
291 is_dev: VersionInfo::IS_DEVELOPMENT_BUILD,
293 git: VersionInfo::GIT_HASH(),
294 uname: php_uname("a"),
295 php: PHP_VERSION,
296 zend: zend_version(),
297 php_os: PHP_OS,
298 os: Utils::getOS(),
299 composer_libraries: $composerLibraries,
300 );
301 }
302}
static assumeNotFalse(mixed $value, \Closure|string $context="This should never be false")
Definition: Utils.php:623
static getOS(bool $recalculate=false)
Definition: Utils.php:275
static currentTrace(int $skipFrames=0)
Definition: Utils.php:502
static getOpcacheJitMode()
Definition: Utils.php:657
static printableTrace(array $trace, int $maxStringLength=80)
Definition: Utils.php:449