diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 5bfd5cb18..73a5ff678 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -139,9 +139,11 @@ abstract class ArchiveDownloader extends FileDownloader } } - $filesystem->removeDirectory($temporaryDir); - $self->removeCleanupPath($package, $temporaryDir); - $self->removeCleanupPath($package, $path); + $promise = $filesystem->removeDirectoryAsync($temporaryDir); + return $promise->then(function() use ($self, $package, $path, $temporaryDir) { + $self->removeCleanupPath($package, $temporaryDir); + $self->removeCleanupPath($package, $path); + }); }, function ($e) use ($cleanup) { $cleanup(); diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 39092f2e8..a3bb16fb6 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -12,6 +12,7 @@ namespace Composer\Util; +use React\Promise\Promise; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use Symfony\Component\Filesystem\Exception\IOException; @@ -96,6 +97,75 @@ class Filesystem */ public function removeDirectory($directory) { + $edgeCaseResult = $this->removeEdgeCases($directory); + if ($edgeCaseResult !== null) { + return $edgeCaseResult; + } + + if (Platform::isWindows()) { + $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory))); + } else { + $cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory)); + } + + $result = $this->getProcess()->execute($cmd, $output) === 0; + + // clear stat cache because external processes aren't tracked by the php stat cache + clearstatcache(); + + if ($result && !is_dir($directory)) { + return true; + } + + return $this->removeDirectoryPhp($directory); + } + + /** + * Recursively remove a directory asynchronously + * + * Uses the process component if proc_open is enabled on the PHP + * installation. + * + * @param string $directory + * @throws \RuntimeException + * @return Promise + */ + public function removeDirectoryAsync($directory) + { + $edgeCaseResult = $this->removeEdgeCases($directory); + if ($edgeCaseResult !== null) { + return \React\Promise\resolve($edgeCaseResult); + } + + if (Platform::isWindows()) { + $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory))); + } else { + $cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory)); + } + + $promise = $this->getProcess()->executeAsync($cmd); + + $self = $this; + return $promise->then(function ($process) use ($directory, $self) { + // clear stat cache because external processes aren't tracked by the php stat cache + clearstatcache(); + + if ($process->isSuccessful()) { + if (!is_dir($directory)) { + return \React\Promise\resolve(true); + } + } + + return \React\Promise\resolve($self->removeDirectoryPhp($directory)); + }); + } + + /** + * @param string $directory + * + * @return bool|null Returns null, when no edge case was hit. Otherwise a bool whether removal was successfull + */ + private function removeEdgeCases($directory) { if ($this->isSymlinkedDirectory($directory)) { return $this->unlinkSymlinkedDirectory($directory); } @@ -120,22 +190,7 @@ class Filesystem return $this->removeDirectoryPhp($directory); } - if (Platform::isWindows()) { - $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory))); - } else { - $cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory)); - } - - $result = $this->getProcess()->execute($cmd, $output) === 0; - - // clear stat cache because external processes aren't tracked by the php stat cache - clearstatcache(); - - if ($result && !is_dir($directory)) { - return true; - } - - return $this->removeDirectoryPhp($directory); + return null; } /**