From c82da84ae1a8cee23670a74a646584ab637308d1 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 7 Aug 2026 20:51:59 +0900 Subject: feat(installer): dispatch package events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InstallationManager left both PRE_PACKAGE_* and POST_PACKAGE_* as empty stubs, so a subscriber never ran at all and the difference from upstream was silent rather than an explicit error. Operations cross the boundary as R-table entities with generated proxy stubs. Materializing them the way a Link crosses is not possible: a materialized value is revived by unserialize() on the child side, so its properties never pass through the wire decoder and a nested handle descriptor would not come back as a stub — and an operation always holds a PackageInterface. execute() now shares one Rc per operation through the whole batch pipeline, so a plugin sees one object for both the pre- and the post-event of an operation, as it does in PHP. POST_PACKAGE_* also moves out of the operation's promise chain into the post-exec callback list PHP runs after waitOnPromises(). The stub generator materializes non-public class constants verbatim now, which the operation classes need for their `protected const TYPE`: a constant has no entity behind it, so a copy in the worker cannot diverge, and keeping the declared visibility exposes nothing the real class hides. The E2E fixture added here compares the recorded events against upstream Composer. It also surfaced that upstream starts an operation's chain where it is built (a null prepare() becomes an already-fulfilled React promise whose handlers run through the immediately drained queue) while this port only drives its futures in wait_on_promises, so the repository state a pre-event observes differs; that half of the comparison is a separate `#[ignore]`d test. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/event_dispatcher/event_dispatcher.rs | 112 +++++++++++----- .../shirabe/src/installer/installation_manager.rs | 98 ++++++++------ crates/shirabe/src/installer/package_event.rs | 24 ++-- crates/shirabe/src/plugin/php_plugin_proxy.rs | 144 +++++++++++++++++++-- 4 files changed, 281 insertions(+), 97 deletions(-) (limited to 'crates/shirabe/src') diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs index 1480188a..b821f647 100644 --- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs +++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs @@ -16,7 +16,7 @@ use crate::installer::PackageEvent; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::plugin::php_plugin_proxy::PluginRpcDispatcher; -use crate::repository::RepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use crate::script::Event as ScriptEvent; use crate::util::Platform; use crate::util::ProcessExecutor; @@ -250,9 +250,9 @@ impl EventDispatcher { &mut self, event_name: &str, dev_mode: bool, - local_repo: Box, - operations: Vec, - operation: AnyOperation, + local_repo: InstalledRepositoryInterfaceHandle, + operations: Vec>, + operation: std::rc::Rc, ) -> anyhow::Result { let composer = self.composer(); assert!( @@ -432,8 +432,7 @@ impl EventDispatcher { crate::io::VERBOSE, ); let stub_class = Self::event_stub_class(event).ok_or_else(|| { - // TODO(plugin): only the base Event and Script\Event proxy stubs exist so - // far; installer/package/plugin events need their own stubs. + // TODO(plugin): installer and plugin events have no proxy stub yet. anyhow::anyhow!(RuntimeException { message: format!( "no proxy stub is available yet for the event `{}` dispatched to {}::{}", @@ -1154,8 +1153,7 @@ try {{ } let stub_class = Self::event_stub_class(event).ok_or_else(|| { - // TODO(plugin): only the base Event and Script\Event proxy stubs exist so far; - // installer/package/plugin events need their own stubs. + // TODO(plugin): installer and plugin events have no proxy stub yet. anyhow::anyhow!(RuntimeException { message: format!( "no proxy stub is available yet for the event `{}` dispatched to {}::{}", @@ -1200,6 +1198,8 @@ try {{ fn event_stub_class(event: &dyn EventInterface) -> Option<&'static str> { if event.as_any().downcast_ref::().is_some() { Some("Composer\\Script\\Event") + } else if event.as_any().downcast_ref::().is_some() { + Some("Composer\\Installer\\PackageEvent") } else if event.as_any().downcast_ref::().is_some() { Some("Composer\\EventDispatcher\\Event") } else { @@ -1755,39 +1755,75 @@ pub(crate) fn dispatch_event_method( event.get_flags().clone(), ))), "isPropagationStopped" => Ok(PluginValue::Bool(event.is_propagation_stopped())), - "isDevMode" => match event.as_any().downcast_ref::() { - Some(script_event) => Ok(PluginValue::Bool(script_event.is_dev_mode())), + "isDevMode" => match (script_event(event), package_event(event)) { + (Some(event), _) => Ok(PluginValue::Bool(event.is_dev_mode())), + (_, Some(event)) => Ok(PluginValue::Bool(event.is_dev_mode())), + _ => Err(runtime_throw( + "isDevMode is only available on script and package events".to_string(), + )), + }, + "getComposer" => { + let composer = match (script_event(event), package_event(event)) { + (Some(event), _) => event.get_composer().upgrade(), + (_, Some(event)) => event.get_composer().upgrade(), + _ => { + return Err(runtime_throw( + "getComposer is only available on script and package events".to_string(), + )); + } + }; + let composer = composer.ok_or_else(|| { + runtime_throw("the Composer instance of this event is gone".to_string()) + })?; + let rhandle = crate::plugin::php_plugin_proxy::register_composer_entity(&composer); + Ok(crate::plugin::php_plugin_proxy::rust_handle_value( + rhandle, + "Composer\\Composer", + )) + } + "getIO" => { + let io = match (script_event(event), package_event(event)) { + (Some(event), _) => event.get_io(), + (_, Some(event)) => event.get_io(), + _ => { + return Err(runtime_throw( + "getIO is only available on script and package events".to_string(), + )); + } + }; + let class = crate::plugin::php_plugin_proxy::io_stub_class(&io) + .map_err(|error| runtime_throw(error.to_string()))?; + let rhandle = crate::plugin::php_plugin_proxy::register_io_entity(&io); + Ok(crate::plugin::php_plugin_proxy::rust_handle_value( + rhandle, class, + )) + } + "getLocalRepo" => match package_event(event) { + Some(event) => crate::plugin::php_plugin_proxy::repository_handle_value( + &event.get_local_repo().as_repository_handle(), + ), None => Err(runtime_throw( - "isDevMode is only available on script events".to_string(), + "getLocalRepo is only available on package events".to_string(), )), }, - "getComposer" => match event.as_any().downcast_ref::() { - Some(script_event) => { - let composer = script_event.get_composer().upgrade().ok_or_else(|| { - runtime_throw("the Composer instance of this event is gone".to_string()) - })?; - let rhandle = crate::plugin::php_plugin_proxy::register_composer_entity(&composer); - Ok(crate::plugin::php_plugin_proxy::rust_handle_value( - rhandle, - "Composer\\Composer", - )) - } + "getOperations" => match package_event(event) { + Some(event) => Ok(PluginValue::List( + event + .get_operations() + .iter() + .map(crate::plugin::php_plugin_proxy::operation_handle_value) + .collect(), + )), None => Err(runtime_throw( - "getComposer is only available on script events".to_string(), + "getOperations is only available on package events".to_string(), )), }, - "getIO" => match event.as_any().downcast_ref::() { - Some(script_event) => { - let io = script_event.get_io(); - let class = crate::plugin::php_plugin_proxy::io_stub_class(&io) - .map_err(|error| runtime_throw(error.to_string()))?; - let rhandle = crate::plugin::php_plugin_proxy::register_io_entity(&io); - Ok(crate::plugin::php_plugin_proxy::rust_handle_value( - rhandle, class, - )) - } + "getOperation" => match package_event(event) { + Some(event) => Ok(crate::plugin::php_plugin_proxy::operation_handle_value( + event.get_operation(), + )), None => Err(runtime_throw( - "getIO is only available on script events".to_string(), + "getOperation is only available on package events".to_string(), )), }, // TODO(plugin): stopPropagation and the rest need full proxying of the object graph @@ -1798,6 +1834,14 @@ pub(crate) fn dispatch_event_method( } } +fn script_event(event: &dyn EventInterface) -> Option<&ScriptEvent> { + event.as_any().downcast_ref::() +} + +fn package_event(event: &dyn EventInterface) -> Option<&PackageEvent> { + event.as_any().downcast_ref::() +} + fn runtime_throw(message: String) -> PhpThrow { PhpThrow { exception_class: "RuntimeException".to_string(), diff --git a/crates/shirabe/src/installer/installation_manager.rs b/crates/shirabe/src/installer/installation_manager.rs index 439026b0..1f1c60a5 100644 --- a/crates/shirabe/src/installer/installation_manager.rs +++ b/crates/shirabe/src/installer/installation_manager.rs @@ -327,16 +327,19 @@ impl InstallationManager { }), ); - let all_operations: Vec = operations.clone(); + // Shared rather than owned so that one operation reaches a plugin as one object, both + // through the whole batch pipeline and through its pre- and post-event. + let all_operations: Vec> = + operations.into_iter().map(std::rc::Rc::new).collect(); 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 mut batches: Vec>> = vec![]; + let mut batch: IndexMap> = IndexMap::new(); + for (index, operation) in all_operations.iter().cloned().enumerate() { let index = index as i64; - let package: Option = match &operation { + let package: Option = match &*operation { AnyOperation::Update(update) => Some(update.get_target_package()), AnyOperation::Install(install) => Some(install.get_package()), _ => None, @@ -409,7 +412,7 @@ impl InstallationManager { async fn download_and_execute_batch( &self, repo: &InstalledRepositoryInterfaceHandle, - operations: IndexMap, + operations: IndexMap>, cleanup_promises: &mut IndexMap< i64, Box< @@ -421,7 +424,7 @@ impl InstallationManager { dev_mode: bool, run_scripts: bool, download_only: bool, - all_operations: Vec, + all_operations: Vec>, ) -> anyhow::Result<()> { let mut promises: Vec< std::pin::Pin>>>, @@ -436,7 +439,7 @@ impl InstallationManager { } let package = operation.get_target_package(); - let initial_package: Option = match operation { + let initial_package: Option = match &**operation { AnyOperation::Update(update_op) => Some(update_op.get_initial_package()), _ => None, }; @@ -505,10 +508,10 @@ impl InstallationManager { // 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(); + let mut batches: Vec>> = vec![]; + let mut batch: IndexMap> = IndexMap::new(); for (index, operation) in operations { - let package: Option = match &operation { + let package: Option = match &*operation { AnyOperation::Update(update) => Some(update.get_target_package()), AnyOperation::Install(install) => Some(install.get_package()), _ => None, @@ -551,7 +554,7 @@ impl InstallationManager { async fn execute_batch( &self, repo: &InstalledRepositoryInterfaceHandle, - operations: IndexMap, + operations: IndexMap>, cleanup_promises: &IndexMap< i64, Box< @@ -562,11 +565,13 @@ impl InstallationManager { >, dev_mode: bool, run_scripts: bool, - all_operations: &[AnyOperation], + all_operations: &[std::rc::Rc], ) -> anyhow::Result<()> { let mut promises: Vec< std::pin::Pin> + '_>>, > = vec![]; + // @var array $postExecCallbacks + let mut post_exec_callbacks: Vec anyhow::Result<()>>> = vec![]; for (index, operation) in operations { let op_type = operation.get_operation_type(); @@ -581,7 +586,7 @@ impl InstallationManager { io_interface::NORMAL, ); } - match &operation { + match &*operation { AnyOperation::MarkAliasInstalled(op) => { self.mark_alias_installed(&mut *repo.borrow_mut(), op)?; } @@ -595,7 +600,7 @@ impl InstallationManager { } let package = operation.get_target_package(); - let initial_package: Option = match &operation { + let initial_package: Option = match &*operation { AnyOperation::Update(update_op) => Some(update_op.get_initial_package()), _ => None, }; @@ -607,11 +612,14 @@ impl InstallationManager { _ => "", }; - 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); + if run_scripts && let Some(event_dispatcher) = &self.event_dispatcher { + event_dispatcher.borrow_mut().dispatch_package_event( + event_name, + dev_mode, + repo.clone(), + all_operations.to_vec(), + operation.clone(), + )?; } let installer = self.get_installer(&package.get_type())?; @@ -620,16 +628,16 @@ impl InstallationManager { // ->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); + // failed"; throw $e; }); // each package gets its own chain and the whole batch resolves via waitOnPromises. + let executed_operation = std::rc::Rc::clone(&operation); promises.push(Box::pin(async move { let chain_result: anyhow::Result<()> = async { installer .prepare(op_type, package.clone(), initial_package.clone()) .await?; - match &operation { + match &*executed_operation { AnyOperation::Install(op) => { self.install(repo, op).await?; } @@ -668,24 +676,32 @@ impl InstallationManager { // 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(()) })); + + let event_name = match op_type { + "install" => PackageEvents::POST_PACKAGE_INSTALL, + "update" => PackageEvents::POST_PACKAGE_UPDATE, + "uninstall" => PackageEvents::POST_PACKAGE_UNINSTALL, + _ => "", + }; + + if run_scripts && let Some(event_dispatcher) = &self.event_dispatcher { + let event_dispatcher = event_dispatcher.clone(); + let repo = repo.clone(); + let all_operations = all_operations.to_vec(); + post_exec_callbacks.push(Box::new(move || { + event_dispatcher.borrow_mut().dispatch_package_event( + event_name, + dev_mode, + repo.clone(), + all_operations.clone(), + operation.clone(), + )?; + + Ok(()) + })); + } } if !promises.is_empty() { @@ -694,6 +710,10 @@ impl InstallationManager { Platform::workaround_filesystem_issues(); + for cb in post_exec_callbacks { + cb()?; + } + Ok(()) } diff --git a/crates/shirabe/src/installer/package_event.rs b/crates/shirabe/src/installer/package_event.rs index 9435a581..5e6ddb34 100644 --- a/crates/shirabe/src/installer/package_event.rs +++ b/crates/shirabe/src/installer/package_event.rs @@ -5,19 +5,21 @@ use crate::dependency_resolver::operation::AnyOperation; use crate::event_dispatcher::Event; use crate::event_dispatcher::EventInterface; use crate::io::IOInterface; -use crate::repository::RepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use indexmap::IndexMap; use shirabe_php_shim::PhpMixed; +/// The operations are shared rather than owned so that the same operation crosses the plugin +/// boundary as one object for both the pre- and the post-event, as it does in PHP. #[derive(Debug)] pub struct PackageEvent { inner: Event, composer: ComposerWeakHandle, io: std::rc::Rc>, dev_mode: bool, - local_repo: Box, - operations: Vec, - operation: AnyOperation, + local_repo: InstalledRepositoryInterfaceHandle, + operations: Vec>, + operation: std::rc::Rc, } impl PackageEvent { @@ -26,9 +28,9 @@ impl PackageEvent { composer: ComposerWeakHandle, io: std::rc::Rc>, dev_mode: bool, - local_repo: Box, - operations: Vec, - operation: AnyOperation, + local_repo: InstalledRepositoryInterfaceHandle, + operations: Vec>, + operation: std::rc::Rc, ) -> Self { Self { inner: Event::new(event_name, vec![], IndexMap::new()), @@ -57,15 +59,15 @@ impl PackageEvent { self.dev_mode } - pub fn get_local_repo(&self) -> &dyn RepositoryInterface { - self.local_repo.as_ref() + pub fn get_local_repo(&self) -> InstalledRepositoryInterfaceHandle { + self.local_repo.clone() } - pub fn get_operations(&self) -> &Vec { + pub fn get_operations(&self) -> &Vec> { &self.operations } - pub fn get_operation(&self) -> &AnyOperation { + pub fn get_operation(&self) -> &std::rc::Rc { &self.operation } } diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs index 541718af..686c05bd 100644 --- a/crates/shirabe/src/plugin/php_plugin_proxy.rs +++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs @@ -8,6 +8,7 @@ use crate::autoload::ClassLoader; use crate::command::BaseCommand; use crate::composer::ComposerHandle; +use crate::dependency_resolver::operation::AnyOperation; use crate::event_dispatcher::event_dispatcher::dispatch_event_method; use crate::event_dispatcher::{ EventInterface, EventSubscriberInterface, SubscribedEventEntry, unwrap_php_result, @@ -52,6 +53,7 @@ enum RustEntity { EventDispatcher( std::rc::Rc>, ), + Operation(std::rc::Rc), } /// The pointer identity backing R-table interning: the same shared instance must always cross @@ -71,6 +73,7 @@ fn entity_ptr_id(entity: &RustEntity) -> usize { RustEntity::EventDispatcher(dispatcher) => { std::rc::Rc::as_ptr(dispatcher) as *const () as usize } + RustEntity::Operation(operation) => std::rc::Rc::as_ptr(operation) as *const () as usize, } } @@ -169,6 +172,21 @@ fn repository_stub_class(repository: &RepositoryInterfaceHandle) -> Result<&'sta } } +/// The proxy stub class matching a solver operation's concrete type. +fn operation_stub_class(operation: &AnyOperation) -> &'static str { + match operation { + AnyOperation::Install(_) => "Composer\\DependencyResolver\\Operation\\InstallOperation", + AnyOperation::Update(_) => "Composer\\DependencyResolver\\Operation\\UpdateOperation", + AnyOperation::Uninstall(_) => "Composer\\DependencyResolver\\Operation\\UninstallOperation", + AnyOperation::MarkAliasInstalled(_) => { + "Composer\\DependencyResolver\\Operation\\MarkAliasInstalledOperation" + } + AnyOperation::MarkAliasUninstalled(_) => { + "Composer\\DependencyResolver\\Operation\\MarkAliasUninstalledOperation" + } + } +} + /// Registers a package and returns its wire descriptor. pub(crate) fn package_handle_value( package: &std::rc::Rc>, @@ -178,6 +196,13 @@ pub(crate) fn package_handle_value( rust_handle_value(rhandle, class) } +/// Registers a solver operation and returns its wire descriptor. +pub(crate) fn operation_handle_value(operation: &std::rc::Rc) -> PluginValue { + let class = operation_stub_class(operation); + let rhandle = register_entity(RustEntity::Operation(operation.clone())); + rust_handle_value(rhandle, class) +} + /// The PHP class name (= proxy stub class) of a Rust IO instance, for the `__class` field of /// its handle descriptor. pub(crate) fn io_stub_class( @@ -335,6 +360,9 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> { Some(RustEntity::EventDispatcher(dispatcher)) => { dispatch_event_dispatcher_method(&dispatcher, method_name, &args) } + Some(RustEntity::Operation(operation)) => { + dispatch_operation_method(&operation, method_name, &args) + } None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))), } } @@ -368,7 +396,13 @@ pub(crate) fn construct_entity(args: &[PluginValue]) -> Result Result { + package_from_arg(&class, ctor_args.get(position))? + .as_alias() + .ok_or_else(|| runtime_throw(format!("{class} expects an AliasPackage"))) + }; + let package = match class.as_str() { "Composer\\Package\\Package" => AnyPackage::Package(crate::package::Package::new( string_arg(0)?, string_arg(1)?, @@ -413,6 +447,50 @@ pub(crate) fn construct_entity(args: &[PluginValue]) -> Result { + return Ok(operation_construction_result(AnyOperation::Install( + crate::dependency_resolver::operation::InstallOperation::new(package_from_arg( + &class, + ctor_args.first(), + )?), + ))); + } + "Composer\\DependencyResolver\\Operation\\UpdateOperation" => { + return Ok(operation_construction_result(AnyOperation::Update( + crate::dependency_resolver::operation::UpdateOperation::new( + package_from_arg(&class, ctor_args.first())?, + package_from_arg(&class, ctor_args.get(1))?, + ), + ))); + } + "Composer\\DependencyResolver\\Operation\\UninstallOperation" => { + return Ok(operation_construction_result(AnyOperation::Uninstall( + crate::dependency_resolver::operation::UninstallOperation::new(package_from_arg( + &class, + ctor_args.first(), + )?), + ))); + } + "Composer\\DependencyResolver\\Operation\\MarkAliasInstalledOperation" => { + return Ok(operation_construction_result( + AnyOperation::MarkAliasInstalled( + crate::dependency_resolver::operation::MarkAliasInstalledOperation::new( + alias_package_arg(0)?, + ), + ), + )); + } + "Composer\\DependencyResolver\\Operation\\MarkAliasUninstalledOperation" => { + return Ok(operation_construction_result( + AnyOperation::MarkAliasUninstalled( + crate::dependency_resolver::operation::MarkAliasUninstalledOperation::new( + alias_package_arg(0)?, + ), + ), + )); + } // TODO(plugin): the remaining proxied classes get a construction story on demand, // driven by explicit errors from real plugins. Each one has to decide what a // plugin-built instance means for the Rust-side graph, which is why none of them is @@ -424,12 +502,20 @@ pub(crate) fn construct_entity(args: &[PluginValue]) -> Result PluginValue { + construction_result(register_entity(RustEntity::Operation(std::rc::Rc::new( + operation, + )))) +} + +/// The `[$rhandle, $epoch]` pair a proxy stub's constructor binds itself to. +fn construction_result(rhandle: u64) -> PluginValue { + PluginValue::List(vec![PluginValue::Int(rhandle as i64), PluginValue::Int(0)]) } /// Serves the `__clone` forwarder every proxy stub carries. Only entities whose Rust type @@ -452,7 +538,8 @@ fn clone_entity(entity: &RustEntity) -> Result { | RustEntity::InstallationManager(_) | RustEntity::RepositoryManager(_) | RustEntity::Repository(_) - | RustEntity::EventDispatcher(_) => { + | RustEntity::EventDispatcher(_) + | RustEntity::Operation(_) => { return Err(runtime_throw( "cloning this Rust-side entity over RPC is not supported".to_string(), )); @@ -810,12 +897,7 @@ fn dispatch_repository_manager_method( method_name: &str, ) -> Result { match method_name { - "getLocalRepository" => { - let local = rm.borrow().get_local_repository(); - let class = repository_stub_class(&local)?; - let rhandle = register_entity(RustEntity::Repository(local)); - Ok(rust_handle_value(rhandle, class)) - } + "getLocalRepository" => repository_handle_value(&rm.borrow().get_local_repository()), // TODO(plugin): the remaining RepositoryManager surface is widened on demand, driven // by explicit errors from real plugins. other => Err(runtime_throw(format!( @@ -1761,6 +1843,42 @@ fn dispatch_package_method( } } +fn dispatch_operation_method( + operation: &AnyOperation, + method_name: &str, + args: &[PluginValue], +) -> Result { + match (method_name, operation) { + ("getOperationType", _) => Ok(PluginValue::string(operation.get_operation_type())), + ("show", _) => Ok(PluginValue::string( + operation.show(bool_arg(method_name, args.first())?), + )), + ("__toString", _) => Ok(PluginValue::string(operation.to_string())), + ("getPackage", AnyOperation::Install(op)) => { + Ok(package_handle_value(op.get_package().as_rc())) + } + ("getPackage", AnyOperation::Uninstall(op)) => { + Ok(package_handle_value(op.get_package().as_rc())) + } + ("getPackage", AnyOperation::MarkAliasInstalled(op)) => { + Ok(package_handle_value(op.get_package().as_rc())) + } + ("getPackage", AnyOperation::MarkAliasUninstalled(op)) => { + Ok(package_handle_value(op.get_package().as_rc())) + } + ("getInitialPackage", AnyOperation::Update(op)) => { + Ok(package_handle_value(op.get_initial_package().as_rc())) + } + ("getTargetPackage", AnyOperation::Update(op)) => { + Ok(package_handle_value(op.get_target_package().as_rc())) + } + (other, _) => Err(runtime_throw(format!( + "the operation method `{other}` is not available on a {} over RPC yet", + operation_stub_class(operation) + ))), + } +} + fn dispatch_installation_manager_method( im: &std::rc::Rc>, method_name: &str, -- cgit v1.3.1