mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-06 09:56:06 +00:00
Allow thread errors and their traces to be properly recorded in crashdumps (#5910)
until now, any thread crash would show as a generic crash since we aren't able to get the trace from the crashed thread directly. This uses some dirty tricks to export a partially serialized stack trace to the main thread, where it can be written into a crashdump. This enables us to see proper crash information for async tasks in the crash archive (finally!!!) as well as being able to capture RakLib errors properly.
This commit is contained in:
@ -39,6 +39,7 @@ use pocketmine\network\NetworkInterfaceStartException;
|
||||
use pocketmine\network\PacketHandlingException;
|
||||
use pocketmine\player\GameMode;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\thread\ThreadCrashException;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\Utils;
|
||||
use raklib\generic\DisconnectReason;
|
||||
@ -154,7 +155,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
|
||||
if(!$this->rakLib->isRunning()){
|
||||
$e = $this->rakLib->getCrashInfo();
|
||||
if($e !== null){
|
||||
throw new \RuntimeException("RakLib crashed: " . $e->makePrettyMessage());
|
||||
throw new ThreadCrashException("RakLib crashed", $e);
|
||||
}
|
||||
throw new \Exception("RakLib Thread crashed without crash information");
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ use pocketmine\snooze\SleeperHandlerEntry;
|
||||
use pocketmine\thread\log\ThreadSafeLogger;
|
||||
use pocketmine\thread\NonThreadSafeValue;
|
||||
use pocketmine\thread\Thread;
|
||||
use pocketmine\thread\ThreadCrashException;
|
||||
use raklib\generic\SocketException;
|
||||
use raklib\server\ipc\RakLibToUserThreadMessageSender;
|
||||
use raklib\server\ipc\UserToRakLibThreadMessageReceiver;
|
||||
@ -37,17 +38,12 @@ use raklib\server\ServerSocket;
|
||||
use raklib\server\SimpleProtocolAcceptor;
|
||||
use raklib\utils\ExceptionTraceCleaner;
|
||||
use raklib\utils\InternetAddress;
|
||||
use function error_get_last;
|
||||
use function gc_enable;
|
||||
use function ini_set;
|
||||
use function register_shutdown_function;
|
||||
|
||||
class RakLibServer extends Thread{
|
||||
protected bool $cleanShutdown = false;
|
||||
protected bool $ready = false;
|
||||
protected string $mainPath;
|
||||
/** @phpstan-var NonThreadSafeValue<RakLibThreadCrashInfo>|null */
|
||||
public ?NonThreadSafeValue $crashInfo = null;
|
||||
/** @phpstan-var NonThreadSafeValue<InternetAddress> */
|
||||
protected NonThreadSafeValue $address;
|
||||
|
||||
@ -69,86 +65,51 @@ class RakLibServer extends Thread{
|
||||
$this->address = new NonThreadSafeValue($address);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function shutdownHandler(){
|
||||
if($this->cleanShutdown !== true && $this->crashInfo === null){
|
||||
$error = error_get_last();
|
||||
|
||||
if($error !== null){
|
||||
$this->logger->emergency("Fatal error: " . $error["message"] . " in " . $error["file"] . " on line " . $error["line"]);
|
||||
$this->setCrashInfo(RakLibThreadCrashInfo::fromLastErrorInfo($error));
|
||||
}else{
|
||||
$this->logger->emergency("RakLib shutdown unexpectedly");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getCrashInfo() : ?RakLibThreadCrashInfo{
|
||||
return $this->crashInfo?->deserialize();
|
||||
}
|
||||
|
||||
private function setCrashInfo(RakLibThreadCrashInfo $info) : void{
|
||||
$this->synchronized(function() use ($info) : void{
|
||||
$this->crashInfo = new NonThreadSafeValue($info);
|
||||
$this->notify();
|
||||
});
|
||||
}
|
||||
|
||||
public function startAndWait(int $options = NativeThread::INHERIT_NONE) : void{
|
||||
$this->start($options);
|
||||
$this->synchronized(function() : void{
|
||||
while(!$this->ready && $this->crashInfo === null){
|
||||
while(!$this->ready && $this->getCrashInfo() === null){
|
||||
$this->wait();
|
||||
}
|
||||
$crashInfo = $this->crashInfo?->deserialize();
|
||||
$crashInfo = $this->getCrashInfo();
|
||||
if($crashInfo !== null){
|
||||
if($crashInfo->getClass() === SocketException::class){
|
||||
if($crashInfo->getType() === SocketException::class){
|
||||
throw new SocketException($crashInfo->getMessage());
|
||||
}
|
||||
throw new \RuntimeException("RakLib failed to start: " . $crashInfo->makePrettyMessage());
|
||||
throw new ThreadCrashException("RakLib failed to start", $crashInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function onRun() : void{
|
||||
try{
|
||||
gc_enable();
|
||||
ini_set("display_errors", '1');
|
||||
ini_set("display_startup_errors", '1');
|
||||
gc_enable();
|
||||
ini_set("display_errors", '1');
|
||||
ini_set("display_startup_errors", '1');
|
||||
|
||||
register_shutdown_function([$this, "shutdownHandler"]);
|
||||
|
||||
try{
|
||||
$socket = new ServerSocket($this->address->deserialize());
|
||||
}catch(SocketException $e){
|
||||
$this->setCrashInfo(RakLibThreadCrashInfo::fromThrowable($e));
|
||||
return;
|
||||
}
|
||||
$manager = new Server(
|
||||
$this->serverId,
|
||||
$this->logger,
|
||||
$socket,
|
||||
$this->maxMtuSize,
|
||||
new SimpleProtocolAcceptor($this->protocolVersion),
|
||||
new UserToRakLibThreadMessageReceiver(new PthreadsChannelReader($this->mainToThreadBuffer)),
|
||||
new RakLibToUserThreadMessageSender(new SnoozeAwarePthreadsChannelWriter($this->threadToMainBuffer, $this->sleeperEntry->createNotifier())),
|
||||
new ExceptionTraceCleaner($this->mainPath)
|
||||
);
|
||||
$this->synchronized(function() : void{
|
||||
$this->ready = true;
|
||||
$this->notify();
|
||||
});
|
||||
while(!$this->isKilled){
|
||||
$manager->tickProcessor();
|
||||
}
|
||||
$manager->waitShutdown();
|
||||
$this->cleanShutdown = true;
|
||||
}catch(\Throwable $e){
|
||||
$this->setCrashInfo(RakLibThreadCrashInfo::fromThrowable($e));
|
||||
$this->logger->logException($e);
|
||||
$socket = new ServerSocket($this->address->deserialize());
|
||||
$manager = new Server(
|
||||
$this->serverId,
|
||||
$this->logger,
|
||||
$socket,
|
||||
$this->maxMtuSize,
|
||||
new SimpleProtocolAcceptor($this->protocolVersion),
|
||||
new UserToRakLibThreadMessageReceiver(new PthreadsChannelReader($this->mainToThreadBuffer)),
|
||||
new RakLibToUserThreadMessageSender(new SnoozeAwarePthreadsChannelWriter($this->threadToMainBuffer, $this->sleeperEntry->createNotifier())),
|
||||
new ExceptionTraceCleaner($this->mainPath)
|
||||
);
|
||||
$this->synchronized(function() : void{
|
||||
$this->ready = true;
|
||||
$this->notify();
|
||||
});
|
||||
while(!$this->isKilled){
|
||||
$manager->tickProcessor();
|
||||
}
|
||||
$manager->waitShutdown();
|
||||
}
|
||||
|
||||
protected function onUncaughtException(\Throwable $e) : void{
|
||||
parent::onUncaughtException($e);
|
||||
$this->logger->logException($e);
|
||||
}
|
||||
|
||||
public function getThreadName() : string{
|
||||
|
@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\raklib;
|
||||
|
||||
use pocketmine\utils\Filesystem;
|
||||
use function get_class;
|
||||
use function sprintf;
|
||||
|
||||
final class RakLibThreadCrashInfo{
|
||||
|
||||
public function __construct(
|
||||
private ?string $class,
|
||||
private string $message,
|
||||
private string $file,
|
||||
private int $line
|
||||
){}
|
||||
|
||||
public static function fromThrowable(\Throwable $e) : self{
|
||||
return new self(get_class($e), $e->getMessage(), $e->getFile(), $e->getLine());
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param array{message: string, file: string, line: int} $info
|
||||
*/
|
||||
public static function fromLastErrorInfo(array $info) : self{
|
||||
return new self(null, $info["message"], $info["file"], $info["line"]);
|
||||
}
|
||||
|
||||
public function getClass() : ?string{ return $this->class; }
|
||||
|
||||
public function getMessage() : string{ return $this->message; }
|
||||
|
||||
public function getFile() : string{ return $this->file; }
|
||||
|
||||
public function getLine() : int{ return $this->line; }
|
||||
|
||||
public function makePrettyMessage() : string{
|
||||
return sprintf("%s: \"%s\" in %s on line %d", $this->class ?? "Fatal error", $this->message, Filesystem::cleanPath($this->file), $this->line);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user