Created
January 1, 2025 18:32
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage:
Example output: