22declare(strict_types=1);
24namespace pocketmine\crash;
26use Composer\InstalledVersions;
38use Symfony\Component\Filesystem\Path;
39use
function array_map;
40use
function base64_encode;
41use
function error_get_last;
43use
function file_exists;
44use
function file_get_contents;
45use
function get_loaded_extensions;
46use
function json_encode;
50use
function mb_strtoupper;
51use
function microtime;
52use
function ob_end_clean;
53use
function ob_get_contents;
55use
function php_uname;
57use
function phpversion;
58use
function preg_replace;
60use
function str_split;
61use
function str_starts_with;
64use
function zend_version;
65use
function zlib_encode;
66use
const E_COMPILE_ERROR;
67use
const E_CORE_ERROR;
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;
78use
const ZLIB_ENCODING_DEFLATE;
88 private const FORMAT_VERSION = 4;
90 public const PLUGIN_INVOLVEMENT_NONE =
"none";
91 public const PLUGIN_INVOLVEMENT_DIRECT =
"direct";
92 public const PLUGIN_INVOLVEMENT_INDIRECT =
"indirect";
94 public const FATAL_ERROR_MASK =
95 E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR;
98 private string $encodedData;
100 public function __construct(
104 $now = microtime(
true);
107 $this->data->format_version = self::FORMAT_VERSION;
108 $this->data->time = $now;
109 $this->data->uptime = $now - $this->
server->getStartTime();
112 $this->generalData();
113 $this->pluginsData();
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");
121 public function getEncodedData() :
string{
122 return $this->encodedData;
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);
137 $renderer->addLine(
"===END CRASH DUMP===");
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();
148 version: $d->getVersion(),
149 authors: $d->getAuthors(),
150 api: $d->getCompatibleApis(),
151 enabled: $p->isEnabled(),
152 depends: $d->getDepend(),
153 softDepends: $d->getSoftDepend(),
155 load: mb_strtoupper($d->getOrder()->name),
156 website: $d->getWebsite()
162 private function extraData() :
void{
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");
170 if(($pocketmineDotYml = @file_get_contents(Path::join($this->
server->getDataPath(),
"pocketmine.yml"))) !==
false){
171 $this->data->pocketmineDotYml = $pocketmineDotYml;
175 foreach(get_loaded_extensions() as $ext){
176 $version = phpversion($ext);
177 $extensions[$ext] = $version !==
false ? $version :
"**UNKNOWN**";
179 $this->data->extensions = $extensions;
183 if($this->
server->getConfigGroup()->getPropertyBool(YmlServerProperties::AUTO_REPORT_SEND_PHPINFO,
true)){
186 $this->data->phpinfo = ob_get_contents();
191 private function baseCrash() :
void{
192 global $lastExceptionError, $lastError;
194 if(isset($lastExceptionError)){
195 $error = $lastExceptionError;
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()?");
202 $error[
"fullFile"] = $error[
"file"];
203 $error[
"file"] = Filesystem::cleanPath($error[
"file"]);
206 }
catch(\InvalidArgumentException $e){
209 if(($pos = strpos($error[
"message"],
"\n")) !==
false){
210 $error[
"message"] = substr($error[
"message"], 0, $pos);
212 $error[
"thread"] =
"Main";
214 $error[
"message"] = mb_scrub($error[
"message"],
'UTF-8');
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());
222 $this->data->error = $error;
223 unset($this->data->error[
"fullFile"]);
224 unset($this->data->error[
"trace"]);
226 $this->data->plugin_involvement = self::PLUGIN_INVOLVEMENT_NONE;
227 if(!$this->determinePluginFromFile($error[
"fullFile"],
true)){
228 foreach($error[
"trace"] as $frame){
229 $frameFile = $frame->getFile();
230 if($frameFile ===
null){
233 if($this->determinePluginFromFile($frameFile,
false)){
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);
242 for($l = max(0, $error[
"line"] - 10); $l < $error[
"line"] + 10 && isset($file[$l]); ++$l){
243 $this->data->code[$l + 1] = $file[$l];
248 $this->data->trace = array_map(array: $error[
"trace"], callback: fn(
ThreadCrashInfoFrame $frame) => $frame->getPrintableFrame());
249 $this->data->thread = $error[
"thread"];
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)){
256 $this->data->plugin_involvement = self::PLUGIN_INVOLVEMENT_DIRECT;
258 $this->data->plugin_involvement = self::PLUGIN_INVOLVEMENT_INDIRECT;
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();
277 private function generalData() :
void{
278 $composerLibraries = [];
279 foreach(InstalledVersions::getInstalledPackages() as $package){
280 $composerLibraries[$package] = sprintf(
282 InstalledVersions::getPrettyVersion($package) ??
"unknown",
283 InstalledVersions::getReference($package) ??
"unknown"
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"),
296 zend: zend_version(),
299 composer_libraries: $composerLibraries,
static get(int $errorType)
static assumeNotFalse(mixed $value, \Closure|string $context="This should never be false")
static getOS(bool $recalculate=false)
static currentTrace(int $skipFrames=0)
static getOpcacheJitMode()
static printableTrace(array $trace, int $maxStringLength=80)