aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs140
-rw-r--r--crates/shirabe/tests/plugin/e2e_http_downloader_test.rs5
-rw-r--r--crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/plugin/src/Plugin.php42
3 files changed, 186 insertions, 1 deletions
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs
index 0465eb6d..b070930b 100644
--- a/crates/shirabe/src/plugin/php_plugin_proxy.rs
+++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs
@@ -60,6 +60,7 @@ enum RustEntity {
Plugin(std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>),
ProcessExecutor(std::rc::Rc<std::cell::RefCell<crate::util::ProcessExecutor>>),
HttpDownloader(std::rc::Rc<std::cell::RefCell<crate::util::HttpDownloader>>),
+ Loop(std::rc::Rc<std::cell::RefCell<crate::util::r#loop::Loop>>),
}
/// The pointer identity backing R-table interning: the same shared instance must always cross
@@ -86,6 +87,7 @@ fn entity_ptr_id(entity: &RustEntity) -> usize {
RustEntity::HttpDownloader(downloader) => {
std::rc::Rc::as_ptr(downloader) as *const () as usize
}
+ RustEntity::Loop(r#loop) => std::rc::Rc::as_ptr(r#loop) as *const () as usize,
}
}
@@ -389,6 +391,7 @@ pub(crate) fn dispatch_r_table_method(
Some(RustEntity::HttpDownloader(downloader)) => {
dispatch_http_downloader_method(&downloader, method_name, args)
}
+ Some(RustEntity::Loop(r#loop)) => dispatch_loop_method(&r#loop, method_name, args),
None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
}
}
@@ -426,6 +429,11 @@ pub(crate) fn construct_entity(args: &[PluginValue]) -> Result<PluginValue, PhpT
let config_arg = |position: usize| {
arg::<std::rc::Rc<std::cell::RefCell<crate::config::Config>>>(&class, ctor_args, position)
};
+ let http_downloader_arg = |position: usize| {
+ arg::<std::rc::Rc<std::cell::RefCell<crate::util::HttpDownloader>>>(
+ &class, ctor_args, position,
+ )
+ };
let alias_package_arg =
|position: usize| -> Result<crate::package::AliasPackageHandle, PhpThrow> {
package_arg(position)?
@@ -551,6 +559,20 @@ pub(crate) fn construct_entity(args: &[PluginValue]) -> Result<PluginValue, PhpT
)));
return Ok(construction_result(rhandle));
}
+ // A loop owns nothing of its own: it holds the two services it drives and enables the
+ // async surface on each, so a plugin-built one is a second driver over whichever
+ // instances it was handed rather than a second copy of them.
+ "Composer\\Util\\Loop" => {
+ let process = match ctor_args.get(1) {
+ None | Some(PluginValue::Null) => None,
+ _ => Some(process_executor_arg(1)?),
+ };
+ let rhandle =
+ register_entity(RustEntity::Loop(std::rc::Rc::new(std::cell::RefCell::new(
+ crate::util::r#loop::Loop::new(http_downloader_arg(0)?, process),
+ ))));
+ return Ok(construction_result(rhandle));
+ }
// 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
@@ -603,7 +625,8 @@ fn clone_entity(entity: &RustEntity) -> Result<PluginValue, PhpThrow> {
| RustEntity::Operation(_)
| RustEntity::Plugin(_)
| RustEntity::ProcessExecutor(_)
- | RustEntity::HttpDownloader(_) => {
+ | RustEntity::HttpDownloader(_)
+ | RustEntity::Loop(_) => {
return Err(runtime_throw(
"cloning this Rust-side entity over RPC is not supported".to_string(),
));
@@ -724,6 +747,11 @@ fn dispatch_composer_method(
"Composer\\Downloader\\DownloadManager",
))
}
+ "getLoop" => {
+ let r#loop = composer.borrow().get_loop();
+ let rhandle = register_entity(RustEntity::Loop(r#loop));
+ Ok(rust_handle_value(rhandle, "Composer\\Util\\Loop"))
+ }
// TODO(plugin): the remaining Composer object graph (getLocker, getPluginManager, ...)
// becomes reachable over RPC on demand, driven by explicit errors from real plugins.
other => Err(runtime_throw(format!(
@@ -915,6 +943,94 @@ fn rejected_promise(throw: PhpThrow) -> Result<PluginValue, PhpThrow> {
}
}
+/// Drains one promise a plugin handed to `Loop::wait()`. React settles synchronously, so an
+/// already-settled promise yields its value here and a rejected one yields its reason as the
+/// throw; one that is still pending is an explicit error.
+fn settle_promise(method_name: &str, promise: PluginValue) -> Result<PluginValue, PhpThrow> {
+ let handle = match promise {
+ PluginValue::PhpHandle(handle) => handle,
+ other => return Err(arg_throw(method_name, 0, "a promise", Some(&other))),
+ };
+ let phandle = handle.phandle;
+ let settled = call_function_with_dispatcher(
+ "__shirabe_settle_promise",
+ vec![PluginValue::PhpHandle(handle)],
+ Some(&mut PluginRpcDispatcher::default()),
+ );
+ // The promise entity was interned in the worker's P table when it crossed; the plugin's own
+ // reference keeps the object alive, and nothing on this side owns it past this call.
+ let _ = release_php_handle(phandle);
+ match settled {
+ Ok(outcome) => outcome,
+ Err(error) => Err(runtime_throw(format!(
+ "draining a promise in the plugin process failed: {error:#}"
+ ))),
+ }
+}
+
+/// Serves the `Loop` proxy stub. The two services it hands out are the ones it was built over,
+/// and `wait()` drains the promises it is given.
+fn dispatch_loop_method(
+ r#loop: &std::rc::Rc<std::cell::RefCell<crate::util::r#loop::Loop>>,
+ method_name: &str,
+ args: &[PluginValue],
+) -> Result<PluginValue, PhpThrow> {
+ match method_name {
+ "getHttpDownloader" => {
+ let downloader = r#loop.borrow().get_http_downloader().clone();
+ let rhandle = register_entity(RustEntity::HttpDownloader(downloader));
+ Ok(rust_handle_value(rhandle, "Composer\\Util\\HttpDownloader"))
+ }
+ "getProcessExecutor" => {
+ let process = r#loop.borrow().get_process_executor().cloned();
+ match process {
+ Some(process) => {
+ let rhandle = register_entity(RustEntity::ProcessExecutor(process));
+ Ok(rust_handle_value(
+ rhandle,
+ "Composer\\Util\\ProcessExecutor",
+ ))
+ }
+ None => Ok(PluginValue::Null),
+ }
+ }
+ "wait" => {
+ // TODO(symfony,plugin): the progress bar is a symfony/console object each world runs
+ // its own implementation of, so there is none to hand to the Rust side.
+ if !matches!(args.get(1), None | Some(PluginValue::Null)) {
+ return Err(runtime_throw(
+ "Shirabe does not support passing a ProgressBar to Loop::wait() from a plugin yet"
+ .to_string(),
+ ));
+ }
+ let promises = match args.first() {
+ Some(PluginValue::List(promises)) => promises.clone(),
+ Some(PluginValue::Array(promises)) => promises.values().cloned().collect(),
+ other => return Err(arg_throw(method_name, 0, "an array of promises", other)),
+ };
+ // PHP waits on every promise of the group and rethrows the first rejection once the
+ // group is done, which is what `React\Promise\all()` hands it.
+ let mut uncaught: Option<PhpThrow> = None;
+ for promise in promises {
+ if let Err(throw) = settle_promise(method_name, promise)
+ && uncaught.is_none()
+ {
+ uncaught = Some(throw);
+ }
+ }
+ match uncaught {
+ Some(throw) => Err(throw),
+ None => Ok(PluginValue::Null),
+ }
+ }
+ "abortJobs" => {
+ r#loop.borrow().abort_jobs();
+ Ok(PluginValue::Null)
+ }
+ other => Err(runtime_throw(format!("unknown Loop method `{other}`"))),
+ }
+}
+
fn dispatch_filesystem_method(
fs: &std::rc::Rc<std::cell::RefCell<crate::util::Filesystem>>,
method_name: &str,
@@ -1787,6 +1903,28 @@ impl FromPluginArg for std::rc::Rc<std::cell::RefCell<crate::config::Config>> {
}
}
+/// Resolves a downloader argument back to the Rust-side entity its proxy stub stands for.
+impl FromPluginArg for std::rc::Rc<std::cell::RefCell<crate::util::HttpDownloader>> {
+ fn from_arg(
+ method: &str,
+ position: usize,
+ value: Option<&PluginValue>,
+ ) -> Result<Self, PhpThrow> {
+ match value {
+ Some(PluginValue::RustHandle(handle)) => {
+ match R_TABLE.with(|table| table.borrow().get(&handle.rhandle).cloned()) {
+ Some(RustEntity::HttpDownloader(downloader)) => Ok(downloader),
+ _ => Err(runtime_throw(format!(
+ "{method} expects an HttpDownloader handle, got Rust handle {}",
+ handle.rhandle
+ ))),
+ }
+ }
+ other => Err(arg_throw(method, position, "an HttpDownloader", other)),
+ }
+ }
+}
+
/// Resolves a process executor argument back to the Rust-side entity its proxy stub stands for.
impl FromPluginArg for std::rc::Rc<std::cell::RefCell<crate::util::ProcessExecutor>> {
fn from_arg(
diff --git a/crates/shirabe/tests/plugin/e2e_http_downloader_test.rs b/crates/shirabe/tests/plugin/e2e_http_downloader_test.rs
index f5fdfa97..f19b992a 100644
--- a/crates/shirabe/tests/plugin/e2e_http_downloader_test.rs
+++ b/crates/shirabe/tests/plugin/e2e_http_downloader_test.rs
@@ -82,6 +82,11 @@ add-missing=ok rejected=\"Composer\\\\Downloader\\\\TransportException\"
addCopy=ok file=\"{\\\"probe\\\":true,\\\"n\\\":42}\"
countActiveJobs=0 wait=ok
collect=ok
+loop class=\"Composer\\\\Util\\\\Loop\" downloader=\"Composer\\\\Util\\\\HttpDownloader\" same=true executor=\"Composer\\\\Util\\\\ProcessExecutor\"
+loop wait=ok bodies=[\"{\\\"probe\\\":true,\\\"n\\\":42}\",\"{\\\"probe\\\":true,\\\"n\\\":42}\"]
+loop wait-rejected=\"Composer\\\\Downloader\\\\TransportException\"
+abortJobs=ok
+own loop=ok
",
upstream.trace
);
diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/plugin/src/Plugin.php b/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/plugin/src/Plugin.php
index 6e91ad00..b60c8df0 100644
--- a/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/plugin/src/Plugin.php
+++ b/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/plugin/src/Plugin.php
@@ -137,6 +137,48 @@ class Plugin implements PluginInterface, EventSubscriberInterface
$response->collect();
});
+ // The route Composer's own docblocks point plugin authors at: the loop the run built
+ // hands out the downloader and the executor the rest of the run uses.
+ $loop = $event->getComposer()->getLoop();
+ $shared = $loop->getHttpDownloader();
+ $lines[] = 'loop class=' . json_encode(\get_class($loop))
+ . ' downloader=' . json_encode(\get_class($shared))
+ . ' same=' . json_encode($shared === $loop->getHttpDownloader())
+ . ' executor=' . json_encode($loop->getProcessExecutor() === null ? null : \get_class($loop->getProcessExecutor()));
+
+ // The shared downloader is the one the run enabled async on, so it takes a request group
+ // without the plugin having to enable anything.
+ $waited = [];
+ $lines[] = 'loop wait=' . $this->describe(static function () use ($loop, $shared, $payload, &$waited): void {
+ $loop->wait([
+ $shared->add('file://' . $payload)->then(static function ($result) use (&$waited): void {
+ $waited[] = $result->getBody();
+ }),
+ $shared->add('file://' . $payload)->then(static function ($result) use (&$waited): void {
+ $waited[] = $result->getBody();
+ }),
+ ]);
+ }) . ' bodies=' . json_encode($waited);
+
+ // Only the class: the reason a stream failed to open comes from the PHP warning the
+ // reader raised, which this port does not have.
+ $rejected = null;
+ try {
+ $loop->wait([$shared->add('file:///shirabe-probe-missing.json')]);
+ } catch (\Throwable $e) {
+ $rejected = $e;
+ }
+ $lines[] = 'loop wait-rejected=' . json_encode($rejected === null ? null : \get_class($rejected));
+
+ $lines[] = 'abortJobs=' . $this->describe(static function () use ($loop): void {
+ $loop->abortJobs();
+ });
+
+ // A plugin may drive its own loop over the services it already holds.
+ $lines[] = 'own loop=' . $this->describe(static function () use ($downloader): void {
+ (new \Composer\Util\Loop($downloader))->wait([]);
+ });
+
file_put_contents('http-downloader-trace.txt', implode("\n", $lines) . "\n");
}