aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/console/application.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-05 03:58:03 +0900
committernsfisis <nsfisis@gmail.com>2026-08-05 03:58:03 +0900
commit9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec (patch)
treeb1041b55bc3ed36a391370cc2f5ececc8cb386b2 /crates/shirabe/src/console/application.rs
parentbe3458128ef09b3d1074b134f2822162e077e165 (diff)
downloadphp-shirabe-9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec.tar.gz
php-shirabe-9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec.tar.zst
php-shirabe-9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec.zip
feat(plugin): run plugin-provided commands in a worker-side application
A same-FQCN Composer\Console\Application, hand-written under the new php/runtime/ tree, hosts CommandProvider commands inside the PHP worker: PhpCommandProxy overrides run() and forwards the stringified input, so the real Symfony machinery binds, validates and executes against the live command object, while help/list render Rust-side from a definition read back at construction. Reverse \Shirabe\RustCommandStub rows let a plugin command invoke built-in commands back in the Rust process, keeping every command on the side whose helper set it was written for. Composer\EventDispatcher\Event moves from a generated stub to a dual-mode runtime class: the real BaseCommand::initialize constructs a PreCommandRunEvent natively in the worker, which a proxy-only constructor guard rejected. Its PRE_COMMAND_RUN dispatch reaches a new EventDispatcher stub whose dispatch supports the observably-no-op no-listener case and fails explicitly otherwise. The stub generator now accepts runtime-provided classes as stub bases (never as targets) and cross-checks the Application handoff property table against the real class. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/console/application.rs')
-rw-r--r--crates/shirabe/src/console/application.rs102
1 files changed, 102 insertions, 0 deletions
diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs
index cf57a5bf..7be18796 100644
--- a/crates/shirabe/src/console/application.rs
+++ b/crates/shirabe/src/console/application.rs
@@ -595,6 +595,8 @@ impl Application {
) -> anyhow::Result<Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> {
let mut commands: Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> = vec![];
+ crate::plugin::reset_pending_plugin_command_handles();
+
let mut composer = self.get_composer(false, Some(false), None)?;
if composer.is_none() {
let disable_plugins = if self.disable_plugins_by_default {
@@ -637,6 +639,42 @@ impl Application {
}),
);
}
+
+ if !commands.is_empty() {
+ // Publish the handoff the worker-side console application boots from when one
+ // of these commands actually runs: the shared object graph, plus metadata
+ // mirrors of the built-in commands (plugin commands are not registered yet, so
+ // this snapshot is exactly the Rust-implemented set).
+ let mut seen: Vec<*const ()> = Vec::new();
+ let mut rust_commands: Vec<crate::plugin::RustCommandMetadata> = Vec::new();
+ for command in self.commands.values() {
+ let ptr = std::rc::Rc::as_ptr(command) as *const ();
+ if seen.contains(&ptr) {
+ continue;
+ }
+ seen.push(ptr);
+ let command = command.borrow();
+ let Some(name) = command.get_name() else {
+ continue;
+ };
+ rust_commands.push(crate::plugin::RustCommandMetadata {
+ name,
+ description: command.get_description(),
+ aliases: command.get_aliases(),
+ hidden: command.is_hidden(),
+ });
+ }
+ crate::plugin::publish_console_application_context(
+ &composer,
+ &self.io,
+ self.get_initial_working_directory(),
+ self.disable_plugins_by_default,
+ self.disable_scripts_by_default,
+ rust_commands,
+ crate::plugin::take_pending_plugin_command_handles(),
+ );
+ register_worker_reverse_application(self.me.clone());
+ }
}
Ok(commands)
@@ -3122,3 +3160,67 @@ fn borrow_output_mut(
) -> std::cell::RefMut<'_, dyn OutputInterface> {
output.borrow_mut()
}
+
+thread_local! {
+ /// The application answering `__shirabe_run_rust_command` callbacks from the plugin
+ /// worker's reverse command stubs; registered when plugin commands are collected.
+ static WORKER_REVERSE_APPLICATION: std::cell::RefCell<
+ Option<std::rc::Weak<std::cell::RefCell<Application>>>,
+ > = const { std::cell::RefCell::new(None) };
+}
+
+/// Registers the application the worker's reverse command stubs call back into.
+pub(crate) fn register_worker_reverse_application(
+ application: std::rc::Weak<std::cell::RefCell<Application>>,
+) {
+ WORKER_REVERSE_APPLICATION.with(|slot| *slot.borrow_mut() = Some(application));
+}
+
+/// Runs a built-in command on behalf of a plugin-provided command executing in the worker
+/// (the reverse half of the two-world command split): the stringified input the plugin passed
+/// is re-parsed here and the command runs against this side's application state — helper set
+/// included — writing to the same stdio the worker inherited.
+pub(crate) fn run_worker_reverse_command(name: &str, input_line: &str) -> anyhow::Result<i64> {
+ let application = WORKER_REVERSE_APPLICATION
+ .with(|slot| slot.borrow().as_ref().and_then(std::rc::Weak::upgrade))
+ .ok_or_else(|| {
+ anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: format!(
+ "cannot run command {name}: no application is registered for worker callbacks"
+ ),
+ code: 0,
+ })
+ })?;
+ let command = application.borrow_mut().find(name)?;
+ // PHP's `(string) $input` omits the command name when the input was built without an
+ // explicit `command` entry, but the merged definition binds the first positional token to
+ // the `command` argument, so the name is prepended when the first token is not this
+ // command.
+ let trimmed = input_line.trim();
+ let first_token = trimmed.split_whitespace().next().unwrap_or("");
+ let is_named = {
+ let command = command.borrow();
+ command.get_name().as_deref() == Some(first_token)
+ || command
+ .get_aliases()
+ .iter()
+ .any(|alias| alias == first_token)
+ };
+ let line = if is_named {
+ trimmed.to_string()
+ } else if trimmed.is_empty() {
+ name.to_string()
+ } else {
+ format!("{name} {trimmed}")
+ };
+ let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(
+ shirabe_external_packages::symfony::console::input::string_input::StringInput::new(
+ &line,
+ )?,
+ ));
+ let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = std::rc::Rc::new(
+ std::cell::RefCell::new(ConsoleOutput::new(None, None, None)?),
+ );
+ command.borrow().run(input, output)
+}