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