//! ref: composer/src/Composer/Installer/InstallationManager.php use crate::dependency_resolver::operation::AnyOperation; use crate::dependency_resolver::operation::InstallOperation; use crate::dependency_resolver::operation::MarkAliasInstalledOperation; use crate::dependency_resolver::operation::MarkAliasUninstalledOperation; use crate::dependency_resolver::operation::UninstallOperation; use crate::dependency_resolver::operation::UpdateOperation; use crate::downloader::FileDownloader; use crate::event_dispatcher::EventDispatcher; use crate::installer::InstallerInterface; use crate::installer::PackageEvents; use crate::io::ConsoleIO; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::io::io_interface; use crate::package::PackageInterfaceHandle; use crate::repository::InstalledRepositoryInterface; use crate::util::Platform; use crate::util::r#loop::Loop; use crate::util::sync_executor; use indexmap::IndexMap; use shirabe_external_packages::seld::signal::SignalHandler; use shirabe_php_shim::{ InvalidArgumentException, PhpMixed, array_splice, array_unshift, http_build_query, json_encode, str_contains, str_replace, strpos, strtolower, }; /// Package operation manager. #[derive(Debug)] pub struct InstallationManager { /// Rc rather than Box so `get_installer` can hand out shareable handles: the download/cleanup /// futures collected for Loop::wait must own their installer beyond the loop iteration that /// created them (PHP closures capture $installer the same way). installers: Vec>, /// Maps a package type to the index of its installer in `installers`. PHP caches the installer /// instance itself; here we store an index instead. The index never dangles because both /// `add_installer` and `remove_installer` clear the cache whenever `installers` changes. /// RefCell so lookups can populate the cache through `&self` from concurrent operation chains. cache: std::cell::RefCell>, /// RefCell so mark_for_notification works through `&self` from concurrent operation chains. notifiable_packages: std::cell::RefCell>>, loop_: std::rc::Rc>, io: std::rc::Rc>, event_dispatcher: Option>>, output_progress: bool, /// For testing only: present iff this manager behaves like /// `Composer\Test\Mock\InstallationManagerMock`, recording operations instead of executing /// them. `None` in production. mock: Option, } /// For testing only: recorded operations for the `InstallationManagerMock` behavior. #[derive(Debug, Default)] struct InstallationManagerMockState { installed: Vec, updated: Vec<(PackageInterfaceHandle, PackageInterfaceHandle)>, uninstalled: Vec, trace: Vec, } impl InstallationManager { pub fn new( loop_: std::rc::Rc>, io: std::rc::Rc>, event_dispatcher: Option>>, ) -> Self { Self { installers: vec![], cache: std::cell::RefCell::new(IndexMap::new()), notifiable_packages: std::cell::RefCell::new(IndexMap::new()), loop_, io, event_dispatcher, output_progress: false, mock: None, } } /// For testing only: builds a manager that records operations instead of executing them, /// mirroring `Composer\Test\Mock\InstallationManagerMock`. pub fn __new_mock( loop_: std::rc::Rc>, io: std::rc::Rc>, event_dispatcher: Option>>, ) -> Self { Self { mock: Some(InstallationManagerMockState::default()), ..Self::new(loop_, io, event_dispatcher) } } /// For testing only: the trace of stringified operations recorded by the mock. pub fn __get_trace(&self) -> Vec { self.mock .as_ref() .map(|m| m.trace.clone()) .unwrap_or_default() } /// For testing only: packages passed to install (and markAliasInstalled) operations. pub fn __get_installed_packages(&self) -> Vec { self.mock .as_ref() .map(|m| m.installed.clone()) .unwrap_or_default() } /// For testing only: (initial, target) package pairs passed to update operations. pub fn __get_updated_packages(&self) -> Vec<(PackageInterfaceHandle, PackageInterfaceHandle)> { self.mock .as_ref() .map(|m| m.updated.clone()) .unwrap_or_default() } /// For testing only: packages passed to uninstall (and markAliasUninstalled) operations. pub fn __get_uninstalled_packages(&self) -> Vec { self.mock .as_ref() .map(|m| m.uninstalled.clone()) .unwrap_or_default() } pub fn reset(&mut self) { self.notifiable_packages = std::cell::RefCell::new(IndexMap::new()); FileDownloader::reset_download_metadata(); } /// Adds installer pub fn add_installer(&mut self, installer: Box) { array_unshift(&mut self.installers, std::rc::Rc::from(installer)); self.cache = std::cell::RefCell::new(IndexMap::new()); } /// For testing only: adds an installer as a pre-built shared handle, so the caller keeps an /// identity handle usable for PHP `assertSame`-style comparisons (`Rc::ptr_eq`) and for /// `remove_installer`. `add_installer` cannot serve because `Rc::from(Box)` reallocates, /// losing the caller's pointer identity. pub fn __add_installer(&mut self, installer: std::rc::Rc) { array_unshift(&mut self.installers, installer); self.cache = std::cell::RefCell::new(IndexMap::new()); } /// Removes installer pub fn remove_installer(&mut self, installer: &dyn InstallerInterface) { let target = installer as *const dyn InstallerInterface as *const (); let key = self .installers .iter() .position(|inst| &**inst as *const dyn InstallerInterface as *const () == target); if let Some(k) = key { array_splice(&mut self.installers, k as i64, Some(1), vec![]); self.cache = std::cell::RefCell::new(IndexMap::new()); } } /// Disables plugins. /// /// We prevent any plugins from being instantiated by /// disabling the PluginManager. This ensures that no third-party /// code is ever executed. pub fn disable_plugins(&mut self) { for installer in self.installers.iter() { if let Some(plugin_installer) = installer.as_plugin_installer() { plugin_installer.disable_plugins(); } } } /// Returns installer for a specific package type. pub fn get_installer( &self, r#type: &str, ) -> anyhow::Result> { let r#type = strtolower(r#type); if let Some(&index) = self.cache.borrow().get(&r#type) { return Ok(self.installers[index].clone()); } let index = self .installers .iter() .position(|installer| installer.supports(&r#type)); if let Some(index) = index { self.cache.borrow_mut().insert(r#type, index); return Ok(self.installers[index].clone()); } Err(InvalidArgumentException { message: format!("Unknown installer type: {}", r#type), code: 0, } .into()) } /// Checks whether provided package is installed in one of the registered installers. pub fn is_package_installed( &self, repo: &mut dyn InstalledRepositoryInterface, package: PackageInterfaceHandle, ) -> anyhow::Result { // For testing only (ref InstallationManagerMock::isPackageInstalled). if self.mock.is_some() { return repo.has_package(package); } if let Some(alias) = package.as_alias() { let alias_of: PackageInterfaceHandle = alias.get_alias_of().into(); return Ok(repo.has_package(package)? && self.is_package_installed(repo, alias_of)?); } self.get_installer(&package.get_type())? .is_installed(repo, package) } /// Install binary for the given package. /// If the installer associated to this package doesn't handle that function, it'll do nothing. pub fn ensure_binaries_presence(&self, package: PackageInterfaceHandle) { let installer = self.get_installer(&package.get_type()); let installer = match installer { Ok(i) => i, Err(_e) => { // no installer found for the current package type (@see `getInstaller()`) return; } }; // if the given installer support installing binaries if let Some(bp) = installer.as_binary_presence_interface() { bp.ensure_binaries_presence(package); } } /// Executes solver operation. pub fn execute( &mut self, repo: &mut dyn InstalledRepositoryInterface, operations: Vec, dev_mode: bool, run_scripts: bool, download_only: bool, ) -> anyhow::Result<()> { // For testing only: the mock records each operation and mutates the repo directly, // skipping the download step (ref InstallationManagerMock::execute). The alias operations' // repo mutation is inlined (rather than calling mark_alias_*) so `self.mock` can stay // borrowed across the loop without also borrowing `&self`. if let Some(mock) = self.mock.as_mut() { let _ = (dev_mode, run_scripts, download_only); for operation in operations { let trace = shirabe_php_shim::strip_tags(&operation.to_string()); match operation { AnyOperation::Install(op) => { let package = op.get_package(); mock.installed.push(package.clone()); mock.trace.push(trace); repo.add_package(PackageInterfaceHandle::dup(&package)); } AnyOperation::Update(op) => { let initial = op.get_initial_package(); let target = op.get_target_package(); mock.updated.push((initial.clone(), target.clone())); mock.trace.push(trace); repo.remove_package(initial); if !repo.has_package(target.clone())? { repo.add_package(PackageInterfaceHandle::dup(&target)); } } AnyOperation::Uninstall(op) => { let package = op.get_package(); mock.uninstalled.push(package.clone()); mock.trace.push(trace); repo.remove_package(package); } AnyOperation::MarkAliasInstalled(op) => { let package: PackageInterfaceHandle = op.get_package().into(); mock.installed.push(package.clone()); mock.trace.push(trace); if !repo.has_package(package.clone())? { repo.add_package(PackageInterfaceHandle::dup(&package)); } } AnyOperation::MarkAliasUninstalled(op) => { let package: PackageInterfaceHandle = op.get_package().into(); mock.uninstalled.push(package.clone()); mock.trace.push(trace); repo.remove_package(package); } } } return Ok(()); } // @var array> $cleanupPromises let mut cleanup_promises: IndexMap< i64, Box< dyn Fn() -> Option< std::pin::Pin>>>, >, >, > = IndexMap::new(); let signal_handler = SignalHandler::create( vec![ SignalHandler::SIGINT.to_string(), SignalHandler::SIGTERM.to_string(), SignalHandler::SIGHUP.to_string(), ], // TODO(phase-b): closure captures &mut self via &mut cleanup_promises Box::new(move |signal: String, handler: &SignalHandler| { // TODO(phase-b): self.io.write_error(...); self.run_cleanup(&cleanup_promises); let _ = signal; handler.exit_with_last_signal(); }), ); let all_operations: Vec = operations.clone(); // The concurrent operation chains share the repository; each chain borrows it only in // synchronous sections, never across an await. let repo_cell: std::cell::RefCell<&mut dyn InstalledRepositoryInterface> = std::cell::RefCell::new(repo); let result: anyhow::Result<()> = (|| -> anyhow::Result<()> { // execute operations in batches to make sure download-modifying-plugins are installed // before the other packages get downloaded let mut batches: Vec> = vec![]; let mut batch: IndexMap = IndexMap::new(); for (index, operation) in operations.into_iter().enumerate() { let index = index as i64; let package: Option = match &operation { AnyOperation::Update(update) => Some(update.get_target_package()), AnyOperation::Install(install) => Some(install.get_package()), _ => None, }; if let Some(package) = package && package.get_type() == "composer-plugin" { let extra = package.get_extra(); if extra .get("plugin-modifies-downloads") .and_then(|v| v.as_bool()) == Some(true) { if (batch.len() as i64) > 0 { batches.push(std::mem::take(&mut batch)); } let mut single = IndexMap::new(); single.insert(index, operation); batches.push(single); continue; } } batch.insert(index, operation); } if (batch.len() as i64) > 0 { batches.push(batch); } for batch_to_execute in batches { sync_executor::block_on(self.download_and_execute_batch( &repo_cell, batch_to_execute, &mut cleanup_promises, dev_mode, run_scripts, download_only, all_operations.clone(), ))?; } Ok(()) })(); // finally signal_handler.unregister(); match result { Ok(()) => {} Err(e) => { sync_executor::block_on(self.run_cleanup(&cleanup_promises)); return Err(e); } } if download_only { return Ok(()); } // 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_cell.into_inner().write(dev_mode, self); Ok(()) } #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")] async fn download_and_execute_batch( &self, repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, operations: IndexMap, cleanup_promises: &mut IndexMap< i64, Box< dyn Fn() -> Option< std::pin::Pin>>>, >, >, >, dev_mode: bool, run_scripts: bool, download_only: bool, all_operations: Vec, ) -> anyhow::Result<()> { let mut promises: Vec< std::pin::Pin>>>, > = vec![]; for (index, operation) in &operations { let op_type = operation.get_operation_type(); // ignoring alias ops as they don't need to execute anything at this stage if !["update", "install", "uninstall"].contains(&op_type) { continue; } let package = operation.get_target_package(); let initial_package: Option = match operation { AnyOperation::Update(update_op) => Some(update_op.get_initial_package()), _ => None, }; let installer = self.get_installer(&package.get_type())?; // PHP: $cleanupPromises[$index] = static function () use ($opType, $installer, $package, $initialPackage) { // if (null === $package->getInstallationSource()) { return \React\Promise\resolve(null); } // return $installer->cleanup($opType, $package, $initialPackage); }; let cleanup: Box< dyn Fn() -> Option< std::pin::Pin>>>, >, > = { let installer = installer.clone(); let package = package.clone(); let initial_package = initial_package.clone(); Box::new(move || { // avoid calling cleanup if the download was not even initialized for a package // as without installation source configured nothing will work if package.get_installation_source().is_none() { let fut: std::pin::Pin< Box>>, > = Box::pin(async { Ok(()) }); return Some(fut); } let installer = installer.clone(); let package = package.clone(); let initial_package = initial_package.clone(); let fut: std::pin::Pin< Box>>, > = Box::pin(async move { installer .cleanup(op_type, package, initial_package) .await .map(|_| ()) }); Some(fut) }) }; cleanup_promises.insert(*index, cleanup); if op_type != "uninstall" { let installer = installer.clone(); let package = package.clone(); let initial_package = initial_package.clone(); promises.push(Box::pin(async move { installer .download(package, initial_package) .await .map(|_| ()) })); } } // execute all downloads first if !promises.is_empty() { self.wait_on_promises(promises).await?; } if download_only { self.run_cleanup(cleanup_promises).await; return Ok(()); } // 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 let mut batches: Vec> = vec![]; let mut batch: IndexMap = IndexMap::new(); for (index, operation) in operations { let package: Option = match &operation { AnyOperation::Update(update) => Some(update.get_target_package()), AnyOperation::Install(install) => Some(install.get_package()), _ => None, }; if let Some(package) = package { let pkg_type = package.get_type(); if pkg_type == "composer-plugin" || pkg_type == "composer-installer" { if (batch.len() as i64) > 0 { batches.push(std::mem::take(&mut batch)); } let mut single = IndexMap::new(); single.insert(index, operation); batches.push(single); continue; } } batch.insert(index, operation); } if (batch.len() as i64) > 0 { batches.push(batch); } for batch_to_execute in batches { self.execute_batch( repo, batch_to_execute, cleanup_promises, dev_mode, run_scripts, &all_operations, ) .await?; } Ok(()) } async fn execute_batch( &self, repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, operations: IndexMap, cleanup_promises: &IndexMap< i64, Box< dyn Fn() -> Option< std::pin::Pin>>>, >, >, >, dev_mode: bool, run_scripts: bool, all_operations: &[AnyOperation], ) -> anyhow::Result<()> { let mut promises: Vec< std::pin::Pin> + '_>>, > = vec![]; for (index, operation) in operations { let op_type = operation.get_operation_type(); // ignoring alias ops as they don't need to execute anything if !["update", "install", "uninstall"].contains(&op_type) { // output alias ops in debug verbosity as they have no output otherwise if self.io.is_debug() { self.io.write_error3( &format!(" - {}", operation.show(false)), true, io_interface::NORMAL, ); } match &operation { AnyOperation::MarkAliasInstalled(op) => { self.mark_alias_installed(&mut **repo.borrow_mut(), op)?; } AnyOperation::MarkAliasUninstalled(op) => { self.mark_alias_uninstalled(&mut **repo.borrow_mut(), op); } _ => {} } continue; } let package = operation.get_target_package(); let initial_package: Option = match &operation { AnyOperation::Update(update_op) => Some(update_op.get_initial_package()), _ => None, }; let event_name = match op_type { "install" => PackageEvents::PRE_PACKAGE_INSTALL, "update" => PackageEvents::PRE_PACKAGE_UPDATE, "uninstall" => PackageEvents::PRE_PACKAGE_UNINSTALL, _ => "", }; if run_scripts && self.event_dispatcher.is_some() { // TODO(phase-c): dispatch_package_event takes Box/Vec> // but we hold a RefCell'd &mut dyn here. Needs structural rework (likely shared Rc // on repo and ops). let _ = (event_name, dev_mode, &repo, &all_operations, &operation); } let installer = self.get_installer(&package.get_type())?; // PHP: $promise = $installer->prepare(...) // ->then(fn() => $this->{$opType}($repo, $operation)) // ->then($cleanupPromises[$index]) // ->then(fn() => $repo->write($devMode, $this), fn($e) => { " of // failed"; throw $e; }) // ->then(fn() => dispatch POST_PACKAGE_* event); // each package gets its own chain and the whole batch resolves via waitOnPromises. promises.push(Box::pin(async move { let chain_result: anyhow::Result<()> = async { installer .prepare(op_type, package.clone(), initial_package.clone()) .await?; match &operation { AnyOperation::Install(op) => { self.install(repo, op).await?; } AnyOperation::Update(op) => { self.update(repo, op).await?; } AnyOperation::Uninstall(op) => { self.uninstall(repo, op).await?; } AnyOperation::MarkAliasInstalled(_) | AnyOperation::MarkAliasUninstalled(_) => { unreachable!("alias operations were skipped above") } } if let Some(cleanup) = cleanup_promises.get(&index) && let Some(fut) = cleanup() { fut.await?; } Ok(()) } .await; // PHP rejects the promise with an " of failed" message before rethrowing. if let Err(e) = chain_result { self.io.write_error(&format!( " {} of {} failed", shirabe_php_shim::ucfirst(op_type), package.get_pretty_name() )); return Err(e); } // PHP: ->then(fn() => $repo->write($devMode, $this)) persists the repository after each op. repo.borrow_mut().write(dev_mode, self); let event_name_post = match op_type { "install" => PackageEvents::POST_PACKAGE_INSTALL, "update" => PackageEvents::POST_PACKAGE_UPDATE, "uninstall" => PackageEvents::POST_PACKAGE_UNINSTALL, _ => "", }; if run_scripts && self.event_dispatcher.is_some() { // PHP dispatches the POST_PACKAGE_* event at the end of the chain via the event // dispatcher with repo/all_operations/operation. // TODO(phase-c): dispatch_package_event takes Box/ // Vec> but we hold a RefCell'd &mut dyn here. Needs structural rework // (likely shared Rc on repo and ops). let _ = event_name_post; } Ok(()) })); } if !promises.is_empty() { self.wait_on_promises(promises).await?; } Platform::workaround_filesystem_issues(); Ok(()) } /// Executes download operation. pub async fn download(&self, package: PackageInterfaceHandle) -> Option { let installer = self.get_installer(&package.get_type()).ok()?; installer.cleanup("install", package, None).await.ok()? } /// Executes install operation. pub async fn install( &self, repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, operation: &InstallOperation, ) -> anyhow::Result> { let package = operation.get_package(); let package_type = package.get_type(); let installer = self.get_installer(&package_type)?; let promise = installer.install(repo, package.clone()).await?; self.mark_for_notification(package.clone()); Ok(promise) } /// Executes update operation. pub async fn update( &self, repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, operation: &UpdateOperation, ) -> anyhow::Result> { let initial = operation.get_initial_package().clone(); let target = operation.get_target_package().clone(); let initial_type = initial.get_type(); let target_type = target.get_type(); if initial_type == target_type { let installer = self.get_installer(&initial_type)?; let promise = installer.update(repo, initial, target.clone()).await?; self.mark_for_notification(target.clone()); Ok(promise) } else { // PHP: uninstall initial, then install target via the target-type installer. let _ = self .get_installer(&initial_type)? .uninstall(repo, initial) .await?; let installer = self.get_installer(&target_type)?; installer.install(repo, target).await } } /// Uninstalls package. pub async fn uninstall( &self, repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, operation: &UninstallOperation, ) -> anyhow::Result> { let package = operation.get_package(); let package_type = package.get_type(); let installer = self.get_installer(&package_type)?; installer.uninstall(repo, package).await } /// Executes markAliasInstalled operation. pub fn mark_alias_installed( &self, repo: &mut dyn InstalledRepositoryInterface, operation: &MarkAliasInstalledOperation, ) -> anyhow::Result<()> { let package = operation.get_package(); if !repo.has_package(package.clone().into())? { repo.add_package(crate::package::PackageInterfaceHandle::dup(&package.into())); } Ok(()) } /// Executes markAlias operation. pub fn mark_alias_uninstalled( &self, repo: &mut dyn InstalledRepositoryInterface, operation: &MarkAliasUninstalledOperation, ) { let package = operation.get_package(); repo.remove_package(package.into()); } /// Returns the installation path of a package pub fn get_install_path(&self, package: PackageInterfaceHandle) -> Option { // For testing only (ref InstallationManagerMock::getInstallPath). if self.mock.is_some() { return Some(format!("vendor/{}", package.get_name())); } let installer = self.get_installer(&package.get_type()).ok()?; installer.get_install_path(package) } pub fn set_output_progress(&mut self, output_progress: bool) { self.output_progress = output_progress; } pub fn notify_installs(&mut self, _io: std::rc::Rc>) { // For testing only (ref InstallationManagerMock::notifyInstalls is a noop). if self.mock.is_some() { return; } // TODO(phase-c-promise): PHP collects every http_downloader.add() promise and runs them via // Loop::wait; the single-threaded sync bridge block_on's each notification serially instead. let result: anyhow::Result<()> = (|| -> anyhow::Result<()> { for (repo_url, packages) in self.notifiable_packages.borrow().iter() { // non-batch API, deprecated if str_contains(repo_url, "%package%") { for package in packages { let url = str_replace("%package%", &package.get_pretty_name(), repo_url); let mut params: IndexMap = IndexMap::new(); params.insert("version".to_string(), package.get_pretty_version()); params.insert("version_normalized".to_string(), package.get_version()); let mut opts: IndexMap = IndexMap::new(); opts.insert("retry-auth-failure".to_string(), PhpMixed::Bool(false)); let mut http: IndexMap = IndexMap::new(); http.insert("method".to_string(), PhpMixed::String("POST".to_string())); http.insert( "header".to_string(), PhpMixed::List(vec![PhpMixed::String( "Content-type: application/x-www-form-urlencoded".to_string(), )]), ); let params_vec: Vec<(&str, &str)> = params .iter() .map(|(k, v)| (k.as_str(), v.as_str())) .collect(); http.insert( "content".to_string(), PhpMixed::String(http_build_query(¶ms_vec, "", "&")), ); http.insert("timeout".to_string(), PhpMixed::Int(3)); opts.insert( "http".to_string(), PhpMixed::Array(http.into_iter().collect()), ); sync_executor::block_on( self.loop_ .borrow() .get_http_downloader() .borrow_mut() .add(&url, opts), )?; } continue; } let mut post_data: IndexMap = IndexMap::new(); post_data.insert("downloads".to_string(), PhpMixed::List(vec![])); for package in packages { let mut package_notification: IndexMap = IndexMap::new(); package_notification.insert( "name".to_string(), PhpMixed::String(package.get_pretty_name()), ); package_notification.insert( "version".to_string(), PhpMixed::String(package.get_version()), ); if strpos(repo_url, "packagist.org/").is_some() { if let Some(metadata) = FileDownloader::download_metadata().get(&package.get_name()) { package_notification.insert("downloaded".to_string(), metadata.clone()); } else { package_notification .insert("downloaded".to_string(), PhpMixed::Bool(false)); } } if let Some(PhpMixed::List(downloads)) = post_data.get_mut("downloads") { downloads.push(PhpMixed::Array(package_notification.into_iter().collect())); } } let mut opts: IndexMap = IndexMap::new(); opts.insert("retry-auth-failure".to_string(), PhpMixed::Bool(false)); let mut http: IndexMap = IndexMap::new(); http.insert("method".to_string(), PhpMixed::String("POST".to_string())); http.insert( "header".to_string(), PhpMixed::List(vec![PhpMixed::String( "Content-Type: application/json".to_string(), )]), ); http.insert( "content".to_string(), PhpMixed::String( json_encode(&PhpMixed::Array(post_data.into_iter().collect())) .unwrap_or_default(), ), ); http.insert("timeout".to_string(), PhpMixed::Int(6)); opts.insert( "http".to_string(), PhpMixed::Array(http.into_iter().collect()), ); sync_executor::block_on( self.loop_ .borrow() .get_http_downloader() .borrow_mut() .add(repo_url, opts), )?; } Ok(()) })(); // PHP swallows the exception silently here let _ = result; self.reset(); } fn mark_for_notification(&self, package: PackageInterfaceHandle) { if let Some(notification_url) = package.get_notification_url() { self.notifiable_packages .borrow_mut() .entry(notification_url) .or_default() .push(package); } } /// PHP: waitOnPromises() creates a ProgressBar up front and Loop::wait advances it while the /// concurrent promises resolve. /// TODO(phase-c-promise): Loop::wait has no active-job counter to feed the bar yet, so a /// single 0% -> 100% jump is rendered after the wait instead of PHP's timing-driven /// intermediate snapshots. async fn wait_on_promises<'p>( &self, promises: Vec< std::pin::Pin> + 'p>>, >, ) -> anyhow::Result<()> { let promise_count = promises.len() as i64; let show_progress = self.output_progress && !Platform::get_env("CI").is_some_and(|v| !v.is_empty() && v != "0") && !self.io.is_debug() && promise_count > 1; let result = self.loop_.borrow_mut().wait(promises, None).await; if result.is_ok() && show_progress { let bar = { let io_ref = self.io.borrow(); io_ref .as_any() .downcast_ref::() .map(|console_io| console_io.get_progress_bar(promise_count)) }; if let Some(mut bar) = bar { bar.start(Some(promise_count))?; bar.set_progress(promise_count)?; bar.finish()?; bar.clear()?; // ProgressBar in non-decorated output does not output a final line-break and clear() does nothing if !self.io.is_decorated() { self.io.write_error(""); } } } result } async fn run_cleanup( &self, cleanup_promises: &IndexMap< i64, Box< dyn Fn() -> Option< std::pin::Pin>>>, >, >, >, ) { let mut promises: Vec< std::pin::Pin>>>, > = vec![]; self.loop_.borrow().abort_jobs(); for (_, cleanup) in cleanup_promises { // PHP wraps a missing cleanup promise in \React\Promise\resolve(null). let promise = cleanup(); if let Some(p) = promise { promises.push(p); } else { promises.push(Box::pin(async { Ok(()) })); } } if (promises.len() as i64) > 0 { let _ = self.loop_.borrow_mut().wait(promises, None).await; } } } // Composer's PartialComposer::setInstallationManager() accepts any InstallationManager subclass, so // plugins may swap in a replacement. The interface captures the methods reached through Composer's // accessor and through the `&mut dyn InstallationManagerInterface` references fed from it. pub trait InstallationManagerInterface: std::fmt::Debug { /// For testing only: lets a test recover the concrete manager (e.g. the recording mock) from a /// trait object returned by `Composer::get_installation_manager`. fn as_any(&self) -> &dyn std::any::Any { unimplemented!("as_any is only implemented for the concrete InstallationManager") } fn add_installer(&mut self, installer: Box); fn remove_installer(&mut self, installer: &dyn InstallerInterface); fn disable_plugins(&mut self); fn is_package_installed( &mut self, repo: &mut dyn InstalledRepositoryInterface, package: PackageInterfaceHandle, ) -> anyhow::Result; fn ensure_binaries_presence(&mut self, package: PackageInterfaceHandle); fn execute( &mut self, repo: &mut dyn InstalledRepositoryInterface, operations: Vec, dev_mode: bool, run_scripts: bool, download_only: bool, ) -> anyhow::Result<()>; fn get_install_path(&self, package: PackageInterfaceHandle) -> Option; fn set_output_progress(&mut self, output_progress: bool); fn notify_installs(&mut self, io: std::rc::Rc>); } impl InstallationManagerInterface for InstallationManager { fn as_any(&self) -> &dyn std::any::Any { self } fn add_installer(&mut self, installer: Box) { self.add_installer(installer); } fn remove_installer(&mut self, installer: &dyn InstallerInterface) { self.remove_installer(installer); } fn disable_plugins(&mut self) { self.disable_plugins(); } fn is_package_installed( &mut self, repo: &mut dyn InstalledRepositoryInterface, package: PackageInterfaceHandle, ) -> anyhow::Result { InstallationManager::is_package_installed(self, repo, package) } fn ensure_binaries_presence(&mut self, package: PackageInterfaceHandle) { InstallationManager::ensure_binaries_presence(self, package); } fn execute( &mut self, repo: &mut dyn InstalledRepositoryInterface, operations: Vec, dev_mode: bool, run_scripts: bool, download_only: bool, ) -> anyhow::Result<()> { self.execute(repo, operations, dev_mode, run_scripts, download_only) } fn get_install_path(&self, package: PackageInterfaceHandle) -> Option { self.get_install_path(package) } fn set_output_progress(&mut self, output_progress: bool) { self.set_output_progress(output_progress); } fn notify_installs(&mut self, io: std::rc::Rc>) { self.notify_installs(io); } }