From 7df0d5e4d6ee5cace429d8ba57d5c388a90b9584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 2 Sep 2026 10:47:22 +0200 Subject: [PATCH 1/4] Clean up BrowserMetric files --- src/Snappdf.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Snappdf.php b/src/Snappdf.php index 38f2c00..6b220c0 100644 --- a/src/Snappdf.php +++ b/src/Snappdf.php @@ -194,7 +194,7 @@ public function getKeepTemporaryFiles(): bool return (bool) $this->keepTemporaryFiles; } - private function cleanup(string $tempFile, array $content): void + private function cleanup(string $tempFile, array $content, string $userDataDirectory): void { if ($this->keepTemporaryFiles) { return; @@ -205,6 +205,9 @@ private function cleanup(string $tempFile, array $content): void if ($content['type'] === 'html') { unlink($content['content']); } + + $filesystem = new Filesystem(); + $filesystem->remove($userDataDirectory); } public function generate(): ?string @@ -235,6 +238,15 @@ public function generate(): ?string $pdf = tempnam(sys_get_temp_dir(), 'pdf_'); rename($pdf, $pdf .= '.pdf'); + // Chromium writes its profile (BrowserMetrics-*.pma etc.) into the user data + // directory. Without an explicit --user-data-dir it falls back to a temp + // profile under /tmp/.org.chromium.Chromium.* that is never cleaned up. Point + // it at a directory we own and remove it after the run. + + $userDataDirectory = sys_get_temp_dir() . '/' . uniqid('snappdf_', true); + $filesystem = new Filesystem(); + $filesystem->mkdir($userDataDirectory); + $commandInput = [$this->getChromiumPath()]; foreach ($this->getChromiumArguments() as $argument) { @@ -243,6 +255,7 @@ public function generate(): ?string array_push( $commandInput, + '--user-data-dir=' . $userDataDirectory, '--print-to-pdf=' . $pdf, $content['content'], ); @@ -261,7 +274,7 @@ public function generate(): ?string $pdfContent = file_get_contents($pdf); - $this->cleanup($pdf, $content); + $this->cleanup($pdf, $content, $userDataDirectory); return $pdfContent; } From a9e1385a4422c3aed62b49ac4e7becbfde87d723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 2 Sep 2026 10:47:28 +0200 Subject: [PATCH 2/4] Update tests --- tests/SnappdfTest.php | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/SnappdfTest.php b/tests/SnappdfTest.php index 16e5ca1..2673bf9 100644 --- a/tests/SnappdfTest.php +++ b/tests/SnappdfTest.php @@ -5,6 +5,7 @@ use Beganovich\Snappdf\Exception\MissingContent; use Beganovich\Snappdf\Snappdf; use PHPUnit\Framework\TestCase; +use Symfony\Component\Filesystem\Filesystem; class SnappdfTest extends TestCase { @@ -155,4 +156,43 @@ public function testArgumentCanBeRemoved() $this->assertEquals($argumentsCount, count($snappdf->getChromiumArguments()) + 1); } + + public function testManagedUserDataDirectoryIsPassedToChromium() + { + $snappdf = new Snappdf(); + $snappdf->setKeepTemporaryFiles(true); + + $snappdf + ->setHtml('

Hello world!

') + ->generate(); + + $managed = glob(sys_get_temp_dir() . '/snappdf_*'); + + try { + $this->assertNotEmpty( + $this->directoryContents($managed[0] ?? ''), + 'Chromium must honor --user-data-dir by writing its profile into the managed directory instead of the default /tmp/.org.chromium.Chromium.* temp profile.' + ); + } finally { + foreach ($managed as $dir) { + $filesystem = new Filesystem(); + $filesystem->remove($dir); + } + } + } + + private function directoryContents(string $directory): array + { + if ('' === $directory) { + return []; + } + + $files = glob($directory . '/*'); + + if (false === $files) { + return []; + } + + return $files; + } } From 48e1c7389a93dde675352cc893e27b73192c361c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 2 Sep 2026 11:03:36 +0200 Subject: [PATCH 3/4] Grab list of orphaned files --- src/Snappdf.php | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/Snappdf.php b/src/Snappdf.php index 6b220c0..600b4c1 100644 --- a/src/Snappdf.php +++ b/src/Snappdf.php @@ -194,7 +194,7 @@ public function getKeepTemporaryFiles(): bool return (bool) $this->keepTemporaryFiles; } - private function cleanup(string $tempFile, array $content, string $userDataDirectory): void + private function cleanup(string $tempFile, array $content, array $existingChromiumProfiles): void { if ($this->keepTemporaryFiles) { return; @@ -206,8 +206,27 @@ private function cleanup(string $tempFile, array $content, string $userDataDirec unlink($content['content']); } + // Chromium writes its profile (BrowserMetrics-*.pma etc.) into a temp directory + // of the form /tmp/.org.chromium.Chromium.. On a clean exit it removes + // the directory itself; on an abnormal exit (crash, timeout, killed process) it + // is left behind. Remove any we created during this run. + $this->removeOrphanedChromiumProfiles($existingChromiumProfiles); + } + + /** + * Remove Chromium temp profile directories that did not exist before this run. + * + * @param string[] $existingProfiles Directories present before the browser started. + */ + private function removeOrphanedChromiumProfiles(array $existingProfiles): void + { $filesystem = new Filesystem(); - $filesystem->remove($userDataDirectory); + + foreach (glob(sys_get_temp_dir() . DIRECTORY_SEPARATOR . '.org.chromium.Chromium.*') as $profile) { + if (!in_array($profile, $existingProfiles, true)) { + $filesystem->remove($profile); + } + } } public function generate(): ?string @@ -238,14 +257,7 @@ public function generate(): ?string $pdf = tempnam(sys_get_temp_dir(), 'pdf_'); rename($pdf, $pdf .= '.pdf'); - // Chromium writes its profile (BrowserMetrics-*.pma etc.) into the user data - // directory. Without an explicit --user-data-dir it falls back to a temp - // profile under /tmp/.org.chromium.Chromium.* that is never cleaned up. Point - // it at a directory we own and remove it after the run. - - $userDataDirectory = sys_get_temp_dir() . '/' . uniqid('snappdf_', true); - $filesystem = new Filesystem(); - $filesystem->mkdir($userDataDirectory); + $existingChromiumProfiles = glob(sys_get_temp_dir() . DIRECTORY_SEPARATOR . '.org.chromium.Chromium.*') ?: []; $commandInput = [$this->getChromiumPath()]; @@ -255,7 +267,6 @@ public function generate(): ?string array_push( $commandInput, - '--user-data-dir=' . $userDataDirectory, '--print-to-pdf=' . $pdf, $content['content'], ); @@ -274,7 +285,7 @@ public function generate(): ?string $pdfContent = file_get_contents($pdf); - $this->cleanup($pdf, $content, $userDataDirectory); + $this->cleanup($pdf, $content, $existingChromiumProfiles); return $pdfContent; } From 6be422a86232f444ddfd49fc719774ba0464d051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 2 Sep 2026 11:03:40 +0200 Subject: [PATCH 4/4] Update tests --- tests/SnappdfTest.php | 45 +++++++++++++++---------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/tests/SnappdfTest.php b/tests/SnappdfTest.php index 2673bf9..f6211df 100644 --- a/tests/SnappdfTest.php +++ b/tests/SnappdfTest.php @@ -157,42 +157,29 @@ public function testArgumentCanBeRemoved() $this->assertEquals($argumentsCount, count($snappdf->getChromiumArguments()) + 1); } - public function testManagedUserDataDirectoryIsPassedToChromium() + public function testOrphanedChromiumProfilesAreRemovedAfterRun() { $snappdf = new Snappdf(); - $snappdf->setKeepTemporaryFiles(true); - $snappdf - ->setHtml('

Hello world!

') - ->generate(); + $preExisting = sys_get_temp_dir() . DIRECTORY_SEPARATOR . '.org.chromium.Chromium.PREEXISTING'; + $orphan = sys_get_temp_dir() . DIRECTORY_SEPARATOR . '.org.chromium.Chromium.ORPHANED'; - $managed = glob(sys_get_temp_dir() . '/snappdf_*'); + $filesystem = new Filesystem(); + $filesystem->mkdir($preExisting); + $filesystem->touch($preExisting . '/keep.txt'); + $filesystem->mkdir($orphan); try { - $this->assertNotEmpty( - $this->directoryContents($managed[0] ?? ''), - 'Chromium must honor --user-data-dir by writing its profile into the managed directory instead of the default /tmp/.org.chromium.Chromium.* temp profile.' - ); - } finally { - foreach ($managed as $dir) { - $filesystem = new Filesystem(); - $filesystem->remove($dir); - } - } - } + // Chromium snapshots the profiles present before the run; an orphan created + // during the run (abnormal exit) is absent from that snapshot and must go. + $removeOrphaned = new \ReflectionMethod($snappdf, 'removeOrphanedChromiumProfiles'); + $removeOrphaned->invoke($snappdf, [$preExisting]); - private function directoryContents(string $directory): array - { - if ('' === $directory) { - return []; - } - - $files = glob($directory . '/*'); - - if (false === $files) { - return []; + $this->assertTrue(is_dir($preExisting), 'Pre-existing .org.chromium profile must be preserved.'); + $this->assertFalse(is_dir($orphan), 'Orphaned profile from this run must be removed.'); + } finally { + $filesystem->remove($preExisting); + $filesystem->remove($orphan); } - - return $files; } }