From 446f719f6c34453f027d5ccdbf81b16e63f4c982 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 9 Aug 2026 11:14:42 +0900 Subject: refactor(symfony-process): extract symfony/process into the shirabe-symfony-process crate Move `Symfony\Component\Process` out of shirabe-external-packages and into its own crate, so the path is `shirabe_symfony_process::Process` instead of `shirabe_external_packages::symfony::process::Process`. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe-external-packages/Cargo.toml | 1 + crates/shirabe-external-packages/src/symfony.rs | 1 - .../console/command/dump_completion_command.rs | 2 +- .../src/symfony/console/helper/process_helper.rs | 4 +- .../src/symfony/process.rs | 11 - .../src/symfony/process/exception.rs | 13 - .../exception/invalid_argument_exception.rs | 20 - .../symfony/process/exception/logic_exception.rs | 20 - .../process/exception/process_failed_exception.rs | 48 - .../exception/process_signaled_exception.rs | 34 - .../exception/process_timed_out_exception.rs | 31 - .../symfony/process/exception/runtime_exception.rs | 20 - .../src/symfony/process/executable_finder.rs | 116 -- .../src/symfony/process/php_executable_finder.rs | 70 -- .../src/symfony/process/pipes.rs | 4 - .../src/symfony/process/pipes/abstract_pipes.rs | 104 -- .../src/symfony/process/pipes/pipes_interface.rs | 28 - .../src/symfony/process/pipes/unix_pipes.rs | 136 -- .../src/symfony/process/pipes/windows_pipes.rs | 60 - .../src/symfony/process/process.rs | 1293 -------------------- .../src/symfony/process/process_utils.rs | 43 - crates/shirabe-php-rpc/Cargo.toml | 1 + crates/shirabe-php-rpc/src/lib.rs | 2 +- crates/shirabe-php-rpc/tests/generated_stubs.rs | 2 +- crates/shirabe-php-rpc/tests/oracle.rs | 2 +- crates/shirabe-symfony-process/Cargo.toml | 12 + crates/shirabe-symfony-process/src/exception.rs | 13 + .../src/exception/invalid_argument_exception.rs | 20 + .../src/exception/logic_exception.rs | 20 + .../src/exception/process_failed_exception.rs | 48 + .../src/exception/process_signaled_exception.rs | 34 + .../src/exception/process_timed_out_exception.rs | 31 + .../src/exception/runtime_exception.rs | 20 + .../src/executable_finder.rs | 116 ++ crates/shirabe-symfony-process/src/lib.rs | 11 + .../src/php_executable_finder.rs | 70 ++ crates/shirabe-symfony-process/src/pipes.rs | 4 + .../src/pipes/abstract_pipes.rs | 104 ++ .../src/pipes/pipes_interface.rs | 28 + .../src/pipes/unix_pipes.rs | 136 ++ .../src/pipes/windows_pipes.rs | 60 + crates/shirabe-symfony-process/src/process.rs | 1293 ++++++++++++++++++++ .../shirabe-symfony-process/src/process_utils.rs | 43 + crates/shirabe/Cargo.toml | 1 + crates/shirabe/src/command/diagnose_command.rs | 2 +- crates/shirabe/src/console/application.rs | 2 +- crates/shirabe/src/downloader/zip_downloader.rs | 2 +- .../src/event_dispatcher/event_dispatcher.rs | 4 +- crates/shirabe/src/platform/hhvm_detector.rs | 2 +- crates/shirabe/src/util/perforce.rs | 4 +- crates/shirabe/src/util/process_executor.rs | 10 +- .../tests/command/self_update_command_test.rs | 2 +- .../archiver/archivable_files_finder_test.rs | 2 +- .../tests/package/archiver/archive_manager_test.rs | 2 +- .../shirabe/tests/platform/hhvm_detector_test.rs | 2 +- .../shirabe/tests/plugin/plugin_installer_test.rs | 2 +- 56 files changed, 2090 insertions(+), 2076 deletions(-) delete mode 100644 crates/shirabe-external-packages/src/symfony/process.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/exception.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/exception/invalid_argument_exception.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/exception/logic_exception.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/exception/process_failed_exception.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/exception/process_signaled_exception.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/exception/process_timed_out_exception.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/exception/runtime_exception.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/executable_finder.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/php_executable_finder.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/pipes.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/pipes/abstract_pipes.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/pipes/pipes_interface.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/pipes/unix_pipes.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/pipes/windows_pipes.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/process.rs delete mode 100644 crates/shirabe-external-packages/src/symfony/process/process_utils.rs create mode 100644 crates/shirabe-symfony-process/Cargo.toml create mode 100644 crates/shirabe-symfony-process/src/exception.rs create mode 100644 crates/shirabe-symfony-process/src/exception/invalid_argument_exception.rs create mode 100644 crates/shirabe-symfony-process/src/exception/logic_exception.rs create mode 100644 crates/shirabe-symfony-process/src/exception/process_failed_exception.rs create mode 100644 crates/shirabe-symfony-process/src/exception/process_signaled_exception.rs create mode 100644 crates/shirabe-symfony-process/src/exception/process_timed_out_exception.rs create mode 100644 crates/shirabe-symfony-process/src/exception/runtime_exception.rs create mode 100644 crates/shirabe-symfony-process/src/executable_finder.rs create mode 100644 crates/shirabe-symfony-process/src/lib.rs create mode 100644 crates/shirabe-symfony-process/src/php_executable_finder.rs create mode 100644 crates/shirabe-symfony-process/src/pipes.rs create mode 100644 crates/shirabe-symfony-process/src/pipes/abstract_pipes.rs create mode 100644 crates/shirabe-symfony-process/src/pipes/pipes_interface.rs create mode 100644 crates/shirabe-symfony-process/src/pipes/unix_pipes.rs create mode 100644 crates/shirabe-symfony-process/src/pipes/windows_pipes.rs create mode 100644 crates/shirabe-symfony-process/src/process.rs create mode 100644 crates/shirabe-symfony-process/src/process_utils.rs (limited to 'crates') diff --git a/crates/shirabe-external-packages/Cargo.toml b/crates/shirabe-external-packages/Cargo.toml index c1e70046..c9897725 100644 --- a/crates/shirabe-external-packages/Cargo.toml +++ b/crates/shirabe-external-packages/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true shirabe-pcre.workspace = true shirabe-php-shim.workspace = true shirabe-semver.workspace = true +shirabe-symfony-process.workspace = true shirabe-symfony-string.workspace = true anyhow.workspace = true chrono.workspace = true diff --git a/crates/shirabe-external-packages/src/symfony.rs b/crates/shirabe-external-packages/src/symfony.rs index e0d9e4fe..23422146 100644 --- a/crates/shirabe-external-packages/src/symfony.rs +++ b/crates/shirabe-external-packages/src/symfony.rs @@ -1,4 +1,3 @@ pub mod console; pub mod filesystem; pub mod finder; -pub mod process; diff --git a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs index 31a94f0f..cc46366c 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs @@ -9,8 +9,8 @@ use crate::symfony::console::input::input_argument::InputArgument; use crate::symfony::console::input::input_interface::InputInterface; use crate::symfony::console::input::input_option::InputOption; use crate::symfony::console::output::output_interface::{self, OutputInterface}; -use crate::symfony::process::process::Process; use shirabe_php_shim::{PhpMixed, impl_php_class}; +use shirabe_symfony_process::process::Process; use std::ops::{Deref, DerefMut}; /// __DIR__.'/../Resources/completion.bash', embedded at compile time (this port ships as a 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 0f81348d..303a4569 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 @@ -6,8 +6,8 @@ use crate::symfony::console::helper::helper_interface::HelperInterface; use crate::symfony::console::helper::helper_set::HelperSet; use crate::symfony::console::output::ConsoleOutputInterface; use crate::symfony::console::output::output_interface::{self, OutputInterface}; -use crate::symfony::process::exception::process_failed_exception::ProcessFailedException; -use crate::symfony::process::process::Process; +use shirabe_symfony_process::exception::process_failed_exception::ProcessFailedException; +use shirabe_symfony_process::process::Process; /// The ProcessHelper class provides helpers to run external processes. /// diff --git a/crates/shirabe-external-packages/src/symfony/process.rs b/crates/shirabe-external-packages/src/symfony/process.rs deleted file mode 100644 index 3a5656c6..00000000 --- a/crates/shirabe-external-packages/src/symfony/process.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod exception; -pub mod executable_finder; -pub mod php_executable_finder; -pub(crate) mod pipes; -pub mod process; -pub(crate) mod process_utils; - -pub use exception::*; -pub use executable_finder::*; -pub use php_executable_finder::*; -pub use process::*; diff --git a/crates/shirabe-external-packages/src/symfony/process/exception.rs b/crates/shirabe-external-packages/src/symfony/process/exception.rs deleted file mode 100644 index 689a64b6..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/exception.rs +++ /dev/null @@ -1,13 +0,0 @@ -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::*; -pub use runtime_exception::*; 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 deleted file mode 100644 index c9a42653..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/exception/invalid_argument_exception.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! ref: composer/vendor/symfony/process/Exception/InvalidArgumentException.php - -#[derive(Debug)] -pub struct InvalidArgumentException { - inner: shirabe_php_shim::InvalidArgumentException, -} - -impl InvalidArgumentException { - pub fn new(message: String) -> Self { - Self { - inner: shirabe_php_shim::InvalidArgumentException::new(message), - } - } -} - -shirabe_php_shim::impl_php_exception!( - InvalidArgumentException, - inner, - r"Symfony\Component\Process\Exception\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 deleted file mode 100644 index 36e9bcca..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/exception/logic_exception.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! ref: composer/vendor/symfony/process/Exception/LogicException.php - -#[derive(Debug)] -pub struct LogicException { - inner: shirabe_php_shim::LogicException, -} - -impl LogicException { - pub fn new(message: String) -> Self { - Self { - inner: shirabe_php_shim::LogicException::new(message), - } - } -} - -shirabe_php_shim::impl_php_exception!( - LogicException, - inner, - r"Symfony\Component\Process\Exception\LogicException" -); 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 deleted file mode 100644 index e5d6d7fc..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/exception/process_failed_exception.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! ref: composer/vendor/symfony/process/Exception/ProcessFailedException.php - -use crate::symfony::process::exception::invalid_argument_exception::InvalidArgumentException; -use crate::symfony::process::exception::runtime_exception::RuntimeException; -use crate::symfony::process::process::Process; - -#[derive(Debug)] -pub struct ProcessFailedException { - inner: RuntimeException, -} - -impl ProcessFailedException { - pub fn new(process: &mut Process) -> anyhow::Result { - 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(), - ); - - error += &format!( - "\n\nOutput:\n================\n{}\n\nError Output:\n================\n{}", - process.get_output()?, - process.get_error_output()?, - ); - - Ok(Self { - inner: RuntimeException::new(error), - }) - } -} - -shirabe_php_shim::impl_php_exception!( - ProcessFailedException, - inner, - r"Symfony\Component\Process\Exception\ProcessFailedException" -); 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 deleted file mode 100644 index 9f325750..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/exception/process_signaled_exception.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! ref: composer/vendor/symfony/process/Exception/ProcessSignaledException.php - -use crate::symfony::process::exception::runtime_exception::RuntimeException; -use crate::symfony::process::process::Process; - -#[derive(Debug)] -pub struct ProcessSignaledException { - inner: RuntimeException, - signal: i64, -} - -impl ProcessSignaledException { - pub fn new(process: &mut Process) -> anyhow::Result { - let signal = process.get_term_signal()?; - - Ok(Self { - inner: RuntimeException::new(format!( - "The process has been signaled with signal \"{}\".", - signal - )), - signal, - }) - } - - pub fn get_signal(&self) -> i64 { - self.signal - } -} - -shirabe_php_shim::impl_php_exception!( - ProcessSignaledException, - inner, - r"Symfony\Component\Process\Exception\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 deleted file mode 100644 index 9485d1c2..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/exception/process_timed_out_exception.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! ref: composer/vendor/symfony/process/Exception/ProcessTimedOutException.php - -use crate::symfony::process::exception::runtime_exception::RuntimeException; -use crate::symfony::process::process::Process; - -#[derive(Debug)] -pub struct ProcessTimedOutException { - inner: RuntimeException, -} - -impl ProcessTimedOutException { - pub fn new(process: &Process) -> Self { - let exceeded_timeout = process.get_timeout(); - - 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 { - inner: RuntimeException::new(message), - } - } -} - -shirabe_php_shim::impl_php_exception!( - ProcessTimedOutException, - inner, - r"Symfony\Component\Process\Exception\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 deleted file mode 100644 index e9cc31e0..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/exception/runtime_exception.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! ref: composer/vendor/symfony/process/Exception/RuntimeException.php - -#[derive(Debug)] -pub struct RuntimeException { - inner: shirabe_php_shim::RuntimeException, -} - -impl RuntimeException { - pub fn new(message: String) -> Self { - Self { - inner: shirabe_php_shim::RuntimeException::new(message), - } - } -} - -shirabe_php_shim::impl_php_exception!( - RuntimeException, - inner, - r"Symfony\Component\Process\Exception\RuntimeException" -); diff --git a/crates/shirabe-external-packages/src/symfony/process/executable_finder.rs b/crates/shirabe-external-packages/src/symfony/process/executable_finder.rs deleted file mode 100644 index 5b1c0994..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/executable_finder.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! ref: composer/vendor/symfony/process/ExecutableFinder.php - -const CMD_BUILTINS: &[&str] = &[ - "assoc", "break", "call", "cd", "chdir", "cls", "color", "copy", "date", "del", "dir", "echo", - "endlocal", "erase", "exit", "for", "ftype", "goto", "help", "if", "label", "md", "mkdir", - "mklink", "move", "path", "pause", "popd", "prompt", "pushd", "rd", "rem", "ren", "rename", - "rmdir", "set", "setlocal", "shift", "start", "time", "title", "type", "ver", "vol", -]; - -#[derive(Debug)] -pub struct ExecutableFinder { - suffixes: Vec, -} - -impl Default for ExecutableFinder { - fn default() -> Self { - Self::new() - } -} - -impl ExecutableFinder { - pub fn new() -> Self { - Self { suffixes: vec![] } - } - - pub fn find(&self, name: &str, default: Option<&str>, extra_dirs: &[String]) -> Option { - // windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes - if cfg!(windows) && CMD_BUILTINS.contains(&shirabe_php_shim::strtolower(name).as_str()) { - return Some(name.to_string()); - } - - let path = shirabe_php_shim::getenv("PATH") - .or_else(|| shirabe_php_shim::getenv("Path")) - .map(|v| v.to_string_lossy().into_owned()) - .unwrap_or_default(); - let mut dirs: Vec = std::env::split_paths(&path) - .map(|dir| dir.into_os_string().into_string().unwrap()) - .collect(); - dirs.extend_from_slice(extra_dirs); - - let mut suffixes: Vec = vec![]; - if cfg!(windows) { - let path_ext = - shirabe_php_shim::getenv("PATHEXT").map(|v| v.to_string_lossy().into_owned()); - suffixes = self.suffixes.clone(); - let exts = match path_ext { - Some(ref ext) if !ext.is_empty() => std::env::split_paths(ext) - .map(|e| e.into_os_string().into_string().unwrap()) - .collect(), - _ => vec![ - ".exe".to_string(), - ".bat".to_string(), - ".cmd".to_string(), - ".com".to_string(), - ], - }; - suffixes.extend(exts); - } - suffixes = - if !shirabe_php_shim::pathinfo(name, shirabe_php_shim::PATHINFO_EXTENSION).is_empty() { - let mut s = vec![String::new()]; - s.extend(suffixes); - s - } else { - suffixes.push(String::new()); - suffixes - }; - for suffix in &suffixes { - for dir in &dirs { - let dir = if dir.is_empty() { "." } else { dir.as_str() }; - let file = std::path::Path::new(dir) - .join(format!("{name}{suffix}")) - .into_os_string() - .into_string() - .unwrap(); - if shirabe_php_shim::is_file(&file) - && (cfg!(windows) || shirabe_php_shim::is_executable(&file)) - { - return Some(file); - } - - if !shirabe_php_shim::is_dir(dir) - && shirabe_php_shim::basename(dir) == format!("{name}{suffix}") - && shirabe_php_shim::is_executable(dir) - { - return Some(dir.to_string()); - } - } - } - - if cfg!(windows) - || name.len() - != shirabe_php_shim::strcspn(name, &format!("/{}", std::path::MAIN_SEPARATOR)) - { - return default.map(ToString::to_string); - } - - let exec_result = shirabe_php_shim::exec( - &format!("command -v -- {}", shirabe_php_shim::escapeshellarg(name)), - None, - None, - ) - .unwrap_or_default(); - - let executable_path = shirabe_php_shim::substr( - &exec_result, - 0, - shirabe_php_shim::strpos(&exec_result, shirabe_php_shim::PHP_EOL).map(|i| i as i64), - ); - if !executable_path.is_empty() && shirabe_php_shim::is_executable(&executable_path) { - return Some(executable_path); - } - - default.map(ToString::to_string) - } -} diff --git a/crates/shirabe-external-packages/src/symfony/process/php_executable_finder.rs b/crates/shirabe-external-packages/src/symfony/process/php_executable_finder.rs deleted file mode 100644 index e8661866..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/php_executable_finder.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! ref: composer/vendor/symfony/process/PhpExecutableFinder.php - -use super::executable_finder::ExecutableFinder; - -#[derive(Debug)] -pub struct PhpExecutableFinder { - executable_finder: ExecutableFinder, -} - -impl Default for PhpExecutableFinder { - fn default() -> Self { - Self::new() - } -} - -impl PhpExecutableFinder { - pub fn new() -> Self { - Self { - executable_finder: ExecutableFinder::new(), - } - } - - /// Finds The PHP executable. - pub fn find(&self, _include_args: bool) -> Option { - if let Some(php) = shirabe_php_shim::getenv("PHP_BINARY").filter(|v| !v.is_empty()) { - let mut php = php.to_string_lossy().into_owned(); - if !shirabe_php_shim::is_executable(&php) { - match self.executable_finder.find(&php, None, &[]) { - Some(found) => php = found, - None => return None, - } - } - - if shirabe_php_shim::is_dir(&php) { - return None; - } - - return Some(php); - } - - // The original `\PHP_BINARY && \PHP_SAPI` branch describes the running PHP interpreter. - // These constants cannot be obtained in Rust, the branch is skipped here. - - if let Some(php) = shirabe_php_shim::getenv("PHP_PATH").filter(|v| !v.is_empty()) { - let php = php.to_string_lossy().into_owned(); - if !shirabe_php_shim::is_executable(&php) || shirabe_php_shim::is_dir(&php) { - return None; - } - - return Some(php); - } - - if let Some(php) = shirabe_php_shim::getenv("PHP_PEAR_PHP_BIN").filter(|v| !v.is_empty()) { - let php = php.to_string_lossy().into_owned(); - if shirabe_php_shim::is_executable(&php) && !shirabe_php_shim::is_dir(&php) { - return Some(php); - } - } - - // Even if `\PHP_BINDIR` is unavailable, searching `$PATH` should be performed. - self.executable_finder.find("php", None, &[]) - } - - /// Finds the PHP executable arguments. - pub fn find_arguments(&self) -> Vec { - // If PHP_SAPI is not "phpdbg", returns an empty array. In Rust, PHP_SAPI is always "cli", - // so always returns an empty array. - vec![] - } -} diff --git a/crates/shirabe-external-packages/src/symfony/process/pipes.rs b/crates/shirabe-external-packages/src/symfony/process/pipes.rs deleted file mode 100644 index 6c3ea7dd..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/pipes.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod abstract_pipes; -pub mod pipes_interface; -pub mod unix_pipes; -pub mod windows_pipes; 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 deleted file mode 100644 index 8741926c..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/pipes/abstract_pipes.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! ref: composer/vendor/symfony/process/Pipes/AbstractPipes.php - -use indexmap::IndexMap; -use shirabe_php_shim::{PhpMixed, PhpResource}; - -#[derive(Debug)] -pub struct AbstractPipes { - pub pipes: IndexMap, - - input_buffer: String, - input: PhpMixed, - blocked: bool, - last_error: Option, -} - -impl AbstractPipes { - pub fn new(input: PhpMixed) -> Self { - let input_buffer; - let stored_input; - // TODO(plugin): `$input instanceof \Iterator` is not modeled. The PHP `is_resource($input)` - // branch never applies: a PhpMixed is never a resource, so input is never stored as-is here. - 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: IndexMap::new(), - input_buffer, - input: stored_input, - blocked: true, - last_error: None, - } - } - - pub fn close(&mut self) { - for (_, pipe) in &self.pipes { - shirabe_php_shim::fclose(pipe); - } - self.pipes = IndexMap::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) { - if !self.blocked { - return; - } - - for (_, pipe) in &self.pipes { - shirabe_php_shim::stream_set_blocking(pipe, false); - } - // The `is_resource($this->input)` branch does not apply: `input` is never a resource in this - // port (is_resource on a PhpMixed is always false). - - self.blocked = false; - } - - /// Writes input to stdin. - pub(crate) fn write(&mut self) -> Option> { - let stdin = self.pipes.get(&0)?.clone(); - - // TODO(plugin): the `$input instanceof \Iterator` branch is not modeled. `input` is never a - // resource here, so the fread($input)/stream_set_blocking($input) paths do not apply and - // only the input buffer is written to stdin. - - let mut r: Vec = Vec::new(); - let mut e: Vec = Vec::new(); - let mut w: Vec = vec![stdin.clone()]; - - // let's have a look if something changed in streams - shirabe_php_shim::stream_select(&mut r, &mut w, &mut e, 0, Some(0))?; - - if !self.input_buffer.is_empty() { - let written = - shirabe_php_shim::fwrite(&stdin, &self.input_buffer, None).unwrap_or(0) as usize; - self.input_buffer = self.input_buffer.get(written..).unwrap_or("").to_string(); - if !self.input_buffer.is_empty() { - return Some(vec![stdin]); - } - } - - // no input to read on resource, buffer is empty - if self.input_buffer.is_empty() && !shirabe_php_shim::php_truthy(&self.input) { - self.input = PhpMixed::Null; - shirabe_php_shim::fclose(&stdin); - self.pipes.shift_remove(&0); - } - - None - } -} 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 deleted file mode 100644 index 46609bb8..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/pipes/pipes_interface.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! ref: composer/vendor/symfony/process/Pipes/PipesInterface.php - -use indexmap::IndexMap; -use shirabe_php_shim::{Descriptor, PhpResource}; - -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; - - /// Returns an array of filenames indexed by their related stream in case these pipes use temporary files. - fn get_files(&self) -> IndexMap; - - /// Reads data in file handles and pipes. - fn read_and_write(&mut self, blocking: bool, close: bool) -> IndexMap; - - /// Returns if the current state has open file handles or pipes. - fn are_open(&self) -> bool; - - /// Closes file handles and pipes. - fn close(&mut self); - - /// Accessor for the `pipes` property populated by proc_open, keyed by fd index. - fn pipes(&self) -> &IndexMap; - fn pipes_mut(&mut self) -> &mut IndexMap; -} 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 deleted file mode 100644 index a7026318..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/pipes/unix_pipes.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! ref: composer/vendor/symfony/process/Pipes/UnixPipes.php - -use crate::symfony::process::pipes::abstract_pipes::AbstractPipes; -use crate::symfony::process::pipes::pipes_interface::{CHUNK_SIZE, PipesInterface}; -use crate::symfony::process::process::Process; -use indexmap::IndexMap; -use shirabe_php_shim::{Descriptor, PhpMixed, PhpResource}; - -/// UnixPipes implementation uses unix pipes as handles. -#[derive(Debug)] -pub struct UnixPipes { - inner: AbstractPipes, - tty_mode: Option, -} - -impl UnixPipes { - pub fn new(tty_mode: Option, input: PhpMixed) -> Self { - Self { - inner: AbstractPipes::new(input), - tty_mode, - } - } -} - -fn descriptor(items: &[&str]) -> Descriptor { - match items { - ["pipe", mode] => Descriptor::Pipe(mode.to_string()), - ["file", path, mode] => Descriptor::File(path.to_string(), mode.to_string()), - _ => panic!("unsupported descriptor spec: {:?}", items), - } -} - -impl PipesInterface for UnixPipes { - fn get_descriptors(&mut self) -> Vec { - if self.tty_mode == Some(true) { - return vec![ - descriptor(&["file", "/dev/tty", "r"]), - descriptor(&["file", "/dev/tty", "w"]), - descriptor(&["file", "/dev/tty", "w"]), - ]; - } - - vec![ - descriptor(&["pipe", "r"]), - descriptor(&["pipe", "w"]), - descriptor(&["pipe", "w"]), - ] - } - - fn get_files(&self) -> IndexMap { - IndexMap::new() - } - - fn read_and_write(&mut self, blocking: bool, close: bool) -> IndexMap { - self.inner.unblock(); - let w = self.inner.write(); - - let mut read: IndexMap = IndexMap::new(); - // $r = $this->pipes; unset($r[0]); - let r: Vec<(i64, PhpResource)> = self - .inner - .pipes - .iter() - .filter(|(fd, _)| **fd != 0) - .map(|(fd, pipe)| (*fd, pipe.clone())) - .collect(); - - // TODO(plugin): set_error_handler/restore_error_handler around stream_select is not modeled. - let mut r_sel: Vec = r.iter().map(|(_, p)| p.clone()).collect(); - let mut w_sel: Vec = w.clone().unwrap_or_default(); - let mut e_sel: Vec = Vec::new(); - - // let's have a look if something changed in streams - if (!r_sel.is_empty() || w.is_some()) - && shirabe_php_shim::stream_select( - &mut r_sel, - &mut w_sel, - &mut e_sel, - 0, - Some(if blocking { - (Process::TIMEOUT_PRECISION * 1e6) as i64 - } else { - 0 - }), - ) - .is_none() - { - // if a system call has been interrupted, forget about it, let's try again - // otherwise, an error occurred, let's reset pipes - if !self.inner.has_system_call_been_interrupted() { - self.inner.pipes = IndexMap::new(); - } - - return read; - } - - for (fd, pipe) in &r { - let mut data = String::new(); - loop { - let chunk = shirabe_php_shim::fread(pipe, CHUNK_SIZE).unwrap_or_default(); - let len = chunk.len() as i64; - data.push_str(&chunk); - if !(len > 0 && (close || len >= CHUNK_SIZE)) { - break; - } - } - - if !data.is_empty() { - read.insert(*fd, data); - } - - if close && shirabe_php_shim::feof(pipe) { - shirabe_php_shim::fclose(pipe); - self.inner.pipes.shift_remove(fd); - } - } - - read - } - - fn are_open(&self) -> bool { - !self.inner.pipes.is_empty() - } - - fn close(&mut self) { - self.inner.close(); - } - - fn pipes(&self) -> &IndexMap { - &self.inner.pipes - } - - fn pipes_mut(&mut self) -> &mut IndexMap { - &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 deleted file mode 100644 index cf4f3414..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/pipes/windows_pipes.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! ref: composer/vendor/symfony/process/Pipes/WindowsPipes.php - -use crate::symfony::process::pipes::abstract_pipes::AbstractPipes; -use crate::symfony::process::pipes::pipes_interface::PipesInterface; -use indexmap::IndexMap; -use shirabe_php_shim::{Descriptor, PhpMixed, PhpResource}; - -/// WindowsPipes implementation uses temporary files as handles. -#[derive(Debug)] -pub struct WindowsPipes { - inner: AbstractPipes, - files: IndexMap, - file_handles: IndexMap, - lock_handles: IndexMap, - read_bytes: IndexMap, -} - -impl WindowsPipes { - pub fn new(_input: PhpMixed) -> Self { - // Windows-only path: never constructed on non-Windows targets. - todo!() - } -} - -impl PipesInterface for WindowsPipes { - fn get_descriptors(&mut self) -> Vec { - let _ = ( - &self.files, - &self.file_handles, - &self.lock_handles, - &self.read_bytes, - ); - todo!() - } - - fn get_files(&self) -> IndexMap { - self.files.clone() - } - - fn read_and_write(&mut self, _blocking: bool, _close: bool) -> IndexMap { - todo!() - } - - fn are_open(&self) -> bool { - !self.inner.pipes.is_empty() && !self.file_handles.is_empty() - } - - fn close(&mut self) { - self.inner.close(); - todo!() - } - - fn pipes(&self) -> &IndexMap { - &self.inner.pipes - } - - fn pipes_mut(&mut self) -> &mut IndexMap { - &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 deleted file mode 100644 index 3fb3fab2..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/process.rs +++ /dev/null @@ -1,1293 +0,0 @@ -//! ref: composer/vendor/symfony/process/Process.php - -use crate::symfony::process::exception::invalid_argument_exception::InvalidArgumentException; -use crate::symfony::process::exception::logic_exception::LogicException; -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; -use indexmap::IndexMap; -use shirabe_php_shim::{Descriptor, PhpMixed, PhpResource, php_regex}; -use std::sync::OnceLock; - -/// A user-supplied callback invoked with the output type ("out"/"err") and a chunk of output. -pub type UserCallback = Box 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 bool>; - -/// PHP `$this->commandline` is `array|string`. -#[derive(Debug, Clone)] -enum CommandLine { - Array(Vec), - String(String), -} - -/// Test-only behaviour for a Process fabricated via [`Process::__mock`]: `getOutput`/ -/// `getErrorOutput`/`getExitCode`/`isSuccessful` return these fixed values instead of reading a -/// real subprocess. Mirrors PHPUnit's `getMockBuilder(Process::class)->disableOriginalConstructor()` -/// mocks used by the Composer test suite (e.g. `ZipDownloaderTest`). Held in [`Process::mock`]; -/// always `None` in production. -#[derive(Debug, Clone)] -pub struct ProcessMock { - pub exit_code: i64, - pub stdout: String, - pub stderr: String, -} - -/// Process is a thin wrapper around proc_* functions to easily -/// start independent PHP processes. -pub struct Process { - callback: Option, - commandline: CommandLine, - cwd: Option, - env: IndexMap, - input: PhpMixed, - starttime: Option, - timeout: Option, - exitcode: Option, - fallback_status: IndexMap, - process_information: Option>, - stdout: Option, - stderr: Option, - process: Option, - status: String, - incremental_output_offset: i64, - incremental_error_output_offset: i64, - tty: bool, - options: IndexMap, - use_file_handles: bool, - process_pipes: Option>, - latest_signal: Option, - cached_exit_code: Option, - /// Test-only mock state. `None` in production; set via [`Process::__mock`] in tests. - mock: Option, -} - -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() - } -} - -fn descriptor(items: &[&str]) -> Descriptor { - match items { - ["pipe", mode] => Descriptor::Pipe(mode.to_string()), - ["file", path, mode] => Descriptor::File(path.to_string(), mode.to_string()), - _ => panic!("unsupported descriptor spec: {:?}", items), - } -} - -/// 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 STDOUT: i64 = 1; - pub const STDERR: i64 = 2; - - // Timeout Precision in seconds. - pub const TIMEOUT_PRECISION: f64 = 0.2; - - /// 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, - commandline: CommandLine::Array(Vec::new()), - cwd: None, - env: IndexMap::new(), - input: PhpMixed::Null, - starttime: None, - timeout: None, - exitcode: None, - fallback_status: IndexMap::new(), - process_information: None, - stdout: None, - stderr: None, - process: None, - status: Self::STATUS_READY.to_string(), - incremental_output_offset: 0, - incremental_error_output_offset: 0, - tty: false, - options, - use_file_handles: false, - process_pipes: None, - latest_signal: None, - cached_exit_code: None, - mock: None, - } - } - - /// For testing only. Builds an already-terminated mock Process whose getOutput/ - /// getErrorOutput/getExitCode/isSuccessful return the configured values, without spawning a - /// real subprocess. - pub fn __mock(mock: ProcessMock) -> Self { - let mut this = Self::empty(); - this.status = Self::STATUS_TERMINATED.to_string(); - this.exitcode = Some(mock.exit_code); - this.mock = Some(mock); - this - } - - pub fn new( - command: Vec, - cwd: Option, - env: Option>, - input: PhpMixed, - timeout: Option, - ) -> anyhow::Result { - if !shirabe_php_shim::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 - // PHP: null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR) - // `\defined('ZEND_THREAD_SAFE')` is unconditionally true in modern PHP (the constant always - // exists; its value merely reflects the NTS/ZTS build), so the disjunction is always true and - // the cwd is defaulted to getcwd() whenever it was null. - if this.cwd.is_none() { - this.cwd = shirabe_php_shim::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 = cfg!(windows); - - 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>, - input: PhpMixed, - timeout: Option, - ) -> anyhow::Result { - let mut process = Self::new(Vec::new(), cwd.map(String::from), env, input, timeout)?; - process.commandline = CommandLine::String(command.to_string()); - - Ok(process) - } - - /// Runs the process. - pub fn run( - &mut self, - callback: Option, - env: IndexMap, - ) -> anyhow::Result { - self.start(callback, env)?; - - self.wait(None) - } - - /// Starts the process and returns after writing the input to STDIN. - pub fn start( - &mut self, - callback: Option, - mut env: IndexMap, - ) -> anyhow::Result<()> { - if self.is_running() { - return Err(RuntimeException::new("Process is already running.".to_string()).into()); - } - - self.reset_process_data(); - self.starttime = Some(shirabe_php_shim::microtime()); - self.callback = Some(self.build_callback(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::>() - .join(" "); - - if !cfg!(windows) { - // 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 cfg!(windows) { - 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 = shirabe_php_shim::fopen("Process.php", "r"); - } - - let mut env_pairs: Vec = 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(shirabe_php_shim::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(); - shirabe_php_shim::proc_open( - &commandline, - &descriptors, - pipes, - cwd.as_deref().map(std::path::Path::new), - Some(&env_pairs), - Some(&options), - ) - }; - self.process = process.ok(); - - if self.process.is_none() { - 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 = self - .process_pipes - .as_ref() - .unwrap() - .pipes() - .get(&3) - .cloned(); - let pid = pipe3 - .and_then(|p| shirabe_php_shim::fgets(&p, None)) - .map(|s| s.trim().parse::().unwrap_or(0)) - .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(()) - } - - /// Waits for the process to terminate. - pub fn wait(&mut self, callback: Option) -> anyhow::Result { - self.require_process_is_started("wait")?; - - self.update_status(false); - - if let Some(callback) = callback { - self.callback = Some(self.build_callback(Some(callback))); - } - - loop { - self.check_timeout()?; - let running = self.is_running() - && (cfg!(windows) || self.process_pipes.as_ref().unwrap().are_open()); - self.read_pipes(running, !cfg!(windows) || !running); - if !running { - break; - } - } - - while self.is_running() { - self.check_timeout()?; - shirabe_php_shim::usleep(1000); - } - - let signaled = self - .process_information - .as_ref() - .and_then(|i| i.get("signaled")) - .map(shirabe_php_shim::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)) - } - - /// Returns the Pid (process identifier), if applicable. - pub fn get_pid(&mut self) -> Option { - if self.is_running() { - self.process_information - .as_ref() - .and_then(|i| i.get("pid")) - .and_then(|v| v.as_int()) - } else { - None - } - } - - /// Returns the current output of the process (STDOUT). - pub fn get_output(&mut self) -> anyhow::Result { - if let Some(mock) = &self.mock { - return Ok(mock.stdout.clone()); - } - - self.read_pipes_for_output("getOutput", false)?; - - Ok( - shirabe_php_shim::stream_get_contents3(self.stdout.as_ref().unwrap(), -1, 0) - .unwrap_or_default(), - ) - } - - /// Returns the current error output of the process (STDERR). - pub fn get_error_output(&mut self) -> anyhow::Result { - if let Some(mock) = &self.mock { - return Ok(mock.stderr.clone()); - } - - self.read_pipes_for_output("getErrorOutput", false)?; - - Ok( - shirabe_php_shim::stream_get_contents3(self.stderr.as_ref().unwrap(), -1, 0) - .unwrap_or_default(), - ) - } - - /// Returns the exit code returned by the process. - pub fn get_exit_code(&mut self) -> Option { - if self.mock.is_some() { - return self.exitcode; - } - - 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 { - 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 the number of the signal that caused the child process to terminate. - pub fn get_term_signal(&mut self) -> anyhow::Result { - 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)) - } - - /// 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(shirabe_php_shim::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 { - Self::STATUS_READY != self.status - } - - /// Checks if the process is terminated. - pub fn is_terminated(&mut self) -> bool { - self.update_status(false); - - Self::STATUS_TERMINATED == self.status - } - - /// Stops the process. - pub fn stop(&mut self, timeout: f64, signal: Option) -> Option { - let timeout_micro = shirabe_php_shim::microtime() + 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 { - shirabe_php_shim::usleep(1000); - if !(self.is_running() && shirabe_php_shim::microtime() < 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 - } - - /// Adds a line to the STDOUT stream. - pub fn add_output(&mut self, line: &str) { - let stdout = self.stdout.as_ref().unwrap(); - shirabe_php_shim::fseek(stdout, 0, shirabe_php_shim::SEEK_END); - shirabe_php_shim::fwrite(stdout, line, Some(line.len() as i64)); - shirabe_php_shim::fseek( - stdout, - self.incremental_output_offset, - shirabe_php_shim::SEEK_SET, - ); - } - - /// Adds a line to the STDERR stream. - pub fn add_error_output(&mut self, line: &str) { - let stderr = self.stderr.as_ref().unwrap(); - shirabe_php_shim::fseek(stderr, 0, shirabe_php_shim::SEEK_END); - shirabe_php_shim::fwrite(stderr, line, Some(line.len() as i64)); - shirabe_php_shim::fseek( - stderr, - self.incremental_error_output_offset, - shirabe_php_shim::SEEK_SET, - ); - } - - /// 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::>() - .join(" "), - CommandLine::String(s) => s.clone(), - } - } - - /// Gets the process timeout in seconds (max. runtime). - pub fn get_timeout(&self) -> Option { - self.timeout - } - - /// Sets the process timeout (max. runtime) in seconds. - pub fn set_timeout(&mut self, timeout: Option) -> anyhow::Result<&mut Self> { - self.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 cfg!(windows) && 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 - } - - /// Gets the working directory. - pub fn get_working_directory(&self) -> Option { - 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 shirabe_php_shim::getcwd().filter(|s| !s.is_empty()); - } - - self.cwd.clone() - } - - /// Sets the environment variables. - pub fn set_env(&mut self, env: IndexMap) -> &mut Self { - self.env = env; - - self - } - - /// 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 - && timeout < shirabe_php_shim::microtime() - self.starttime.unwrap_or(0.0) - { - self.stop(0.0, None); - - return Err(ProcessTimedOutException::new(self).into()); - } - - Ok(()) - } - - /// Returns whether TTY is supported on the current operating system. - pub fn is_tty_supported() -> bool { - static IS_TTY_SUPPORTED: OnceLock = OnceLock::new(); - - *IS_TTY_SUPPORTED.get_or_init(|| { - let mut pipes = IndexMap::new(); - shirabe_php_shim::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, - ) - .is_ok() - }) - } - - /// Creates the descriptors needed by the proc_open. - fn get_descriptors(&mut self) -> Vec { - // TODO(plugin): $this->input instanceof \Iterator -> rewind() is not modeled. - if cfg!(windows) { - self.process_pipes = Some(Box::new(WindowsPipes::new(self.input.clone()))); - } else { - self.process_pipes = Some(Box::new(UnixPipes::new( - Some(self.is_tty()), - self.input.clone(), - ))); - } - - self.process_pipes.as_mut().unwrap().get_descriptors() - } - - /// Builds up the callback used by wait(). - fn build_callback(&self, callback: Option) -> ProcessCallback { - let mut callback = callback; - 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(shirabe_php_shim::proc_get_status( - self.process.as_ref().unwrap(), - )); - let running = self - .process_information - .as_ref() - .unwrap() - .get("running") - .map(shirabe_php_shim::php_truthy) - .unwrap_or(false); - - // In PHP < 8.3, "proc_get_status" only returns the correct exit status on the first call. - if shirabe_php_shim::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 - && !running - && exitcode == Some(-1) - { - self.process_information - .as_mut() - .unwrap() - .insert("exitcode".to_string(), PhpMixed::Int(cached)); - } - } - - self.read_pipes(running && blocking, !cfg!(windows) || !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(); - } - } - - /// Returns whether PHP has been compiled with the '--enable-sigchild' option or not. - fn is_sigchild_enabled(&self) -> bool { - static SIGCHILD: OnceLock = OnceLock::new(); - - if let Some(v) = SIGCHILD.get() { - return *v; - } - - if !shirabe_php_shim::function_exists("phpinfo") { - return *SIGCHILD.get_or_init(|| false); - } - - shirabe_php_shim::ob_start(); - shirabe_php_shim::phpinfo(shirabe_php_shim::INFO_GENERAL); - - *SIGCHILD.get_or_init(|| { - shirabe_php_shim::str_contains( - &shirabe_php_shim::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<()> { - self.require_process_is_started(caller)?; - - self.update_status(blocking); - Ok(()) - } - - /// Validates and returns the filtered timeout. - fn validate_timeout(&self, timeout: Option) -> anyhow::Result> { - 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 self.process.is_some() { - shirabe_php_shim::proc_close(self.process.as_ref().unwrap()); - self.process = None; - } - 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(shirabe_php_shim::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() - && 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; - // php://temp is an in-memory stream; fopen never fails for it. - self.stdout = Some( - shirabe_php_shim::fopen(&format!("php://temp/maxmemory:{}", 1024 * 1024), "w+") - .unwrap(), - ); - self.stderr = Some( - shirabe_php_shim::fopen(&format!("php://temp/maxmemory:{}", 1024 * 1024), "w+") - .unwrap(), - ); - self.process = None; - 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 { - 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 cfg!(windows) { - let mut output: Vec = Vec::new(); - let mut exit_code: i64 = 0; - shirabe_php_shim::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 = shirabe_php_shim::proc_terminate(self.process.as_ref().unwrap(), signal); - } else if shirabe_php_shim::function_exists("posix_kill") { - ok = shirabe_php_shim::posix_kill(pid, signal); - } else { - let mut pipes = IndexMap::new(); - let opened = shirabe_php_shim::proc_open( - &format!("kill -{} {}", signal, pid), - &[ - Descriptor::Inherit, - Descriptor::Inherit, - descriptor(&["pipe", "w"]), - ], - &mut pipes, - None, - None, - None, - ); - ok = match opened { - Ok(_) => pipes - .get(&2) - .and_then(|p| shirabe_php_shim::fgets(p, None)) - .is_none(), - Err(_) => false, - }; - } - 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, - ) -> anyhow::Result { - let uid = shirabe_php_shim::uniqid("", true); - let mut var_count = 0; - let mut var_cache: IndexMap = IndexMap::new(); - let cmd = shirabe_php_shim::preg_replace_callback( - php_regex!( - r#"/"(?:( - [^"%!^]*+ - (?: - (?: !LF! | "(?:\^[%!^])?+" ) - [^"%!^]*+ - )++ - ) | [^"]*+ )"/x"# - ), - |m: &[Option]| -> anyhow::Result { - 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 shirabe_php_shim::strpbrk(&value, "\"%!\n").is_none() { - return Ok(format!("\"{}\"", value)); - } - - for (from, to) in [ - ("!LF!", "\n"), - ("\"^!\"", "!"), - ("\"^%\"", "%"), - ("\"^^\"", "^"), - ("\"\"", "\""), - ] { - value = value.replace(from, to); - } - value = format!( - "\"{}\"", - shirabe_php_shim::preg_replace(php_regex!(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> = OnceLock::new(); - let com_spec = COM_SPEC - .get_or_init(|| { - ExecutableFinder::new() - .find("cmd.exe", None, &[]) - .map(|spec| { - format!( - "\"{}\"", - shirabe_php_shim::preg_replace( - php_regex!(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 !cfg!(windows) { - return format!("'{}'", argument.replace('\'', "'\\''")); - } - let mut argument = argument.to_string(); - if argument.contains('\0') { - argument = argument.replace('\0', "?"); - } - if !shirabe_php_shim::preg_match( - php_regex!(r#"/[()%!^"<>&|\s\[\]=;*?'$]/"#), - &argument, - &mut Vec::new(), - ) { - return argument; - } - argument = shirabe_php_shim::preg_replace(php_regex!(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, - ) -> anyhow::Result { - shirabe_php_shim::preg_replace_callback( - php_regex!(r#"/"\$\{:([_a-zA-Z]+[_a-zA-Z0-9]*)\}"/"#), - |matches: &[Option]| -> anyhow::Result { - 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(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 { - let env: IndexMap = shirabe_php_shim::getenv_all() - .map(|(k, v)| { - ( - k.to_string_lossy().into_owned(), - v.to_string_lossy().into_owned(), - ) - }) - .collect(); - let server = shirabe_php_shim::PHP_SERVER.lock().unwrap(); - - // non-Windows: array_intersect_key($env, $_SERVER) ?: $env - let mut intersect: IndexMap = IndexMap::new(); - for (k, v) in &env { - if server.get(k).is_some() { - intersect.insert(k.clone(), PhpMixed::String(v.clone())); - } - } - let env_map: IndexMap = if intersect.is_empty() { - env.into_iter() - .map(|(k, v)| (k, PhpMixed::String(v))) - .collect() - } else { - intersect - }; - - // $_ENV + env_map - let mut result: IndexMap = shirabe_php_shim::PHP_ENV - .lock() - .unwrap() - .get_all() - .map(|(k, v)| { - ( - k.to_string_lossy().into_owned(), - PhpMixed::String(v.to_string_lossy().into_owned()), - ) - }) - .collect(); - for (k, v) in env_map { - result.entry(k).or_insert(v); - } - result - } -} - -impl Drop for Process { - fn drop(&mut self) { - 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 deleted file mode 100644 index 71943b80..00000000 --- a/crates/shirabe-external-packages/src/symfony/process/process_utils.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! ref: composer/vendor/symfony/process/ProcessUtils.php - -use crate::symfony::process::exception::invalid_argument_exception::InvalidArgumentException; -use shirabe_php_shim::PhpMixed; - -/// 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 { - if !input.is_null() { - if shirabe_php_shim::is_string(&input) { - return Ok(input); - } - if shirabe_php_shim::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-rpc/Cargo.toml b/crates/shirabe-php-rpc/Cargo.toml index e4675faa..76775319 100644 --- a/crates/shirabe-php-rpc/Cargo.toml +++ b/crates/shirabe-php-rpc/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true shirabe-external-packages.workspace = true shirabe-php-shim.workspace = true shirabe-php-src.workspace = true +shirabe-symfony-process.workspace = true anyhow.workspace = true indexmap.workspace = true nix.workspace = true diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs index eac6f523..6dfdebf5 100644 --- a/crates/shirabe-php-rpc/src/lib.rs +++ b/crates/shirabe-php-rpc/src/lib.rs @@ -8,8 +8,8 @@ pub use value::{PhpClassHandle, PhpObjHandle, PhpObject, PluginValue, RustObjHan use frame::Frame; use indexmap::IndexMap; -use shirabe_external_packages::symfony::process::PhpExecutableFinder; use shirabe_php_shim::PhpMixed; +use shirabe_symfony_process::PhpExecutableFinder; use std::os::unix::net::UnixStream; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{LazyLock, Mutex, OnceLock}; diff --git a/crates/shirabe-php-rpc/tests/generated_stubs.rs b/crates/shirabe-php-rpc/tests/generated_stubs.rs index b4ff8197..0980745e 100644 --- a/crates/shirabe-php-rpc/tests/generated_stubs.rs +++ b/crates/shirabe-php-rpc/tests/generated_stubs.rs @@ -6,7 +6,7 @@ //! report; when any of those is missing the test returns early, following the non-mock test //! convention of this crate. -use shirabe_external_packages::symfony::process::PhpExecutableFinder; +use shirabe_symfony_process::PhpExecutableFinder; use std::path::Path; #[test] diff --git a/crates/shirabe-php-rpc/tests/oracle.rs b/crates/shirabe-php-rpc/tests/oracle.rs index e1e1143b..b98f2203 100644 --- a/crates/shirabe-php-rpc/tests/oracle.rs +++ b/crates/shirabe-php-rpc/tests/oracle.rs @@ -7,9 +7,9 @@ //! focus areas. use indexmap::IndexMap; -use shirabe_external_packages::symfony::process::PhpExecutableFinder; use shirabe_php_rpc::value::{serialize, unserialize}; use shirabe_php_rpc::{PhpObject, PluginValue, call_function}; +use shirabe_symfony_process::PhpExecutableFinder; fn php_available() -> bool { PhpExecutableFinder::new().find(false).is_some() diff --git a/crates/shirabe-symfony-process/Cargo.toml b/crates/shirabe-symfony-process/Cargo.toml new file mode 100644 index 00000000..e0dfa684 --- /dev/null +++ b/crates/shirabe-symfony-process/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "shirabe-symfony-process" +version.workspace = true +edition.workspace = true + +[dependencies] +shirabe-php-shim.workspace = true +anyhow.workspace = true +indexmap.workspace = true + +[lints] +workspace = true diff --git a/crates/shirabe-symfony-process/src/exception.rs b/crates/shirabe-symfony-process/src/exception.rs new file mode 100644 index 00000000..689a64b6 --- /dev/null +++ b/crates/shirabe-symfony-process/src/exception.rs @@ -0,0 +1,13 @@ +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::*; +pub use runtime_exception::*; diff --git a/crates/shirabe-symfony-process/src/exception/invalid_argument_exception.rs b/crates/shirabe-symfony-process/src/exception/invalid_argument_exception.rs new file mode 100644 index 00000000..c9a42653 --- /dev/null +++ b/crates/shirabe-symfony-process/src/exception/invalid_argument_exception.rs @@ -0,0 +1,20 @@ +//! ref: composer/vendor/symfony/process/Exception/InvalidArgumentException.php + +#[derive(Debug)] +pub struct InvalidArgumentException { + inner: shirabe_php_shim::InvalidArgumentException, +} + +impl InvalidArgumentException { + pub fn new(message: String) -> Self { + Self { + inner: shirabe_php_shim::InvalidArgumentException::new(message), + } + } +} + +shirabe_php_shim::impl_php_exception!( + InvalidArgumentException, + inner, + r"Symfony\Component\Process\Exception\InvalidArgumentException" +); diff --git a/crates/shirabe-symfony-process/src/exception/logic_exception.rs b/crates/shirabe-symfony-process/src/exception/logic_exception.rs new file mode 100644 index 00000000..36e9bcca --- /dev/null +++ b/crates/shirabe-symfony-process/src/exception/logic_exception.rs @@ -0,0 +1,20 @@ +//! ref: composer/vendor/symfony/process/Exception/LogicException.php + +#[derive(Debug)] +pub struct LogicException { + inner: shirabe_php_shim::LogicException, +} + +impl LogicException { + pub fn new(message: String) -> Self { + Self { + inner: shirabe_php_shim::LogicException::new(message), + } + } +} + +shirabe_php_shim::impl_php_exception!( + LogicException, + inner, + r"Symfony\Component\Process\Exception\LogicException" +); diff --git a/crates/shirabe-symfony-process/src/exception/process_failed_exception.rs b/crates/shirabe-symfony-process/src/exception/process_failed_exception.rs new file mode 100644 index 00000000..76520b33 --- /dev/null +++ b/crates/shirabe-symfony-process/src/exception/process_failed_exception.rs @@ -0,0 +1,48 @@ +//! ref: composer/vendor/symfony/process/Exception/ProcessFailedException.php + +use crate::exception::invalid_argument_exception::InvalidArgumentException; +use crate::exception::runtime_exception::RuntimeException; +use crate::process::Process; + +#[derive(Debug)] +pub struct ProcessFailedException { + inner: RuntimeException, +} + +impl ProcessFailedException { + pub fn new(process: &mut Process) -> anyhow::Result { + 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(), + ); + + error += &format!( + "\n\nOutput:\n================\n{}\n\nError Output:\n================\n{}", + process.get_output()?, + process.get_error_output()?, + ); + + Ok(Self { + inner: RuntimeException::new(error), + }) + } +} + +shirabe_php_shim::impl_php_exception!( + ProcessFailedException, + inner, + r"Symfony\Component\Process\Exception\ProcessFailedException" +); diff --git a/crates/shirabe-symfony-process/src/exception/process_signaled_exception.rs b/crates/shirabe-symfony-process/src/exception/process_signaled_exception.rs new file mode 100644 index 00000000..e570c80d --- /dev/null +++ b/crates/shirabe-symfony-process/src/exception/process_signaled_exception.rs @@ -0,0 +1,34 @@ +//! ref: composer/vendor/symfony/process/Exception/ProcessSignaledException.php + +use crate::exception::runtime_exception::RuntimeException; +use crate::process::Process; + +#[derive(Debug)] +pub struct ProcessSignaledException { + inner: RuntimeException, + signal: i64, +} + +impl ProcessSignaledException { + pub fn new(process: &mut Process) -> anyhow::Result { + let signal = process.get_term_signal()?; + + Ok(Self { + inner: RuntimeException::new(format!( + "The process has been signaled with signal \"{}\".", + signal + )), + signal, + }) + } + + pub fn get_signal(&self) -> i64 { + self.signal + } +} + +shirabe_php_shim::impl_php_exception!( + ProcessSignaledException, + inner, + r"Symfony\Component\Process\Exception\ProcessSignaledException" +); diff --git a/crates/shirabe-symfony-process/src/exception/process_timed_out_exception.rs b/crates/shirabe-symfony-process/src/exception/process_timed_out_exception.rs new file mode 100644 index 00000000..b7e6595d --- /dev/null +++ b/crates/shirabe-symfony-process/src/exception/process_timed_out_exception.rs @@ -0,0 +1,31 @@ +//! ref: composer/vendor/symfony/process/Exception/ProcessTimedOutException.php + +use crate::exception::runtime_exception::RuntimeException; +use crate::process::Process; + +#[derive(Debug)] +pub struct ProcessTimedOutException { + inner: RuntimeException, +} + +impl ProcessTimedOutException { + pub fn new(process: &Process) -> Self { + let exceeded_timeout = process.get_timeout(); + + 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 { + inner: RuntimeException::new(message), + } + } +} + +shirabe_php_shim::impl_php_exception!( + ProcessTimedOutException, + inner, + r"Symfony\Component\Process\Exception\ProcessTimedOutException" +); diff --git a/crates/shirabe-symfony-process/src/exception/runtime_exception.rs b/crates/shirabe-symfony-process/src/exception/runtime_exception.rs new file mode 100644 index 00000000..e9cc31e0 --- /dev/null +++ b/crates/shirabe-symfony-process/src/exception/runtime_exception.rs @@ -0,0 +1,20 @@ +//! ref: composer/vendor/symfony/process/Exception/RuntimeException.php + +#[derive(Debug)] +pub struct RuntimeException { + inner: shirabe_php_shim::RuntimeException, +} + +impl RuntimeException { + pub fn new(message: String) -> Self { + Self { + inner: shirabe_php_shim::RuntimeException::new(message), + } + } +} + +shirabe_php_shim::impl_php_exception!( + RuntimeException, + inner, + r"Symfony\Component\Process\Exception\RuntimeException" +); diff --git a/crates/shirabe-symfony-process/src/executable_finder.rs b/crates/shirabe-symfony-process/src/executable_finder.rs new file mode 100644 index 00000000..5b1c0994 --- /dev/null +++ b/crates/shirabe-symfony-process/src/executable_finder.rs @@ -0,0 +1,116 @@ +//! ref: composer/vendor/symfony/process/ExecutableFinder.php + +const CMD_BUILTINS: &[&str] = &[ + "assoc", "break", "call", "cd", "chdir", "cls", "color", "copy", "date", "del", "dir", "echo", + "endlocal", "erase", "exit", "for", "ftype", "goto", "help", "if", "label", "md", "mkdir", + "mklink", "move", "path", "pause", "popd", "prompt", "pushd", "rd", "rem", "ren", "rename", + "rmdir", "set", "setlocal", "shift", "start", "time", "title", "type", "ver", "vol", +]; + +#[derive(Debug)] +pub struct ExecutableFinder { + suffixes: Vec, +} + +impl Default for ExecutableFinder { + fn default() -> Self { + Self::new() + } +} + +impl ExecutableFinder { + pub fn new() -> Self { + Self { suffixes: vec![] } + } + + pub fn find(&self, name: &str, default: Option<&str>, extra_dirs: &[String]) -> Option { + // windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes + if cfg!(windows) && CMD_BUILTINS.contains(&shirabe_php_shim::strtolower(name).as_str()) { + return Some(name.to_string()); + } + + let path = shirabe_php_shim::getenv("PATH") + .or_else(|| shirabe_php_shim::getenv("Path")) + .map(|v| v.to_string_lossy().into_owned()) + .unwrap_or_default(); + let mut dirs: Vec = std::env::split_paths(&path) + .map(|dir| dir.into_os_string().into_string().unwrap()) + .collect(); + dirs.extend_from_slice(extra_dirs); + + let mut suffixes: Vec = vec![]; + if cfg!(windows) { + let path_ext = + shirabe_php_shim::getenv("PATHEXT").map(|v| v.to_string_lossy().into_owned()); + suffixes = self.suffixes.clone(); + let exts = match path_ext { + Some(ref ext) if !ext.is_empty() => std::env::split_paths(ext) + .map(|e| e.into_os_string().into_string().unwrap()) + .collect(), + _ => vec![ + ".exe".to_string(), + ".bat".to_string(), + ".cmd".to_string(), + ".com".to_string(), + ], + }; + suffixes.extend(exts); + } + suffixes = + if !shirabe_php_shim::pathinfo(name, shirabe_php_shim::PATHINFO_EXTENSION).is_empty() { + let mut s = vec![String::new()]; + s.extend(suffixes); + s + } else { + suffixes.push(String::new()); + suffixes + }; + for suffix in &suffixes { + for dir in &dirs { + let dir = if dir.is_empty() { "." } else { dir.as_str() }; + let file = std::path::Path::new(dir) + .join(format!("{name}{suffix}")) + .into_os_string() + .into_string() + .unwrap(); + if shirabe_php_shim::is_file(&file) + && (cfg!(windows) || shirabe_php_shim::is_executable(&file)) + { + return Some(file); + } + + if !shirabe_php_shim::is_dir(dir) + && shirabe_php_shim::basename(dir) == format!("{name}{suffix}") + && shirabe_php_shim::is_executable(dir) + { + return Some(dir.to_string()); + } + } + } + + if cfg!(windows) + || name.len() + != shirabe_php_shim::strcspn(name, &format!("/{}", std::path::MAIN_SEPARATOR)) + { + return default.map(ToString::to_string); + } + + let exec_result = shirabe_php_shim::exec( + &format!("command -v -- {}", shirabe_php_shim::escapeshellarg(name)), + None, + None, + ) + .unwrap_or_default(); + + let executable_path = shirabe_php_shim::substr( + &exec_result, + 0, + shirabe_php_shim::strpos(&exec_result, shirabe_php_shim::PHP_EOL).map(|i| i as i64), + ); + if !executable_path.is_empty() && shirabe_php_shim::is_executable(&executable_path) { + return Some(executable_path); + } + + default.map(ToString::to_string) + } +} diff --git a/crates/shirabe-symfony-process/src/lib.rs b/crates/shirabe-symfony-process/src/lib.rs new file mode 100644 index 00000000..3a5656c6 --- /dev/null +++ b/crates/shirabe-symfony-process/src/lib.rs @@ -0,0 +1,11 @@ +pub mod exception; +pub mod executable_finder; +pub mod php_executable_finder; +pub(crate) mod pipes; +pub mod process; +pub(crate) mod process_utils; + +pub use exception::*; +pub use executable_finder::*; +pub use php_executable_finder::*; +pub use process::*; diff --git a/crates/shirabe-symfony-process/src/php_executable_finder.rs b/crates/shirabe-symfony-process/src/php_executable_finder.rs new file mode 100644 index 00000000..e8661866 --- /dev/null +++ b/crates/shirabe-symfony-process/src/php_executable_finder.rs @@ -0,0 +1,70 @@ +//! ref: composer/vendor/symfony/process/PhpExecutableFinder.php + +use super::executable_finder::ExecutableFinder; + +#[derive(Debug)] +pub struct PhpExecutableFinder { + executable_finder: ExecutableFinder, +} + +impl Default for PhpExecutableFinder { + fn default() -> Self { + Self::new() + } +} + +impl PhpExecutableFinder { + pub fn new() -> Self { + Self { + executable_finder: ExecutableFinder::new(), + } + } + + /// Finds The PHP executable. + pub fn find(&self, _include_args: bool) -> Option { + if let Some(php) = shirabe_php_shim::getenv("PHP_BINARY").filter(|v| !v.is_empty()) { + let mut php = php.to_string_lossy().into_owned(); + if !shirabe_php_shim::is_executable(&php) { + match self.executable_finder.find(&php, None, &[]) { + Some(found) => php = found, + None => return None, + } + } + + if shirabe_php_shim::is_dir(&php) { + return None; + } + + return Some(php); + } + + // The original `\PHP_BINARY && \PHP_SAPI` branch describes the running PHP interpreter. + // These constants cannot be obtained in Rust, the branch is skipped here. + + if let Some(php) = shirabe_php_shim::getenv("PHP_PATH").filter(|v| !v.is_empty()) { + let php = php.to_string_lossy().into_owned(); + if !shirabe_php_shim::is_executable(&php) || shirabe_php_shim::is_dir(&php) { + return None; + } + + return Some(php); + } + + if let Some(php) = shirabe_php_shim::getenv("PHP_PEAR_PHP_BIN").filter(|v| !v.is_empty()) { + let php = php.to_string_lossy().into_owned(); + if shirabe_php_shim::is_executable(&php) && !shirabe_php_shim::is_dir(&php) { + return Some(php); + } + } + + // Even if `\PHP_BINDIR` is unavailable, searching `$PATH` should be performed. + self.executable_finder.find("php", None, &[]) + } + + /// Finds the PHP executable arguments. + pub fn find_arguments(&self) -> Vec { + // If PHP_SAPI is not "phpdbg", returns an empty array. In Rust, PHP_SAPI is always "cli", + // so always returns an empty array. + vec![] + } +} diff --git a/crates/shirabe-symfony-process/src/pipes.rs b/crates/shirabe-symfony-process/src/pipes.rs new file mode 100644 index 00000000..6c3ea7dd --- /dev/null +++ b/crates/shirabe-symfony-process/src/pipes.rs @@ -0,0 +1,4 @@ +pub mod abstract_pipes; +pub mod pipes_interface; +pub mod unix_pipes; +pub mod windows_pipes; diff --git a/crates/shirabe-symfony-process/src/pipes/abstract_pipes.rs b/crates/shirabe-symfony-process/src/pipes/abstract_pipes.rs new file mode 100644 index 00000000..8741926c --- /dev/null +++ b/crates/shirabe-symfony-process/src/pipes/abstract_pipes.rs @@ -0,0 +1,104 @@ +//! ref: composer/vendor/symfony/process/Pipes/AbstractPipes.php + +use indexmap::IndexMap; +use shirabe_php_shim::{PhpMixed, PhpResource}; + +#[derive(Debug)] +pub struct AbstractPipes { + pub pipes: IndexMap, + + input_buffer: String, + input: PhpMixed, + blocked: bool, + last_error: Option, +} + +impl AbstractPipes { + pub fn new(input: PhpMixed) -> Self { + let input_buffer; + let stored_input; + // TODO(plugin): `$input instanceof \Iterator` is not modeled. The PHP `is_resource($input)` + // branch never applies: a PhpMixed is never a resource, so input is never stored as-is here. + 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: IndexMap::new(), + input_buffer, + input: stored_input, + blocked: true, + last_error: None, + } + } + + pub fn close(&mut self) { + for (_, pipe) in &self.pipes { + shirabe_php_shim::fclose(pipe); + } + self.pipes = IndexMap::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) { + if !self.blocked { + return; + } + + for (_, pipe) in &self.pipes { + shirabe_php_shim::stream_set_blocking(pipe, false); + } + // The `is_resource($this->input)` branch does not apply: `input` is never a resource in this + // port (is_resource on a PhpMixed is always false). + + self.blocked = false; + } + + /// Writes input to stdin. + pub(crate) fn write(&mut self) -> Option> { + let stdin = self.pipes.get(&0)?.clone(); + + // TODO(plugin): the `$input instanceof \Iterator` branch is not modeled. `input` is never a + // resource here, so the fread($input)/stream_set_blocking($input) paths do not apply and + // only the input buffer is written to stdin. + + let mut r: Vec = Vec::new(); + let mut e: Vec = Vec::new(); + let mut w: Vec = vec![stdin.clone()]; + + // let's have a look if something changed in streams + shirabe_php_shim::stream_select(&mut r, &mut w, &mut e, 0, Some(0))?; + + if !self.input_buffer.is_empty() { + let written = + shirabe_php_shim::fwrite(&stdin, &self.input_buffer, None).unwrap_or(0) as usize; + self.input_buffer = self.input_buffer.get(written..).unwrap_or("").to_string(); + if !self.input_buffer.is_empty() { + return Some(vec![stdin]); + } + } + + // no input to read on resource, buffer is empty + if self.input_buffer.is_empty() && !shirabe_php_shim::php_truthy(&self.input) { + self.input = PhpMixed::Null; + shirabe_php_shim::fclose(&stdin); + self.pipes.shift_remove(&0); + } + + None + } +} diff --git a/crates/shirabe-symfony-process/src/pipes/pipes_interface.rs b/crates/shirabe-symfony-process/src/pipes/pipes_interface.rs new file mode 100644 index 00000000..46609bb8 --- /dev/null +++ b/crates/shirabe-symfony-process/src/pipes/pipes_interface.rs @@ -0,0 +1,28 @@ +//! ref: composer/vendor/symfony/process/Pipes/PipesInterface.php + +use indexmap::IndexMap; +use shirabe_php_shim::{Descriptor, PhpResource}; + +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; + + /// Returns an array of filenames indexed by their related stream in case these pipes use temporary files. + fn get_files(&self) -> IndexMap; + + /// Reads data in file handles and pipes. + fn read_and_write(&mut self, blocking: bool, close: bool) -> IndexMap; + + /// Returns if the current state has open file handles or pipes. + fn are_open(&self) -> bool; + + /// Closes file handles and pipes. + fn close(&mut self); + + /// Accessor for the `pipes` property populated by proc_open, keyed by fd index. + fn pipes(&self) -> &IndexMap; + fn pipes_mut(&mut self) -> &mut IndexMap; +} diff --git a/crates/shirabe-symfony-process/src/pipes/unix_pipes.rs b/crates/shirabe-symfony-process/src/pipes/unix_pipes.rs new file mode 100644 index 00000000..bfc93d44 --- /dev/null +++ b/crates/shirabe-symfony-process/src/pipes/unix_pipes.rs @@ -0,0 +1,136 @@ +//! ref: composer/vendor/symfony/process/Pipes/UnixPipes.php + +use crate::pipes::abstract_pipes::AbstractPipes; +use crate::pipes::pipes_interface::{CHUNK_SIZE, PipesInterface}; +use crate::process::Process; +use indexmap::IndexMap; +use shirabe_php_shim::{Descriptor, PhpMixed, PhpResource}; + +/// UnixPipes implementation uses unix pipes as handles. +#[derive(Debug)] +pub struct UnixPipes { + inner: AbstractPipes, + tty_mode: Option, +} + +impl UnixPipes { + pub fn new(tty_mode: Option, input: PhpMixed) -> Self { + Self { + inner: AbstractPipes::new(input), + tty_mode, + } + } +} + +fn descriptor(items: &[&str]) -> Descriptor { + match items { + ["pipe", mode] => Descriptor::Pipe(mode.to_string()), + ["file", path, mode] => Descriptor::File(path.to_string(), mode.to_string()), + _ => panic!("unsupported descriptor spec: {:?}", items), + } +} + +impl PipesInterface for UnixPipes { + fn get_descriptors(&mut self) -> Vec { + if self.tty_mode == Some(true) { + return vec![ + descriptor(&["file", "/dev/tty", "r"]), + descriptor(&["file", "/dev/tty", "w"]), + descriptor(&["file", "/dev/tty", "w"]), + ]; + } + + vec![ + descriptor(&["pipe", "r"]), + descriptor(&["pipe", "w"]), + descriptor(&["pipe", "w"]), + ] + } + + fn get_files(&self) -> IndexMap { + IndexMap::new() + } + + fn read_and_write(&mut self, blocking: bool, close: bool) -> IndexMap { + self.inner.unblock(); + let w = self.inner.write(); + + let mut read: IndexMap = IndexMap::new(); + // $r = $this->pipes; unset($r[0]); + let r: Vec<(i64, PhpResource)> = self + .inner + .pipes + .iter() + .filter(|(fd, _)| **fd != 0) + .map(|(fd, pipe)| (*fd, pipe.clone())) + .collect(); + + // TODO(plugin): set_error_handler/restore_error_handler around stream_select is not modeled. + let mut r_sel: Vec = r.iter().map(|(_, p)| p.clone()).collect(); + let mut w_sel: Vec = w.clone().unwrap_or_default(); + let mut e_sel: Vec = Vec::new(); + + // let's have a look if something changed in streams + if (!r_sel.is_empty() || w.is_some()) + && shirabe_php_shim::stream_select( + &mut r_sel, + &mut w_sel, + &mut e_sel, + 0, + Some(if blocking { + (Process::TIMEOUT_PRECISION * 1e6) as i64 + } else { + 0 + }), + ) + .is_none() + { + // if a system call has been interrupted, forget about it, let's try again + // otherwise, an error occurred, let's reset pipes + if !self.inner.has_system_call_been_interrupted() { + self.inner.pipes = IndexMap::new(); + } + + return read; + } + + for (fd, pipe) in &r { + let mut data = String::new(); + loop { + let chunk = shirabe_php_shim::fread(pipe, CHUNK_SIZE).unwrap_or_default(); + let len = chunk.len() as i64; + data.push_str(&chunk); + if !(len > 0 && (close || len >= CHUNK_SIZE)) { + break; + } + } + + if !data.is_empty() { + read.insert(*fd, data); + } + + if close && shirabe_php_shim::feof(pipe) { + shirabe_php_shim::fclose(pipe); + self.inner.pipes.shift_remove(fd); + } + } + + read + } + + fn are_open(&self) -> bool { + !self.inner.pipes.is_empty() + } + + fn close(&mut self) { + self.inner.close(); + } + + fn pipes(&self) -> &IndexMap { + &self.inner.pipes + } + + fn pipes_mut(&mut self) -> &mut IndexMap { + &mut self.inner.pipes + } +} diff --git a/crates/shirabe-symfony-process/src/pipes/windows_pipes.rs b/crates/shirabe-symfony-process/src/pipes/windows_pipes.rs new file mode 100644 index 00000000..87bf5126 --- /dev/null +++ b/crates/shirabe-symfony-process/src/pipes/windows_pipes.rs @@ -0,0 +1,60 @@ +//! ref: composer/vendor/symfony/process/Pipes/WindowsPipes.php + +use crate::pipes::abstract_pipes::AbstractPipes; +use crate::pipes::pipes_interface::PipesInterface; +use indexmap::IndexMap; +use shirabe_php_shim::{Descriptor, PhpMixed, PhpResource}; + +/// WindowsPipes implementation uses temporary files as handles. +#[derive(Debug)] +pub struct WindowsPipes { + inner: AbstractPipes, + files: IndexMap, + file_handles: IndexMap, + lock_handles: IndexMap, + read_bytes: IndexMap, +} + +impl WindowsPipes { + pub fn new(_input: PhpMixed) -> Self { + // Windows-only path: never constructed on non-Windows targets. + todo!() + } +} + +impl PipesInterface for WindowsPipes { + fn get_descriptors(&mut self) -> Vec { + let _ = ( + &self.files, + &self.file_handles, + &self.lock_handles, + &self.read_bytes, + ); + todo!() + } + + fn get_files(&self) -> IndexMap { + self.files.clone() + } + + fn read_and_write(&mut self, _blocking: bool, _close: bool) -> IndexMap { + todo!() + } + + fn are_open(&self) -> bool { + !self.inner.pipes.is_empty() && !self.file_handles.is_empty() + } + + fn close(&mut self) { + self.inner.close(); + todo!() + } + + fn pipes(&self) -> &IndexMap { + &self.inner.pipes + } + + fn pipes_mut(&mut self) -> &mut IndexMap { + &mut self.inner.pipes + } +} diff --git a/crates/shirabe-symfony-process/src/process.rs b/crates/shirabe-symfony-process/src/process.rs new file mode 100644 index 00000000..6a078b9e --- /dev/null +++ b/crates/shirabe-symfony-process/src/process.rs @@ -0,0 +1,1293 @@ +//! ref: composer/vendor/symfony/process/Process.php + +use crate::exception::invalid_argument_exception::InvalidArgumentException; +use crate::exception::logic_exception::LogicException; +use crate::exception::process_signaled_exception::ProcessSignaledException; +use crate::exception::process_timed_out_exception::ProcessTimedOutException; +use crate::exception::runtime_exception::RuntimeException; +use crate::executable_finder::ExecutableFinder; +use crate::pipes::pipes_interface::PipesInterface; +use crate::pipes::unix_pipes::UnixPipes; +use crate::pipes::windows_pipes::WindowsPipes; +use crate::process_utils::ProcessUtils; +use indexmap::IndexMap; +use shirabe_php_shim::{Descriptor, PhpMixed, PhpResource, php_regex}; +use std::sync::OnceLock; + +/// A user-supplied callback invoked with the output type ("out"/"err") and a chunk of output. +pub type UserCallback = Box 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 bool>; + +/// PHP `$this->commandline` is `array|string`. +#[derive(Debug, Clone)] +enum CommandLine { + Array(Vec), + String(String), +} + +/// Test-only behaviour for a Process fabricated via [`Process::__mock`]: `getOutput`/ +/// `getErrorOutput`/`getExitCode`/`isSuccessful` return these fixed values instead of reading a +/// real subprocess. Mirrors PHPUnit's `getMockBuilder(Process::class)->disableOriginalConstructor()` +/// mocks used by the Composer test suite (e.g. `ZipDownloaderTest`). Held in [`Process::mock`]; +/// always `None` in production. +#[derive(Debug, Clone)] +pub struct ProcessMock { + pub exit_code: i64, + pub stdout: String, + pub stderr: String, +} + +/// Process is a thin wrapper around proc_* functions to easily +/// start independent PHP processes. +pub struct Process { + callback: Option, + commandline: CommandLine, + cwd: Option, + env: IndexMap, + input: PhpMixed, + starttime: Option, + timeout: Option, + exitcode: Option, + fallback_status: IndexMap, + process_information: Option>, + stdout: Option, + stderr: Option, + process: Option, + status: String, + incremental_output_offset: i64, + incremental_error_output_offset: i64, + tty: bool, + options: IndexMap, + use_file_handles: bool, + process_pipes: Option>, + latest_signal: Option, + cached_exit_code: Option, + /// Test-only mock state. `None` in production; set via [`Process::__mock`] in tests. + mock: Option, +} + +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() + } +} + +fn descriptor(items: &[&str]) -> Descriptor { + match items { + ["pipe", mode] => Descriptor::Pipe(mode.to_string()), + ["file", path, mode] => Descriptor::File(path.to_string(), mode.to_string()), + _ => panic!("unsupported descriptor spec: {:?}", items), + } +} + +/// 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 STDOUT: i64 = 1; + pub const STDERR: i64 = 2; + + // Timeout Precision in seconds. + pub const TIMEOUT_PRECISION: f64 = 0.2; + + /// 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, + commandline: CommandLine::Array(Vec::new()), + cwd: None, + env: IndexMap::new(), + input: PhpMixed::Null, + starttime: None, + timeout: None, + exitcode: None, + fallback_status: IndexMap::new(), + process_information: None, + stdout: None, + stderr: None, + process: None, + status: Self::STATUS_READY.to_string(), + incremental_output_offset: 0, + incremental_error_output_offset: 0, + tty: false, + options, + use_file_handles: false, + process_pipes: None, + latest_signal: None, + cached_exit_code: None, + mock: None, + } + } + + /// For testing only. Builds an already-terminated mock Process whose getOutput/ + /// getErrorOutput/getExitCode/isSuccessful return the configured values, without spawning a + /// real subprocess. + pub fn __mock(mock: ProcessMock) -> Self { + let mut this = Self::empty(); + this.status = Self::STATUS_TERMINATED.to_string(); + this.exitcode = Some(mock.exit_code); + this.mock = Some(mock); + this + } + + pub fn new( + command: Vec, + cwd: Option, + env: Option>, + input: PhpMixed, + timeout: Option, + ) -> anyhow::Result { + if !shirabe_php_shim::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 + // PHP: null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR) + // `\defined('ZEND_THREAD_SAFE')` is unconditionally true in modern PHP (the constant always + // exists; its value merely reflects the NTS/ZTS build), so the disjunction is always true and + // the cwd is defaulted to getcwd() whenever it was null. + if this.cwd.is_none() { + this.cwd = shirabe_php_shim::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 = cfg!(windows); + + 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>, + input: PhpMixed, + timeout: Option, + ) -> anyhow::Result { + let mut process = Self::new(Vec::new(), cwd.map(String::from), env, input, timeout)?; + process.commandline = CommandLine::String(command.to_string()); + + Ok(process) + } + + /// Runs the process. + pub fn run( + &mut self, + callback: Option, + env: IndexMap, + ) -> anyhow::Result { + self.start(callback, env)?; + + self.wait(None) + } + + /// Starts the process and returns after writing the input to STDIN. + pub fn start( + &mut self, + callback: Option, + mut env: IndexMap, + ) -> anyhow::Result<()> { + if self.is_running() { + return Err(RuntimeException::new("Process is already running.".to_string()).into()); + } + + self.reset_process_data(); + self.starttime = Some(shirabe_php_shim::microtime()); + self.callback = Some(self.build_callback(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::>() + .join(" "); + + if !cfg!(windows) { + // 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 cfg!(windows) { + 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 = shirabe_php_shim::fopen("Process.php", "r"); + } + + let mut env_pairs: Vec = 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(shirabe_php_shim::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(); + shirabe_php_shim::proc_open( + &commandline, + &descriptors, + pipes, + cwd.as_deref().map(std::path::Path::new), + Some(&env_pairs), + Some(&options), + ) + }; + self.process = process.ok(); + + if self.process.is_none() { + 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 = self + .process_pipes + .as_ref() + .unwrap() + .pipes() + .get(&3) + .cloned(); + let pid = pipe3 + .and_then(|p| shirabe_php_shim::fgets(&p, None)) + .map(|s| s.trim().parse::().unwrap_or(0)) + .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(()) + } + + /// Waits for the process to terminate. + pub fn wait(&mut self, callback: Option) -> anyhow::Result { + self.require_process_is_started("wait")?; + + self.update_status(false); + + if let Some(callback) = callback { + self.callback = Some(self.build_callback(Some(callback))); + } + + loop { + self.check_timeout()?; + let running = self.is_running() + && (cfg!(windows) || self.process_pipes.as_ref().unwrap().are_open()); + self.read_pipes(running, !cfg!(windows) || !running); + if !running { + break; + } + } + + while self.is_running() { + self.check_timeout()?; + shirabe_php_shim::usleep(1000); + } + + let signaled = self + .process_information + .as_ref() + .and_then(|i| i.get("signaled")) + .map(shirabe_php_shim::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)) + } + + /// Returns the Pid (process identifier), if applicable. + pub fn get_pid(&mut self) -> Option { + if self.is_running() { + self.process_information + .as_ref() + .and_then(|i| i.get("pid")) + .and_then(|v| v.as_int()) + } else { + None + } + } + + /// Returns the current output of the process (STDOUT). + pub fn get_output(&mut self) -> anyhow::Result { + if let Some(mock) = &self.mock { + return Ok(mock.stdout.clone()); + } + + self.read_pipes_for_output("getOutput", false)?; + + Ok( + shirabe_php_shim::stream_get_contents3(self.stdout.as_ref().unwrap(), -1, 0) + .unwrap_or_default(), + ) + } + + /// Returns the current error output of the process (STDERR). + pub fn get_error_output(&mut self) -> anyhow::Result { + if let Some(mock) = &self.mock { + return Ok(mock.stderr.clone()); + } + + self.read_pipes_for_output("getErrorOutput", false)?; + + Ok( + shirabe_php_shim::stream_get_contents3(self.stderr.as_ref().unwrap(), -1, 0) + .unwrap_or_default(), + ) + } + + /// Returns the exit code returned by the process. + pub fn get_exit_code(&mut self) -> Option { + if self.mock.is_some() { + return self.exitcode; + } + + 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 { + 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 the number of the signal that caused the child process to terminate. + pub fn get_term_signal(&mut self) -> anyhow::Result { + 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)) + } + + /// 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(shirabe_php_shim::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 { + Self::STATUS_READY != self.status + } + + /// Checks if the process is terminated. + pub fn is_terminated(&mut self) -> bool { + self.update_status(false); + + Self::STATUS_TERMINATED == self.status + } + + /// Stops the process. + pub fn stop(&mut self, timeout: f64, signal: Option) -> Option { + let timeout_micro = shirabe_php_shim::microtime() + 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 { + shirabe_php_shim::usleep(1000); + if !(self.is_running() && shirabe_php_shim::microtime() < 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 + } + + /// Adds a line to the STDOUT stream. + pub fn add_output(&mut self, line: &str) { + let stdout = self.stdout.as_ref().unwrap(); + shirabe_php_shim::fseek(stdout, 0, shirabe_php_shim::SEEK_END); + shirabe_php_shim::fwrite(stdout, line, Some(line.len() as i64)); + shirabe_php_shim::fseek( + stdout, + self.incremental_output_offset, + shirabe_php_shim::SEEK_SET, + ); + } + + /// Adds a line to the STDERR stream. + pub fn add_error_output(&mut self, line: &str) { + let stderr = self.stderr.as_ref().unwrap(); + shirabe_php_shim::fseek(stderr, 0, shirabe_php_shim::SEEK_END); + shirabe_php_shim::fwrite(stderr, line, Some(line.len() as i64)); + shirabe_php_shim::fseek( + stderr, + self.incremental_error_output_offset, + shirabe_php_shim::SEEK_SET, + ); + } + + /// 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::>() + .join(" "), + CommandLine::String(s) => s.clone(), + } + } + + /// Gets the process timeout in seconds (max. runtime). + pub fn get_timeout(&self) -> Option { + self.timeout + } + + /// Sets the process timeout (max. runtime) in seconds. + pub fn set_timeout(&mut self, timeout: Option) -> anyhow::Result<&mut Self> { + self.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 cfg!(windows) && 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 + } + + /// Gets the working directory. + pub fn get_working_directory(&self) -> Option { + 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 shirabe_php_shim::getcwd().filter(|s| !s.is_empty()); + } + + self.cwd.clone() + } + + /// Sets the environment variables. + pub fn set_env(&mut self, env: IndexMap) -> &mut Self { + self.env = env; + + self + } + + /// 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 + && timeout < shirabe_php_shim::microtime() - self.starttime.unwrap_or(0.0) + { + self.stop(0.0, None); + + return Err(ProcessTimedOutException::new(self).into()); + } + + Ok(()) + } + + /// Returns whether TTY is supported on the current operating system. + pub fn is_tty_supported() -> bool { + static IS_TTY_SUPPORTED: OnceLock = OnceLock::new(); + + *IS_TTY_SUPPORTED.get_or_init(|| { + let mut pipes = IndexMap::new(); + shirabe_php_shim::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, + ) + .is_ok() + }) + } + + /// Creates the descriptors needed by the proc_open. + fn get_descriptors(&mut self) -> Vec { + // TODO(plugin): $this->input instanceof \Iterator -> rewind() is not modeled. + if cfg!(windows) { + self.process_pipes = Some(Box::new(WindowsPipes::new(self.input.clone()))); + } else { + self.process_pipes = Some(Box::new(UnixPipes::new( + Some(self.is_tty()), + self.input.clone(), + ))); + } + + self.process_pipes.as_mut().unwrap().get_descriptors() + } + + /// Builds up the callback used by wait(). + fn build_callback(&self, callback: Option) -> ProcessCallback { + let mut callback = callback; + 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(shirabe_php_shim::proc_get_status( + self.process.as_ref().unwrap(), + )); + let running = self + .process_information + .as_ref() + .unwrap() + .get("running") + .map(shirabe_php_shim::php_truthy) + .unwrap_or(false); + + // In PHP < 8.3, "proc_get_status" only returns the correct exit status on the first call. + if shirabe_php_shim::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 + && !running + && exitcode == Some(-1) + { + self.process_information + .as_mut() + .unwrap() + .insert("exitcode".to_string(), PhpMixed::Int(cached)); + } + } + + self.read_pipes(running && blocking, !cfg!(windows) || !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(); + } + } + + /// Returns whether PHP has been compiled with the '--enable-sigchild' option or not. + fn is_sigchild_enabled(&self) -> bool { + static SIGCHILD: OnceLock = OnceLock::new(); + + if let Some(v) = SIGCHILD.get() { + return *v; + } + + if !shirabe_php_shim::function_exists("phpinfo") { + return *SIGCHILD.get_or_init(|| false); + } + + shirabe_php_shim::ob_start(); + shirabe_php_shim::phpinfo(shirabe_php_shim::INFO_GENERAL); + + *SIGCHILD.get_or_init(|| { + shirabe_php_shim::str_contains( + &shirabe_php_shim::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<()> { + self.require_process_is_started(caller)?; + + self.update_status(blocking); + Ok(()) + } + + /// Validates and returns the filtered timeout. + fn validate_timeout(&self, timeout: Option) -> anyhow::Result> { + 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 self.process.is_some() { + shirabe_php_shim::proc_close(self.process.as_ref().unwrap()); + self.process = None; + } + 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(shirabe_php_shim::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() + && 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; + // php://temp is an in-memory stream; fopen never fails for it. + self.stdout = Some( + shirabe_php_shim::fopen(&format!("php://temp/maxmemory:{}", 1024 * 1024), "w+") + .unwrap(), + ); + self.stderr = Some( + shirabe_php_shim::fopen(&format!("php://temp/maxmemory:{}", 1024 * 1024), "w+") + .unwrap(), + ); + self.process = None; + 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 { + 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 cfg!(windows) { + let mut output: Vec = Vec::new(); + let mut exit_code: i64 = 0; + shirabe_php_shim::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 = shirabe_php_shim::proc_terminate(self.process.as_ref().unwrap(), signal); + } else if shirabe_php_shim::function_exists("posix_kill") { + ok = shirabe_php_shim::posix_kill(pid, signal); + } else { + let mut pipes = IndexMap::new(); + let opened = shirabe_php_shim::proc_open( + &format!("kill -{} {}", signal, pid), + &[ + Descriptor::Inherit, + Descriptor::Inherit, + descriptor(&["pipe", "w"]), + ], + &mut pipes, + None, + None, + None, + ); + ok = match opened { + Ok(_) => pipes + .get(&2) + .and_then(|p| shirabe_php_shim::fgets(p, None)) + .is_none(), + Err(_) => false, + }; + } + 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, + ) -> anyhow::Result { + let uid = shirabe_php_shim::uniqid("", true); + let mut var_count = 0; + let mut var_cache: IndexMap = IndexMap::new(); + let cmd = shirabe_php_shim::preg_replace_callback( + php_regex!( + r#"/"(?:( + [^"%!^]*+ + (?: + (?: !LF! | "(?:\^[%!^])?+" ) + [^"%!^]*+ + )++ + ) | [^"]*+ )"/x"# + ), + |m: &[Option]| -> anyhow::Result { + 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 shirabe_php_shim::strpbrk(&value, "\"%!\n").is_none() { + return Ok(format!("\"{}\"", value)); + } + + for (from, to) in [ + ("!LF!", "\n"), + ("\"^!\"", "!"), + ("\"^%\"", "%"), + ("\"^^\"", "^"), + ("\"\"", "\""), + ] { + value = value.replace(from, to); + } + value = format!( + "\"{}\"", + shirabe_php_shim::preg_replace(php_regex!(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> = OnceLock::new(); + let com_spec = COM_SPEC + .get_or_init(|| { + ExecutableFinder::new() + .find("cmd.exe", None, &[]) + .map(|spec| { + format!( + "\"{}\"", + shirabe_php_shim::preg_replace( + php_regex!(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 !cfg!(windows) { + return format!("'{}'", argument.replace('\'', "'\\''")); + } + let mut argument = argument.to_string(); + if argument.contains('\0') { + argument = argument.replace('\0', "?"); + } + if !shirabe_php_shim::preg_match( + php_regex!(r#"/[()%!^"<>&|\s\[\]=;*?'$]/"#), + &argument, + &mut Vec::new(), + ) { + return argument; + } + argument = shirabe_php_shim::preg_replace(php_regex!(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, + ) -> anyhow::Result { + shirabe_php_shim::preg_replace_callback( + php_regex!(r#"/"\$\{:([_a-zA-Z]+[_a-zA-Z0-9]*)\}"/"#), + |matches: &[Option]| -> anyhow::Result { + 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(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 { + let env: IndexMap = shirabe_php_shim::getenv_all() + .map(|(k, v)| { + ( + k.to_string_lossy().into_owned(), + v.to_string_lossy().into_owned(), + ) + }) + .collect(); + let server = shirabe_php_shim::PHP_SERVER.lock().unwrap(); + + // non-Windows: array_intersect_key($env, $_SERVER) ?: $env + let mut intersect: IndexMap = IndexMap::new(); + for (k, v) in &env { + if server.get(k).is_some() { + intersect.insert(k.clone(), PhpMixed::String(v.clone())); + } + } + let env_map: IndexMap = if intersect.is_empty() { + env.into_iter() + .map(|(k, v)| (k, PhpMixed::String(v))) + .collect() + } else { + intersect + }; + + // $_ENV + env_map + let mut result: IndexMap = shirabe_php_shim::PHP_ENV + .lock() + .unwrap() + .get_all() + .map(|(k, v)| { + ( + k.to_string_lossy().into_owned(), + PhpMixed::String(v.to_string_lossy().into_owned()), + ) + }) + .collect(); + for (k, v) in env_map { + result.entry(k).or_insert(v); + } + result + } +} + +impl Drop for Process { + fn drop(&mut self) { + self.stop(0.0, None); + } +} diff --git a/crates/shirabe-symfony-process/src/process_utils.rs b/crates/shirabe-symfony-process/src/process_utils.rs new file mode 100644 index 00000000..7c4ca21f --- /dev/null +++ b/crates/shirabe-symfony-process/src/process_utils.rs @@ -0,0 +1,43 @@ +//! ref: composer/vendor/symfony/process/ProcessUtils.php + +use crate::exception::invalid_argument_exception::InvalidArgumentException; +use shirabe_php_shim::PhpMixed; + +/// 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 { + if !input.is_null() { + if shirabe_php_shim::is_string(&input) { + return Ok(input); + } + if shirabe_php_shim::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/Cargo.toml b/crates/shirabe/Cargo.toml index 3ac40475..64aa14e0 100644 --- a/crates/shirabe/Cargo.toml +++ b/crates/shirabe/Cargo.toml @@ -12,6 +12,7 @@ shirabe-php-rpc.workspace = true shirabe-php-shim.workspace = true shirabe-semver.workspace = true shirabe-spdx-licenses.workspace = true +shirabe-symfony-process.workspace = true anyhow.workspace = true async-trait.workspace = true base64.workspace = true diff --git a/crates/shirabe/src/command/diagnose_command.rs b/crates/shirabe/src/command/diagnose_command.rs index 9f834315..b6b1ffad 100644 --- a/crates/shirabe/src/command/diagnose_command.rs +++ b/crates/shirabe/src/command/diagnose_command.rs @@ -36,7 +36,6 @@ use indexmap::IndexMap; use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; -use shirabe_external_packages::symfony::process::ExecutableFinder; use shirabe_pcre::{CaptureKey, Preg}; use shirabe_php_shim::Catch as _; use shirabe_php_shim::{ @@ -45,6 +44,7 @@ use shirabe_php_shim::{ is_string, php_regex, rtrim, str_contains, str_replace, str_starts_with, strpos, strstr, strstr3, strtolower, trim, version_compare, }; +use shirabe_symfony_process::ExecutableFinder; #[derive(Debug)] pub struct DiagnoseCommand { diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs index 05cd1d22..8b60c988 100644 --- a/crates/shirabe/src/console/application.rs +++ b/crates/shirabe/src/console/application.rs @@ -92,7 +92,6 @@ use shirabe_external_packages::symfony::console::signal_registry::signal_registr use shirabe_external_packages::symfony::console::style::style_interface::StyleInterface; use shirabe_external_packages::symfony::console::style::symfony_style::SymfonyStyle; use shirabe_external_packages::symfony::console::terminal::Terminal; -use shirabe_external_packages::symfony::process::exception::ProcessTimedOutException; use shirabe_php_shim::Catch as _; use shirabe_php_shim::{ LogicException as ShimLogicException, PHP_VERSION, PHP_VERSION_ID, PhpMixed, RuntimeException, @@ -103,6 +102,7 @@ use shirabe_php_shim::{ php_uname, posix_getuid, random_bytes, realpath, restore_error_handler, round, str_contains, str_replace, strpos, strtoupper, sys_get_temp_dir, time, unlink, }; +use shirabe_symfony_process::exception::ProcessTimedOutException; /// The PHP `Composer\Console\Application` and `Symfony\Component\Console\Application` are /// flattened into a single struct. Methods that are overridden by subclass and called via diff --git a/crates/shirabe/src/downloader/zip_downloader.rs b/crates/shirabe/src/downloader/zip_downloader.rs index fd7d66d3..ddbb6788 100644 --- a/crates/shirabe/src/downloader/zip_downloader.rs +++ b/crates/shirabe/src/downloader/zip_downloader.rs @@ -8,7 +8,6 @@ use crate::package::PackageInterfaceHandle; use crate::util::IniHelper; use crate::util::Platform; use indexmap::IndexMap; -use shirabe_external_packages::symfony::process::ExecutableFinder; use shirabe_pcre::{CaptureKey, Preg}; use shirabe_php_shim::Catch as _; use shirabe_php_shim::{ @@ -17,6 +16,7 @@ use shirabe_php_shim::{ impl_php_class, is_file, json_encode, php_regex, random_int, str_contains, str_replace, strlen, substr, version_compare, }; +use shirabe_symfony_process::ExecutableFinder; use std::sync::Mutex; static UNZIP_COMMANDS: Mutex>>> = Mutex::new(None); diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs index 4386204f..04c590c6 100644 --- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs +++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs @@ -22,8 +22,6 @@ use crate::util::Platform; use crate::util::ProcessExecutor; use indexmap::IndexMap; use shirabe_external_packages::symfony::console::output::output_interface; -use shirabe_external_packages::symfony::process::ExecutableFinder; -use shirabe_external_packages::symfony::process::PhpExecutableFinder; use shirabe_pcre::{CaptureKey, Preg}; use shirabe_php_rpc::{ PhpThrow, PluginValue, RustMethodDispatcher, RustObjHandle, call_function, @@ -38,6 +36,8 @@ use shirabe_php_shim::{ str_contains, str_ends_with, str_replace, str_starts_with, strlen, strpos, strtoupper, substr, trim, }; +use shirabe_symfony_process::ExecutableFinder; +use shirabe_symfony_process::PhpExecutableFinder; /// Represents a callable listener. PHP's `callable` may be a string (command, script, or /// "Class::method"), a `[object|string, method]` pair, or a `\Closure`. diff --git a/crates/shirabe/src/platform/hhvm_detector.rs b/crates/shirabe/src/platform/hhvm_detector.rs index 69871f1f..26fb5e12 100644 --- a/crates/shirabe/src/platform/hhvm_detector.rs +++ b/crates/shirabe/src/platform/hhvm_detector.rs @@ -2,8 +2,8 @@ use crate::util::Platform; use crate::util::ProcessExecutor; -use shirabe_external_packages::symfony::process::ExecutableFinder; use shirabe_php_shim::{HHVM_VERSION, defined}; +use shirabe_symfony_process::ExecutableFinder; use std::sync::Mutex; // None = null (uninitialized), Some(None) = false (not found), Some(Some(v)) = version diff --git a/crates/shirabe/src/util/perforce.rs b/crates/shirabe/src/util/perforce.rs index f03e4835..1820936c 100644 --- a/crates/shirabe/src/util/perforce.rs +++ b/crates/shirabe/src/util/perforce.rs @@ -6,14 +6,14 @@ use crate::util::Filesystem; use crate::util::Platform; use crate::util::ProcessExecutor; use indexmap::IndexMap; -use shirabe_external_packages::symfony::process::ExecutableFinder; -use shirabe_external_packages::symfony::process::Process; use shirabe_pcre::Preg; use shirabe_php_shim::{ Exception, PHP_EOL, PhpMixed, PhpResource, chdir, date, explode, fclose, feof, fgets, file_get_contents, fopen, fwrite, gethostname, json_decode, php_regex, str_replace_array, strcmp, strlen, strpos, strrpos, substr, time, trim, }; +use shirabe_symfony_process::ExecutableFinder; +use shirabe_symfony_process::Process; /// @phpstan-type RepoConfig array{unique_perforce_client_name?: string, depot?: string, branch?: string, p4user?: string, p4password?: string} #[derive(Debug)] diff --git a/crates/shirabe/src/util/process_executor.rs b/crates/shirabe/src/util/process_executor.rs index 2b4c0d89..56199868 100644 --- a/crates/shirabe/src/util/process_executor.rs +++ b/crates/shirabe/src/util/process_executor.rs @@ -7,11 +7,6 @@ use crate::util::GitHub; use crate::util::Platform; use indexmap::IndexMap; use shirabe_external_packages::seld::signal::SignalHandler; -use shirabe_external_packages::symfony::process::ExecutableFinder; -use shirabe_external_packages::symfony::process::Process; -use shirabe_external_packages::symfony::process::ProcessMock; -use shirabe_external_packages::symfony::process::exception::ProcessSignaledException; -use shirabe_external_packages::symfony::process::exception::RuntimeException as SymfonyProcessRuntimeException; use shirabe_pcre::{CaptureKey, Preg}; use shirabe_php_shim::Catch as _; use shirabe_php_shim::{ @@ -20,6 +15,11 @@ use shirabe_php_shim::{ php_regex, rtrim, str_replace, strcspn, strlen, strpbrk, strtolower, strtr_array, substr_replace, trim, }; +use shirabe_symfony_process::ExecutableFinder; +use shirabe_symfony_process::Process; +use shirabe_symfony_process::ProcessMock; +use shirabe_symfony_process::exception::ProcessSignaledException; +use shirabe_symfony_process::exception::RuntimeException as SymfonyProcessRuntimeException; use std::sync::{LazyLock, Mutex}; static EXECUTABLES: LazyLock>> = diff --git a/crates/shirabe/tests/command/self_update_command_test.rs b/crates/shirabe/tests/command/self_update_command_test.rs index 6dc64104..1f83af66 100644 --- a/crates/shirabe/tests/command/self_update_command_test.rs +++ b/crates/shirabe/tests/command/self_update_command_test.rs @@ -3,8 +3,8 @@ use crate::test_case::{RunOptions, get_application_tester, init_temp_composer}; use indexmap::IndexMap; use serial_test::serial; -use shirabe_external_packages::symfony::process::Process; use shirabe_php_shim::{PHP_BINARY, PhpMixed}; +use shirabe_symfony_process::Process; /// ref: SelfUpdateCommandTest::setUp. The `composer-test.phar` copy PHP also performs here lives in /// `set_up_with_phar` instead, so the one test that never touches the phar is not blocked by the diff --git a/crates/shirabe/tests/package/archiver/archivable_files_finder_test.rs b/crates/shirabe/tests/package/archiver/archivable_files_finder_test.rs index 2e5aa738..499e08dc 100644 --- a/crates/shirabe/tests/package/archiver/archivable_files_finder_test.rs +++ b/crates/shirabe/tests/package/archiver/archivable_files_finder_test.rs @@ -3,9 +3,9 @@ use indexmap::IndexMap; use shirabe::package::archiver::ArchivableFilesFinder; use shirabe::util::Filesystem; -use shirabe_external_packages::symfony::process::Process; use shirabe_pcre::Preg; use shirabe_php_shim::{PhpMixed, ZipArchive, dirname, file_put_contents, preg_quote}; +use shirabe_symfony_process::Process; use tempfile::TempDir; struct SetUp { diff --git a/crates/shirabe/tests/package/archiver/archive_manager_test.rs b/crates/shirabe/tests/package/archiver/archive_manager_test.rs index 84feb5bc..43e2f84a 100644 --- a/crates/shirabe/tests/package/archiver/archive_manager_test.rs +++ b/crates/shirabe/tests/package/archiver/archive_manager_test.rs @@ -12,11 +12,11 @@ use shirabe::util::Filesystem; use shirabe::util::ProcessExecutor; use shirabe::util::http_downloader::HttpDownloader; use shirabe::util::r#loop::Loop; -use shirabe_external_packages::symfony::process::Process; use shirabe_php_shim::Catch as _; use shirabe_php_shim::{ PhpMixed, file_exists, file_put_contents, realpath, sys_get_temp_dir, unlink, }; +use shirabe_symfony_process::Process; use tempfile::TempDir; // ref: ArchiverTestCase::setUp + ArchiveManagerTest::setUp. diff --git a/crates/shirabe/tests/platform/hhvm_detector_test.rs b/crates/shirabe/tests/platform/hhvm_detector_test.rs index 340bf888..ea75ec8b 100644 --- a/crates/shirabe/tests/platform/hhvm_detector_test.rs +++ b/crates/shirabe/tests/platform/hhvm_detector_test.rs @@ -4,9 +4,9 @@ use shirabe::platform::hhvm_detector::HhvmDetector; use shirabe::platform::hhvm_detector::HhvmDetectorInterface; use shirabe::util::Platform; use shirabe::util::ProcessExecutor; -use shirabe_external_packages::symfony::process::ExecutableFinder; use shirabe_php_shim::PhpMixed; use shirabe_semver::VersionParser; +use shirabe_symfony_process::ExecutableFinder; fn set_up() -> HhvmDetector { let hhvm_detector = HhvmDetector::new(None, None); diff --git a/crates/shirabe/tests/plugin/plugin_installer_test.rs b/crates/shirabe/tests/plugin/plugin_installer_test.rs index 0c82a11a..65a8872b 100644 --- a/crates/shirabe/tests/plugin/plugin_installer_test.rs +++ b/crates/shirabe/tests/plugin/plugin_installer_test.rs @@ -30,10 +30,10 @@ use shirabe::util::http_downloader::HttpDownloader; use shirabe::util::r#loop::Loop; use shirabe::util::process_executor::ProcessExecutor; use shirabe_external_packages::symfony::console::output::output_interface::VERBOSITY_NORMAL; -use shirabe_external_packages::symfony::process::PhpExecutableFinder; use shirabe_php_shim::Catch as _; use shirabe_php_shim::PhpMixed; use shirabe_semver::VersionParser; +use shirabe_symfony_process::PhpExecutableFinder; use tempfile::TempDir; /// The register/activate flow runs the plugin in the real PHP worker; without a PHP binary the -- cgit v1.3.1-4-g156e