diff --git a/src/PocketMine.php b/src/PocketMine.php index d13cf33a1a..d6e0ae5061 100644 --- a/src/PocketMine.php +++ b/src/PocketMine.php @@ -326,7 +326,8 @@ JIT_WARNING Terminal::init(); } - $logger = new MainLogger(Path::join($dataPath, "server.log"), Terminal::hasFormattingCodes(), "Server", new \DateTimeZone(Timezone::get())); + $logger = new MainLogger(Path::join($dataPath, "server.log"), Path::join($dataPath, "log_archive"), Terminal::hasFormattingCodes(), "Server", new \DateTimeZone(Timezone::get())); + \GlobalLogger::set($logger); emit_performance_warnings($logger); diff --git a/src/utils/MainLogger.php b/src/utils/MainLogger.php index ffd56b041a..20f9fb6e76 100644 --- a/src/utils/MainLogger.php +++ b/src/utils/MainLogger.php @@ -44,7 +44,7 @@ class MainLogger extends AttachableThreadSafeLogger implements \BufferedLogger{ /** * @throws \RuntimeException */ - public function __construct(string $logFile, bool $useFormattingCodes, string $mainThreadName, \DateTimeZone $timezone, bool $logDebug = false){ + public function __construct(string $logFile, string $logArchiveDir, bool $useFormattingCodes, string $mainThreadName, \DateTimeZone $timezone, bool $logDebug = false){ parent::__construct(); $this->logDebug = $logDebug; @@ -52,7 +52,7 @@ class MainLogger extends AttachableThreadSafeLogger implements \BufferedLogger{ $this->mainThreadName = $mainThreadName; $this->timezone = $timezone->getName(); - $this->logWriterThread = new MainLoggerThread($logFile); + $this->logWriterThread = new MainLoggerThread($logFile, $logArchiveDir); $this->logWriterThread->start(NativeThread::INHERIT_NONE); } diff --git a/src/utils/MainLoggerThread.php b/src/utils/MainLoggerThread.php index 548e23a4f1..e7acf97376 100644 --- a/src/utils/MainLoggerThread.php +++ b/src/utils/MainLoggerThread.php @@ -25,23 +25,42 @@ namespace pocketmine\utils; use pmmp\thread\Thread; use pmmp\thread\ThreadSafeArray; +use function clearstatcache; +use function date; use function fclose; +use function file_exists; use function fopen; +use function fstat; use function fwrite; +use function is_dir; +use function is_file; use function is_resource; +use function mkdir; +use function pathinfo; +use function rename; +use function strlen; use function touch; +use const PATHINFO_EXTENSION; +use const PATHINFO_FILENAME; final class MainLoggerThread extends Thread{ + /** @phpstan-var ThreadSafeArray */ private ThreadSafeArray $buffer; private bool $syncFlush = false; private bool $shutdown = false; public function __construct( - private string $logFile + private string $logFile, + private string $archiveDir, + private readonly int $maxFileSize = 32 * 1024 * 1024 //32 MB ){ $this->buffer = new ThreadSafeArray(); touch($this->logFile); + if(!@mkdir($this->archiveDir) && !is_dir($this->archiveDir)){ + throw new \RuntimeException("Unable to create archive directory: " . ( + is_file($this->archiveDir) ? "it already exists and is not a directory" : "permission denied")); + } } public function write(string $line) : void{ @@ -71,12 +90,64 @@ final class MainLoggerThread extends Thread{ $this->join(); } + /** @return resource */ + private function openLogFile(string $file, int &$size){ + $logResource = fopen($file, "ab"); + if(!is_resource($logResource)){ + throw new \RuntimeException("Couldn't open log file"); + } + $stat = fstat($logResource); + if($stat === false){ + throw new AssumptionFailedError("fstat() should not fail here"); + } + $size = $stat['size']; + return $logResource; + } + + /** + * @param resource $logResource + * @return resource + */ + private function archiveLogFile($logResource, int &$size){ + fclose($logResource); + + clearstatcache(); + + $i = 0; + $date = date("Y-m-d\TH.i.s"); + $baseName = pathinfo($this->logFile, PATHINFO_FILENAME); + $extension = pathinfo($this->logFile, PATHINFO_EXTENSION); + do{ + //this shouldn't be necessary, but in case the user messes with the system time for some reason ... + $fileName = "$baseName.$date.$i.$extension"; + $out = $this->archiveDir . "/" . $fileName; + $i++; + }while(file_exists($out)); + + //the user may have externally deleted the whole directory - make sure it exists before we do anything + @mkdir($this->archiveDir); + rename($this->logFile, $out); + + $logResource = $this->openLogFile($this->logFile, $size); + fwrite($logResource, "--- Starting new log file - old log file archived as $fileName ---\n"); + + return $logResource; + } + + private function logFileReadyToArchive(int $size) : bool{ + return $size >= $this->maxFileSize; + } + /** * @param resource $logResource */ - private function writeLogStream($logResource) : void{ + private function writeLogStream(&$logResource, int &$size) : void{ while(($chunk = $this->buffer->shift()) !== null){ fwrite($logResource, $chunk); + $size += strlen($chunk); + if($this->logFileReadyToArchive($size)){ + $logResource = $this->archiveLogFile($logResource, $size); + } } $this->synchronized(function() : void{ @@ -88,13 +159,14 @@ final class MainLoggerThread extends Thread{ } public function run() : void{ - $logResource = fopen($this->logFile, "ab"); - if(!is_resource($logResource)){ - throw new \RuntimeException("Couldn't open log file"); + $size = 0; + $logResource = $this->openLogFile($this->logFile, $size); + if($this->logFileReadyToArchive($size)){ + $logResource = $this->archiveLogFile($logResource, $size); } while(!$this->shutdown){ - $this->writeLogStream($logResource); + $this->writeLogStream($logResource, $size); $this->synchronized(function() : void{ if(!$this->shutdown && !$this->syncFlush){ $this->wait(); @@ -102,7 +174,7 @@ final class MainLoggerThread extends Thread{ }); } - $this->writeLogStream($logResource); + $this->writeLogStream($logResource, $size); fclose($logResource); } diff --git a/tests/phpunit/scheduler/AsyncPoolTest.php b/tests/phpunit/scheduler/AsyncPoolTest.php index 54c8ccafda..51622dd227 100644 --- a/tests/phpunit/scheduler/AsyncPoolTest.php +++ b/tests/phpunit/scheduler/AsyncPoolTest.php @@ -45,7 +45,7 @@ class AsyncPoolTest extends TestCase{ public function setUp() : void{ @define('pocketmine\\COMPOSER_AUTOLOADER_PATH', dirname(__DIR__, 3) . '/vendor/autoload.php'); - $this->mainLogger = new MainLogger(tempnam(sys_get_temp_dir(), "pmlog"), false, "Main", new \DateTimeZone('UTC')); + $this->mainLogger = new MainLogger(tempnam(sys_get_temp_dir(), "pmlog"), sys_get_temp_dir(), false, "Main", new \DateTimeZone('UTC')); $this->pool = new AsyncPool(2, 1024, new ThreadSafeClassLoader(), $this->mainLogger, new SleeperHandler()); }