* @author Jordi Boggiano * @author Nils Adermann */ class InstallationManager { /** @var array */ private $installers = []; /** @var array */ private $cache = []; /** @var array> */ private $notifiablePackages = []; /** @var Loop */ private $loop; /** @var IOInterface */ private $io; /** @var ?EventDispatcher */ private $eventDispatcher; /** @var bool */ private $outputProgress; public function __construct(Loop $loop, IOInterface $io, ?EventDispatcher $eventDispatcher = null) { $this->loop = $loop; $this->io = $io; $this->eventDispatcher = $eventDispatcher; } public function reset(): void { $this->notifiablePackages = []; FileDownloader::$downloadMetadata = []; } /** * Adds installer * * @param InstallerInterface $installer installer instance */ public function addInstaller(InstallerInterface $installer): void { array_unshift($this->installers, $installer); $this->cache = []; } /** * Removes installer * * @param InstallerInterface $installer installer instance */ public function removeInstaller(InstallerInterface $installer): void { if (false !== ($key = array_search($installer, $this->installers, true))) { array_splice($this->installers, $key, 1); $this->cache = []; } } /** * Disables plugins. * * We prevent any plugins from being instantiated by simply * deactivating the installer for them. This ensure that no third-party * code is ever executed. */ public function disablePlugins(): void { foreach ($this->installers as $i => $installer) { if (!$installer instanceof PluginInstaller) { continue; } unset($this->installers[$i]); } } /** * Returns installer for a specific package type. * * @param string $type package type * * @throws \InvalidArgumentException if installer for provided type is not registered */ public function getInstaller(string $type): InstallerInterface { $type = strtolower($type); if (isset($this->cache[$type])) { return $this->cache[$type]; } foreach ($this->installers as $installer) { if ($installer->supports($type)) { return $this->cache[$type] = $installer; } } throw new \InvalidArgumentException('Unknown installer type: '.$type); } /** * Checks whether provided package is installed in one of the registered installers. * * @param InstalledRepositoryInterface $repo repository in which to check * @param PackageInterface $package package instance */ public function isPackageInstalled(InstalledRepositoryInterface $repo, PackageInterface $package): bool { if ($package instanceof AliasPackage) { return $repo->hasPackage($package) && $this->isPackageInstalled($repo, $package->getAliasOf()); } return $this->getInstaller($package->getType())->isInstalled($repo, $package); } /** * Install binary for the given package. * If the installer associated to this package doesn't handle that function, it'll do nothing. * * @param PackageInterface $package Package instance */ public function ensureBinariesPresence(PackageInterface $package): void { try { $installer = $this->getInstaller($package->getType()); } catch (\InvalidArgumentException $e) { // no installer found for the current package type (@see `getInstaller()`) return; } // if the given installer support installing binaries if ($installer instanceof BinaryPresenceInterface) { $installer->ensureBinariesPresence($package); } } /** * Executes solver operation. * * @param InstalledRepositoryInterface $repo repository in which to add/remove/update packages * @param OperationInterface[] $operations operations to execute * @param bool $devMode whether the install is being run in dev mode * @param bool $runScripts whether to dispatch script events * @param bool $downloadOnly whether to only download packages */ public function execute( InstalledRepositoryInterface $repo, array $operations, bool $devMode = true, bool $runScripts = true, bool $downloadOnly = false, ): void { $cleanupPromises = []; $signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal, SignalHandler $handler) use (&$cleanupPromises) { $this->io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG); $this->runCleanup($cleanupPromises); $handler->exitWithLastSignal(); }); try { // execute operations in batches to make sure download-modifying-plugins are installed // before the other packages get downloaded $batches = []; $batch = []; foreach ($operations as $index => $operation) { if ($operation instanceof UpdateOperation || $operation instanceof InstallOperation) { $package = $operation instanceof UpdateOperation ? $operation->getTargetPackage() : $operation->getPackage(); if ($package->getType() === 'composer-plugin' && ($extra = $package->getExtra()) && isset($extra['plugin-modifies-downloads']) && $extra['plugin-modifies-downloads'] === true) { if ($batch) { $batches[] = $batch; } $batches[] = [$index => $operation]; $batch = []; continue; } } $batch[$index] = $operation; } if ($batch) { $batches[] = $batch; } foreach ($batches as $batch) { $this->downloadAndExecuteBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts, $downloadOnly, $operations); } } catch (\Exception $e) { $this->runCleanup($cleanupPromises); throw $e; } finally { $signalHandler->unregister(); } if ($downloadOnly) { return; } // do a last write so that we write the repository even if nothing changed // as that can trigger an update of some files like InstalledVersions.php if // running a new composer version $repo->write($devMode, $this); } /** * @param OperationInterface[] $operations List of operations to execute in this batch * @param array $cleanupPromises * @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners */ private function downloadAndExecuteBatch( InstalledRepositoryInterface $repo, array $operations, array &$cleanupPromises, bool $devMode, bool $runScripts, bool $downloadOnly, array $allOperations, ): void { $promises = []; foreach ($operations as $index => $operation) { $opType = $operation->getOperationType(); // ignoring alias ops as they don't need to execute anything at this stage if (!in_array($opType, ['update', 'install', 'uninstall'])) { continue; } if ($opType === 'update') { /** @var UpdateOperation $operation */ $package = $operation->getTargetPackage(); $initialPackage = $operation->getInitialPackage(); } else { /** @var InstallOperation|MarkAliasInstalledOperation|MarkAliasUninstalledOperation|UninstallOperation $operation */ $package = $operation->getPackage(); $initialPackage = null; } $installer = $this->getInstaller($package->getType()); $cleanupPromises[$index] = static function () use ($opType, $installer, $package, $initialPackage): ?PromiseInterface { // avoid calling cleanup if the download was not even initialized for a package // as without installation source configured nothing will work if (!$package->getInstallationSource()) { return \React\Promise\resolve(null); } return $installer->cleanup($opType, $package, $initialPackage); }; if ($opType !== 'uninstall') { $promise = $installer->download($package, $initialPackage); if ($promise) { $promises[] = $promise; } } } // execute all downloads first if (count($promises)) { $this->waitOnPromises($promises); } if ($downloadOnly) { $this->runCleanup($cleanupPromises); return; } // execute operations in batches to make sure every plugin is installed in the // right order and activated before the packages depending on it are installed $batches = []; $batch = []; foreach ($operations as $index => $operation) { if ($operation instanceof InstallOperation || $operation instanceof UpdateOperation) { $package = $operation instanceof UpdateOperation ? $operation->getTargetPackage() : $operation->getPackage(); if ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer') { if ($batch) { $batches[] = $batch; } $batches[] = [$index => $operation]; $batch = []; continue; } } $batch[$index] = $operation; } if ($batch) { $batches[] = $batch; } foreach ($batches as $batch) { $this->executeBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts, $allOperations); } } /** * @param OperationInterface[] $operations List of operations to execute in this batch * @param array $cleanupPromises * @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners */ private function executeBatch( InstalledRepositoryInterface $repo, array $operations, array $cleanupPromises, bool $devMode, bool $runScripts, array $allOperations, ): void { $promises = []; $postExecCallbacks = []; foreach ($operations as $index => $operation) { $opType = $operation->getOperationType(); // ignoring alias ops as they don't need to execute anything if (!in_array($opType, ['update', 'install', 'uninstall'])) { // output alias ops in debug verbosity as they have no output otherwise if ($this->io->isDebug()) { $this->io->writeError(' - ' . $operation->show(false)); } $this->{$opType}($repo, $operation); continue; } if ($opType === 'update') { /** @var UpdateOperation $operation */ $package = $operation->getTargetPackage(); $initialPackage = $operation->getInitialPackage(); } else { /** @var InstallOperation|MarkAliasInstalledOperation|MarkAliasUninstalledOperation|UninstallOperation $operation */ $package = $operation->getPackage(); $initialPackage = null; } $installer = $this->getInstaller($package->getType()); $eventName = [ 'install' => PackageEvents::PRE_PACKAGE_INSTALL, 'update' => PackageEvents::PRE_PACKAGE_UPDATE, 'uninstall' => PackageEvents::PRE_PACKAGE_UNINSTALL, ][$opType] ?? null; if (null !== $eventName && $runScripts && $this->eventDispatcher) { $this->eventDispatcher->dispatchPackageEvent($eventName, $devMode, $repo, $allOperations, $operation); } $dispatcher = $this->eventDispatcher; $io = $this->io; $promise = $installer->prepare($opType, $package, $initialPackage); if (!$promise instanceof PromiseInterface) { $promise = \React\Promise\resolve(null); } $promise = $promise->then(function () use ($opType, $repo, $operation) { return $this->{$opType}($repo, $operation); })->then($cleanupPromises[$index]) ->then(function () use ($devMode, $repo): void { $repo->write($devMode, $this); }, static function ($e) use ($opType, $package, $io): void { $io->writeError(' ' . ucfirst($opType) .' of '.$package->getPrettyName().' failed'); throw $e; }); $eventName = [ 'install' => PackageEvents::POST_PACKAGE_INSTALL, 'update' => PackageEvents::POST_PACKAGE_UPDATE, 'uninstall' => PackageEvents::POST_PACKAGE_UNINSTALL, ][$opType] ?? null; if (null !== $eventName && $runScripts && $dispatcher) { $postExecCallbacks[] = static function () use ($dispatcher, $eventName, $devMode, $repo, $allOperations, $operation): void { $dispatcher->dispatchPackageEvent($eventName, $devMode, $repo, $allOperations, $operation); }; } $promises[] = $promise; } // execute all prepare => installs/updates/removes => cleanup steps if (count($promises)) { $this->waitOnPromises($promises); } Platform::workaroundFilesystemIssues(); foreach ($postExecCallbacks as $cb) { $cb(); } } /** * @param PromiseInterface[] $promises */ private function waitOnPromises(array $promises): void { $progress = null; if ( $this->outputProgress && $this->io instanceof ConsoleIO && !Platform::getEnv('CI') && !$this->io->isDebug() && count($promises) > 1 ) { $progress = $this->io->getProgressBar(); } $this->loop->wait($promises, $progress); if ($progress) { $progress->clear(); // ProgressBar in non-decorated output does not output a final line-break and clear() does nothing if (!$this->io->isDecorated()) { $this->io->writeError(''); } } } /** * Executes download operation. * * $param PackageInterface $package */ public function download(PackageInterface $package): ?PromiseInterface { $installer = $this->getInstaller($package->getType()); $promise = $installer->cleanup("install", $package); return $promise; } /** * Executes install operation. * * @param InstalledRepositoryInterface $repo repository in which to check * @param InstallOperation $operation operation instance */ public function install(InstalledRepositoryInterface $repo, InstallOperation $operation): ?PromiseInterface { $package = $operation->getPackage(); $installer = $this->getInstaller($package->getType()); $promise = $installer->install($repo, $package); $this->markForNotification($package); return $promise; } /** * Executes update operation. * * @param InstalledRepositoryInterface $repo repository in which to check * @param UpdateOperation $operation operation instance */ public function update(InstalledRepositoryInterface $repo, UpdateOperation $operation): ?PromiseInterface { $initial = $operation->getInitialPackage(); $target = $operation->getTargetPackage(); $initialType = $initial->getType(); $targetType = $target->getType(); if ($initialType === $targetType) { $installer = $this->getInstaller($initialType); $promise = $installer->update($repo, $initial, $target); $this->markForNotification($target); } else { $promise = $this->getInstaller($initialType)->uninstall($repo, $initial); if (!$promise instanceof PromiseInterface) { $promise = \React\Promise\resolve(null); } $installer = $this->getInstaller($targetType); $promise = $promise->then(static function () use ($installer, $repo, $target): PromiseInterface { $promise = $installer->install($repo, $target); if ($promise instanceof PromiseInterface) { return $promise; } return \React\Promise\resolve(null); }); } return $promise; } /** * Uninstalls package. * * @param InstalledRepositoryInterface $repo repository in which to check * @param UninstallOperation $operation operation instance */ public function uninstall(InstalledRepositoryInterface $repo, UninstallOperation $operation): ?PromiseInterface { $package = $operation->getPackage(); $installer = $this->getInstaller($package->getType()); return $installer->uninstall($repo, $package); } /** * Executes markAliasInstalled operation. * * @param InstalledRepositoryInterface $repo repository in which to check * @param MarkAliasInstalledOperation $operation operation instance */ public function markAliasInstalled(InstalledRepositoryInterface $repo, MarkAliasInstalledOperation $operation): void { $package = $operation->getPackage(); if (!$repo->hasPackage($package)) { $repo->addPackage(clone $package); } } /** * Executes markAlias operation. * * @param InstalledRepositoryInterface $repo repository in which to check * @param MarkAliasUninstalledOperation $operation operation instance */ public function markAliasUninstalled( InstalledRepositoryInterface $repo, MarkAliasUninstalledOperation $operation, ): void { $package = $operation->getPackage(); $repo->removePackage($package); } /** * Returns the installation path of a package * * @return string path */ public function getInstallPath(PackageInterface $package): string { $installer = $this->getInstaller($package->getType()); return $installer->getInstallPath($package); } public function setOutputProgress(bool $outputProgress): void { $this->outputProgress = $outputProgress; } public function notifyInstalls(IOInterface $io): void { $promises = []; try { foreach ($this->notifiablePackages as $repoUrl => $packages) { // non-batch API, deprecated if (strpos($repoUrl, '%package%')) { foreach ($packages as $package) { $url = str_replace('%package%', $package->getPrettyName(), $repoUrl); $params = [ 'version' => $package->getPrettyVersion(), 'version_normalized' => $package->getVersion(), ]; $opts = [ 'retry-auth-failure' => false, 'http' => [ 'method' => 'POST', 'header' => ['Content-type: application/x-www-form-urlencoded'], 'content' => http_build_query($params, '', '&'), 'timeout' => 3, ], ]; $promises[] = $this->loop->getHttpDownloader()->add($url, $opts); } continue; } $postData = ['downloads' => []]; foreach ($packages as $package) { $packageNotification = [ 'name' => $package->getPrettyName(), 'version' => $package->getVersion(), ]; if (strpos($repoUrl, 'packagist.org/') !== false) { if (isset(FileDownloader::$downloadMetadata[$package->getName()])) { $packageNotification['downloaded'] = FileDownloader::$downloadMetadata[$package->getName()]; } else { $packageNotification['downloaded'] = false; } } $postData['downloads'][] = $packageNotification; } $opts = [ 'retry-auth-failure' => false, 'http' => [ 'method' => 'POST', 'header' => ['Content-Type: application/json'], 'content' => json_encode($postData), 'timeout' => 6, ], ]; $promises[] = $this->loop->getHttpDownloader()->add($repoUrl, $opts); } $this->loop->wait($promises); } catch (\Exception $e) { } $this->reset(); } private function markForNotification(PackageInterface $package): void { if ($package->getNotificationUrl()) { $this->notifiablePackages[$package->getNotificationUrl()][$package->getName()] = $package; } } /** * @param array $cleanupPromises * @return void */ private function runCleanup(array $cleanupPromises): void { $promises = []; $this->loop->abortJobs(); foreach ($cleanupPromises as $cleanup) { $promises[] = new \React\Promise\Promise(static function ($resolve, $reject) use ($cleanup): void { $promise = $cleanup(); if (!$promise instanceof PromiseInterface) { $resolve(); } else { $promise->then(static function () use ($resolve): void { $resolve(); }); } }); } if (!empty($promises)) { $this->loop->wait($promises); } } } __halt_compiler();----SIGNATURE:----bhXbaEuT+7Yf1QFK+DVsmUAufRSoDfDwk2dN5kJxX8wORC/D1AZnp+MOVGhmjHjNB1QBtFp/MwQWh+f7zZlS3c6nmKZPrKwoK+mdEAK5NaLIo6ZmzRMYrJMOS7piQOsChiwqkKWUQ77yvT8JPnKj4dBEm1Wl1KYQ9EKurKN17KECgymCgZ6yO9HIMBHqUiQEFny+0jFyZSJY+xhPB+UVJLfR2zoQs/iZRf5TSqv8UHcr9uFm2IN8wCkB0pEH/qpGDPTO/0KPKhUEZ+N1p68qA29EiBWpUTLeWRN81mMvxcRuTXe+gy1monobDlvIiLNTVFM7+E5urvfcoPIWFYwAAlPfBUvVher65wWFLgXfyqyUfW9i2Jwchbz+C4uLSRvcoMFoCOurQyIETpC6vyL1ybzcK58iWBc0+gNEyP3q4cmB94+sSD7tB6UAGLB944pwithcUpQaDyViJymSHOLZjauh2U8Kxl8KaxJ3mSj/I1iCNrYmerBJ20heKeiLSX14dfqxI1TLI1gRuQlVWaMKIVXni6VWHK94lO33fO9kYFKUjbCR8kwMzY6iATzKojtj5hhc7ddPpur7DwrRvUS4gp3LS6lyvA4GpYyx+BIRLMlaQ/bNFFrC25kTbfkhal20ALXw6qnLBHa8oZ4Cf1unOczBBBzOy2W/15Q6oDzOcVE=----ATTACHMENT:----MTgyMzQ3OTA0OTYwOTg5MCAxMTQzNDI4ODkzMzQ0NzI1IDU2MTQwMDc0MDk0NzE2OTI=