diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-16 14:18:41 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-16 14:35:48 +0900 |
| commit | aa2124fe5d0c96078c034a6e4044b7e81acdf692 (patch) | |
| tree | c9b1597fef555220676f6a5ae554dc81579a4269 /crates | |
| parent | b4ab3df2ec85fbe477d7721344a8cd3630b437a1 (diff) | |
| download | php-shirabe-aa2124fe5d0c96078c034a6e4044b7e81acdf692.tar.gz php-shirabe-aa2124fe5d0c96078c034a6e4044b7e81acdf692.tar.zst php-shirabe-aa2124fe5d0c96078c034a6e4044b7e81acdf692.zip | |
feat(php-rpc): replay Rust-side env writes into the PHP worker
The worker is a long-lived child holding the environment it was handed
at spawn, so `@putenv`, the bin dir the event dispatcher prepends to
PATH, and COMPOSER_DEV_MODE never reached the PHP code running in it.
The shim now journals every write to the three storages PHP exposes, and
the outermost rpc_call replays the entries the worker has not seen yet
through __shirabe_sync_env. Replaying the writes rather than pushing a
whole snapshot keeps the worker's own $_SERVER entries intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/shirabe-php-rpc/php/worker.php | 34 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/src/env.rs | 57 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/src/lib.rs | 6 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/src/session.rs | 27 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/env.rs | 57 | ||||
| -rw-r--r-- | crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs | 8 |
6 files changed, 166 insertions, 23 deletions
diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php index ce75f9d1..164d73ea 100644 --- a/crates/shirabe-php-rpc/php/worker.php +++ b/crates/shirabe-php-rpc/php/worker.php @@ -851,6 +851,40 @@ ShirabeRpcRuntime::$dispatch = [ } return $value; }, + // Replays environment writes the Rust side made after this process was spawned, which only + // inherited the environment as it stood then. The three storages PHP exposes are distinct + // (docs/dev/env-vars-porting.md), so each write names the one it targets; a null value is an + // unset. + '__shirabe_sync_env' => static function ($args) { + foreach ($args[0] as [$storage, $key, $value]) { + switch ($storage) { + case 'process': + if ($value === null) { + putenv($key); + } else { + putenv("{$key}={$value}"); + } + break; + case '_ENV': + if ($value === null) { + unset($_ENV[$key]); + } else { + $_ENV[$key] = $value; + } + break; + case '_SERVER': + if ($value === null) { + unset($_SERVER[$key]); + } else { + $_SERVER[$key] = $value; + } + break; + default: + throw new RuntimeException("__shirabe_sync_env got an unknown storage `{$storage}`"); + } + } + return true; + }, // For testing only: reads a public property of a P-table entity (PHPUnit asserts like // `$plugins[0]->version` have no method to call). '__shirabe_get_property' => static function ($args) { diff --git a/crates/shirabe-php-rpc/src/env.rs b/crates/shirabe-php-rpc/src/env.rs new file mode 100644 index 00000000..1655168f --- /dev/null +++ b/crates/shirabe-php-rpc/src/env.rs @@ -0,0 +1,57 @@ +//! Propagation of environment mutations into the PHP worker. +//! +//! The worker is a long-lived child: it holds the environment it was handed at spawn, so a +//! `putenv()` or a `$_ENV`/`$_SERVER` write the Rust side makes afterwards is invisible to the +//! PHP code running in it — `@putenv` in a `scripts` entry, the bin dir the event dispatcher +//! prepends to `PATH`, `COMPOSER_DEV_MODE`. The shim records every such write; this module +//! replays the ones the worker has not seen yet, before the next call crosses the boundary. +//! +//! TODO(php-runtime): the reverse direction is missing — a write PHP code makes to its own +//! environment is not reflected back into the Rust-side storages. + +use crate::PluginValue; +use shirabe_php_shim::EnvStorageKind; +use std::os::unix::ffi::OsStrExt as _; + +/// How many recorded mutations the worker has already been told about. +static SYNCED: std::sync::Mutex<usize> = std::sync::Mutex::new(0); + +/// The name `__shirabe_sync_env` reads each mutation's target storage under. +fn storage_name(storage: EnvStorageKind) -> &'static str { + match storage { + EnvStorageKind::Process => "process", + EnvStorageKind::Env => "_ENV", + EnvStorageKind::Server => "_SERVER", + } +} + +/// Replays the environment mutations recorded since the last flush into the worker. +/// +/// Called from the outermost `rpc_call` only, so the `__shirabe_sync_env` call it issues re-enters +/// the session instead of recursing into another flush. +pub(crate) fn flush() -> anyhow::Result<()> { + let mut synced = SYNCED.lock().unwrap(); + let (cursor, mutations) = shirabe_php_shim::env_mutations_since(*synced); + if mutations.is_empty() { + return Ok(()); + } + + let mutations = mutations + .into_iter() + .map(|mutation| { + PluginValue::List(vec![ + PluginValue::string(storage_name(mutation.storage)), + PluginValue::String(mutation.key.as_bytes().to_vec()), + match mutation.value { + Some(value) => PluginValue::String(value.as_bytes().to_vec()), + None => PluginValue::Null, + }, + ]) + }) + .collect(); + let outcome = crate::call_function("__shirabe_sync_env", vec![PluginValue::List(mutations)])?; + // The worker only writes the three storages; nothing it does there can throw. + outcome.unwrap_or_else(|throw| panic!("PHP RPC: __shirabe_sync_env threw: {}", throw.message)); + *synced = cursor; + Ok(()) +} diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs index d818b364..47304ba8 100644 --- a/crates/shirabe-php-rpc/src/lib.rs +++ b/crates/shirabe-php-rpc/src/lib.rs @@ -1,6 +1,7 @@ //! Rust-to-PHP RPC over a Unix domain socket. See `docs/dev/php-rpc.md`. pub mod composer_runtime; +mod env; pub mod frame; pub mod session; pub mod value; @@ -780,7 +781,10 @@ fn rpc_call( ) -> anyhow::Result<Result<PluginValue, PhpThrow>> { // Held for the whole logical call session; nested calls from the same thread (issued by a // dispatcher handler) re-enter immediately, other threads are serialized. - let _session = session::SessionGuard::enter(); + let session = session::SessionGuard::enter(); + if session.is_outermost() { + env::flush()?; + } let my_id = NEXT_CORR_ID.fetch_add(2, Ordering::Relaxed); send_frame(&request(my_id))?; loop { diff --git a/crates/shirabe-php-rpc/src/session.rs b/crates/shirabe-php-rpc/src/session.rs index 29bd89f3..3e1a817d 100644 --- a/crates/shirabe-php-rpc/src/session.rs +++ b/crates/shirabe-php-rpc/src/session.rs @@ -16,18 +16,19 @@ struct SessionLock { } impl SessionLock { - fn acquire(&self) { + /// Returns whether this acquisition opened the session rather than re-entering one. + fn acquire(&self) -> bool { let mut owner = self.owner.lock().unwrap(); let me = std::thread::current().id(); loop { match *owner { None => { *owner = Some((me, 1)); - return; + return true; } Some((tid, depth)) if tid == me => { *owner = Some((me, depth + 1)); - return; + return false; } Some(_) => { owner = self.cvar.wait(owner).unwrap(); @@ -60,12 +61,20 @@ static SESSION: LazyLock<SessionLock> = LazyLock::new(|| SessionLock { /// RAII guard; acquired once at the outermost `rpc_call`, re-entered (depth += 1, no blocking) /// by nested calls from the same thread. -pub struct SessionGuard; +pub struct SessionGuard { + outermost: bool, +} impl SessionGuard { pub fn enter() -> Self { - SESSION.acquire(); - SessionGuard + let outermost = SESSION.acquire(); + SessionGuard { outermost } + } + + /// Whether this guard opened the session. Work that must happen once per logical call + /// session, before anything else crosses the boundary, keys off this. + pub fn is_outermost(&self) -> bool { + self.outermost } } @@ -81,8 +90,10 @@ mod tests { #[test] fn same_thread_reenters_without_blocking() { - let _outer = SessionGuard::enter(); - let _inner = SessionGuard::enter(); + let outer = SessionGuard::enter(); + let inner = SessionGuard::enter(); + assert!(outer.is_outermost()); + assert!(!inner.is_outermost()); } #[test] diff --git a/crates/shirabe-php-shim/src/env.rs b/crates/shirabe-php-shim/src/env.rs index 8facfb4a..861c0243 100644 --- a/crates/shirabe-php-shim/src/env.rs +++ b/crates/shirabe-php-shim/src/env.rs @@ -15,6 +15,7 @@ pub fn getenv<K: AsRef<std::ffi::OsStr>>(key: K) -> Option<std::ffi::OsString> { /// duration of this call. pub unsafe fn putenv<K: AsRef<std::ffi::OsStr>, V: AsRef<std::ffi::OsStr>>(key: K, value: V) { // TODO(php-semantics): validate key and value format to avoid panic? + record(EnvStorageKind::Process, key.as_ref(), Some(value.as_ref())); unsafe { std::env::set_var(key, value) } } @@ -25,19 +26,59 @@ pub unsafe fn putenv<K: AsRef<std::ffi::OsStr>, V: AsRef<std::ffi::OsStr>>(key: /// duration of this call. pub unsafe fn putenv_clear<K: AsRef<std::ffi::OsStr>>(key: K) { // TODO(php-semantics): validate key and value format to avoid panic? + record(EnvStorageKind::Process, key.as_ref(), None); unsafe { std::env::remove_var(key) } } +/// Which of the three environment storages a recorded mutation writes to. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EnvStorageKind { + /// The real process environment, as `putenv()` writes it. + Process, + /// The `$_ENV` superglobal. + Env, + /// The `$_SERVER` superglobal. + Server, +} + +/// One recorded write. `value` is `None` for an unset. +#[derive(Debug, Clone)] +pub struct EnvMutation { + pub storage: EnvStorageKind, + pub key: std::ffi::OsString, + pub value: Option<std::ffi::OsString>, +} + +static ENV_MUTATIONS: std::sync::Mutex<Vec<EnvMutation>> = std::sync::Mutex::new(Vec::new()); + +fn record(storage: EnvStorageKind, key: &std::ffi::OsStr, value: Option<&std::ffi::OsStr>) { + ENV_MUTATIONS.lock().unwrap().push(EnvMutation { + storage, + key: key.to_os_string(), + value: value.map(|value| value.to_os_string()), + }); +} + +/// The mutations recorded after the first `cursor` ones, and the cursor that follows them. +/// +/// A separate PHP runtime holds the environment it was handed when it started; replaying these +/// writes in order is what brings its three storages back in line with this process's. +pub fn env_mutations_since(cursor: usize) -> (usize, Vec<EnvMutation>) { + let mutations = ENV_MUTATIONS.lock().unwrap(); + (mutations.len(), mutations[cursor..].to_vec()) +} + pub struct Superglobal { + storage: EnvStorageKind, vars: indexmap::IndexMap<std::ffi::OsString, std::ffi::OsString>, } pub struct SuperglobalServer(Superglobal); impl Superglobal { - fn from_env_vars() -> Self { + fn from_env_vars(storage: EnvStorageKind) -> Self { let vars = std::env::vars_os().collect(); - Self { vars } + Self { storage, vars } } pub fn get_all(&self) -> impl Iterator<Item = (std::ffi::OsString, std::ffi::OsString)> + '_ { @@ -49,17 +90,19 @@ impl Superglobal { } pub fn put(&mut self, key: std::ffi::OsString, value: std::ffi::OsString) { + record(self.storage, &key, Some(&value)); self.vars.insert(key, value); } pub fn clear<K: AsRef<std::ffi::OsStr>>(&mut self, key: K) { + record(self.storage, key.as_ref(), None); self.vars.shift_remove(key.as_ref()); } } impl SuperglobalServer { fn from_env_vars() -> Self { - Self(Superglobal::from_env_vars()) + Self(Superglobal::from_env_vars(EnvStorageKind::Server)) } pub fn get_all(&self) -> impl Iterator<Item = (std::ffi::OsString, std::ffi::OsString)> + '_ { @@ -89,12 +132,14 @@ impl SuperglobalServer { /// PHP superglobal $_SERVER. $_SERVER is a snapshot at startup. Modifying it does not affect the /// real environment variables, while putenv() does. -/// TODO(php-runtime): modify the real PHP's $_SERVER. +/// TODO(php-runtime): a write PHP code makes to its own $_SERVER is not reflected back here. pub static PHP_SERVER: std::sync::LazyLock<std::sync::Mutex<SuperglobalServer>> = std::sync::LazyLock::new(|| std::sync::Mutex::new(SuperglobalServer::from_env_vars())); /// PHP superglobal $_ENV. $_ENV is a snapshot at startup. Modifying it does not affect the real /// environment variables, while putenv() does. -/// TODO(php-runtime): modify the real PHP's $_ENV. +/// TODO(php-runtime): a write PHP code makes to its own $_ENV is not reflected back here. pub static PHP_ENV: std::sync::LazyLock<std::sync::Mutex<Superglobal>> = - std::sync::LazyLock::new(|| std::sync::Mutex::new(Superglobal::from_env_vars())); + std::sync::LazyLock::new(|| { + std::sync::Mutex::new(Superglobal::from_env_vars(EnvStorageKind::Env)) + }); diff --git a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs index 7a98adc8..e3f5c9a5 100644 --- a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs +++ b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs @@ -740,12 +740,8 @@ fn test_dispatcher_can_execute_cli_and_php_in_same_event_script_stack() { assert_eq!(expected, io.borrow().get_output()); } -// TODO(php-runtime): `@putenv` writes the environment of the Shirabe process, while the listener -// reads `getenv()` inside the PHP worker — a long-lived child that keeps the environment it -// inherited when it was spawned. Nothing propagates the write across the boundary. #[test] #[serial] -#[ignore = "`@putenv ABC=123` does not reach the PHP worker the listener runs in"] fn test_dispatcher_can_put_env() { let _tear_down = TearDown; if !php_runtime_available() { @@ -786,12 +782,8 @@ fn test_dispatcher_can_put_env() { assert_eq!(expected, io.borrow().get_output()); } -// TODO(php-runtime): the bin dir is appended to the PATH of the Shirabe process, while the -// listeners read `getenv('PATH')` inside the PHP worker — a long-lived child that keeps the -// environment it inherited when it was spawned. Nothing propagates the append across the boundary. #[test] #[serial] -#[ignore = "the bin dir the dispatcher appends to PATH does not reach the PHP worker the listeners run in"] fn test_dispatcher_appends_dir_bin_on_path_for_every_listener() { let _tear_down = TearDown; if !php_runtime_available() { |
