Skip to content

Instantly share code, notes, and snippets.

@Muqsit
Created January 1, 2025 18:32
Show Gist options
  • Save Muqsit/149db57543dbe41a0066911ca715019b to your computer and use it in GitHub Desktop.
Save Muqsit/149db57543dbe41a0066911ca715019b to your computer and use it in GitHub Desktop.
PHP: Apply destructor hooks to all class in a .phar file to debug memory leaks
<?php
declare(strict_types=1);
function apply_destructor_hook(string $contents) : ?string{
$destructor_body = PHP_EOL;
$destructor_body .= "echo 'Destroyed ', \$this::class,";
$destructor_body .= "' (', \spl_object_id(\$this), ')...',";
$destructor_body .= "\PHP_EOL,";
$destructor_body .= "(new \Exception)->getTraceAsString(),";
$destructor_body .= "PHP_EOL;";
$destructor_body .= PHP_EOL;
$tokens = PhpToken::tokenize($contents);
$file_offset = 0; // offset of file contents (when we modify file the succeeding contents are offsetted)
$offset = 0; // offset of tokens
$state = "find_class";
while(isset($tokens[$offset])){
$token = $tokens[$offset++];
if($state !== "find_class" && !isset($tokens[$offset])){
$state = "write_destructor";
}
if($state === "find_class"){
if(isset($tokens[$offset - 2]) && $tokens[$offset - 2]->id === T_DOUBLE_COLON){
continue;
}
if($token->id === T_CLASS){
$state_class_begin = null;
$state_existing_destructor = null;
$state = "find_class_begin";
}
}elseif($state === "find_class_begin"){
if($token->text === "{"){
$state_class_begin = $token->pos + 1;
$state = "find_existing_destructor";
}
}elseif($state === "find_existing_destructor"){
if($token->id === T_CLASS){
$state = "write_destructor";
}elseif($token->id === T_FUNCTION){
$state = "check_if_destructor";
}
}elseif($state === "check_if_destructor"){
if($token->id === T_STRING && $token->text === "__destruct"){
$state = "find_existing_destructor_begin";
}
}elseif($state === "find_existing_destructor_begin"){
if($token->text === "{"){
$state_existing_destructor = $token->pos + 1;
$state = "write_destructor";
}
}elseif($state === "write_destructor"){
if($state_existing_destructor !== null){
$contents = (string) substr_replace($contents, $destructor_body, $state_existing_destructor + $file_offset, 0);
$file_offset += strlen($destructor_body);
}elseif($state_class_begin !== null){
$body = PHP_EOL . "public function __destruct(){{$destructor_body}}" . PHP_EOL;
$contents = (string) substr_replace($contents, $body, $state_class_begin + $file_offset, 0);
$file_offset += strlen($body);
}
$state = "find_class";
}
}
return $file_offset > 0 ? $contents : null;
}
function apply_destructor_hooks(string $path, array $ignored_path_matcher) : void{
/** @var SplFileInfo $file */
foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)) as $file){
if($file->getExtension() !== "php"){
continue;
}
$path = $file->getPathname();
foreach($ignored_path_matcher as $ignored_path){
if(fnmatch($ignored_path, $path)){
continue 2;
}
}
$contents = file_get_contents($path);
$contents !== false || throw new RuntimeException("Failed to read file: {$path}");
$contents = apply_destructor_hook($contents);
if($contents !== null){
file_put_contents($path, $contents);
//echo "Changed ", $path, PHP_EOL;
}else{
//echo "No changes for ", $path, PHP_EOL;
}
}
}
$path = $argv[1];
try{
$phar = new Phar($path);
}catch(UnexpectedValueException){
echo "Cannot open phar file: ", $path, PHP_EOL;
exit(1);
}
$current_dir = getcwd();
$current_dir !== false || throw new RuntimeException("Failed to get current directory");
$temp_dir = tempnam($current_dir, "apply-destructor-hooks-");
unlink($temp_dir) || throw new RuntimeException("Failed to create temp path");
mkdir($temp_dir) || throw new RuntimeException("Failed to create temp path");
$phar->extractTo($temp_dir);
apply_destructor_hooks($temp_dir, [
"*Task.php",
"*ResourcePack*.php",
"*AwaitGenerator*",
"*cosmicpe\\config\\*"
]);
echo "Extracted to ", $temp_dir, PHP_EOL;
echo "Building phar...", PHP_EOL;
$out = new Phar($temp_dir . ".phar");
$out->startBuffering();
$out->buildFromDirectory($temp_dir);
$out->stopBuffering();
try{
$out->compressFiles(Phar::GZ);
}catch(BadMethodCallException){}
echo "Phar written to ", $temp_dir, ".phar", PHP_EOL;
@Muqsit
Copy link
Author

Muqsit commented Jan 1, 2025

Usage:

php apply_destructor_hooks.php /path/to/myproject.phar

Example output:

Destroyed cosmicpe\world\region\flag\handler\utils\PriorityRegionMap (1099639)...
#0 phar:///home/cosmicpe/spirit/plugins/CosmicPE_v0.0.3.phar/src/cosmicpe/world/WorldInstance.php(173): cosmicpe\world\region\flag\handler\utils\PriorityRegionMap->__destruct()
#1 phar:///tmp/PocketMine-MP-phar-cache.0/PMMPbkY9B0.tar/src/scheduler/ClosureTask.php(57): cosmicpe\world\WorldInstance->cosmicpe\world\{closure}()
#2 phar:///tmp/PocketMine-MP-phar-cache.0/PMMPbkY9B0.tar/src/scheduler/TaskHandler.php(122): pocketmine\scheduler\ClosureTask->onRun()
#3 phar:///tmp/PocketMine-MP-phar-cache.0/PMMPbkY9B0.tar/src/scheduler/TaskScheduler.php(175): pocketmine\scheduler\TaskHandler->run()
#4 phar:///tmp/PocketMine-MP-phar-cache.0/PMMPbkY9B0.tar/src/plugin/PluginManager.php(540): pocketmine\scheduler\TaskScheduler->mainThreadHeartbeat(2401)
#5 phar:///tmp/PocketMine-MP-phar-cache.0/PMMPbkY9B0.tar/src/Server.php(1841): pocketmine\plugin\PluginManager->tickSchedulers(2401)
#6 phar:///tmp/PocketMine-MP-phar-cache.0/PMMPbkY9B0.tar/src/Server.php(1730): pocketmine\Server->tick()
#7 phar:///tmp/PocketMine-MP-phar-cache.0/PMMPbkY9B0.tar/src/Server.php(1097): pocketmine\Server->tickProcessor()
#8 phar:///tmp/PocketMine-MP-phar-cache.0/PMMPbkY9B0.tar/src/PocketMine.php(355): pocketmine\Server->__construct(Object(pocketmine\thread\ThreadSafeClassLoader), Object(pocketmine\utils\MainLogger), '/home/cosmicpe/...', '/home/cosmicpe/...')
#9 phar:///tmp/PocketMine-MP-phar-cache.0/PMMPbkY9B0.tar/src/PocketMine.php(378): pocketmine\server()
#10 /home/cosmicpe/spirit/PocketMine-MP.phar(168): require('phar:///tmp/Poc...')
#11 {main}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment