diff --git a/src/Snappdf.php b/src/Snappdf.php index 38f2c00..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): void + private function cleanup(string $tempFile, array $content, array $existingChromiumProfiles): void { if ($this->keepTemporaryFiles) { return; @@ -205,6 +205,28 @@ private function cleanup(string $tempFile, array $content): void if ($content['type'] === 'html') { 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(); + + 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 @@ -235,6 +257,8 @@ public function generate(): ?string $pdf = tempnam(sys_get_temp_dir(), 'pdf_'); rename($pdf, $pdf .= '.pdf'); + $existingChromiumProfiles = glob(sys_get_temp_dir() . DIRECTORY_SEPARATOR . '.org.chromium.Chromium.*') ?: []; + $commandInput = [$this->getChromiumPath()]; foreach ($this->getChromiumArguments() as $argument) { @@ -261,7 +285,7 @@ public function generate(): ?string $pdfContent = file_get_contents($pdf); - $this->cleanup($pdf, $content); + $this->cleanup($pdf, $content, $existingChromiumProfiles); return $pdfContent; } diff --git a/tests/SnappdfTest.php b/tests/SnappdfTest.php index 16e5ca1..f6211df 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,30 @@ public function testArgumentCanBeRemoved() $this->assertEquals($argumentsCount, count($snappdf->getChromiumArguments()) + 1); } + + public function testOrphanedChromiumProfilesAreRemovedAfterRun() + { + $snappdf = new Snappdf(); + + $preExisting = sys_get_temp_dir() . DIRECTORY_SEPARATOR . '.org.chromium.Chromium.PREEXISTING'; + $orphan = sys_get_temp_dir() . DIRECTORY_SEPARATOR . '.org.chromium.Chromium.ORPHANED'; + + $filesystem = new Filesystem(); + $filesystem->mkdir($preExisting); + $filesystem->touch($preExisting . '/keep.txt'); + $filesystem->mkdir($orphan); + + try { + // 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]); + + $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); + } + } }