//! ref: composer/src/Composer/Installer/InstallationManager.php use crate::dependency_resolver::operation::InstallOperation; use crate::dependency_resolver::operation::MarkAliasInstalledOperation; use crate::dependency_resolver::operation::MarkAliasUninstalledOperation; use crate::dependency_resolver::operation::OperationInterface; 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::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 anyhow::Result; 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 { 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 to avoid sharing ownership of the boxed /// installer. The index never dangles because both `add_installer` and `remove_installer` /// clear the cache whenever `installers` changes. cache: IndexMap, notifiable_packages: IndexMap>, loop_: std::rc::Rc>, io: std::rc::Rc>, event_dispatcher: Option>>, output_progress: bool, } impl InstallationManager { pub fn new( loop_: std::rc::Rc>, io: std::rc::Rc>, event_dispatcher: Option>>, ) -> Self { Self { installers: vec![], cache: IndexMap::new(), notifiable_packages: IndexMap::new(), loop_, io, event_dispatcher, output_progress: false, } } pub fn reset(&mut self) { self.notifiable_packages = IndexMap::new(); FileDownloader::reset_download_metadata(); } /// Adds installer pub fn add_installer(&mut self, installer: Box) { array_unshift(&mut self.installers, installer); self.cache = 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_ref() 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 = 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_mut() { if let Some(plugin_installer) = installer.as_plugin_installer_mut() { plugin_installer.disable_plugins(); } } } /// Returns installer for a specific package type. pub fn get_installer(&mut self, r#type: &str) -> Result<&mut dyn InstallerInterface> { let r#type = strtolower(r#type); if let Some(&index) = self.cache.get(&r#type) { return Ok(self.installers[index].as_mut()); } let index = self .installers .iter() .position(|installer| installer.supports(&r#type)); if let Some(index) = index { self.cache.insert(r#type.clone(), index); return Ok(self.installers[index].as_mut()); } 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( &mut self, repo: &dyn InstalledRepositoryInterface, package: PackageInterfaceHandle, ) -> Result { if let Some(alias) = package.as_alias() { let alias_of: PackageInterfaceHandle = alias.get_alias_of().into(); return Ok( repo.has_package(package.clone()) && self.is_package_installed(repo, alias_of)? ); } Ok(self .get_installer(&package.get_type())? .is_installed(repo, package.clone())) } /// 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(&mut 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, ) -> Result<()> { // @var array> $cleanupPromises let mut cleanup_promises: IndexMap< i64, Box< dyn Fn() -> Option>>>>, >, > = 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(); let result: Result<()> = (|| -> 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 = if let Some(update) = operation.as_update_operation() { Some(update.get_target_package()) } else { operation .as_install_operation() .map(|install| install.get_package()) }; 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, 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.write(dev_mode, self); Ok(()) } #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")] async fn download_and_execute_batch( &mut self, repo: &mut dyn InstalledRepositoryInterface, operations: IndexMap>, cleanup_promises: &mut IndexMap< i64, Box< dyn Fn() -> Option>>>>, >, >, dev_mode: bool, run_scripts: bool, download_only: bool, all_operations: Vec>, ) -> Result<()> { 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.as_str()) { continue; } let package: PackageInterfaceHandle; let initial_package: Option; if op_type == "update" { // @var UpdateOperation $operation let update_op = operation .as_update_operation() .expect("op_type == \"update\" implies UpdateOperation"); package = update_op.get_target_package(); initial_package = Some(update_op.get_initial_package()); } else { // @var InstallOperation|MarkAliasInstalledOperation|MarkAliasUninstalledOperation|UninstallOperation $operation package = operation.get_package(); initial_package = None; } let installer = self.get_installer(&package.get_type())?; // PHP: $cleanupPromises[$index] = function () use ($index, $installer, $type, $package) { // if (null === $package->getInstallationSource()) { return \React\Promise\resolve(null); } // return $installer->cleanup($type, $package); }; // TODO(phase-c): the cleanup callable must capture the installer and package and invoke // installer.cleanup(...) returning a React promise. It is a 'static closure stored in // cleanup_promises, so installer/package must be Rc-shared (the installer registry is // not Rc yet, see get_installer) and the promise type must be modelled. Both depend on // the async/React-Promise rework, so a no-op future is stored instead. let _ = installer; let op_type_clone = op_type.clone(); let cleanup: Box< dyn Fn() -> Option>>>>, > = Box::new(move || { // avoid calling cleanup if the download was not even initialized for a package // as without installation source configured nothing will work // TODO(phase-b): if (null === $package->getInstallationSource()) return resolve(null); let _ = &op_type_clone; // TODO(phase-c-promise): build the real installer.cleanup() future once the installer // can be shared into a 'static cleanup closure (Stage 2 Rc/Arc). let fut: std::pin::Pin>>> = Box::pin(async { Ok(()) }); Some(fut) }); cleanup_promises.insert(*index, cleanup); if op_type != "uninstall" { // TODO(phase-c-promise): PHP collects every download and runs them concurrently via // Loop::wait; the single-threaded loop awaits each serially instead. let installer = self.get_installer(&package.get_type())?; installer.download(package, initial_package).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 = if let Some(update) = operation.as_update_operation() { Some(update.get_target_package()) } else { operation .as_install_operation() .map(|install| install.get_package()) }; 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( &mut self, repo: &mut dyn InstalledRepositoryInterface, operations: IndexMap>, cleanup_promises: &IndexMap< i64, Box< dyn Fn() -> Option>>>>, >, >, dev_mode: bool, run_scripts: bool, all_operations: &[std::rc::Rc], ) -> Result<()> { let mut post_exec_callbacks: Vec> = 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.as_str()) { // 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 op_type.as_str() { "markAliasInstalled" => { let op = operation .as_any() .downcast_ref::() .expect( "op_type == \"markAliasInstalled\" implies MarkAliasInstalledOperation", ); self.mark_alias_installed(repo, op); } "markAliasUninstalled" => { let op = operation .as_any() .downcast_ref::() .expect( "op_type == \"markAliasUninstalled\" implies MarkAliasUninstalledOperation", ); self.mark_alias_uninstalled(repo, op); } _ => {} } continue; } let package: PackageInterfaceHandle; let initial_package: Option; if op_type == "update" { let update_op = operation .as_update_operation() .expect("op_type == \"update\" implies UpdateOperation"); package = update_op.get_target_package(); initial_package = Some(update_op.get_initial_package()); } else { package = operation.get_package(); initial_package = None; } let event_name = match op_type.as_str() { "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 &mut dyn here. Needs structural rework (likely shared Rc on repo and ops). let _ = ( event_name, dev_mode, &repo, &all_operations, operation.as_ref(), ); } let _dispatcher = self.event_dispatcher.as_ref(); let _io = self.io.as_ref(); let installer = self.get_installer(&package.get_type())?; // TODO(phase-c-promise): PHP chains prepare()->then(install/update/uninstall)->then(cleanup // + repo.write); the single-threaded loop awaits prepare and leaves the rest as phase-b work. installer .prepare(&op_type, package, initial_package) .await?; // PHP: $promise = $promise->then(fn() => $this->{$type}(...))->then($cleanupPromises[$index]) // ->then(fn() => $repo->write($devMode, $this->io)); the chained steps run install/ // update/uninstall, then cleanup, then persist the repository. // TODO(phase-c): this promise chain (install step -> cleanup_promises[index] -> // repo.write) needs the React\Promise model and the cleanup callables wired (see above); // both stay todo!(), so only prepare() is awaited here. let _ = cleanup_promises.get(&index); let event_name_post = match op_type.as_str() { "install" => PackageEvents::POST_PACKAGE_INSTALL, "update" => PackageEvents::POST_PACKAGE_UPDATE, "uninstall" => PackageEvents::POST_PACKAGE_UNINSTALL, _ => "", }; if run_scripts && self.event_dispatcher.is_some() { // PHP appends a post-exec step to the promise chain that dispatches the // POST_PACKAGE_* event via the event dispatcher with repo/all_operations/operation. // TODO(phase-c): the callback captures the event dispatcher (&mut) and the operation // and must outlive the loop body; that requires the dispatcher behind Rc // and the deferred event dispatch to be wired into the promise chain (todo!()). let _ = event_name_post; post_exec_callbacks.push(Box::new(|| { // dispatcher.dispatch_package_event(event_name_post, dev_mode, repo, all_operations, operation); })); } } Platform::workaround_filesystem_issues(); for cb in &post_exec_callbacks { cb(); } Ok(()) } /// Executes download operation. pub async fn download(&mut 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( &mut self, repo: &mut dyn InstalledRepositoryInterface, operation: &InstallOperation, ) -> Option { let package = operation.get_package(); let package_type = package.get_type(); let installer = self.get_installer(&package_type).ok()?; let promise = installer.install(repo, package.clone()).await.ok()?; self.mark_for_notification(package.clone()); promise } /// Executes update operation. pub async fn update( &mut self, repo: &mut dyn InstalledRepositoryInterface, operation: &UpdateOperation, ) -> Option { 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).ok()?; let promise = installer.update(repo, initial, target.clone()).await.ok()?; self.mark_for_notification(target.clone()); promise } else { // PHP: uninstall initial, then install target via the target-type installer. let _ = self .get_installer(&initial_type) .ok()? .uninstall(repo, initial) .await .ok()?; let installer = self.get_installer(&target_type).ok()?; installer.install(repo, target).await.ok()? } } /// Uninstalls package. pub async fn uninstall( &mut self, repo: &mut dyn InstalledRepositoryInterface, operation: &UninstallOperation, ) -> Option { let package = operation.get_package(); let package_type = package.get_type(); let installer = self.get_installer(&package_type).ok()?; installer.uninstall(repo, package).await.ok()? } /// Executes markAliasInstalled operation. pub fn mark_alias_installed( &self, repo: &mut dyn InstalledRepositoryInterface, operation: &MarkAliasInstalledOperation, ) { let package = operation.get_package(); if !repo.has_package(package.clone().into()) { repo.add_package(crate::package::PackageInterfaceHandle::dup(&package.into())); } } /// Executes markAlias operation. pub fn mark_alias_uninstalled( &self, repo: &mut dyn InstalledRepositoryInterface, operation: &MarkAliasUninstalledOperation, ) { let package = operation.get_package(); repo.remove_package(package.clone().into()); } /// Returns the installation path of a package pub fn get_install_path(&mut self, package: PackageInterfaceHandle) -> Option { 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>) { // 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: Result<()> = (|| -> Result<()> { for (repo_url, packages) in &self.notifiable_packages { // 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(&mut self, package: PackageInterfaceHandle) { if let Some(notification_url) = package.get_notification_url() { self.notifiable_packages .entry(notification_url) .or_default() .push(package.clone()); } } async fn run_cleanup( &mut self, cleanup_promises: &IndexMap< i64, Box< dyn Fn() -> Option>>>>, >, >, ) { let mut promises: Vec>>>> = 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 { 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: &dyn InstalledRepositoryInterface, package: PackageInterfaceHandle, ) -> 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, ) -> Result<()>; fn get_install_path(&mut 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 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: &dyn InstalledRepositoryInterface, package: PackageInterfaceHandle, ) -> Result { self.is_package_installed(repo, package) } fn ensure_binaries_presence(&mut self, package: PackageInterfaceHandle) { self.ensure_binaries_presence(package); } fn execute( &mut self, repo: &mut dyn InstalledRepositoryInterface, operations: Vec>, dev_mode: bool, run_scripts: bool, download_only: bool, ) -> Result<()> { self.execute(repo, operations, dev_mode, run_scripts, download_only) } fn get_install_path(&mut 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); } }