aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-07-23 22:54:18 +0900
committernsfisis <nsfisis@gmail.com>2026-07-23 22:54:18 +0900
commit48a6566f469cc16300232b6faa783d09aa69cdfd (patch)
treeede30385b56636c7550d47f4520b631cf6c242d8
parent2d89dc761237b055ddb8c6dc817c46c0cd6dd44d (diff)
downloadphp-shirabe-48a6566f469cc16300232b6faa783d09aa69cdfd.tar.gz
php-shirabe-48a6566f469cc16300232b6faa783d09aa69cdfd.tar.zst
php-shirabe-48a6566f469cc16300232b6faa783d09aa69cdfd.zip
feat(process-executor): stub plugin-facing execute_async_php path
A plugin can reach ProcessExecutor::executeAsync() through the rust-proxy stub, which resolves with a real Symfony Process instance that can't be reconstructed on the Rust side (its state is tied to whichever process calls proc_open(), and it refuses serialization). Add execute_async_php() as a todo!() stub, documented as a dual- instantiation split in plugin-class-classification.md: Rust-internal callers keep using execute_async(), while the plugin path must forward spawning to the PHP child once the RPC channel exists.
-rw-r--r--crates/shirabe/src/util/process_executor.rs48
-rw-r--r--docs/dev/plugin-class-classification.md15
2 files changed, 57 insertions, 6 deletions
diff --git a/crates/shirabe/src/util/process_executor.rs b/crates/shirabe/src/util/process_executor.rs
index 1f00eaeb..1f167840 100644
--- a/crates/shirabe/src/util/process_executor.rs
+++ b/crates/shirabe/src/util/process_executor.rs
@@ -37,8 +37,8 @@ pub struct ProcessExecutor {
/// @var int
max_jobs: i64,
/// PHP throttles async jobs through the $jobs queue and $maxJobs; here concurrent
- /// `execute_async` calls hold a permit for the duration of the child process instead
- /// (same design as HttpDownloader).
+ /// `execute_async`/`execute_async_php` calls hold a permit for the duration of the
+ /// child process instead (same design as HttpDownloader).
semaphore: std::rc::Rc<tokio::sync::Semaphore>,
/// @var bool
allow_async: bool,
@@ -593,13 +593,18 @@ impl ProcessExecutor {
}
}
- /// starts a process on the commandline in async mode
+ /// starts a process on the commandline in async mode, for callers within Rust-ported Composer
+ /// code (i.e. everything except a plugin holding a `ProcessExecutor` RPC handle — see
+ /// `execute_async_php` and docs/dev/plugin-class-classification.md, "Process: dual
+ /// instantiation split by caller"). This is the direct 1:1 port of PHP's `executeAsync()`:
+ /// almost all calls go through here, spawning in Rust with no PHP child involved.
///
/// The returned future does NOT borrow the executor: everything it needs is captured up
/// front, so callers can drop their `Ref`/`RefMut` on the shared `Rc<RefCell<ProcessExecutor>>`
- /// before awaiting (`let fut = pe.borrow_mut().execute_async(...); fut.await`). Holding a
- /// borrow across the await would panic as soon as a sibling future or a sync `execute()` call
- /// touches the same executor. The max_jobs throttle is enforced by the semaphore.
+ /// before awaiting (`let fut = pe.borrow_mut().execute_async(...); fut.await`). Holding
+ /// a borrow across the await would panic as soon as a sibling future or a sync `execute()`
+ /// call touches the same executor. The max_jobs throttle is enforced by the semaphore, shared
+ /// with `execute_async_php`.
///
/// Takes `&mut self` (unlike the `&self` used while this only read `self.mock`) so the mock
/// branch can update `error_output`/`capture_output` before returning, mirroring
@@ -699,6 +704,37 @@ impl ProcessExecutor {
})
}
+ /// Plugin-facing counterpart of `execute_async`. Reached only when the RPC dispatcher
+ /// relays a plugin's `executeAsync()` call made on the `rust-proxy` `ProcessExecutor` stub
+ /// (a plugin obtained the handle via `Loop::getProcessExecutor()`) — see
+ /// docs/dev/plugin-class-classification.md, "Process: dual instantiation split by caller".
+ ///
+ /// A `Symfony\Component\Process\Process` cannot be reconstructed on the Rust side: its state
+ /// (the `proc_open()` resource, the OS pipes) belongs to whichever process calls `start()`,
+ /// and it refuses serialization outright. So unlike `execute_async`, this must not
+ /// spawn in Rust: the real `Process::start()` has to run in the PHP child, and the plugin's
+ /// `.then()` callback must receive that genuine PHP-side object.
+ // TODO(plugin): once the plugin RPC channel exists, acquire a permit from `self.semaphore`
+ // (shared with `execute_async`, so the combined job budget — including any shared cap
+ // with HttpDownloader — stays correct regardless of which path runs a given job), then send
+ // the spawn request to the PHP child over that channel instead of calling `Process::start()`
+ // here. Release the permit on the child's completion notification, not by polling a
+ // Rust-owned process handle. The return type below is provisional: the real deliverable is a
+ // handle to the live PHP-side Process object, not a `shirabe_external_packages` `Process`
+ // value, so this signature will need to change once the RPC plumbing exists.
+ pub fn execute_async_php<C>(
+ &mut self,
+ _command: C,
+ _cwd: Option<&str>,
+ ) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<()>>>>
+ where
+ C: IntoExecCommand,
+ {
+ todo!(
+ "forward the spawn to the PHP child over the plugin RPC channel and await its completion notification"
+ )
+ }
+
fn output_handler(
capture_output: bool,
io: &mut Option<std::rc::Rc<std::cell::RefCell<dyn IOInterface>>>,
diff --git a/docs/dev/plugin-class-classification.md b/docs/dev/plugin-class-classification.md
index 263161cb..13286248 100644
--- a/docs/dev/plugin-class-classification.md
+++ b/docs/dev/plugin-class-classification.md
@@ -375,6 +375,21 @@ PHP-locally (or the stub `NewObject` constructor story lands); until the
user decides, the tool reports them as `unsupported` with the constructing
site named.
+### Process: dual instantiation split by caller
+
+`ProcessExecutor::executeAsync()` resolves its promise with a
+`Symfony\Component\Process\Process` instance, so it may cross the language
+boundary despite being a wholesale-`php-native` vendor class. A `Process`
+can't be reconstructed PHP-side from Rust-generated data because its state
+is stored in a `resource` created by `proc_open()`.
+
+Resolution: split `ProcessExecutor`'s Rust implementation by caller.
+Rust-ported Composer code (`VersionGuesser`, `Git`, …) calls `execute_async()`
+directly and spawns in Rust. A plugin holding a `ProcessExecutor` handle
+(`Loop::getProcessExecutor()`) instead hits the `rust-proxy` stub's RPC entry,
+which forwards the spawn to the PHP child so the real `Process::start()` runs
+there. The plugin gets the genuine object, never a fake one.
+
### Package and CompletePackage
They classify as `rust-proxy` mechanically: they carry setters