diff --git a/src/pocketmine/MemoryManager.php b/src/pocketmine/MemoryManager.php index 0bf70c333..e0ea52815 100644 --- a/src/pocketmine/MemoryManager.php +++ b/src/pocketmine/MemoryManager.php @@ -260,4 +260,128 @@ class MemoryManager{ "object" => $includeObject ? $object : null ]; } + + public function dumpServerMemory($outputFolder, $maxNesting, $maxStringSize){ + gc_enable(); + gc_collect_cycles(); //Cleanup counts + + if(!file_exists($outputFolder)){ + mkdir($outputFolder, 0777, true); + } + + $this->server->getLogger()->notice("[Dump] After the memory dump is done, the server might crash"); + + $obData = fopen($outputFolder . "/objects.js", "wb+"); + + $staticProperties = []; + + $data = []; + + $objects = []; + + $refCounts = []; + + $this->continueDump($this->server, $data, $objects, $refCounts, 0, $maxNesting, $maxStringSize); + + do{ + $continue = false; + foreach($objects as $hash => $object){ + if(!is_object($object)){ + continue; + } + $continue = true; + + $className = get_class($object); + + $objects[$hash] = true; + + $reflection = new \ReflectionObject($object); + + $info = [ + "information" => "$hash@$className", + "properties" => [] + ]; + + if($reflection->getParentClass()){ + $info["parent"] = $reflection->getParentClass()->getName(); + } + + if(count($reflection->getInterfaceNames()) > 0){ + $info["implements"] = implode(", ", $reflection->getInterfaceNames()); + } + + foreach($reflection->getProperties() as $property){ + if($property->isStatic()){ + continue; + } + + if(!$property->isPublic()){ + $property->setAccessible(true); + } + $this->continueDump($property->getValue($object), $info["properties"][$property->getName()], $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + + fwrite($obData, "$hash@$className: ". json_encode($info, JSON_UNESCAPED_SLASHES) . "\n"); + + if(!isset($objects["staticProperties"][$className])){ + $staticProperties[$className] = []; + foreach($reflection->getProperties() as $property){ + if(!$property->isStatic() or $property->getDeclaringClass()->getName() !== $className){ + continue; + } + + if(!$property->isPublic()){ + $property->setAccessible(true); + } + $this->continueDump($property->getValue($object), $staticProperties[$className][$property->getName()], $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + } + } + + echo "[Dump] Wrote " . count($objects) . " objects\n"; + }while($continue); + + file_put_contents($outputFolder . "/staticProperties.js", json_encode($staticProperties, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + file_put_contents($outputFolder . "/serverEntry.js", json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + file_put_contents($outputFolder . "/referenceCounts.js", json_encode($refCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + + echo "[Dump] Finished!\n"; + + $this->server->forceShutdown(); + } + + private function continueDump($from, &$data, &$objects, &$refCounts, $recursion, $maxNesting, $maxStringSize){ + if($maxNesting <= 0){ + $data = "(error) NESTING LIMIT REACHED"; + return; + } + + --$maxNesting; + + if(is_object($from)){ + if(!isset($objects[$hash = spl_object_hash($from)])){ + $objects[$hash] = $from; + $refCounts[$hash] = 0; + } + + ++$refCounts[$hash]; + + $data = "(object) $hash@" . get_class($from); + }elseif(is_array($from)){ + if($recursion >= 5){ + $data = "(error) ARRAY RECURSION LIMIT REACHED"; + return; + } + $data = []; + foreach($from as $key => $value){ + $this->continueDump($value, $data[$key], $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize); + } + }elseif(is_string($from)){ + $data = "(string) len(".strlen($from).") " . substr(Utils::printable($from), 0, $maxStringSize); + }elseif(is_resource($from)){ + $data = "(resource) " . print_r($from, true); + }else{ + $data = $from; + } + } } \ No newline at end of file diff --git a/src/pocketmine/command/SimpleCommandMap.php b/src/pocketmine/command/SimpleCommandMap.php index 54f7b2299..da16e26f8 100644 --- a/src/pocketmine/command/SimpleCommandMap.php +++ b/src/pocketmine/command/SimpleCommandMap.php @@ -27,6 +27,7 @@ use pocketmine\command\defaults\BanListCommand; use pocketmine\command\defaults\DefaultGamemodeCommand; use pocketmine\command\defaults\DeopCommand; use pocketmine\command\defaults\DifficultyCommand; +use pocketmine\command\defaults\DumpMemoryCommand; use pocketmine\command\defaults\EffectCommand; use pocketmine\command\defaults\GamemodeCommand; use pocketmine\command\defaults\GarbageCollectorCommand; @@ -117,6 +118,7 @@ class SimpleCommandMap implements CommandMap{ if($this->server->getProperty("debug.commands", false)){ $this->register("pocketmine", new StatusCommand("status")); $this->register("pocketmine", new GarbageCollectorCommand("gc")); + $this->register("pocketmine", new DumpMemoryCommand("dumpmemory")); } } diff --git a/src/pocketmine/command/defaults/DumpMemoryCommand.php b/src/pocketmine/command/defaults/DumpMemoryCommand.php new file mode 100644 index 000000000..4d35d16e7 --- /dev/null +++ b/src/pocketmine/command/defaults/DumpMemoryCommand.php @@ -0,0 +1,58 @@ + [path]" + ); + $this->setPermission("pocketmine.command.dumpmemory"); + } + + public function execute(CommandSender $sender, $currentAlias, array $args){ + if(!$this->testPermission($sender)){ + return true; + } + + $token = strtoupper(substr(sha1(BOOTUP_RANDOM . ":" . $sender->getServer()->getServerUniqueId() . ":" . self::$executions), 6, 6)); + + if(count($args) < 1 or strtoupper($args[0]) !== $token){ + $sender->sendMessage("Usage: /" . $this->getName() . " " . $token); + return true; + } + + ++self::$executions; + + $sender->getServer()->getMemoryManager()->dumpServerMemory(isset($args[1]) ? $args[1] : $sender->getServer()->getDataPath() . "/memoryDump_$token", 48, 80); + return true; + } +} diff --git a/src/pocketmine/command/defaults/StatusCommand.php b/src/pocketmine/command/defaults/StatusCommand.php index 393f6a2db..441dec7ff 100644 --- a/src/pocketmine/command/defaults/StatusCommand.php +++ b/src/pocketmine/command/defaults/StatusCommand.php @@ -41,7 +41,7 @@ class StatusCommand extends VanillaCommand{ return true; } - $rUsage = Utils::getRealMemoryUsage(true); + $rUsage = Utils::getRealMemoryUsage(); $mUsage = Utils::getMemoryUsage(true); $server = $sender->getServer(); diff --git a/src/pocketmine/permission/DefaultPermissions.php b/src/pocketmine/permission/DefaultPermissions.php index 459b9a381..5fa6b5740 100644 --- a/src/pocketmine/permission/DefaultPermissions.php +++ b/src/pocketmine/permission/DefaultPermissions.php @@ -117,6 +117,7 @@ abstract class DefaultPermissions{ self::registerPermission(new Permission(self::ROOT . ".command.seed", "Allows the user to view the seed of the world", Permission::DEFAULT_OP), $commands); self::registerPermission(new Permission(self::ROOT . ".command.status", "Allows the user to view the server performance", Permission::DEFAULT_OP), $commands); self::registerPermission(new Permission(self::ROOT . ".command.gc", "Allows the user to fire garbage collection tasks", Permission::DEFAULT_OP), $commands); + self::registerPermission(new Permission(self::ROOT . ".command.dumpmemory", "Allows the user to dump memory contents", Permission::DEFAULT_OP), $commands); self::registerPermission(new Permission(self::ROOT . ".command.timings", "Allows the user to records timings for all plugin events", Permission::DEFAULT_OP), $commands); self::registerPermission(new Permission(self::ROOT . ".command.spawnpoint", "Allows the user to change player's spawnpoint", Permission::DEFAULT_OP), $commands); self::registerPermission(new Permission(self::ROOT . ".command.setworldspawn", "Allows the user to change the world spawn", Permission::DEFAULT_OP), $commands);