aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--crates/shirabe-php-rpc/php/worker.php34
-rw-r--r--crates/shirabe-php-rpc/src/env.rs57
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs6
-rw-r--r--crates/shirabe-php-rpc/src/session.rs27
-rw-r--r--crates/shirabe-php-shim/src/env.rs57
-rw-r--r--crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs8
-rw-r--r--docs/dev/env-vars-porting.md19
-rw-r--r--docs/dev/php-rpc.md4
8 files changed, 182 insertions, 30 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() {
diff --git a/docs/dev/env-vars-porting.md b/docs/dev/env-vars-porting.md
index fbee8703..af4c947a 100644
--- a/docs/dev/env-vars-porting.md
+++ b/docs/dev/env-vars-porting.md
@@ -33,12 +33,17 @@ each call site.
| `$_ENV` | `PHP_ENV` |
| `$_SERVER` | `PHP_SERVER` |
-## TODOs
+## Reaching the PHP runtime
+
+The PHP worker is a separate, long-lived process (`docs/dev/php-rpc.md`): it holds the environment
+it was handed at spawn, so a later write on the Rust side would be invisible to the PHP code
+running in it. Every write to one of the three storages is therefore recorded in an ordered journal
+(`env_mutations_since`), and `shirabe-php-rpc` replays the entries the worker has not seen yet —
+through `__shirabe_sync_env`, which writes each entry to the storage it names — before the next call
+crosses the boundary. Replaying the writes rather than pushing a whole snapshot is what keeps the
+worker's own `$_SERVER` entries (`argv`, `SCRIPT_NAME`, ...) intact.
-The current implementation only models the Rust side. Two things remain unimplemented:
+## TODOs
-* Propagating `$_ENV`/`$_SERVER` into the real PHP runtime. When Shirabe hands control to PHP
- (for the plugin API), the PHP side needs to see the same `$_ENV`/`$_SERVER` snapshot Shirabe
- holds. This is marked `TODO(php-runtime)` in `env.rs` and is not yet wired up.
-* Reflecting PHP-side mutations of `$_ENV`/`$_SERVER` back into Shirabe. How to handle the case
- where PHP code rewrites `$_ENV` or `$_SERVER` is still TBD.
+Reflecting PHP-side mutations of `$_ENV`/`$_SERVER` back into Shirabe is unimplemented. How to
+handle the case where PHP code rewrites `$_ENV` or `$_SERVER` is still TBD.
diff --git a/docs/dev/php-rpc.md b/docs/dev/php-rpc.md
index f91b8e54..eb93db19 100644
--- a/docs/dev/php-rpc.md
+++ b/docs/dev/php-rpc.md
@@ -165,6 +165,10 @@ Notable internal helpers:
- `__shirabe_settle_promise` — the inverse: drains a promise a plugin returned to Rust. React
settles synchronously, so an already-settled promise yields its value here (a rejection is
re-thrown as the Throw reply); one that is still pending is an explicit error.
+- `__shirabe_sync_env` — replays the environment writes the Rust side made after the worker was
+ spawned into the storage each one names (`putenv()`, `$_ENV`, `$_SERVER`); see
+ [docs/dev/env-vars-porting.md](./env-vars-porting.md). The outermost `rpc_call` of a session
+ issues it whenever the journal has moved on.
- `__shirabe_get_property` — for testing only: reads a public property of a P-table entity.
- `__shirabe_oracle_roundtrip` — codec oracle support for tests.
- `__shirabe_console_application_boot` — builds the worker-side `Composer\Console\Application`