aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-20 23:37:42 +0900
committernsfisis <nsfisis@gmail.com>2026-06-20 23:37:42 +0900
commit84655bf3a4a21dbbe7aec8e3aee1e661505bbb6d (patch)
tree189f8ce5d7d53246632d5e2419c392d58f06e2b8 /crates
parent10840ec1fd7f1140305449fc94175d60998572c1 (diff)
downloadphp-shirabe-84655bf3a4a21dbbe7aec8e3aee1e661505bbb6d.tar.gz
php-shirabe-84655bf3a4a21dbbe7aec8e3aee1e661505bbb6d.tar.zst
php-shirabe-84655bf3a4a21dbbe7aec8e3aee1e661505bbb6d.zip
feat(symfony-process): port full Process class from PHP
Faithfully port every method, field and constant of Symfony's Process.php into process.rs, replacing the reduced stub. Add the supporting pipes module (PipesInterface/AbstractPipes/UnixPipes/ WindowsPipes), ProcessUtils and the missing process exceptions (LogicException/InvalidArgumentException) with constructors. Methods now return anyhow::Result where PHP throws, take the env argument and a bool-returning callback, and borrow &mut for status-updating accessors; all callers are updated accordingly. Extend the php-shim with proc_open/proc_close (PHP-compatible signatures), proc_get_status, proc_terminate, posix_kill, uniqid, ftruncate, ftell_stream, fseek3, stream_get_contents3 and env helpers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/cursor.rs7
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs30
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/terminal.rs5
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/exception/invalid_argument_exception.rs21
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/exception/logic_exception.rs21
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/exception/mod.rs4
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/exception/process_failed_exception.rs34
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/exception/process_signaled_exception.rs19
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/exception/process_timed_out_exception.rs42
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/exception/runtime_exception.rs6
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/mod.rs4
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/pipes/abstract_pipes.rs75
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/pipes/mod.rs9
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/pipes/pipes_interface.rs31
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/pipes/unix_pipes.rs102
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/pipes/windows_pipes.rs66
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/process.rs1715
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/process_utils.rs47
-rw-r--r--crates/shirabe-php-shim/src/lib.rs61
-rw-r--r--crates/shirabe/src/downloader/zip_downloader.rs4
-rw-r--r--crates/shirabe/src/package/version/version_guesser.rs4
-rw-r--r--crates/shirabe/src/repository/vcs/perforce_driver.rs2
-rw-r--r--crates/shirabe/src/util/filesystem.rs2
-rw-r--r--crates/shirabe/src/util/perforce.rs37
-rw-r--r--crates/shirabe/src/util/process_executor.rs57
25 files changed, 2293 insertions, 112 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/cursor.rs b/crates/shirabe-external-packages/src/symfony/console/cursor.rs
index 9b031ab..42efff7 100644
--- a/crates/shirabe-external-packages/src/symfony/console/cursor.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/cursor.rs
@@ -192,7 +192,7 @@ impl Cursor {
let is_tty_supported = if shirabe_php_shim::function_exists("proc_open") {
*IS_TTY_SUPPORTED.get_or_init(|| {
let mut pipes = shirabe_php_shim::PhpMixed::Null;
- shirabe_php_shim::proc_open(
+ shirabe_php_shim::php_truthy(&shirabe_php_shim::proc_open(
"echo 1 >/dev/null",
&vec![
shirabe_php_shim::PhpMixed::List(vec![
@@ -212,7 +212,10 @@ impl Cursor {
]),
],
&mut pipes,
- )
+ None,
+ None,
+ None,
+ ))
})
} else {
false
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs
index a32c95d..e62b9ac 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs
@@ -85,7 +85,13 @@ impl ProcessHelper {
ProcessHelperCmdElement::Process(_) => unreachable!(),
})
.collect();
- process = Process::new(command, None, None, None, Some(60.0));
+ process = Process::new(
+ command,
+ None,
+ None,
+ shirabe_php_shim::PhpMixed::Null,
+ Some(60.0),
+ )?;
cmd = vec![];
}
Some(ProcessHelperCmdElement::Process(_)) => {
@@ -124,7 +130,23 @@ impl ProcessHelper {
};
// PHP passes the remaining `$cmd` array as the `$env` argument to Process::run.
- process.run(callback);
+ let env: indexmap::IndexMap<String, shirabe_php_shim::PhpMixed> = cmd
+ .iter()
+ .enumerate()
+ .filter_map(|(i, element)| match element {
+ ProcessHelperCmdElement::String(s) => {
+ Some((i.to_string(), shirabe_php_shim::PhpMixed::String(s.clone())))
+ }
+ ProcessHelperCmdElement::Process(_) => None,
+ })
+ .collect();
+ let callback: Option<Box<dyn FnMut(&str, &str) -> bool>> = callback.map(|mut cb| {
+ Box::new(move |r#type: &str, buffer: &str| -> bool {
+ cb(r#type, buffer);
+ false
+ }) as Box<dyn FnMut(&str, &str) -> bool>
+ });
+ process.run(callback, env)?;
if verbosity <= output.borrow().get_verbosity() {
let message = if process.is_successful() {
@@ -183,7 +205,7 @@ impl ProcessHelper {
error: Option<&str>,
callback: Option<Box<dyn FnMut(&str, &str)>>,
) -> anyhow::Result<Process> {
- let process = self.run(
+ let mut process = self.run(
output,
cmd,
error,
@@ -192,7 +214,7 @@ impl ProcessHelper {
)?;
if !process.is_successful() {
- anyhow::bail!(ProcessFailedException::new(&process));
+ anyhow::bail!(ProcessFailedException::new(&mut process)?);
}
Ok(process)
diff --git a/crates/shirabe-external-packages/src/symfony/console/terminal.rs b/crates/shirabe-external-packages/src/symfony/console/terminal.rs
index 377d344..e0de3f2 100644
--- a/crates/shirabe-external-packages/src/symfony/console/terminal.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/terminal.rs
@@ -226,8 +226,9 @@ impl Terminal {
};
let mut pipes = PhpMixed::Null;
- let process = shirabe_php_shim::proc_open(command, &descriptorspec, &mut pipes);
- if !process {
+ let process =
+ shirabe_php_shim::proc_open(command, &descriptorspec, &mut pipes, None, None, None);
+ if !shirabe_php_shim::php_truthy(&process) {
return None;
}
diff --git a/crates/shirabe-external-packages/src/symfony/process/exception/invalid_argument_exception.rs b/crates/shirabe-external-packages/src/symfony/process/exception/invalid_argument_exception.rs
new file mode 100644
index 0000000..b70a1d3
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/process/exception/invalid_argument_exception.rs
@@ -0,0 +1,21 @@
+//! ref: composer/vendor/symfony/process/Exception/InvalidArgumentException.php
+
+#[derive(Debug)]
+pub struct InvalidArgumentException {
+ pub message: String,
+ pub code: i64,
+}
+
+impl InvalidArgumentException {
+ pub fn new(message: String) -> Self {
+ Self { message, code: 0 }
+ }
+}
+
+impl std::fmt::Display for InvalidArgumentException {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.message)
+ }
+}
+
+impl std::error::Error for InvalidArgumentException {}
diff --git a/crates/shirabe-external-packages/src/symfony/process/exception/logic_exception.rs b/crates/shirabe-external-packages/src/symfony/process/exception/logic_exception.rs
new file mode 100644
index 0000000..c9a3466
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/process/exception/logic_exception.rs
@@ -0,0 +1,21 @@
+//! ref: composer/vendor/symfony/process/Exception/LogicException.php
+
+#[derive(Debug)]
+pub struct LogicException {
+ pub message: String,
+ pub code: i64,
+}
+
+impl LogicException {
+ pub fn new(message: String) -> Self {
+ Self { message, code: 0 }
+ }
+}
+
+impl std::fmt::Display for LogicException {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.message)
+ }
+}
+
+impl std::error::Error for LogicException {}
diff --git a/crates/shirabe-external-packages/src/symfony/process/exception/mod.rs b/crates/shirabe-external-packages/src/symfony/process/exception/mod.rs
index 67bad98..689a64b 100644
--- a/crates/shirabe-external-packages/src/symfony/process/exception/mod.rs
+++ b/crates/shirabe-external-packages/src/symfony/process/exception/mod.rs
@@ -1,8 +1,12 @@
+pub mod invalid_argument_exception;
+pub mod logic_exception;
pub mod process_failed_exception;
pub mod process_signaled_exception;
pub mod process_timed_out_exception;
pub mod runtime_exception;
+pub use invalid_argument_exception::*;
+pub use logic_exception::*;
pub use process_failed_exception::*;
pub use process_signaled_exception::*;
pub use process_timed_out_exception::*;
diff --git a/crates/shirabe-external-packages/src/symfony/process/exception/process_failed_exception.rs b/crates/shirabe-external-packages/src/symfony/process/exception/process_failed_exception.rs
index 8957226..a3645d6 100644
--- a/crates/shirabe-external-packages/src/symfony/process/exception/process_failed_exception.rs
+++ b/crates/shirabe-external-packages/src/symfony/process/exception/process_failed_exception.rs
@@ -1,5 +1,6 @@
//! ref: composer/vendor/symfony/process/Exception/ProcessFailedException.php
+use crate::symfony::process::exception::invalid_argument_exception::InvalidArgumentException;
use crate::symfony::process::process::Process;
#[derive(Debug)]
@@ -9,8 +10,37 @@ pub struct ProcessFailedException {
}
impl ProcessFailedException {
- pub fn new(_process: &Process) -> Self {
- todo!()
+ pub fn new(process: &mut Process) -> anyhow::Result<Self> {
+ if process.is_successful() {
+ return Err(InvalidArgumentException::new(
+ "Expected a failed process, but the given process was successful.".to_string(),
+ )
+ .into());
+ }
+
+ let mut error = format!(
+ "The command \"{}\" failed.\n\nExit Code: {}({})\n\nWorking directory: {}",
+ process.get_command_line(),
+ process
+ .get_exit_code()
+ .map(|c| c.to_string())
+ .unwrap_or_default(),
+ process.get_exit_code_text().unwrap_or_default(),
+ process.get_working_directory().unwrap_or_default(),
+ );
+
+ if !process.is_output_disabled() {
+ error += &format!(
+ "\n\nOutput:\n================\n{}\n\nError Output:\n================\n{}",
+ process.get_output()?,
+ process.get_error_output()?,
+ );
+ }
+
+ Ok(Self {
+ message: error,
+ code: 0,
+ })
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/process/exception/process_signaled_exception.rs b/crates/shirabe-external-packages/src/symfony/process/exception/process_signaled_exception.rs
index 8b30c8d..03c4df2 100644
--- a/crates/shirabe-external-packages/src/symfony/process/exception/process_signaled_exception.rs
+++ b/crates/shirabe-external-packages/src/symfony/process/exception/process_signaled_exception.rs
@@ -1,9 +1,28 @@
//! ref: composer/vendor/symfony/process/Exception/ProcessSignaledException.php
+use crate::symfony::process::process::Process;
+
#[derive(Debug)]
pub struct ProcessSignaledException {
pub message: String,
pub code: i64,
+ signal: i64,
+}
+
+impl ProcessSignaledException {
+ pub fn new(process: &mut Process) -> anyhow::Result<Self> {
+ let signal = process.get_term_signal()?;
+
+ Ok(Self {
+ message: format!("The process has been signaled with signal \"{}\".", signal),
+ code: 0,
+ signal,
+ })
+ }
+
+ pub fn get_signal(&self) -> i64 {
+ self.signal
+ }
}
impl std::fmt::Display for ProcessSignaledException {
diff --git a/crates/shirabe-external-packages/src/symfony/process/exception/process_timed_out_exception.rs b/crates/shirabe-external-packages/src/symfony/process/exception/process_timed_out_exception.rs
index 8b7bafb..45cb693 100644
--- a/crates/shirabe-external-packages/src/symfony/process/exception/process_timed_out_exception.rs
+++ b/crates/shirabe-external-packages/src/symfony/process/exception/process_timed_out_exception.rs
@@ -1,9 +1,51 @@
//! ref: composer/vendor/symfony/process/Exception/ProcessTimedOutException.php
+use crate::symfony::process::process::Process;
+
#[derive(Debug)]
pub struct ProcessTimedOutException {
pub message: String,
pub code: i64,
+ timeout_type: i64,
+ exceeded_timeout: Option<f64>,
+}
+
+impl ProcessTimedOutException {
+ pub const TYPE_GENERAL: i64 = 1;
+ pub const TYPE_IDLE: i64 = 2;
+
+ pub fn new(process: &Process, timeout_type: i64) -> Self {
+ let exceeded_timeout = match timeout_type {
+ Self::TYPE_GENERAL => process.get_timeout(),
+ Self::TYPE_IDLE => process.get_idle_timeout(),
+ _ => panic!("Unknown timeout type \"{}\".", timeout_type),
+ };
+
+ let message = format!(
+ "The process \"{}\" exceeded the timeout of {} seconds.",
+ process.get_command_line(),
+ exceeded_timeout.map(|t| t.to_string()).unwrap_or_default(),
+ );
+
+ Self {
+ message,
+ code: 0,
+ timeout_type,
+ exceeded_timeout,
+ }
+ }
+
+ pub fn is_general_timeout(&self) -> bool {
+ Self::TYPE_GENERAL == self.timeout_type
+ }
+
+ pub fn is_idle_timeout(&self) -> bool {
+ Self::TYPE_IDLE == self.timeout_type
+ }
+
+ pub fn get_exceeded_timeout(&self) -> Option<f64> {
+ self.exceeded_timeout
+ }
}
impl std::fmt::Display for ProcessTimedOutException {
diff --git a/crates/shirabe-external-packages/src/symfony/process/exception/runtime_exception.rs b/crates/shirabe-external-packages/src/symfony/process/exception/runtime_exception.rs
index 076d157..42422af 100644
--- a/crates/shirabe-external-packages/src/symfony/process/exception/runtime_exception.rs
+++ b/crates/shirabe-external-packages/src/symfony/process/exception/runtime_exception.rs
@@ -6,6 +6,12 @@ pub struct RuntimeException {
pub code: i64,
}
+impl RuntimeException {
+ pub fn new(message: String) -> Self {
+ Self { message, code: 0 }
+ }
+}
+
impl std::fmt::Display for RuntimeException {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
diff --git a/crates/shirabe-external-packages/src/symfony/process/mod.rs b/crates/shirabe-external-packages/src/symfony/process/mod.rs
index 6dd64e1..15967be 100644
--- a/crates/shirabe-external-packages/src/symfony/process/mod.rs
+++ b/crates/shirabe-external-packages/src/symfony/process/mod.rs
@@ -1,9 +1,13 @@
pub mod exception;
pub mod executable_finder;
pub mod php_executable_finder;
+pub mod pipes;
pub mod process;
+pub mod process_utils;
pub use exception::*;
pub use executable_finder::*;
pub use php_executable_finder::*;
+pub use pipes::*;
pub use process::*;
+pub use process_utils::*;
diff --git a/crates/shirabe-external-packages/src/symfony/process/pipes/abstract_pipes.rs b/crates/shirabe-external-packages/src/symfony/process/pipes/abstract_pipes.rs
new file mode 100644
index 0000000..47db6b9
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/process/pipes/abstract_pipes.rs
@@ -0,0 +1,75 @@
+//! ref: composer/vendor/symfony/process/Pipes/AbstractPipes.php
+
+use indexmap::IndexMap;
+use shirabe_php_shim::{self as php, PhpMixed};
+
+#[derive(Debug)]
+pub struct AbstractPipes {
+ pub pipes: PhpMixed,
+
+ input_buffer: String,
+ input: PhpMixed,
+ blocked: bool,
+ last_error: Option<String>,
+}
+
+impl AbstractPipes {
+ pub fn new(input: PhpMixed) -> Self {
+ let mut input_buffer = String::new();
+ let stored_input;
+ if php::is_resource(&input) {
+ stored_input = input;
+ } else if let PhpMixed::String(s) = &input {
+ input_buffer = s.clone();
+ stored_input = PhpMixed::Null;
+ } else {
+ input_buffer = input.as_string().map(|s| s.to_string()).unwrap_or_default();
+ stored_input = PhpMixed::Null;
+ }
+
+ Self {
+ pipes: PhpMixed::List(Vec::new()),
+ input_buffer,
+ input: stored_input,
+ blocked: true,
+ last_error: None,
+ }
+ }
+
+ pub fn close(&mut self) {
+ if let PhpMixed::List(pipes) = &self.pipes {
+ for pipe in pipes {
+ if php::is_resource(pipe) {
+ php::fclose(pipe.clone());
+ }
+ }
+ }
+ self.pipes = PhpMixed::List(Vec::new());
+ }
+
+ /// Returns true if a system call has been interrupted.
+ pub(crate) fn has_system_call_been_interrupted(&mut self) -> bool {
+ let last_error = self.last_error.take();
+
+ // stream_select returns false when the `select` system call is interrupted by an incoming signal
+ last_error
+ .map(|e| e.to_lowercase().contains("interrupted system call"))
+ .unwrap_or(false)
+ }
+
+ /// Unblocks streams.
+ pub(crate) fn unblock(&mut self) {
+ let _ = &self.input_buffer;
+ let _ = &self.blocked;
+ todo!()
+ }
+
+ /// Writes input to stdin.
+ pub(crate) fn write(&mut self) -> Option<IndexMap<i64, PhpMixed>> {
+ todo!()
+ }
+
+ pub fn handle_error(&mut self, _type: i64, msg: String) {
+ self.last_error = Some(msg);
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/process/pipes/mod.rs b/crates/shirabe-external-packages/src/symfony/process/pipes/mod.rs
new file mode 100644
index 0000000..6a77f5f
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/process/pipes/mod.rs
@@ -0,0 +1,9 @@
+pub mod abstract_pipes;
+pub mod pipes_interface;
+pub mod unix_pipes;
+pub mod windows_pipes;
+
+pub use abstract_pipes::*;
+pub use pipes_interface::*;
+pub use unix_pipes::*;
+pub use windows_pipes::*;
diff --git a/crates/shirabe-external-packages/src/symfony/process/pipes/pipes_interface.rs b/crates/shirabe-external-packages/src/symfony/process/pipes/pipes_interface.rs
new file mode 100644
index 0000000..e10b3b3
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/process/pipes/pipes_interface.rs
@@ -0,0 +1,31 @@
+//! ref: composer/vendor/symfony/process/Pipes/PipesInterface.php
+
+use indexmap::IndexMap;
+use shirabe_php_shim::PhpMixed;
+
+pub const CHUNK_SIZE: i64 = 16384;
+
+/// PipesInterface manages descriptors and pipes for the use of proc_open.
+pub trait PipesInterface: std::fmt::Debug {
+ /// Returns an array of descriptors for the use of proc_open.
+ fn get_descriptors(&mut self) -> Vec<PhpMixed>;
+
+ /// Returns an array of filenames indexed by their related stream in case these pipes use temporary files.
+ fn get_files(&self) -> IndexMap<i64, String>;
+
+ /// Reads data in file handles and pipes.
+ fn read_and_write(&mut self, blocking: bool, close: bool) -> IndexMap<i64, String>;
+
+ /// Returns if the current state has open file handles or pipes.
+ fn are_open(&self) -> bool;
+
+ /// Returns if pipes are able to read output.
+ fn have_read_support(&self) -> bool;
+
+ /// Closes file handles and pipes.
+ fn close(&mut self);
+
+ /// Accessor for the `pipes` property populated by proc_open.
+ fn pipes(&self) -> &PhpMixed;
+ fn pipes_mut(&mut self) -> &mut PhpMixed;
+}
diff --git a/crates/shirabe-external-packages/src/symfony/process/pipes/unix_pipes.rs b/crates/shirabe-external-packages/src/symfony/process/pipes/unix_pipes.rs
new file mode 100644
index 0000000..387bcdf
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/process/pipes/unix_pipes.rs
@@ -0,0 +1,102 @@
+//! ref: composer/vendor/symfony/process/Pipes/UnixPipes.php
+
+use indexmap::IndexMap;
+use shirabe_php_shim::PhpMixed;
+
+use crate::symfony::process::pipes::abstract_pipes::AbstractPipes;
+use crate::symfony::process::pipes::pipes_interface::PipesInterface;
+use crate::symfony::process::process::Process;
+
+/// UnixPipes implementation uses unix pipes as handles.
+#[derive(Debug)]
+pub struct UnixPipes {
+ inner: AbstractPipes,
+ tty_mode: Option<bool>,
+ pty_mode: bool,
+ have_read_support: bool,
+}
+
+impl UnixPipes {
+ pub fn new(
+ tty_mode: Option<bool>,
+ pty_mode: bool,
+ input: PhpMixed,
+ have_read_support: bool,
+ ) -> Self {
+ Self {
+ inner: AbstractPipes::new(input),
+ tty_mode,
+ pty_mode,
+ have_read_support,
+ }
+ }
+}
+
+fn descriptor(items: &[&str]) -> PhpMixed {
+ PhpMixed::List(
+ items
+ .iter()
+ .map(|s| PhpMixed::String(s.to_string()))
+ .collect(),
+ )
+}
+
+impl PipesInterface for UnixPipes {
+ fn get_descriptors(&mut self) -> Vec<PhpMixed> {
+ if !self.have_read_support {
+ let nullstream = shirabe_php_shim::fopen("/dev/null", "c");
+
+ return vec![descriptor(&["pipe", "r"]), nullstream.clone(), nullstream];
+ }
+
+ if self.tty_mode == Some(true) {
+ return vec![
+ descriptor(&["file", "/dev/tty", "r"]),
+ descriptor(&["file", "/dev/tty", "w"]),
+ descriptor(&["file", "/dev/tty", "w"]),
+ ];
+ }
+
+ if self.pty_mode && Process::is_pty_supported() {
+ return vec![
+ descriptor(&["pty"]),
+ descriptor(&["pty"]),
+ descriptor(&["pty"]),
+ ];
+ }
+
+ vec![
+ descriptor(&["pipe", "r"]),
+ descriptor(&["pipe", "w"]),
+ descriptor(&["pipe", "w"]),
+ ]
+ }
+
+ fn get_files(&self) -> IndexMap<i64, String> {
+ IndexMap::new()
+ }
+
+ fn read_and_write(&mut self, _blocking: bool, _close: bool) -> IndexMap<i64, String> {
+ todo!()
+ }
+
+ fn have_read_support(&self) -> bool {
+ self.have_read_support
+ }
+
+ fn are_open(&self) -> bool {
+ shirabe_php_shim::php_truthy(&self.inner.pipes)
+ }
+
+ fn close(&mut self) {
+ self.inner.close();
+ }
+
+ fn pipes(&self) -> &PhpMixed {
+ &self.inner.pipes
+ }
+
+ fn pipes_mut(&mut self) -> &mut PhpMixed {
+ &mut self.inner.pipes
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/process/pipes/windows_pipes.rs b/crates/shirabe-external-packages/src/symfony/process/pipes/windows_pipes.rs
new file mode 100644
index 0000000..7602e7d
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/process/pipes/windows_pipes.rs
@@ -0,0 +1,66 @@
+//! ref: composer/vendor/symfony/process/Pipes/WindowsPipes.php
+
+use indexmap::IndexMap;
+use shirabe_php_shim::PhpMixed;
+
+use crate::symfony::process::pipes::abstract_pipes::AbstractPipes;
+use crate::symfony::process::pipes::pipes_interface::PipesInterface;
+
+/// WindowsPipes implementation uses temporary files as handles.
+#[derive(Debug)]
+pub struct WindowsPipes {
+ inner: AbstractPipes,
+ files: IndexMap<i64, String>,
+ file_handles: IndexMap<i64, PhpMixed>,
+ lock_handles: IndexMap<i64, PhpMixed>,
+ read_bytes: IndexMap<i64, i64>,
+ have_read_support: bool,
+}
+
+impl WindowsPipes {
+ pub fn new(_input: PhpMixed, _have_read_support: bool) -> Self {
+ // Windows-only path: never constructed on POSIX (DIRECTORY_SEPARATOR is "/").
+ todo!()
+ }
+}
+
+impl PipesInterface for WindowsPipes {
+ fn get_descriptors(&mut self) -> Vec<PhpMixed> {
+ let _ = (
+ &self.files,
+ &self.file_handles,
+ &self.lock_handles,
+ &self.read_bytes,
+ );
+ todo!()
+ }
+
+ fn get_files(&self) -> IndexMap<i64, String> {
+ self.files.clone()
+ }
+
+ fn read_and_write(&mut self, _blocking: bool, _close: bool) -> IndexMap<i64, String> {
+ todo!()
+ }
+
+ fn have_read_support(&self) -> bool {
+ self.have_read_support
+ }
+
+ fn are_open(&self) -> bool {
+ shirabe_php_shim::php_truthy(&self.inner.pipes) && !self.file_handles.is_empty()
+ }
+
+ fn close(&mut self) {
+ self.inner.close();
+ todo!()
+ }
+
+ fn pipes(&self) -> &PhpMixed {
+ &self.inner.pipes
+ }
+
+ fn pipes_mut(&mut self) -> &mut PhpMixed {
+ &mut self.inner.pipes
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/process/process.rs b/crates/shirabe-external-packages/src/symfony/process/process.rs
index 00934c1..2466613 100644
--- a/crates/shirabe-external-packages/src/symfony/process/process.rs
+++ b/crates/shirabe-external-packages/src/symfony/process/process.rs
@@ -1,118 +1,1719 @@
//! ref: composer/vendor/symfony/process/Process.php
+use std::sync::OnceLock;
+
use indexmap::IndexMap;
+use shirabe_php_shim::{self as php, PhpMixed};
+
+use crate::symfony::process::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::process::exception::logic_exception::LogicException;
+use crate::symfony::process::exception::process_failed_exception::ProcessFailedException;
+use crate::symfony::process::exception::process_signaled_exception::ProcessSignaledException;
+use crate::symfony::process::exception::process_timed_out_exception::ProcessTimedOutException;
+use crate::symfony::process::exception::runtime_exception::RuntimeException;
+use crate::symfony::process::executable_finder::ExecutableFinder;
+use crate::symfony::process::pipes::pipes_interface::PipesInterface;
+use crate::symfony::process::pipes::unix_pipes::UnixPipes;
+use crate::symfony::process::pipes::windows_pipes::WindowsPipes;
+use crate::symfony::process::process_utils::ProcessUtils;
+
+/// A user-supplied callback invoked with the output type ("out"/"err") and a chunk of output.
+pub type UserCallback = Box<dyn FnMut(&str, &str) -> bool>;
+
+/// The callback built by `build_callback`. It receives the owning Process so it can append output
+/// to the internal buffers, mirroring the `$this`-capturing closure produced in PHP.
+type ProcessCallback = Box<dyn FnMut(&mut Process, &str, &str) -> bool>;
+
+/// PHP `$this->commandline` is `array|string`.
+#[derive(Debug, Clone)]
+enum CommandLine {
+ Array(Vec<String>),
+ String(String),
+}
+
+/// Process is a thin wrapper around proc_* functions to easily
+/// start independent PHP processes.
+pub struct Process {
+ callback: Option<ProcessCallback>,
+ has_callback: bool,
+ commandline: CommandLine,
+ cwd: Option<String>,
+ env: IndexMap<String, PhpMixed>,
+ input: PhpMixed,
+ starttime: Option<f64>,
+ last_output_time: Option<f64>,
+ timeout: Option<f64>,
+ idle_timeout: Option<f64>,
+ exitcode: Option<i64>,
+ fallback_status: IndexMap<String, PhpMixed>,
+ process_information: Option<IndexMap<String, PhpMixed>>,
+ output_disabled: bool,
+ stdout: PhpMixed,
+ stderr: PhpMixed,
+ process: PhpMixed,
+ status: String,
+ incremental_output_offset: i64,
+ incremental_error_output_offset: i64,
+ tty: bool,
+ pty: bool,
+ options: IndexMap<String, PhpMixed>,
+ use_file_handles: bool,
+ process_pipes: Option<Box<dyn PipesInterface>>,
+ latest_signal: Option<i64>,
+ cached_exit_code: Option<i64>,
+}
+
+impl std::fmt::Debug for Process {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Process")
+ .field("commandline", &self.commandline)
+ .field("cwd", &self.cwd)
+ .field("status", &self.status)
+ .field("exitcode", &self.exitcode)
+ .finish_non_exhaustive()
+ }
+}
-#[derive(Debug)]
-pub struct Process;
+fn descriptor(items: &[&str]) -> PhpMixed {
+ PhpMixed::List(
+ items
+ .iter()
+ .map(|s| PhpMixed::String(s.to_string()))
+ .collect(),
+ )
+}
+
+/// Index into the `$pipes` array (a PhpMixed list/array) populated by proc_open.
+fn php_pipe(pipes: &PhpMixed, index: i64) -> PhpMixed {
+ match pipes {
+ PhpMixed::List(list) => list.get(index as usize).cloned().unwrap_or(PhpMixed::Null),
+ PhpMixed::Array(array) => array
+ .get(&index.to_string())
+ .cloned()
+ .unwrap_or(PhpMixed::Null),
+ _ => PhpMixed::Null,
+ }
+}
+
+/// PHP `(string)` cast for an environment value or stream payload.
+fn to_php_string(value: &PhpMixed) -> String {
+ match value {
+ PhpMixed::String(s) => s.clone(),
+ PhpMixed::Int(i) => i.to_string(),
+ PhpMixed::Float(f) => f.to_string(),
+ PhpMixed::Bool(b) => {
+ if *b {
+ "1".to_string()
+ } else {
+ String::new()
+ }
+ }
+ _ => String::new(),
+ }
+}
impl Process {
pub const ERR: &'static str = "err";
pub const OUT: &'static str = "out";
+ pub const STATUS_READY: &'static str = "ready";
+ pub const STATUS_STARTED: &'static str = "started";
+ pub const STATUS_TERMINATED: &'static str = "terminated";
+
+ pub const STDIN: i64 = 0;
+ pub const STDOUT: i64 = 1;
+ pub const STDERR: i64 = 2;
+
+ // Timeout Precision in seconds.
+ pub const TIMEOUT_PRECISION: f64 = 0.2;
+
+ pub const ITER_NON_BLOCKING: i64 = 1;
+ pub const ITER_KEEP_OUTPUT: i64 = 2;
+ pub const ITER_SKIP_OUT: i64 = 4;
+ pub const ITER_SKIP_ERR: i64 = 8;
+
+ /// Exit codes translation table.
+ fn exit_code_text(code: i64) -> Option<&'static str> {
+ Some(match code {
+ 0 => "OK",
+ 1 => "General error",
+ 2 => "Misuse of shell builtins",
+
+ 126 => "Invoked command cannot execute",
+ 127 => "Command not found",
+ 128 => "Invalid exit argument",
+
+ // signals
+ 129 => "Hangup",
+ 130 => "Interrupt",
+ 131 => "Quit and dump core",
+ 132 => "Illegal instruction",
+ 133 => "Trace/breakpoint trap",
+ 134 => "Process aborted",
+ 135 => "Bus error: \"access to undefined portion of memory object\"",
+ 136 => "Floating point exception: \"erroneous arithmetic operation\"",
+ 137 => "Kill (terminate immediately)",
+ 138 => "User-defined 1",
+ 139 => "Segmentation violation",
+ 140 => "User-defined 2",
+ 141 => "Write to pipe with no one reading",
+ 142 => "Signal raised by alarm",
+ 143 => "Termination (request to terminate)",
+ // 144 - not defined
+ 145 => "Child process terminated, stopped (or continued*)",
+ 146 => "Continue if stopped",
+ 147 => "Stop executing temporarily",
+ 148 => "Terminal stop signal",
+ 149 => "Background process attempting to read from tty (\"in\")",
+ 150 => "Background process attempting to write to tty (\"out\")",
+ 151 => "Urgent data available on socket",
+ 152 => "CPU time limit exceeded",
+ 153 => "File size limit exceeded",
+ 154 => "Signal raised by timer counting virtual time: \"virtual timer expired\"",
+ 155 => "Profiling timer expired",
+ // 156 - not defined
+ 157 => "Pollable event",
+ // 158 - not defined
+ 159 => "Bad syscall",
+ _ => return None,
+ })
+ }
+
+ fn empty() -> Self {
+ let mut options = IndexMap::new();
+ options.insert("suppress_errors".to_string(), PhpMixed::Bool(true));
+ options.insert("bypass_shell".to_string(), PhpMixed::Bool(true));
+
+ Self {
+ callback: None,
+ has_callback: false,
+ commandline: CommandLine::Array(Vec::new()),
+ cwd: None,
+ env: IndexMap::new(),
+ input: PhpMixed::Null,
+ starttime: None,
+ last_output_time: None,
+ timeout: None,
+ idle_timeout: None,
+ exitcode: None,
+ fallback_status: IndexMap::new(),
+ process_information: None,
+ output_disabled: false,
+ stdout: PhpMixed::Null,
+ stderr: PhpMixed::Null,
+ process: PhpMixed::Null,
+ status: Self::STATUS_READY.to_string(),
+ incremental_output_offset: 0,
+ incremental_error_output_offset: 0,
+ tty: false,
+ pty: false,
+ options,
+ use_file_handles: false,
+ process_pipes: None,
+ latest_signal: None,
+ cached_exit_code: None,
+ }
+ }
+
pub fn new(
- _command: Vec<String>,
- _cwd: Option<String>,
- _env: Option<IndexMap<String, String>>,
- _input: Option<String>,
- _timeout: Option<f64>,
- ) -> Self {
- todo!()
+ command: Vec<String>,
+ cwd: Option<String>,
+ env: Option<IndexMap<String, String>>,
+ input: PhpMixed,
+ timeout: Option<f64>,
+ ) -> anyhow::Result<Self> {
+ if !php::function_exists("proc_open") {
+ return Err(LogicException::new(
+ "The Process class relies on proc_open, which is not available on your PHP installation.".to_string(),
+ )
+ .into());
+ }
+
+ let mut this = Self::empty();
+ this.commandline = CommandLine::Array(command);
+ this.cwd = cwd;
+
+ // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
+ if this.cwd.is_none() && php::DIRECTORY_SEPARATOR == "\\" {
+ this.cwd = php::getcwd();
+ }
+ if let Some(env) = env {
+ this.set_env(
+ env.into_iter()
+ .map(|(k, v)| (k, PhpMixed::String(v)))
+ .collect(),
+ );
+ }
+
+ this.set_input(input)?;
+ this.set_timeout(timeout)?;
+ this.use_file_handles = php::DIRECTORY_SEPARATOR == "\\";
+ this.pty = false;
+
+ Ok(this)
}
+ /// Creates a Process instance as a command-line to be run in a shell wrapper.
pub fn from_shell_commandline(
- _command: &str,
- _cwd: Option<&str>,
- _env: Option<IndexMap<String, String>>,
- _input: Option<String>,
- _timeout: Option<f64>,
- ) -> Self {
- todo!()
- }
+ command: &str,
+ cwd: Option<&str>,
+ env: Option<IndexMap<String, String>>,
+ input: PhpMixed,
+ timeout: Option<f64>,
+ ) -> anyhow::Result<Self> {
+ let mut process = Self::new(Vec::new(), cwd.map(String::from), env, input, timeout)?;
+ process.commandline = CommandLine::String(command.to_string());
- pub fn set_timeout(&mut self, _timeout: Option<f64>) -> &mut Self {
- todo!()
+ Ok(process)
}
- pub fn set_env(&mut self, _env: IndexMap<String, String>) -> &mut Self {
- todo!()
+ pub fn __sleep(&self) -> anyhow::Result<Vec<String>> {
+ Err(php::BadMethodCallException {
+ message: "Cannot serialize Symfony\\Component\\Process\\Process".to_string(),
+ code: 0,
+ }
+ .into())
}
- pub fn set_input(&mut self, _input: Option<String>) -> &mut Self {
- todo!()
+ pub fn __wakeup(&self) -> anyhow::Result<()> {
+ Err(php::BadMethodCallException {
+ message: "Cannot unserialize Symfony\\Component\\Process\\Process".to_string(),
+ code: 0,
+ }
+ .into())
}
- pub fn run(&mut self, _callback: Option<Box<dyn FnMut(&str, &str)>>) -> i64 {
- todo!()
+ /// Runs the process.
+ pub fn run(
+ &mut self,
+ callback: Option<UserCallback>,
+ env: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<i64> {
+ self.start(callback, env)?;
+
+ self.wait(None)
}
+ /// Runs the process and throws if it exits with a non-zero exit code.
pub fn must_run(
&mut self,
- _callback: Option<Box<dyn FnMut(&str, &str)>>,
+ callback: Option<UserCallback>,
+ env: IndexMap<String, PhpMixed>,
) -> anyhow::Result<&mut Self> {
- todo!()
+ if 0 != self.run(callback, env)? {
+ return Err(ProcessFailedException::new(self)?.into());
+ }
+
+ Ok(self)
}
- pub fn start(&mut self, _callback: Option<Box<dyn FnMut(&str, &str)>>) {
- todo!()
+ /// Starts the process and returns after writing the input to STDIN.
+ pub fn start(
+ &mut self,
+ callback: Option<UserCallback>,
+ mut env: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ if self.is_running() {
+ return Err(RuntimeException::new("Process is already running.".to_string()).into());
+ }
+
+ self.reset_process_data();
+ self.starttime = Some(php::microtime(true));
+ self.last_output_time = self.starttime;
+ let has_callback = callback.is_some();
+ self.callback = Some(self.build_callback(callback));
+ self.has_callback = has_callback;
+ let mut descriptors = self.get_descriptors();
+
+ if !self.env.is_empty() {
+ // non-Windows: $env += $this->env;
+ for (k, v) in &self.env {
+ env.entry(k.clone()).or_insert_with(|| v.clone());
+ }
+ }
+
+ for (k, v) in self.get_default_env() {
+ env.entry(k).or_insert(v);
+ }
+
+ let mut commandline = match &self.commandline {
+ CommandLine::Array(args) => {
+ let mut cmd = args
+ .iter()
+ .map(|a| self.escape_argument(Some(a)))
+ .collect::<Vec<_>>()
+ .join(" ");
+
+ if php::DIRECTORY_SEPARATOR != "\\" {
+ // exec is mandatory to deal with sending a signal to the process
+ cmd = format!("exec {}", cmd);
+ }
+ cmd
+ }
+ CommandLine::String(s) => self.replace_placeholders(s, &env)?,
+ };
+
+ if php::DIRECTORY_SEPARATOR == "\\" {
+ commandline = self.prepare_windows_command_line(&commandline, &mut env)?;
+ } else if !self.use_file_handles && self.is_sigchild_enabled() {
+ // last exit code is output on the fourth pipe and caught to work around --enable-sigchild
+ descriptors.push(descriptor(&["pipe", "w"]));
+
+ commandline = format!("{{ ({}) <&3 3<&- 3>/dev/null & }} 3<&0;", commandline);
+ commandline.push_str(
+ "pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code",
+ );
+
+ // Workaround for the bug, when PTS functionality is enabled.
+ let _pts_workaround = php::fopen("Process.php", "r");
+ }
+
+ let mut env_pairs: Vec<String> = Vec::new();
+ for (k, v) in &env {
+ let is_false = matches!(v, PhpMixed::Bool(false));
+ if !is_false && !["argc", "argv", "ARGC", "ARGV"].contains(&k.as_str()) {
+ env_pairs.push(format!("{}={}", k, to_php_string(v)));
+ }
+ }
+
+ if !self.cwd.as_deref().map(php::is_dir).unwrap_or(false) {
+ return Err(RuntimeException::new(format!(
+ "The provided cwd \"{}\" does not exist.",
+ self.cwd.as_deref().unwrap_or("")
+ ))
+ .into());
+ }
+
+ let cwd = self.cwd.clone();
+ let options = self.options.clone();
+ let process = {
+ let pipes = self.process_pipes.as_mut().unwrap().pipes_mut();
+ php::proc_open(
+ &commandline,
+ &descriptors,
+ pipes,
+ cwd.as_deref(),
+ Some(&env_pairs),
+ Some(&options),
+ )
+ };
+ self.process = process;
+
+ if !php::php_truthy(&self.process) {
+ return Err(
+ RuntimeException::new("Unable to launch a new process.".to_string()).into(),
+ );
+ }
+ self.status = Self::STATUS_STARTED.to_string();
+
+ if descriptors.len() > 3 {
+ let pipe3 = php_pipe(self.process_pipes.as_ref().unwrap().pipes(), 3);
+ let pid = php::fgets(pipe3)
+ .and_then(|s| s.trim().parse::<i64>().ok())
+ .unwrap_or(0);
+ self.fallback_status
+ .insert("pid".to_string(), PhpMixed::Int(pid));
+ }
+
+ if self.tty {
+ return Ok(());
+ }
+
+ self.update_status(false);
+ self.check_timeout()?;
+ Ok(())
}
- pub fn wait(&mut self, _callback: Option<Box<dyn FnMut(&str, &str)>>) -> i64 {
- todo!()
+ /// Restarts the process. The process is cloned before being started.
+ pub fn restart(
+ &mut self,
+ callback: Option<UserCallback>,
+ env: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<Process> {
+ if self.is_running() {
+ return Err(RuntimeException::new("Process is already running.".to_string()).into());
+ }
+
+ let mut process = self.clone_process();
+ process.start(callback, env)?;
+
+ Ok(process)
}
- pub fn stop(&mut self, _timeout: f64, _signal: Option<i64>) -> Option<i64> {
- todo!()
+ /// Waits for the process to terminate.
+ pub fn wait(&mut self, callback: Option<UserCallback>) -> anyhow::Result<i64> {
+ self.require_process_is_started("wait")?;
+
+ self.update_status(false);
+
+ if let Some(callback) = callback {
+ if !self.process_pipes.as_ref().unwrap().have_read_support() {
+ self.stop(0.0, None);
+ return Err(LogicException::new(
+ "Pass the callback to the \"Process::start\" method or call enableOutput to use a callback with \"Process::wait\".".to_string(),
+ )
+ .into());
+ }
+ self.callback = Some(self.build_callback(Some(callback)));
+ }
+
+ loop {
+ self.check_timeout()?;
+ let running = self.is_running()
+ && (php::DIRECTORY_SEPARATOR == "\\"
+ || self.process_pipes.as_ref().unwrap().are_open());
+ self.read_pipes(running, php::DIRECTORY_SEPARATOR != "\\" || !running);
+ if !running {
+ break;
+ }
+ }
+
+ while self.is_running() {
+ self.check_timeout()?;
+ php::usleep(1000);
+ }
+
+ let signaled = self
+ .process_information
+ .as_ref()
+ .and_then(|i| i.get("signaled"))
+ .map(php::php_truthy)
+ .unwrap_or(false);
+ let termsig = self
+ .process_information
+ .as_ref()
+ .and_then(|i| i.get("termsig"))
+ .and_then(|v| v.as_int());
+ if signaled && termsig != self.latest_signal {
+ return Err(ProcessSignaledException::new(self)?.into());
+ }
+
+ Ok(self.exitcode.unwrap_or(0))
}
- pub fn is_running(&self) -> bool {
- todo!()
+ /// Waits until the callback returns true.
+ pub fn wait_until(&mut self, callback: UserCallback) -> anyhow::Result<bool> {
+ self.require_process_is_started("waitUntil")?;
+ self.update_status(false);
+
+ if !self.process_pipes.as_ref().unwrap().have_read_support() {
+ self.stop(0.0, None);
+ return Err(LogicException::new(
+ "Pass the callback to the \"Process::start\" method or call enableOutput to use a callback with \"Process::waitUntil\".".to_string(),
+ )
+ .into());
+ }
+ let mut callback = self.build_callback(Some(callback));
+
+ let mut ready = false;
+ loop {
+ self.check_timeout()?;
+ let running = if php::DIRECTORY_SEPARATOR == "\\" {
+ self.is_running()
+ } else {
+ self.process_pipes.as_ref().unwrap().are_open()
+ };
+ let output = self
+ .process_pipes
+ .as_mut()
+ .unwrap()
+ .read_and_write(running, php::DIRECTORY_SEPARATOR != "\\" || !running);
+
+ for (r#type, data) in output {
+ if r#type != 3 {
+ let r = callback(
+ self,
+ if Self::STDOUT == r#type {
+ Self::OUT
+ } else {
+ Self::ERR
+ },
+ &data,
+ );
+ ready = r || ready;
+ } else if !self.fallback_status.contains_key("signaled") {
+ self.fallback_status.insert(
+ "exitcode".to_string(),
+ PhpMixed::Int(data.trim().parse().unwrap_or(0)),
+ );
+ }
+ }
+ if ready {
+ return Ok(true);
+ }
+ if !running {
+ return Ok(false);
+ }
+
+ php::usleep(1000);
+ }
}
- pub fn is_successful(&self) -> bool {
- todo!()
+ /// Returns the Pid (process identifier), if applicable.
+ pub fn get_pid(&mut self) -> Option<i64> {
+ if self.is_running() {
+ self.process_information
+ .as_ref()
+ .and_then(|i| i.get("pid"))
+ .and_then(|v| v.as_int())
+ } else {
+ None
+ }
}
+ /// Sends a POSIX signal to the process.
+ pub fn signal(&mut self, signal: i64) -> anyhow::Result<&mut Self> {
+ self.do_signal(signal, true)?;
+
+ Ok(self)
+ }
+
+ /// Disables fetching output and error output from the underlying process.
+ pub fn disable_output(&mut self) -> anyhow::Result<&mut Self> {
+ if self.is_running() {
+ return Err(RuntimeException::new(
+ "Disabling output while the process is running is not possible.".to_string(),
+ )
+ .into());
+ }
+ if self.idle_timeout.is_some() {
+ return Err(LogicException::new(
+ "Output cannot be disabled while an idle timeout is set.".to_string(),
+ )
+ .into());
+ }
+
+ self.output_disabled = true;
+
+ Ok(self)
+ }
+
+ /// Enables fetching output and error output from the underlying process.
+ pub fn enable_output(&mut self) -> anyhow::Result<&mut Self> {
+ if self.is_running() {
+ return Err(RuntimeException::new(
+ "Enabling output while the process is running is not possible.".to_string(),
+ )
+ .into());
+ }
+
+ self.output_disabled = false;
+
+ Ok(self)
+ }
+
+ pub fn is_output_disabled(&self) -> bool {
+ self.output_disabled
+ }
+
+ /// Returns the current output of the process (STDOUT).
+ pub fn get_output(&mut self) -> anyhow::Result<String> {
+ self.read_pipes_for_output("getOutput", false)?;
+
+ Ok(php::stream_get_contents3(self.stdout.clone(), -1, 0).unwrap_or_default())
+ }
+
+ /// Returns the output incrementally.
+ pub fn get_incremental_output(&mut self) -> anyhow::Result<String> {
+ self.read_pipes_for_output("getIncrementalOutput", false)?;
+
+ let latest =
+ php::stream_get_contents3(self.stdout.clone(), -1, self.incremental_output_offset);
+ self.incremental_output_offset = php::ftell_stream(&self.stdout);
+
+ Ok(latest.unwrap_or_default())
+ }
+
+ /// Returns an iterator to the output of the process, with the output type as keys.
+ ///
+ /// PHP returns a `\Generator`; lacking generators, this collects the yielded chunks eagerly.
+ pub fn get_iterator(&mut self, flags: i64) -> anyhow::Result<Vec<(String, String)>> {
+ self.read_pipes_for_output("getIterator", false)?;
+
+ let clear_output = (Self::ITER_KEEP_OUTPUT & flags) == 0;
+ let blocking = (Self::ITER_NON_BLOCKING & flags) == 0;
+ let yield_out = (Self::ITER_SKIP_OUT & flags) == 0;
+ let yield_err = (Self::ITER_SKIP_ERR & flags) == 0;
+
+ let mut yields = Vec::new();
+ while self.callback.is_some()
+ || (yield_out && !php::feof(self.stdout.clone()))
+ || (yield_err && !php::feof(self.stderr.clone()))
+ {
+ let mut got_out = false;
+ let mut got_err = false;
+
+ if yield_out {
+ let out = php::stream_get_contents3(
+ self.stdout.clone(),
+ -1,
+ self.incremental_output_offset,
+ )
+ .unwrap_or_default();
+
+ if !out.is_empty() {
+ got_out = true;
+ if clear_output {
+ self.clear_output();
+ } else {
+ self.incremental_output_offset = php::ftell_stream(&self.stdout);
+ }
+
+ yields.push((Self::OUT.to_string(), out));
+ }
+ }
+
+ if yield_err {
+ let err = php::stream_get_contents3(
+ self.stderr.clone(),
+ -1,
+ self.incremental_error_output_offset,
+ )
+ .unwrap_or_default();
+
+ if !err.is_empty() {
+ got_err = true;
+ if clear_output {
+ self.clear_error_output();
+ } else {
+ self.incremental_error_output_offset = php::ftell_stream(&self.stderr);
+ }
+
+ yields.push((Self::ERR.to_string(), err));
+ }
+ }
+
+ if !blocking && !got_out && !got_err {
+ yields.push((Self::OUT.to_string(), String::new()));
+ }
+
+ self.check_timeout()?;
+ self.read_pipes_for_output("getIterator", blocking)?;
+ }
+
+ Ok(yields)
+ }
+
+ /// Clears the process output.
+ pub fn clear_output(&mut self) -> &mut Self {
+ php::ftruncate(&self.stdout, 0);
+ php::fseek(self.stdout.clone(), 0);
+ self.incremental_output_offset = 0;
+
+ self
+ }
+
+ /// Returns the current error output of the process (STDERR).
+ pub fn get_error_output(&mut self) -> anyhow::Result<String> {
+ self.read_pipes_for_output("getErrorOutput", false)?;
+
+ Ok(php::stream_get_contents3(self.stderr.clone(), -1, 0).unwrap_or_default())
+ }
+
+ /// Returns the errorOutput incrementally.
+ pub fn get_incremental_error_output(&mut self) -> anyhow::Result<String> {
+ self.read_pipes_for_output("getIncrementalErrorOutput", false)?;
+
+ let latest = php::stream_get_contents3(
+ self.stderr.clone(),
+ -1,
+ self.incremental_error_output_offset,
+ );
+ self.incremental_error_output_offset = php::ftell_stream(&self.stderr);
+
+ Ok(latest.unwrap_or_default())
+ }
+
+ /// Clears the process error output.
+ pub fn clear_error_output(&mut self) -> &mut Self {
+ php::ftruncate(&self.stderr, 0);
+ php::fseek(self.stderr.clone(), 0);
+ self.incremental_error_output_offset = 0;
+
+ self
+ }
+
+ /// Returns the exit code returned by the process.
+ pub fn get_exit_code(&mut self) -> Option<i64> {
+ self.update_status(false);
+
+ self.exitcode
+ }
+
+ /// Returns a string representation for the exit code returned by the process.
+ pub fn get_exit_code_text(&mut self) -> Option<String> {
+ let exitcode = self.get_exit_code()?;
+
+ Some(
+ Self::exit_code_text(exitcode)
+ .unwrap_or("Unknown error")
+ .to_string(),
+ )
+ }
+
+ /// Checks if the process ended successfully.
+ pub fn is_successful(&mut self) -> bool {
+ self.get_exit_code() == Some(0)
+ }
+
+ /// Returns true if the child process has been terminated by an uncaught signal.
+ pub fn has_been_signaled(&mut self) -> anyhow::Result<bool> {
+ self.require_process_is_terminated("hasBeenSignaled")?;
+
+ Ok(self
+ .process_information
+ .as_ref()
+ .and_then(|i| i.get("signaled"))
+ .map(php::php_truthy)
+ .unwrap_or(false))
+ }
+
+ /// Returns the number of the signal that caused the child process to terminate.
+ pub fn get_term_signal(&mut self) -> anyhow::Result<i64> {
+ self.require_process_is_terminated("getTermSignal")?;
+
+ let termsig = self
+ .process_information
+ .as_ref()
+ .and_then(|i| i.get("termsig"))
+ .and_then(|v| v.as_int());
+ if self.is_sigchild_enabled() && termsig == Some(-1) {
+ return Err(RuntimeException::new(
+ "This PHP has been compiled with --enable-sigchild. Term signal cannot be retrieved.".to_string(),
+ )
+ .into());
+ }
+
+ Ok(termsig.unwrap_or(0))
+ }
+
+ /// Returns true if the child process has been stopped by a signal.
+ pub fn has_been_stopped(&mut self) -> anyhow::Result<bool> {
+ self.require_process_is_terminated("hasBeenStopped")?;
+
+ Ok(self
+ .process_information
+ .as_ref()
+ .and_then(|i| i.get("stopped"))
+ .map(php::php_truthy)
+ .unwrap_or(false))
+ }
+
+ /// Returns the number of the signal that caused the child process to stop.
+ pub fn get_stop_signal(&mut self) -> anyhow::Result<i64> {
+ self.require_process_is_terminated("getStopSignal")?;
+
+ Ok(self
+ .process_information
+ .as_ref()
+ .and_then(|i| i.get("stopsig"))
+ .and_then(|v| v.as_int())
+ .unwrap_or(0))
+ }
+
+ /// Checks if the process is currently running.
+ pub fn is_running(&mut self) -> bool {
+ if Self::STATUS_STARTED != self.status {
+ return false;
+ }
+
+ self.update_status(false);
+
+ self.process_information
+ .as_ref()
+ .and_then(|i| i.get("running"))
+ .map(php::php_truthy)
+ .unwrap_or(false)
+ }
+
+ /// Checks if the process has been started with no regard to the current state.
pub fn is_started(&self) -> bool {
- todo!()
+ Self::STATUS_READY != self.status
}
- pub fn is_terminated(&self) -> bool {
- todo!()
+ /// Checks if the process is terminated.
+ pub fn is_terminated(&mut self) -> bool {
+ self.update_status(false);
+
+ Self::STATUS_TERMINATED == self.status
}
- pub fn get_output(&self) -> String {
- todo!()
+ /// Gets the process status (one of: ready, started, terminated).
+ pub fn get_status(&mut self) -> String {
+ self.update_status(false);
+
+ self.status.clone()
}
- pub fn get_error_output(&self) -> String {
- todo!()
+ /// Stops the process.
+ pub fn stop(&mut self, timeout: f64, signal: Option<i64>) -> Option<i64> {
+ let timeout_micro = php::microtime(true) + timeout;
+ if self.is_running() {
+ // given SIGTERM may not be defined and that "proc_terminate" uses the constant value
+ // and not the constant itself, we use the same here
+ let _ = self.do_signal(15, false);
+ loop {
+ php::usleep(1000);
+ if !(self.is_running() && php::microtime(true) < timeout_micro) {
+ break;
+ }
+ }
+
+ if self.is_running() {
+ // Avoid exception here: process is supposed to be running, but it might have
+ // stopped just after this line. Silently discard the error.
+ let _ = self.do_signal(signal.filter(|&s| s != 0).unwrap_or(9), false);
+ }
+ }
+
+ if self.is_running() {
+ if self.fallback_status.contains_key("pid") {
+ self.fallback_status.shift_remove("pid");
+
+ return self.stop(0.0, signal);
+ }
+ self.close();
+ }
+
+ self.exitcode
}
- pub fn get_exit_code(&self) -> Option<i64> {
- todo!()
+ /// Adds a line to the STDOUT stream.
+ pub fn add_output(&mut self, line: &str) {
+ self.last_output_time = Some(php::microtime(true));
+
+ php::fseek3(self.stdout.clone(), 0, php::SEEK_END);
+ php::fwrite(self.stdout.clone(), line, line.len() as i64);
+ php::fseek(self.stdout.clone(), self.incremental_output_offset);
}
- pub fn get_exit_code_text(&self) -> Option<String> {
- todo!()
+ /// Adds a line to the STDERR stream.
+ pub fn add_error_output(&mut self, line: &str) {
+ self.last_output_time = Some(php::microtime(true));
+
+ php::fseek3(self.stderr.clone(), 0, php::SEEK_END);
+ php::fwrite(self.stderr.clone(), line, line.len() as i64);
+ php::fseek(self.stderr.clone(), self.incremental_error_output_offset);
}
- pub fn get_command_line(&self) -> String {
- todo!()
+ /// Gets the last output time in seconds.
+ pub fn get_last_output_time(&self) -> Option<f64> {
+ self.last_output_time
}
- pub fn check_timeout(&self) -> anyhow::Result<()> {
- todo!()
+ /// Gets the command line to be executed.
+ pub fn get_command_line(&self) -> String {
+ match &self.commandline {
+ CommandLine::Array(args) => args
+ .iter()
+ .map(|a| self.escape_argument(Some(a)))
+ .collect::<Vec<_>>()
+ .join(" "),
+ CommandLine::String(s) => s.clone(),
+ }
}
+ /// Gets the process timeout in seconds (max. runtime).
pub fn get_timeout(&self) -> Option<f64> {
- todo!()
+ self.timeout
+ }
+
+ /// Gets the process idle timeout in seconds (max. time since last output).
+ pub fn get_idle_timeout(&self) -> Option<f64> {
+ self.idle_timeout
+ }
+
+ /// Sets the process timeout (max. runtime) in seconds.
+ pub fn set_timeout(&mut self, timeout: Option<f64>) -> anyhow::Result<&mut Self> {
+ self.timeout = self.validate_timeout(timeout)?;
+
+ Ok(self)
+ }
+
+ /// Sets the process idle timeout (max. time since last output) in seconds.
+ pub fn set_idle_timeout(&mut self, timeout: Option<f64>) -> anyhow::Result<&mut Self> {
+ if timeout.is_some() && self.output_disabled {
+ return Err(LogicException::new(
+ "Idle timeout cannot be set while the output is disabled.".to_string(),
+ )
+ .into());
+ }
+
+ self.idle_timeout = self.validate_timeout(timeout)?;
+
+ Ok(self)
+ }
+
+ /// Enables or disables the TTY mode.
+ pub fn set_tty(&mut self, tty: bool) -> anyhow::Result<&mut Self> {
+ if php::DIRECTORY_SEPARATOR == "\\" && tty {
+ return Err(RuntimeException::new(
+ "TTY mode is not supported on Windows platform.".to_string(),
+ )
+ .into());
+ }
+
+ if tty && !Self::is_tty_supported() {
+ return Err(RuntimeException::new(
+ "TTY mode requires /dev/tty to be read/writable.".to_string(),
+ )
+ .into());
+ }
+
+ self.tty = tty;
+
+ Ok(self)
+ }
+
+ /// Checks if the TTY mode is enabled.
+ pub fn is_tty(&self) -> bool {
+ self.tty
+ }
+
+ /// Sets PTY mode.
+ pub fn set_pty(&mut self, bool: bool) -> &mut Self {
+ self.pty = bool;
+
+ self
+ }
+
+ /// Returns PTY state.
+ pub fn is_pty(&self) -> bool {
+ self.pty
+ }
+
+ /// Gets the working directory.
+ pub fn get_working_directory(&self) -> Option<String> {
+ if self.cwd.is_none() {
+ // getcwd() will return false if any one of the parent directories does not have
+ // the readable or search mode set, even if the current directory does
+ return php::getcwd().filter(|s| !s.is_empty());
+ }
+
+ self.cwd.clone()
+ }
+
+ /// Sets the current working directory.
+ pub fn set_working_directory(&mut self, cwd: &str) -> &mut Self {
+ self.cwd = Some(cwd.to_string());
+
+ self
+ }
+
+ /// Gets the environment variables.
+ pub fn get_env(&self) -> &IndexMap<String, PhpMixed> {
+ &self.env
+ }
+
+ /// Sets the environment variables.
+ pub fn set_env(&mut self, env: IndexMap<String, PhpMixed>) -> &mut Self {
+ self.env = env;
+
+ self
+ }
+
+ /// Gets the Process input.
+ pub fn get_input(&self) -> &PhpMixed {
+ &self.input
+ }
+
+ /// Sets the input.
+ pub fn set_input(&mut self, input: PhpMixed) -> anyhow::Result<&mut Self> {
+ if self.is_running() {
+ return Err(LogicException::new(
+ "Input cannot be set while the process is running.".to_string(),
+ )
+ .into());
+ }
+
+ self.input =
+ ProcessUtils::validate_input("Symfony\\Component\\Process\\Process::setInput", input)?;
+
+ Ok(self)
+ }
+
+ /// Performs a check between the timeout definition and the time the process started.
+ pub fn check_timeout(&mut self) -> anyhow::Result<()> {
+ if Self::STATUS_STARTED != self.status {
+ return Ok(());
+ }
+
+ if let Some(timeout) = self.timeout {
+ if timeout < php::microtime(true) - self.starttime.unwrap_or(0.0) {
+ self.stop(0.0, None);
+
+ return Err(ProcessTimedOutException::new(
+ self,
+ ProcessTimedOutException::TYPE_GENERAL,
+ )
+ .into());
+ }
+ }
+
+ if let Some(idle_timeout) = self.idle_timeout {
+ if idle_timeout < php::microtime(true) - self.last_output_time.unwrap_or(0.0) {
+ self.stop(0.0, None);
+
+ return Err(ProcessTimedOutException::new(
+ self,
+ ProcessTimedOutException::TYPE_IDLE,
+ )
+ .into());
+ }
+ }
+
+ Ok(())
+ }
+
+ pub fn get_start_time(&self) -> anyhow::Result<f64> {
+ if !self.is_started() {
+ return Err(LogicException::new(
+ "Start time is only available after process start.".to_string(),
+ )
+ .into());
+ }
+
+ Ok(self.starttime.unwrap())
+ }
+
+ /// Defines options to pass to the underlying proc_open().
+ pub fn set_options(&mut self, options: IndexMap<String, PhpMixed>) -> anyhow::Result<()> {
+ if self.is_running() {
+ return Err(RuntimeException::new(
+ "Setting options while the process is running is not possible.".to_string(),
+ )
+ .into());
+ }
+
+ let default_options = self.options.clone();
+ let existing_options = [
+ "blocking_pipes",
+ "create_process_group",
+ "create_new_console",
+ ];
+
+ for (key, value) in options {
+ if !existing_options.contains(&key.as_str()) {
+ self.options = default_options;
+ return Err(LogicException::new(format!(
+ "Invalid option \"{}\" passed to \"Symfony\\Component\\Process\\Process::setOptions()\". Supported options are \"{}\".",
+ key,
+ existing_options.join("\", \"")
+ ))
+ .into());
+ }
+ self.options.insert(key, value);
+ }
+
+ Ok(())
+ }
+
+ /// Returns whether TTY is supported on the current operating system.
+ pub fn is_tty_supported() -> bool {
+ static IS_TTY_SUPPORTED: OnceLock<bool> = OnceLock::new();
+
+ *IS_TTY_SUPPORTED.get_or_init(|| {
+ let mut pipes = PhpMixed::Null;
+ php::php_truthy(&php::proc_open(
+ "echo 1 >/dev/null",
+ &[
+ descriptor(&["file", "/dev/tty", "r"]),
+ descriptor(&["file", "/dev/tty", "w"]),
+ descriptor(&["file", "/dev/tty", "w"]),
+ ],
+ &mut pipes,
+ None,
+ None,
+ None,
+ ))
+ })
+ }
+
+ /// Returns whether PTY is supported on the current operating system.
+ pub fn is_pty_supported() -> bool {
+ static RESULT: OnceLock<bool> = OnceLock::new();
+
+ *RESULT.get_or_init(|| {
+ if php::DIRECTORY_SEPARATOR == "\\" {
+ return false;
+ }
+
+ let mut pipes = PhpMixed::Null;
+ php::php_truthy(&php::proc_open(
+ "echo 1 >/dev/null",
+ &[
+ descriptor(&["pty"]),
+ descriptor(&["pty"]),
+ descriptor(&["pty"]),
+ ],
+ &mut pipes,
+ None,
+ None,
+ None,
+ ))
+ })
+ }
+
+ /// Creates the descriptors needed by the proc_open.
+ fn get_descriptors(&mut self) -> Vec<PhpMixed> {
+ // TODO(plugin): $this->input instanceof \Iterator -> rewind() is not modeled.
+ if php::DIRECTORY_SEPARATOR == "\\" {
+ self.process_pipes = Some(Box::new(WindowsPipes::new(
+ self.input.clone(),
+ !self.output_disabled || self.has_callback,
+ )));
+ } else {
+ self.process_pipes = Some(Box::new(UnixPipes::new(
+ Some(self.is_tty()),
+ self.is_pty(),
+ self.input.clone(),
+ !self.output_disabled || self.has_callback,
+ )));
+ }
+
+ self.process_pipes.as_mut().unwrap().get_descriptors()
}
- pub fn set_working_directory(&mut self, _cwd: &str) -> &mut Self {
- todo!()
+ /// Builds up the callback used by wait().
+ fn build_callback(&self, callback: Option<UserCallback>) -> ProcessCallback {
+ let mut callback = callback;
+ if self.output_disabled {
+ return Box::new(
+ move |_this: &mut Process, r#type: &str, data: &str| -> bool {
+ match callback.as_mut() {
+ Some(cb) => cb(r#type, data),
+ None => false,
+ }
+ },
+ );
+ }
+
+ let out = Self::OUT;
+
+ Box::new(
+ move |this: &mut Process, r#type: &str, data: &str| -> bool {
+ if out == r#type {
+ this.add_output(data);
+ } else {
+ this.add_error_output(data);
+ }
+
+ match callback.as_mut() {
+ Some(cb) => cb(r#type, data),
+ None => false,
+ }
+ },
+ )
+ }
+
+ /// Updates the status of the process, reads pipes.
+ fn update_status(&mut self, blocking: bool) {
+ if Self::STATUS_STARTED != self.status {
+ return;
+ }
+
+ self.process_information = Some(php::proc_get_status(&self.process));
+ let running = self
+ .process_information
+ .as_ref()
+ .unwrap()
+ .get("running")
+ .map(php::php_truthy)
+ .unwrap_or(false);
+
+ // In PHP < 8.3, "proc_get_status" only returns the correct exit status on the first call.
+ if php::PHP_VERSION_ID < 80300 {
+ let exitcode = self
+ .process_information
+ .as_ref()
+ .unwrap()
+ .get("exitcode")
+ .and_then(|v| v.as_int());
+ if self.cached_exit_code.is_none() && !running && exitcode != Some(-1) {
+ self.cached_exit_code = exitcode;
+ }
+
+ if let Some(cached) = self.cached_exit_code {
+ if !running && exitcode == Some(-1) {
+ self.process_information
+ .as_mut()
+ .unwrap()
+ .insert("exitcode".to_string(), PhpMixed::Int(cached));
+ }
+ }
+ }
+
+ self.read_pipes(
+ running && blocking,
+ php::DIRECTORY_SEPARATOR != "\\" || !running,
+ );
+
+ if !self.fallback_status.is_empty() && self.is_sigchild_enabled() {
+ // processInformation = fallbackStatus + processInformation (fallback keys win)
+ let mut merged = self.fallback_status.clone();
+ for (k, v) in self.process_information.take().unwrap() {
+ merged.entry(k).or_insert(v);
+ }
+ self.process_information = Some(merged);
+ }
+
+ if !running {
+ self.close();
+ }
}
- pub fn set_tty(&mut self, _tty: bool) -> anyhow::Result<&mut Self> {
- todo!()
+ /// Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
+ fn is_sigchild_enabled(&self) -> bool {
+ static SIGCHILD: OnceLock<bool> = OnceLock::new();
+
+ if let Some(v) = SIGCHILD.get() {
+ return *v;
+ }
+
+ if !php::function_exists("phpinfo") {
+ return *SIGCHILD.get_or_init(|| false);
+ }
+
+ php::ob_start();
+ php::phpinfo(php::INFO_GENERAL);
+
+ *SIGCHILD.get_or_init(|| {
+ php::str_contains(
+ &php::ob_get_clean().unwrap_or_default(),
+ "--enable-sigchild",
+ )
+ })
+ }
+
+ /// Reads pipes for the freshest output.
+ fn read_pipes_for_output(&mut self, caller: &str, blocking: bool) -> anyhow::Result<()> {
+ if self.output_disabled {
+ return Err(LogicException::new("Output has been disabled.".to_string()).into());
+ }
+
+ self.require_process_is_started(caller)?;
+
+ self.update_status(blocking);
+ Ok(())
+ }
+
+ /// Validates and returns the filtered timeout.
+ fn validate_timeout(&self, timeout: Option<f64>) -> anyhow::Result<Option<f64>> {
+ let timeout = timeout.unwrap_or(0.0);
+
+ if timeout == 0.0 {
+ Ok(None)
+ } else if timeout < 0.0 {
+ Err(InvalidArgumentException::new(
+ "The timeout value must be a valid positive integer or float number.".to_string(),
+ )
+ .into())
+ } else {
+ Ok(Some(timeout))
+ }
+ }
+
+ /// Reads pipes, executes callback.
+ fn read_pipes(&mut self, blocking: bool, close: bool) {
+ let result = self
+ .process_pipes
+ .as_mut()
+ .unwrap()
+ .read_and_write(blocking, close);
+
+ let mut callback = self.callback.take();
+ for (r#type, data) in result {
+ if r#type != 3 {
+ if let Some(cb) = callback.as_mut() {
+ cb(
+ self,
+ if Self::STDOUT == r#type {
+ Self::OUT
+ } else {
+ Self::ERR
+ },
+ &data,
+ );
+ }
+ } else if !self.fallback_status.contains_key("signaled") {
+ self.fallback_status.insert(
+ "exitcode".to_string(),
+ PhpMixed::Int(data.trim().parse().unwrap_or(0)),
+ );
+ }
+ }
+ self.callback = callback;
+ }
+
+ /// Closes process resource, closes file handles, sets the exitcode.
+ fn close(&mut self) -> i64 {
+ if let Some(p) = self.process_pipes.as_mut() {
+ p.close();
+ }
+ if php::php_truthy(&self.process) {
+ php::proc_close(self.process.clone());
+ self.process = PhpMixed::Null;
+ }
+ self.exitcode = self
+ .process_information
+ .as_ref()
+ .and_then(|i| i.get("exitcode"))
+ .and_then(|v| v.as_int());
+ self.status = Self::STATUS_TERMINATED.to_string();
+
+ if self.exitcode == Some(-1) {
+ let signaled = self
+ .process_information
+ .as_ref()
+ .and_then(|i| i.get("signaled"))
+ .map(php::php_truthy)
+ .unwrap_or(false);
+ let termsig = self
+ .process_information
+ .as_ref()
+ .and_then(|i| i.get("termsig"))
+ .and_then(|v| v.as_int())
+ .unwrap_or(0);
+ if signaled && termsig > 0 {
+ // if process has been signaled, no exitcode but a valid termsig, apply Unix convention
+ self.exitcode = Some(128 + termsig);
+ } else if self.is_sigchild_enabled() {
+ if let Some(i) = self.process_information.as_mut() {
+ i.insert("signaled".to_string(), PhpMixed::Bool(true));
+ i.insert("termsig".to_string(), PhpMixed::Int(-1));
+ }
+ }
+ }
+
+ // Free memory from self-reference callback created by buildCallback
+ self.callback = None;
+
+ self.exitcode.unwrap_or(-1)
+ }
+
+ /// Resets data related to the latest run of the process.
+ fn reset_process_data(&mut self) {
+ self.starttime = None;
+ self.callback = None;
+ self.exitcode = None;
+ self.fallback_status = IndexMap::new();
+ self.process_information = None;
+ self.stdout = php::fopen(&format!("php://temp/maxmemory:{}", 1024 * 1024), "w+");
+ self.stderr = php::fopen(&format!("php://temp/maxmemory:{}", 1024 * 1024), "w+");
+ self.process = PhpMixed::Null;
+ self.latest_signal = None;
+ self.status = Self::STATUS_READY.to_string();
+ self.incremental_output_offset = 0;
+ self.incremental_error_output_offset = 0;
+ }
+
+ /// Sends a POSIX signal to the process.
+ fn do_signal(&mut self, signal: i64, throw_exception: bool) -> anyhow::Result<bool> {
+ let pid = match self.get_pid() {
+ None => {
+ if throw_exception {
+ return Err(LogicException::new(
+ "Cannot send signal on a non running process.".to_string(),
+ )
+ .into());
+ }
+
+ return Ok(false);
+ }
+ Some(pid) => pid,
+ };
+
+ if php::DIRECTORY_SEPARATOR == "\\" {
+ let mut output: Vec<String> = Vec::new();
+ let mut exit_code: i64 = 0;
+ php::exec(
+ &format!("taskkill /F /T /PID {} 2>&1", pid),
+ Some(&mut output),
+ Some(&mut exit_code),
+ );
+ if exit_code != 0 && self.is_running() {
+ if throw_exception {
+ return Err(RuntimeException::new(format!(
+ "Unable to kill the process ({}).",
+ output.join(" ")
+ ))
+ .into());
+ }
+
+ return Ok(false);
+ }
+ } else {
+ let ok;
+ if !self.is_sigchild_enabled() {
+ ok = php::proc_terminate(&self.process, signal);
+ } else if php::function_exists("posix_kill") {
+ ok = php::posix_kill(pid, signal);
+ } else {
+ let mut pipes = PhpMixed::Null;
+ let opened = php::proc_open(
+ &format!("kill -{} {}", signal, pid),
+ &[PhpMixed::Null, PhpMixed::Null, descriptor(&["pipe", "w"])],
+ &mut pipes,
+ None,
+ None,
+ None,
+ );
+ ok = php::php_truthy(&opened) && php::fgets(php_pipe(&pipes, 2)).is_none();
+ }
+ if !ok {
+ if throw_exception {
+ return Err(RuntimeException::new(format!(
+ "Error while sending signal \"{}\".",
+ signal
+ ))
+ .into());
+ }
+
+ return Ok(false);
+ }
+ }
+
+ self.latest_signal = Some(signal);
+ self.fallback_status
+ .insert("signaled".to_string(), PhpMixed::Bool(true));
+ self.fallback_status
+ .insert("exitcode".to_string(), PhpMixed::Int(-1));
+ self.fallback_status.insert(
+ "termsig".to_string(),
+ PhpMixed::Int(self.latest_signal.unwrap()),
+ );
+
+ Ok(true)
+ }
+
+ fn prepare_windows_command_line(
+ &mut self,
+ cmd: &str,
+ env: &mut IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<String> {
+ let uid = php::uniqid("", true);
+ let mut var_count = 0;
+ let mut var_cache: IndexMap<String, String> = IndexMap::new();
+ let cmd = php::preg_replace_callback(
+ r#"/"(?:(
+ [^"%!^]*+
+ (?:
+ (?: !LF! | "(?:\^[%!^])?+" )
+ [^"%!^]*+
+ )++
+ ) | [^"]*+ )"/x"#,
+ |m: &[Option<String>]| -> anyhow::Result<String> {
+ let m0 = m.first().cloned().flatten().unwrap_or_default();
+ let m1 = m.get(1).cloned().flatten();
+ if m1.is_none() {
+ return Ok(m0);
+ }
+ if let Some(cached) = var_cache.get(&m0) {
+ return Ok(cached.clone());
+ }
+ let mut value = m1.unwrap();
+ if value.contains('\0') {
+ value = value.replace('\0', "?");
+ }
+ if php::strpbrk(&value, "\"%!\n").is_none() {
+ return Ok(format!("\"{}\"", value));
+ }
+
+ for (from, to) in [
+ ("!LF!", "\n"),
+ ("\"^!\"", "!"),
+ ("\"^%\"", "%"),
+ ("\"^^\"", "^"),
+ ("\"\"", "\""),
+ ] {
+ value = value.replace(from, to);
+ }
+ value = format!(
+ "\"{}\"",
+ php::preg_replace(r#"/(\\*)"/"#, "$1$1\\\"", &value)
+ );
+ var_count += 1;
+ let var = format!("{}{}", uid, var_count);
+
+ env.insert(var.clone(), PhpMixed::String(value));
+
+ let replacement = format!("!{}!", var);
+ var_cache.insert(m0, replacement.clone());
+ Ok(replacement)
+ },
+ cmd,
+ )?;
+
+ static COM_SPEC: OnceLock<Option<String>> = OnceLock::new();
+ let com_spec = COM_SPEC
+ .get_or_init(|| {
+ ExecutableFinder::new()
+ .find("cmd.exe", None, &[])
+ .map(|spec| {
+ format!(
+ "\"{}\"",
+ php::preg_replace(r#"{(\\*+)"}"#, "$1$1\\\"", &spec)
+ )
+ })
+ })
+ .clone();
+
+ let mut cmd = format!(
+ "{} /V:ON /E:ON /D /C ({})",
+ com_spec.unwrap_or_else(|| "cmd".to_string()),
+ cmd.replace('\n', " ")
+ );
+ for (offset, filename) in self.process_pipes.as_ref().unwrap().get_files() {
+ cmd.push_str(&format!(" {}>\"{}\"", offset, filename));
+ }
+
+ Ok(cmd)
+ }
+
+ /// Ensures the process is running or terminated.
+ fn require_process_is_started(&self, function_name: &str) -> anyhow::Result<()> {
+ if !self.is_started() {
+ return Err(LogicException::new(format!(
+ "Process must be started before calling \"{}()\".",
+ function_name
+ ))
+ .into());
+ }
+ Ok(())
+ }
+
+ /// Ensures the process is terminated.
+ fn require_process_is_terminated(&mut self, function_name: &str) -> anyhow::Result<()> {
+ if !self.is_terminated() {
+ return Err(LogicException::new(format!(
+ "Process must be terminated before calling \"{}()\".",
+ function_name
+ ))
+ .into());
+ }
+ Ok(())
+ }
+
+ /// Escapes a string to be used as a shell argument.
+ fn escape_argument(&self, argument: Option<&str>) -> String {
+ let argument = match argument {
+ None | Some("") => return "\"\"".to_string(),
+ Some(a) => a,
+ };
+ if php::DIRECTORY_SEPARATOR != "\\" {
+ return format!("'{}'", argument.replace('\'', "'\\''"));
+ }
+ let mut argument = argument.to_string();
+ if argument.contains('\0') {
+ argument = argument.replace('\0', "?");
+ }
+ if !php::preg_match(r#"/[()%!^"<>&|\s\[\]=;*?'$]/"#, &argument, &mut Vec::new()) {
+ return argument;
+ }
+ argument = php::preg_replace(r"/(\\+)$/", "$1$1", &argument);
+
+ let mut result = argument;
+ for (from, to) in [
+ ("\"", "\"\""),
+ ("^", "\"^^\""),
+ ("%", "\"^%\""),
+ ("!", "\"^!\""),
+ ("\n", "!LF!"),
+ ] {
+ result = result.replace(from, to);
+ }
+ format!("\"{}\"", result)
+ }
+
+ fn replace_placeholders(
+ &self,
+ commandline: &str,
+ env: &IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<String> {
+ php::preg_replace_callback(
+ r#"/"\$\{:([_a-zA-Z]+[_a-zA-Z0-9]*)\}"/"#,
+ |matches: &[Option<String>]| -> anyhow::Result<String> {
+ let key = matches.get(1).cloned().flatten().unwrap_or_default();
+ match env.get(&key) {
+ None => Err(InvalidArgumentException::new(format!(
+ "Command line is missing a value for parameter \"{}\": {}",
+ key, commandline
+ ))
+ .into()),
+ Some(v) if matches!(v, PhpMixed::Bool(false)) => {
+ Err(InvalidArgumentException::new(format!(
+ "Command line is missing a value for parameter \"{}\": {}",
+ key, commandline
+ ))
+ .into())
+ }
+ Some(v) => Ok(self.escape_argument(Some(&to_php_string(v)))),
+ }
+ },
+ commandline,
+ )
+ }
+
+ fn get_default_env(&self) -> IndexMap<String, PhpMixed> {
+ let env = php::getenv_all();
+ let server = php::php_server();
+
+ // non-Windows: array_intersect_key($env, $_SERVER) ?: $env
+ let mut intersect: IndexMap<String, PhpMixed> = IndexMap::new();
+ for (k, v) in &env {
+ if server.contains_key(k) {
+ intersect.insert(k.clone(), PhpMixed::String(v.clone()));
+ }
+ }
+ let env_map: IndexMap<String, PhpMixed> = if intersect.is_empty() {
+ env.into_iter()
+ .map(|(k, v)| (k, PhpMixed::String(v)))
+ .collect()
+ } else {
+ intersect
+ };
+
+ // $_ENV + env_map
+ let mut result = php::php_env();
+ for (k, v) in env_map {
+ result.entry(k).or_insert(v);
+ }
+ result
+ }
+
+ /// Clone the process configuration, mirroring PHP `clone $this` followed by `__clone`
+ /// (which calls resetProcessData). Runtime state is reset, not copied.
+ fn clone_process(&self) -> Process {
+ let mut process = Self::empty();
+ process.has_callback = self.has_callback;
+ process.commandline = self.commandline.clone();
+ process.cwd = self.cwd.clone();
+ process.env = self.env.clone();
+ process.input = self.input.clone();
+ process.timeout = self.timeout;
+ process.idle_timeout = self.idle_timeout;
+ process.output_disabled = self.output_disabled;
+ process.tty = self.tty;
+ process.pty = self.pty;
+ process.options = self.options.clone();
+ process.use_file_handles = self.use_file_handles;
+ process
+ }
+}
+
+impl Drop for Process {
+ fn drop(&mut self) {
+ if self
+ .options
+ .get("create_new_console")
+ .map(php::php_truthy)
+ .unwrap_or(false)
+ {
+ if let Some(p) = self.process_pipes.as_mut() {
+ p.close();
+ }
+ } else {
+ self.stop(0.0, None);
+ }
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/process/process_utils.rs b/crates/shirabe-external-packages/src/symfony/process/process_utils.rs
new file mode 100644
index 0000000..aec326f
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/process/process_utils.rs
@@ -0,0 +1,47 @@
+//! ref: composer/vendor/symfony/process/ProcessUtils.php
+
+use shirabe_php_shim::{self as php, PhpMixed};
+
+use crate::symfony::process::exception::invalid_argument_exception::InvalidArgumentException;
+
+/// ProcessUtils is a bunch of utility methods.
+#[derive(Debug)]
+pub struct ProcessUtils;
+
+impl ProcessUtils {
+ /// Validates and normalizes a Process input.
+ pub fn validate_input(caller: &str, input: PhpMixed) -> anyhow::Result<PhpMixed> {
+ if !input.is_null() {
+ if php::is_resource(&input) {
+ return Ok(input);
+ }
+ if php::is_string(&input) {
+ return Ok(input);
+ }
+ if php::is_scalar(&input) {
+ let s = match &input {
+ PhpMixed::Bool(b) => {
+ if *b {
+ "1".to_string()
+ } else {
+ String::new()
+ }
+ }
+ PhpMixed::Int(i) => i.to_string(),
+ PhpMixed::Float(f) => f.to_string(),
+ other => other.as_string().map(|s| s.to_string()).unwrap_or_default(),
+ };
+ return Ok(PhpMixed::String(s));
+ }
+ // TODO(plugin): Process / \Iterator / \Traversable inputs are not modeled by PhpMixed.
+
+ return Err(InvalidArgumentException::new(format!(
+ "\"{}\" only accepts strings, Traversable objects or stream resources.",
+ caller
+ ))
+ .into());
+ }
+
+ Ok(input)
+ }
+}
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs
index 4bb309e..b85d9f2 100644
--- a/crates/shirabe-php-shim/src/lib.rs
+++ b/crates/shirabe-php-shim/src/lib.rs
@@ -3467,16 +3467,63 @@ pub fn stdin() -> PhpResource {
PhpResource::Stdin
}
-// TODO(phase-c): reports proc_open as unavailable, so callers (terminal size and
-// tty detection) fall back to their defaults. A real implementation requires a
-// non-lossy signature able to hold the child process and its pipes; defer it to
-// the broader process-subsystem work (ProcessExecutor).
-pub fn proc_open(_command: &str, _descriptorspec: &Vec<PhpMixed>, _pipes: &mut PhpMixed) -> bool {
- false
+// TODO(phase-c): reports proc_open as unavailable (returns false), so callers fall back to
+// their defaults. A real implementation requires holding the child process and its pipes; defer
+// it to the broader process-subsystem work (ProcessExecutor).
+pub fn proc_open(
+ _command: &str,
+ _descriptorspec: &[PhpMixed],
+ _pipes: &mut PhpMixed,
+ _cwd: Option<&str>,
+ _env: Option<&[String]>,
+ _options: Option<&IndexMap<String, PhpMixed>>,
+) -> PhpMixed {
+ PhpMixed::Bool(false)
}
-pub fn proc_close(_process: bool) -> i64 {
+pub fn proc_close(_process: PhpMixed) -> i64 {
-1
}
+pub fn proc_get_status(_process: &PhpMixed) -> IndexMap<String, PhpMixed> {
+ todo!()
+}
+pub fn proc_terminate(_process: &PhpMixed, _signal: i64) -> bool {
+ todo!()
+}
+pub fn posix_kill(_pid: i64, _signal: i64) -> bool {
+ todo!()
+}
+pub fn uniqid(_prefix: &str, _more_entropy: bool) -> String {
+ todo!()
+}
+pub fn ftruncate(_stream: &PhpMixed, _size: i64) -> bool {
+ todo!()
+}
+/// PHP `ftell()` over a PhpMixed stream resource. (`ftell` itself is already defined for the
+/// `PhpResource`-typed stream API used elsewhere.)
+pub fn ftell_stream(_stream: &PhpMixed) -> i64 {
+ todo!()
+}
+pub const SEEK_SET: i64 = 0;
+pub const SEEK_CUR: i64 = 1;
+pub const SEEK_END: i64 = 2;
+pub fn fseek3(_stream: PhpMixed, _offset: i64, _whence: i64) -> i64 {
+ todo!()
+}
+pub fn stream_get_contents3(_stream: PhpMixed, _max_length: i64, _offset: i64) -> Option<String> {
+ todo!()
+}
+/// PHP `getenv()` with no argument: all environment variables.
+pub fn getenv_all() -> IndexMap<String, String> {
+ todo!()
+}
+/// PHP superglobal `$_ENV`.
+pub fn php_env() -> IndexMap<String, PhpMixed> {
+ todo!()
+}
+/// PHP superglobal `$_SERVER`.
+pub fn php_server() -> IndexMap<String, PhpMixed> {
+ todo!()
+}
pub fn sapi_windows_vt100_support(_resource: &PhpResource) -> bool {
todo!()
diff --git a/crates/shirabe/src/downloader/zip_downloader.rs b/crates/shirabe/src/downloader/zip_downloader.rs
index 3d72377..d7508a6 100644
--- a/crates/shirabe/src/downloader/zip_downloader.rs
+++ b/crates/shirabe/src/downloader/zip_downloader.rs
@@ -135,7 +135,7 @@ impl ZipDownloader {
.execute_async(&command, ())
.await;
match process_result {
- Ok(process) => {
+ Ok(mut process) => {
if !process.is_successful() {
if self.cleanup_executed.contains_key(&package.get_name()) {
return Err(RuntimeException {
@@ -148,7 +148,7 @@ impl ZipDownloader {
.into());
}
- let output = process.get_error_output();
+ let output = process.get_error_output()?;
let output =
str_replace(&format!(", {}.zip or {}.ZIP", file, file), "", &output);
diff --git a/crates/shirabe/src/package/version/version_guesser.rs b/crates/shirabe/src/package/version/version_guesser.rs
index 42646cb..7f0625f 100644
--- a/crates/shirabe/src/package/version/version_guesser.rs
+++ b/crates/shirabe/src/package/version/version_guesser.rs
@@ -533,14 +533,14 @@ impl VersionGuesser {
},
&scm_cmdline,
);
- let process = tokio::runtime::Runtime::new()
+ let mut process = tokio::runtime::Runtime::new()
.unwrap()
.block_on(self.process.borrow_mut().execute_async(&cmd_line, path))?;
if !process.is_successful() {
continue;
}
- let output = process.get_output();
+ let output = process.get_output()?;
// overwrite existing if we have a shorter diff, or we have an equal diff and an index that comes later in the array (i.e. older version)
// as newer versions typically have more commits, if the feature branch is based on a newer branch it should have a longer diff to the old version
// but if it doesn't and they have equal diffs, then it probably is based on the old version
diff --git a/crates/shirabe/src/repository/vcs/perforce_driver.rs b/crates/shirabe/src/repository/vcs/perforce_driver.rs
index 3077cff..50d5eb9 100644
--- a/crates/shirabe/src/repository/vcs/perforce_driver.rs
+++ b/crates/shirabe/src/repository/vcs/perforce_driver.rs
@@ -60,7 +60,7 @@ impl PerforceDriver {
self.perforce.as_mut().unwrap().p4_login()?;
self.perforce.as_mut().unwrap().check_stream();
self.perforce.as_mut().unwrap().write_p4_client_spec()?;
- self.perforce.as_mut().unwrap().connect_client();
+ self.perforce.as_mut().unwrap().connect_client()?;
Ok(())
}
diff --git a/crates/shirabe/src/util/filesystem.rs b/crates/shirabe/src/util/filesystem.rs
index 2d73422..7a4885a 100644
--- a/crates/shirabe/src/util/filesystem.rs
+++ b/crates/shirabe/src/util/filesystem.rs
@@ -157,7 +157,7 @@ impl Filesystem {
vec!["rm".to_string(), "-rf".to_string(), directory.to_string()]
};
- let process = self
+ let mut process = self
.get_process()
.execute_async(
PhpMixed::List(cmd.iter().map(|s| PhpMixed::String(s.clone())).collect()),
diff --git a/crates/shirabe/src/util/perforce.rs b/crates/shirabe/src/util/perforce.rs
index 0d56afb..d5facd2 100644
--- a/crates/shirabe/src/util/perforce.rs
+++ b/crates/shirabe/src/util/perforce.rs
@@ -382,7 +382,7 @@ impl Perforce {
Ok(true)
}
- pub fn connect_client(&mut self) {
+ pub fn connect_client(&mut self) -> Result<()> {
let p4_create_client_command =
self.generate_p4_command(vec!["client".to_string(), "-i".to_string()], true);
@@ -390,10 +390,13 @@ impl Perforce {
p4_create_client_command,
None,
None,
- file_get_contents(&self.get_p4_client_spec()),
+ file_get_contents(&self.get_p4_client_spec())
+ .map(PhpMixed::String)
+ .unwrap_or(PhpMixed::Null),
None,
- );
- process.run(None);
+ )?;
+ process.run(None, indexmap::IndexMap::new())?;
+ Ok(())
}
pub fn sync_code_base(&mut self, source_reference: Option<&str>) -> Result<()> {
@@ -531,12 +534,20 @@ impl Perforce {
}
}
- pub fn windows_login(&mut self, password: Option<&str>) -> i64 {
+ pub fn windows_login(&mut self, password: Option<&str>) -> Result<i64> {
let command = self.generate_p4_command(vec!["login".to_string(), "-a".to_string()], true);
- let mut process = Process::new(command, None, None, password.map(|s| s.to_string()), None);
+ let mut process = Process::new(
+ command,
+ None,
+ None,
+ password
+ .map(|s| PhpMixed::String(s.to_string()))
+ .unwrap_or(PhpMixed::Null),
+ None,
+ )?;
- process.run(None)
+ process.run(None, indexmap::IndexMap::new())
}
pub fn p4_login(&mut self) -> Result<()> {
@@ -544,13 +555,19 @@ impl Perforce {
if !self.is_logged_in()? {
let password = self.query_p4_password();
if self.windows_flag {
- self.windows_login(password.as_deref());
+ self.windows_login(password.as_deref())?;
} else {
let command =
self.generate_p4_command(vec!["login".to_string(), "-a".to_string()], false);
- let mut process = Process::new(command, None, None, password, None);
- process.run(None);
+ let mut process = Process::new(
+ command,
+ None,
+ None,
+ password.map(PhpMixed::String).unwrap_or(PhpMixed::Null),
+ None,
+ )?;
+ process.run(None, indexmap::IndexMap::new())?;
if !process.is_successful() {
return Err(Exception {
diff --git a/crates/shirabe/src/util/process_executor.rs b/crates/shirabe/src/util/process_executor.rs
index 2bdf065..61fc0a1 100644
--- a/crates/shirabe/src/util/process_executor.rs
+++ b/crates/shirabe/src/util/process_executor.rs
@@ -221,9 +221,9 @@ impl ProcessExecutor {
&command_str,
cwd,
env.clone(),
- None,
+ PhpMixed::Null,
Some(Self::get_timeout() as f64),
- );
+ )?;
} else if let PhpMixed::List(ref list) = command {
let mut cmd_vec: Vec<String> = list
.iter()
@@ -238,9 +238,9 @@ impl ProcessExecutor {
cmd_vec,
cwd.map(String::from),
env,
- None,
+ PhpMixed::Null,
Some(Self::get_timeout() as f64),
- );
+ )?;
} else {
return Err(LogicException {
message: "Invalid command type".to_string(),
@@ -291,20 +291,23 @@ impl ProcessExecutor {
}),
);
- let result: Result<()> = {
- let _ = process.run(/* callback */ Some(Box::new(|_t: &str, _b: &str| {})));
+ let result: Result<()> = (|| -> Result<()> {
+ process.run(
+ /* callback */ Some(Box::new(|_t: &str, _b: &str| false)),
+ IndexMap::new(),
+ )?;
let output_is_callable_inner = output.as_deref().map(is_callable).unwrap_or(false);
if self.capture_output
&& !output_is_callable_inner
&& let Some(out) = output.as_mut()
{
- **out = PhpMixed::String(process.get_output());
+ **out = PhpMixed::String(process.get_output()?);
}
- self.error_output = process.get_error_output();
+ self.error_output = process.get_error_output()?;
Ok(())
- };
+ })();
let final_result: Result<()> = match result {
Ok(()) => Ok(()),
Err(e) => {
@@ -467,23 +470,23 @@ impl ProcessExecutor {
self.output_command_run(&command, cwd.as_deref(), true);
let process_result: Result<Process> = (if is_string(&command) {
- Ok(Process::from_shell_commandline(
+ Process::from_shell_commandline(
command.as_string().unwrap_or(""),
cwd.as_deref(),
None,
- None,
+ PhpMixed::Null,
Some(Self::get_timeout() as f64),
- ))
+ )
} else if let PhpMixed::List(ref list) = command {
- Ok(Process::new(
+ Process::new(
list.iter()
.map(|v| v.as_string().unwrap_or("").to_string())
.collect(),
cwd.clone(),
None,
- None,
+ PhpMixed::Null,
Some(Self::get_timeout() as f64),
- ))
+ )
} else {
Err(LogicException {
message: "Invalid command type".to_string(),
@@ -509,10 +512,20 @@ impl ProcessExecutor {
}
// PHP: $process->start($callback); — we operate on the stored job.process directly
- if let Some(job) = self.jobs.get_mut(&id)
+ let start_result = if let Some(job) = self.jobs.get_mut(&id)
&& let Some(p) = job.process.as_mut()
{
- p.start(None);
+ p.start(None, IndexMap::new())
+ } else {
+ Ok(())
+ };
+ if let Err(e) = start_result {
+ // PHP: $job['reject']($e) — record the rejection and settle the job as failed.
+ if let Some(job) = self.jobs.get_mut(&id) {
+ job.status = Self::STATUS_FAILED;
+ job.exception = Some(e);
+ }
+ self.mark_job_done();
}
}
@@ -571,8 +584,8 @@ impl ProcessExecutor {
if status == Self::STATUS_STARTED && has_process {
let is_running = self
.jobs
- .get(id)
- .and_then(|j| j.process.as_ref())
+ .get_mut(id)
+ .and_then(|j| j.process.as_mut())
.map(|p| p.is_running())
.unwrap_or(false);
if !is_running {
@@ -580,8 +593,8 @@ impl ProcessExecutor {
// marks the job completed/failed based on the process exit status.
let successful = self
.jobs
- .get(id)
- .and_then(|j| j.process.as_ref())
+ .get_mut(id)
+ .and_then(|j| j.process.as_mut())
.map(|p| p.is_successful())
.unwrap_or(false);
if let Some(job) = self.jobs.get_mut(id) {
@@ -594,7 +607,7 @@ impl ProcessExecutor {
self.mark_job_done();
}
- if let Some(p) = self.jobs.get(id).and_then(|j| j.process.as_ref()) {
+ if let Some(p) = self.jobs.get_mut(id).and_then(|j| j.process.as_mut()) {
p.check_timeout()?;
}
}