aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/util
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-11 02:39:35 +0900
committernsfisis <nsfisis@gmail.com>2026-06-11 02:39:35 +0900
commit6ae7e0d4b3eaf20e2d2cd3d000cf61ab0c9b6e83 (patch)
treee39261f4aa7314fe8c75142670c76591cdaccb92 /crates/shirabe/src/util
parent5d3232a80be4b989e89cc7ae4e3642cc5acae030 (diff)
downloadphp-shirabe-6ae7e0d4b3eaf20e2d2cd3d000cf61ab0c9b6e83.tar.gz
php-shirabe-6ae7e0d4b3eaf20e2d2cd3d000cf61ab0c9b6e83.tar.zst
php-shirabe-6ae7e0d4b3eaf20e2d2cd3d000cf61ab0c9b6e83.zip
feat(console): resolve phase-b TODOs in doRun and IO wiring
Wire up ConsoleIO with HelperSet/QuestionHelper, register the ErrorHandler with the IO instance, and fall back to a default output in run(). Replace resolved phase-b TODOs across the console, command, io, factory, installer, dependency_resolver, and util modules; reclassify the remaining blockers (typed Symfony command registry, stdin resource caching) as phase-c. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/util')
-rw-r--r--crates/shirabe/src/util/auth_helper.rs1
-rw-r--r--crates/shirabe/src/util/error_handler.rs39
-rw-r--r--crates/shirabe/src/util/http/curl_downloader.rs40
-rw-r--r--crates/shirabe/src/util/process_executor.rs13
-rw-r--r--crates/shirabe/src/util/remote_filesystem.rs13
-rw-r--r--crates/shirabe/src/util/svn.rs11
6 files changed, 79 insertions, 38 deletions
diff --git a/crates/shirabe/src/util/auth_helper.rs b/crates/shirabe/src/util/auth_helper.rs
index b54b54b..2373265 100644
--- a/crates/shirabe/src/util/auth_helper.rs
+++ b/crates/shirabe/src/util/auth_helper.rs
@@ -56,7 +56,6 @@ impl AuthHelper {
/// @param 'prompt'|bool $storeAuth
pub fn store_auth(&self, origin: &str, store_auth: StoreAuth) -> Result<()> {
- // TODO(phase-b): config.get_auth_config_source() and ConfigSource methods are stubs
let mut store: Option<()> = None;
let mut config = self.config.borrow_mut();
let config_source = config.get_auth_config_source_mut();
diff --git a/crates/shirabe/src/util/error_handler.rs b/crates/shirabe/src/util/error_handler.rs
index c0648be..e0590d9 100644
--- a/crates/shirabe/src/util/error_handler.rs
+++ b/crates/shirabe/src/util/error_handler.rs
@@ -1,18 +1,21 @@
//! ref: composer/src/Composer/Util/ErrorHandler.php
use crate::io::IOInterface;
+use crate::io::IOInterfaceImmutable;
use shirabe_php_shim::{
E_ALL, E_DEPRECATED, E_USER_DEPRECATED, E_USER_WARNING, E_WARNING, ErrorException,
FILTER_VALIDATE_BOOLEAN, PHP_EOL, PhpMixed, STDERR, debug_backtrace, error_reporting,
filter_var, ini_get, is_resource, set_error_handler,
};
-use std::sync::{Mutex, OnceLock};
+use std::cell::{Cell, RefCell};
+use std::rc::Rc;
-static IO: OnceLock<Mutex<Option<Box<dyn IOInterface + Send>>>> = OnceLock::new();
-static HAS_SHOWN_DEPRECATION_NOTICE: Mutex<i64> = Mutex::new(0);
-
-fn io() -> &'static Mutex<Option<Box<dyn IOInterface + Send>>> {
- IO.get_or_init(|| Mutex::new(None))
+// PHP keeps `$io` / `$hasShownDeprecationNotice` as process-global statics. Composer runs
+// single-threaded, so thread-locals on the main thread reproduce that faithfully while letting
+// us hold the same shared (`Rc<RefCell<dyn IOInterface>>`) IO instance the application uses.
+thread_local! {
+ static IO: RefCell<Option<Rc<RefCell<dyn IOInterface>>>> = const { RefCell::new(None) };
+ static HAS_SHOWN_DEPRECATION_NOTICE: Cell<i64> = const { Cell::new(0) };
}
pub struct ErrorHandler;
@@ -65,18 +68,17 @@ impl ErrorHandler {
});
}
- let mut io_guard = io().lock().unwrap();
- if io_guard.is_some() {
- let has_shown = *HAS_SHOWN_DEPRECATION_NOTICE.lock().unwrap();
- if has_shown > 0 && !io_guard.as_ref().unwrap().is_verbose() {
+ let io = IO.with(|cell| cell.borrow().clone());
+ if let Some(io) = io {
+ let has_shown = HAS_SHOWN_DEPRECATION_NOTICE.with(|c| c.get());
+ if has_shown > 0 && !io.is_verbose() {
if has_shown == 1 {
- io_guard.as_mut().unwrap().write_error("<warning>More deprecation notices were hidden, run again with `-v` to show them.</warning>");
- *HAS_SHOWN_DEPRECATION_NOTICE.lock().unwrap() = 2;
+ io.write_error("<warning>More deprecation notices were hidden, run again with `-v` to show them.</warning>");
+ HAS_SHOWN_DEPRECATION_NOTICE.with(|c| c.set(2));
}
return Ok(true);
}
- *HAS_SHOWN_DEPRECATION_NOTICE.lock().unwrap() = 1;
- drop(io_guard);
+ HAS_SHOWN_DEPRECATION_NOTICE.with(|c| c.set(1));
Self::output_warning(
&format!("Deprecation Notice: {} in {}:{}", message, file, line),
false,
@@ -86,17 +88,17 @@ impl ErrorHandler {
Ok(true)
}
- pub fn register(io: Option<Box<dyn IOInterface + Send>>) {
+ pub fn register(io: Option<Rc<RefCell<dyn IOInterface>>>) {
set_error_handler(|level, message, file, line| {
Self::handle(level, message.to_string(), file.to_string(), line).unwrap_or(true)
});
error_reporting(Some(E_ALL));
- *self::io().lock().unwrap() = io;
+ IO.with(|cell| *cell.borrow_mut() = io);
}
fn output_warning(message: &str, output_even_without_io: bool) {
- let mut io_guard = io().lock().unwrap();
- if let Some(io) = io_guard.as_mut() {
+ let io = IO.with(|cell| cell.borrow().clone());
+ if let Some(io) = io {
io.write_error(&format!("<warning>{}</warning>", message));
if io.is_verbose() {
io.write_error("<warning>Stack trace:</warning>");
@@ -122,7 +124,6 @@ impl ErrorHandler {
}
return;
}
- drop(io_guard);
if output_even_without_io {
if is_resource(&PhpMixed::Int(STDERR)) {
diff --git a/crates/shirabe/src/util/http/curl_downloader.rs b/crates/shirabe/src/util/http/curl_downloader.rs
index c9eb2f8..a0ef54f 100644
--- a/crates/shirabe/src/util/http/curl_downloader.rs
+++ b/crates/shirabe/src/util/http/curl_downloader.rs
@@ -645,7 +645,11 @@ impl CurlDownloader {
);
// curlHandle, headerHandle, bodyHandle, resolve, reject are PHP resources/callables;
// stored as opaque PhpMixed::Null placeholders (real values live in Rust-side fields).
- // TODO(phase-b): wire handle/closure storage properly.
+ // TODO(phase-c): storing the real \CurlHandle and the resolve/reject callables needs the
+ // Job to become a typed struct (an IndexMap<String, PhpMixed> cannot hold a non-Clone
+ // closure and the job is cloned at many sites). That refactor only pays off once the curl
+ // multi-exec loop actually runs — which depends on the curl_multi_* php-shims and the
+ // React\Promise Deferred (external package), both intentionally todo!().
job.insert("curlHandle".to_string(), PhpMixed::Null);
job.insert(
"filename".to_string(),
@@ -659,7 +663,9 @@ impl CurlDownloader {
job.insert("reject".to_string(), PhpMixed::Null);
job.insert("primaryIp".to_string(), PhpMixed::String(String::new()));
- let _ = (resolve, reject); // TODO(phase-b): store callables in Job
+ // TODO(phase-c): store the resolve/reject callables in the Job (see curlHandle note
+ // above) — blocked on the typed-Job refactor and the React\Promise model.
+ let _ = (resolve, reject);
self.jobs.insert(curl_handle_id(&curl_handle), job);
@@ -719,7 +725,10 @@ impl CurlDownloader {
{
let job = self.jobs.get(&id).cloned().unwrap_or_default();
// job['curlHandle'] is the actual \CurlHandle in PHP; in this port we keep
- // handles in Rust-owned storage. TODO(phase-b): wire actual handle removal.
+ // handles in Rust-owned storage.
+ // TODO(phase-c): curl_multi_remove_handle($this->multiHandle, $job['curlHandle']) needs
+ // the real \CurlHandle stored in the Job (typed-Job refactor) and the curl_multi_*
+ // php-shims, which stay todo!().
// curl_multi_remove_handle($this->multiHandle, $job['curlHandle']);
if PHP_VERSION_ID < 80000 {
// curl_close($job['curlHandle']);
@@ -776,7 +785,10 @@ impl CurlDownloader {
.map(|b| (**b).clone())
.unwrap_or(PhpMixed::Null);
let result_code: i64 = progress.get("result").and_then(|b| b.as_int()).unwrap_or(0);
- // TODO(phase-b): correlate handle in `progress['handle']` to its job id.
+ // TODO(phase-c): the job id is `(int) $progress['handle']` — the integer id of the
+ // \CurlHandle reported by curl_multi_info_read. Recovering it needs real curl handle
+ // resources from the curl_multi_* php-shims (todo!()); until then the id cannot be
+ // correlated and this loop body is unreachable in practice.
let i: i64 = 0;
if !self.jobs.contains_key(&i) {
continue;
@@ -1357,10 +1369,12 @@ impl CurlDownloader {
if let Some(PhpMixed::String(filename)) = job.get("filename") {
rename(&format!("{}~", filename), filename);
// job['resolve']($response);
- // TODO(phase-b): invoke stored resolve callable
+ // TODO(phase-c): invoke the stored resolve callable — blocked on the typed-Job
+ // refactor that holds the React\Promise resolve closure (see download()).
} else {
// job['resolve']($response);
- // TODO(phase-b): invoke stored resolve callable
+ // TODO(phase-c): invoke the stored resolve callable — blocked on the typed-Job
+ // refactor that holds the React\Promise resolve closure (see download()).
}
Ok(Ok(()))
})();
@@ -1501,7 +1515,11 @@ impl CurlDownloader {
.and_then(|v| v.as_array())
.and_then(|a| a.get("prevent_ip_access_callable"))
.is_some();
- // TODO(phase-b): invoke prevent_ip_access_callable
+ // PHP: is_callable($cb = $job['options']['prevent_ip_access_callable']) && $cb($primaryIp)
+ // TODO(phase-c): prevent_ip_access_callable is a caller-supplied callable
+ // carried inside the options array as PhpMixed; invoking it needs a typed
+ // callable model (options would have to hold an Rc<dyn Fn>), which the
+ // PhpMixed-keyed options map cannot express yet.
let blocked = prevent_ip_access_callable && false;
if blocked {
let job = self.jobs.get(&i).cloned().unwrap_or_default();
@@ -1794,8 +1812,9 @@ impl CurlDownloader {
Some(PhpMixed::Array(a)) => a.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect(),
_ => IndexMap::new(),
};
- // resolve/reject callables are stored Rust-side; pass placeholders for now.
- // TODO(phase-b): forward stored callables here.
+ // PHP forwards the original job's resolve/reject callables into the restarted download.
+ // TODO(phase-c): those callables are not stored on the Job yet (typed-Job refactor, see
+ // download()), so no-op placeholders are forwarded instead of the real promise callbacks.
let resolve: Box<dyn Fn(PhpMixed) + Send + Sync> = Box::new(|_| {});
let reject: Box<dyn Fn(PhpMixed) + Send + Sync> = Box::new(|_| {});
self.init_download(
@@ -1897,7 +1916,8 @@ impl CurlDownloader {
unlink_silent(&format!("{}~", filename));
}
// job['reject']($e);
- // TODO(phase-b): invoke stored reject callable
+ // TODO(phase-c): invoke the stored reject callable — blocked on the typed-Job refactor
+ // that holds the React\Promise reject closure (see download()).
}
fn check_curl_result(&self, code: i64) -> anyhow::Result<()> {
diff --git a/crates/shirabe/src/util/process_executor.rs b/crates/shirabe/src/util/process_executor.rs
index b62debe..75b40cb 100644
--- a/crates/shirabe/src/util/process_executor.rs
+++ b/crates/shirabe/src/util/process_executor.rs
@@ -261,14 +261,19 @@ impl ProcessExecutor {
}
}
+ // PHP: $callback = is_callable($output) ? $output : fn($type, $buffer) => $this->outputHandler($type, $buffer);
let output_is_callable = output.as_deref().map(|o| is_callable(o)).unwrap_or(false);
let _callback: Box<dyn Fn(&str, &str)> = if output_is_callable {
- // TODO(phase-b): adapt output PhpMixed callable to closure
+ // TODO(phase-c): the user-supplied $output is a PhpMixed callable that cannot be
+ // invoked without a typed callable model (Rc<dyn Fn>); deferred with the callable model.
Box::new(|_t: &str, _b: &str| {})
} else {
- Box::new(|_t: &str, _b: &str| {
- // TODO(phase-b): self.output_handler(t, b) — self is borrowed mutably elsewhere
- })
+ // TODO(phase-c): the fallback must call self.output_handler(type, buffer) (which is
+ // &mut self and writes to io / updates last_message), but this is a 'static
+ // `Box<dyn Fn>` that cannot borrow &mut self. Wiring it needs the handler state shared
+ // (Rc<RefCell<...>>). The callback is also not yet passed to process.run, whose Symfony
+ // Process backing stays todo!().
+ Box::new(|_t: &str, _b: &str| {})
};
let io_for_signal = self.io.clone();
diff --git a/crates/shirabe/src/util/remote_filesystem.rs b/crates/shirabe/src/util/remote_filesystem.rs
index dc6a2a4..ff6f066 100644
--- a/crates/shirabe/src/util/remote_filesystem.rs
+++ b/crates/shirabe/src/util/remote_filesystem.rs
@@ -307,7 +307,13 @@ impl RemoteFilesystem {
let mut error_message = String::new();
let error_code = 0_i64;
let mut result: Option<String> = None;
- // TODO(phase-b): set_error_handler with a closure capturing `error_message` by reference.
+ // TODO(phase-c): faithfully accumulating the file_get_contents warnings into
+ // error_message requires PHP's set_error_handler runtime mechanism — a global handler the
+ // stream layer invokes per warning, appending into error_message by &-reference.
+ // set_error_handler / restore_error_handler are php-shim functions that must stay todo!(),
+ // and get_remote_contents does not surface low-level read warnings through such a side
+ // channel, so error_message stays empty here. Resolving this needs a thread-local
+ // error-handler stack wired into the I/O layer.
set_error_handler(|_code, _msg, _file, _line| true);
let mut http_response_header: Vec<String> = Vec::new();
@@ -611,7 +617,10 @@ impl RemoteFilesystem {
}
let put_error_message = String::new();
- // TODO(phase-b): set_error_handler closure that captures `put_error_message` by reference
+ // TODO(phase-c): capturing the file_put_contents warning into put_error_message
+ // requires PHP's set_error_handler runtime mechanism (see the get() method above);
+ // set_error_handler is a php-shim that must stay todo!() and file_put_contents does not
+ // surface its warning text here, so put_error_message stays empty.
set_error_handler(|_code, _msg, _file, _line| true);
let write_result =
file_put_contents(file_name.as_deref().unwrap(), result_str.as_bytes());
diff --git a/crates/shirabe/src/util/svn.rs b/crates/shirabe/src/util/svn.rs
index 7634b62..8453a43 100644
--- a/crates/shirabe/src/util/svn.rs
+++ b/crates/shirabe/src/util/svn.rs
@@ -140,7 +140,13 @@ impl Svn {
let command = self.get_command(svn_command.clone(), url, path);
let mut output: Option<String> = None;
- // TODO(phase-b): handler captures &mut output and io by reference; restructure for Rust closures
+ // PHP: $handler = function ($type, $buffer) use (&$output, $io, $verbose) { ... };
+ // $status = $this->process->execute($command, $handler, $cwd);
+ // TODO(phase-c): ProcessExecutor::execute does not yet accept a streaming output callback
+ // (its Symfony Process backing stays todo!()), so this handler — which filters by stream
+ // type, drops "Redirecting to URL" lines, accumulates into `output`, and echoes when
+ // verbose — cannot be passed through. The plain-buffer execute_args call below loses that
+ // filtering; resolving needs the process callback model (cf. process_executor.rs).
let _io = &self.io;
let _handler = |r#type: &str, buffer: &str| -> Option<()> {
if r#type != "out" {
@@ -156,7 +162,8 @@ impl Svn {
}
None
};
- // TODO(phase-b): pass handler callback to process.execute
+ // TODO(phase-c): pass the filtering handler above to process.execute once the callback
+ // model lands; for now a plain buffer is used and the output filtering is skipped.
let mut handler_output = String::new();
let status = self.process.borrow_mut().execute_args(
&command,