aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 17:14:07 +0900
committernsfisis <nsfisis@gmail.com>2026-06-26 00:20:05 +0900
commit0dd510dbc3a5b6e888abcc104ae8c4a4eeb1e42b (patch)
tree8b6e7663c82bd7c4bbe91f4594940aa780376d77 /crates/shirabe-php-shim
parent6c137a28151f5ef05c9e22d3232e9ba7b81f6abe (diff)
downloadphp-shirabe-0dd510dbc3a5b6e888abcc104ae8c4a4eeb1e42b.tar.gz
php-shirabe-0dd510dbc3a5b6e888abcc104ae8c4a4eeb1e42b.tar.zst
php-shirabe-0dd510dbc3a5b6e888abcc104ae8c4a4eeb1e42b.zip
feat(shim): implement stream_select/stream_set_blocking via libc
Add fcntl/select extern "C" declarations and a PhpResource::raw_fd seam so the symfony Process pipe loop can read a live child's stdout/stderr. Fix fread on a non-blocking fd (WouldBlock -> "") and the Process null-cwd default. Real subprocess output capture now works; un-ignore the process_executor tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-shim')
-rw-r--r--crates/shirabe-php-shim/src/fs.rs8
-rw-r--r--crates/shirabe-php-shim/src/lib.rs36
-rw-r--r--crates/shirabe-php-shim/src/stream.rs141
3 files changed, 171 insertions, 14 deletions
diff --git a/crates/shirabe-php-shim/src/fs.rs b/crates/shirabe-php-shim/src/fs.rs
index 5558f58..2fb0dc5 100644
--- a/crates/shirabe-php-shim/src/fs.rs
+++ b/crates/shirabe-php-shim/src/fs.rs
@@ -343,7 +343,13 @@ pub fn fread(stream: &PhpResource, length: i64) -> Option<String> {
return None;
}
let mut buf = vec![0u8; cap];
- let n = state.backing.as_rws().read(&mut buf).ok()?;
+ let n = match state.backing.as_rws().read(&mut buf) {
+ Ok(n) => n,
+ // A non-blocking stream with no data ready is not an error in PHP: fread() returns
+ // the empty string and feof() stays false. std surfaces this as WouldBlock.
+ Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Some(String::new()),
+ Err(_) => return None,
+ };
if cap > 0 && n == 0 {
state.eof = true;
}
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs
index 113c9d7..8c8073f 100644
--- a/crates/shirabe-php-shim/src/lib.rs
+++ b/crates/shirabe-php-shim/src/lib.rs
@@ -349,6 +349,32 @@ pub enum PhpResource {
Process(std::rc::Rc<std::cell::RefCell<process::ProcessState>>),
}
+impl PhpResource {
+ /// Returns the underlying OS file descriptor backing this resource, when it has one.
+ /// Used by `stream_set_blocking`/`stream_select` to drive `fcntl(2)`/`select(2)`. In-memory
+ /// streams (`php://memory`/`php://temp`) and process handles have no fd and return `None`.
+ pub(crate) fn raw_fd(&self) -> Option<std::os::unix::io::RawFd> {
+ use std::os::unix::io::AsRawFd;
+ match self {
+ PhpResource::Stdin => Some(std::io::stdin().as_raw_fd()),
+ PhpResource::Stdout => Some(std::io::stdout().as_raw_fd()),
+ PhpResource::Stderr => Some(std::io::stderr().as_raw_fd()),
+ PhpResource::Process(_) => None,
+ PhpResource::Stream(state) => {
+ let state = state.borrow();
+ if state.closed {
+ return None;
+ }
+ match &state.backing {
+ StreamBacking::File(f) => Some(f.as_raw_fd()),
+ StreamBacking::Pipe(p) => Some(p.as_raw_fd()),
+ StreamBacking::Memory(_) => None,
+ }
+ }
+ }
+ }
+}
+
/// Combined capability of every seekable byte stream backing. Both `std::fs::File`
/// and `std::io::Cursor<Vec<u8>>` satisfy it, so a stream can be driven uniformly.
pub trait ReadWriteSeek: std::io::Read + std::io::Write + std::io::Seek {}
@@ -420,6 +446,16 @@ impl std::io::Seek for ChildPipe {
}
}
+impl std::os::unix::io::AsRawFd for ChildPipe {
+ fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
+ match self {
+ ChildPipe::In(i) => i.as_raw_fd(),
+ ChildPipe::Out(o) => o.as_raw_fd(),
+ ChildPipe::Err(e) => e.as_raw_fd(),
+ }
+ }
+}
+
#[derive(Debug)]
pub struct StreamState {
pub(crate) backing: StreamBacking,
diff --git a/crates/shirabe-php-shim/src/stream.rs b/crates/shirabe-php-shim/src/stream.rs
index a8ccbe5..606b202 100644
--- a/crates/shirabe-php-shim/src/stream.rs
+++ b/crates/shirabe-php-shim/src/stream.rs
@@ -259,24 +259,139 @@ fn build_meta_data(
map
}
-pub fn stream_set_blocking(_resource: &PhpResource, _enable: bool) -> bool {
- // TODO(phase-d): toggling O_NONBLOCK requires fcntl(2); a syscall crate is intentionally not
- // introduced here.
- todo!("stream_set_blocking requires fcntl(2) (syscall crate not available)")
+// libc is already linked into every binary, so these can be declared directly without an extra
+// crate (mirrors the `getuid`/`geteuid` declarations in process.rs).
+const F_GETFL: i32 = 3;
+const F_SETFL: i32 = 4;
+const O_NONBLOCK: i32 = 0o4000;
+const FD_SETSIZE: usize = 1024;
+
+unsafe extern "C" {
+ fn fcntl(fd: i32, cmd: i32, ...) -> i32;
+ fn select(
+ nfds: i32,
+ readfds: *mut FdSet,
+ writefds: *mut FdSet,
+ exceptfds: *mut FdSet,
+ timeout: *mut TimeVal,
+ ) -> i32;
+}
+
+#[repr(C)]
+struct TimeVal {
+ tv_sec: i64,
+ tv_usec: i64,
+}
+
+// `fd_set` is a bitmap of `FD_SETSIZE` bits laid out as an array of `long` words.
+#[repr(C)]
+struct FdSet {
+ fds_bits: [i64; FD_SETSIZE / (8 * std::mem::size_of::<i64>())],
+}
+
+impl FdSet {
+ fn zero() -> Self {
+ FdSet {
+ fds_bits: [0; FD_SETSIZE / (8 * std::mem::size_of::<i64>())],
+ }
+ }
+
+ fn set(&mut self, fd: i32) {
+ let bits = 8 * std::mem::size_of::<i64>();
+ self.fds_bits[fd as usize / bits] |= 1i64 << (fd as usize % bits);
+ }
+
+ fn is_set(&self, fd: i32) -> bool {
+ let bits = 8 * std::mem::size_of::<i64>();
+ (self.fds_bits[fd as usize / bits] & (1i64 << (fd as usize % bits))) != 0
+ }
+}
+
+/// PHP `stream_set_blocking()`: toggle `O_NONBLOCK` on the resource's underlying fd via `fcntl(2)`.
+/// Returns `false` (PHP failure) when the resource has no fd or the syscall fails.
+pub fn stream_set_blocking(resource: &PhpResource, enable: bool) -> bool {
+ let Some(fd) = resource.raw_fd() else {
+ return false;
+ };
+ let flags = unsafe { fcntl(fd, F_GETFL) };
+ if flags < 0 {
+ return false;
+ }
+ // `enable` means blocking, i.e. clear O_NONBLOCK.
+ let new_flags = if enable {
+ flags & !O_NONBLOCK
+ } else {
+ flags | O_NONBLOCK
+ };
+ unsafe { fcntl(fd, F_SETFL, new_flags) >= 0 }
}
/// PHP `stream_select`. Returns the number of changed streams, or `None` for the PHP `false`
-/// returned when the underlying `select` is interrupted/fails.
+/// returned when the underlying `select` is interrupted/fails. On success the `read`/`write`/
+/// `except` vectors are narrowed to only the resources that became ready, matching PHP's
+/// in-place rewrite of the passed arrays.
pub fn stream_select(
- _read: &mut Vec<PhpResource>,
- _write: &mut Vec<PhpResource>,
- _except: &mut Vec<PhpResource>,
- _seconds: i64,
- _microseconds: Option<i64>,
+ read: &mut Vec<PhpResource>,
+ write: &mut Vec<PhpResource>,
+ except: &mut Vec<PhpResource>,
+ seconds: i64,
+ microseconds: Option<i64>,
) -> Option<i64> {
- // TODO(phase-d): multiplexing readiness requires select(2)/poll(2); a syscall crate is
- // intentionally not introduced here.
- todo!("stream_select requires select(2)/poll(2) (syscall crate not available)")
+ let mut readfds = FdSet::zero();
+ let mut writefds = FdSet::zero();
+ let mut exceptfds = FdSet::zero();
+ let mut nfds = 0i32;
+
+ // Resources without an fd (in-memory streams, process handles) cannot be waited on; PHP would
+ // emit a warning for them. We skip them here, leaving them out of the ready set.
+ let mut prepare = |set: &mut FdSet, resources: &[PhpResource]| {
+ for resource in resources {
+ if let Some(fd) = resource.raw_fd() {
+ if (fd as usize) < FD_SETSIZE {
+ set.set(fd);
+ if fd + 1 > nfds {
+ nfds = fd + 1;
+ }
+ }
+ }
+ }
+ };
+ prepare(&mut readfds, read);
+ prepare(&mut writefds, write);
+ prepare(&mut exceptfds, except);
+
+ let mut timeout = TimeVal {
+ tv_sec: seconds,
+ tv_usec: microseconds.unwrap_or(0),
+ };
+
+ let ret = unsafe {
+ select(
+ nfds,
+ &mut readfds,
+ &mut writefds,
+ &mut exceptfds,
+ &mut timeout,
+ )
+ };
+
+ if ret < 0 {
+ // select(2) failed (e.g. EINTR). PHP returns false.
+ return None;
+ }
+
+ // Narrow each array in place to the resources whose fd is still set in the result bitmap.
+ let narrow = |set: &FdSet, resources: &mut Vec<PhpResource>| {
+ resources.retain(|resource| match resource.raw_fd() {
+ Some(fd) if (fd as usize) < FD_SETSIZE => set.is_set(fd),
+ _ => false,
+ });
+ };
+ narrow(&readfds, read);
+ narrow(&writefds, write);
+ narrow(&exceptfds, except);
+
+ Some(ret as i64)
}
/// PHP `stream_get_contents($stream, $maxlength, $offset)`. A non-negative `offset` seeks there