diff --git a/src/SPC/util/WindowsCmd.php b/src/SPC/util/WindowsCmd.php deleted file mode 100644 index 4398dc44..00000000 --- a/src/SPC/util/WindowsCmd.php +++ /dev/null @@ -1,78 +0,0 @@ -debug = $debug ?? defined('DEBUG_MODE'); - } - - public function cd(string $dir): WindowsCmd - { - logger()->info('Entering dir: ' . $dir); - $c = clone $this; - $c->cd = $dir; - return $c; - } - - /** - * @throws RuntimeException - */ - public function exec(string $cmd): WindowsCmd - { - /* @phpstan-ignore-next-line */ - logger()->info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); - if ($this->cd !== null) { - $cmd = 'cd /d ' . escapeshellarg($this->cd) . ' && ' . $cmd; - } - if (!$this->debug) { - $cmd .= ' >nul 2>&1'; - } - echo $cmd . PHP_EOL; - - f_passthru($cmd); - return $this; - } - - public function execWithWrapper(string $wrapper, string $args): WindowsCmd - { - return $this->exec($wrapper . ' "' . str_replace('"', '^"', $args) . '"'); - } - - public function execWithResult(string $cmd, bool $with_log = true): array - { - if ($with_log) { - /* @phpstan-ignore-next-line */ - logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd)); - } else { - logger()->debug('Running command with result: ' . $cmd); - } - exec($cmd, $out, $code); - return [$code, $out]; - } - - public function setEnv(array $env): WindowsCmd - { - $this->env = array_merge($this->env, $env); - return $this; - } -} diff --git a/src/SPC/util/executor/UnixAutoconfExecutor.php b/src/SPC/util/executor/UnixAutoconfExecutor.php index aed9b85f..16c4fbc3 100644 --- a/src/SPC/util/executor/UnixAutoconfExecutor.php +++ b/src/SPC/util/executor/UnixAutoconfExecutor.php @@ -8,7 +8,7 @@ use SPC\builder\freebsd\library\BSDLibraryBase; use SPC\builder\linux\library\LinuxLibraryBase; use SPC\builder\macos\library\MacOSLibraryBase; use SPC\exception\RuntimeException; -use SPC\util\UnixShell; +use SPC\util\shell\UnixShell; class UnixAutoconfExecutor extends Executor { diff --git a/src/SPC/util/executor/UnixCMakeExecutor.php b/src/SPC/util/executor/UnixCMakeExecutor.php index cc4263d2..6bd70677 100644 --- a/src/SPC/util/executor/UnixCMakeExecutor.php +++ b/src/SPC/util/executor/UnixCMakeExecutor.php @@ -8,7 +8,7 @@ use SPC\builder\freebsd\library\BSDLibraryBase; use SPC\builder\linux\library\LinuxLibraryBase; use SPC\builder\macos\library\MacOSLibraryBase; use SPC\store\FileSystem; -use SPC\util\UnixShell; +use SPC\util\shell\UnixShell; /** * Unix-like OS cmake command executor. diff --git a/src/SPC/util/shell/Shell.php b/src/SPC/util/shell/Shell.php new file mode 100644 index 00000000..bfa51b86 --- /dev/null +++ b/src/SPC/util/shell/Shell.php @@ -0,0 +1,173 @@ +debug = $debug ?? defined('DEBUG_MODE'); + $this->enable_log_file = $enable_log_file; + } + + /** + * Equivalent to `cd` command in shell. + * + * @param string $dir Directory to change to + */ + public function cd(string $dir): static + { + logger()->debug('Entering dir: ' . $dir); + $c = clone $this; + $c->cd = $dir; + return $c; + } + + public function setEnv(array $env): static + { + foreach ($env as $k => $v) { + if (trim($v) === '') { + continue; + } + $this->env[$k] = trim($v); + } + return $this; + } + + public function appendEnv(array $env): static + { + foreach ($env as $k => $v) { + if ($v === '') { + continue; + } + if (!isset($this->env[$k])) { + $this->env[$k] = $v; + } else { + $this->env[$k] = "{$v} {$this->env[$k]}"; + } + } + return $this; + } + + /** + * Executes a command in the shell. + */ + abstract public function exec(string $cmd): static; + + /** + * Returns the last executed command. + */ + public function getLastCommand(): string + { + return $this->last_cmd; + } + + /** + * Executes a command with console and log file output. + * + * @param string $cmd Full command to execute (including cd and env vars) + * @param bool $console_output If true, output will be printed to console + * @param null|string $original_command Original command string for logging + */ + protected function passthru(string $cmd, bool $console_output = false, ?string $original_command = null): void + { + // write executed command to the log file using fwrite + $file_res = fopen(SPC_SHELL_LOG, 'a'); + if ($console_output) { + $console_res = STDOUT; + } + $descriptors = [ + 0 => ['file', 'php://stdin', 'r'], // stdin + 1 => ['pipe', 'w'], // stdout + 2 => ['pipe', 'w'], // stderr + ]; + $process = proc_open($cmd, $descriptors, $pipes); + + try { + if (!is_resource($process)) { + throw new ExecutionException( + cmd: $original_command ?? $cmd, + message: 'Failed to open process for command, proc_open() failed.', + code: -1, + cd: $this->cd, + env: $this->env + ); + } + // fclose($pipes[0]); + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + while (true) { + $read = [$pipes[1], $pipes[2]]; + $write = null; + $except = null; + + $ready = stream_select($read, $write, $except, 0, 100000); + + if ($ready === false) { + $status = proc_get_status($process); + if (!$status['running']) { + break; + } + continue; + } + + if ($ready > 0) { + foreach ($read as $pipe) { + $chunk = fgets($pipe); + if ($chunk !== false) { + if ($console_output) { + fwrite($console_res, $chunk); + } + if ($this->enable_log_file) { + fwrite($file_res, $chunk); + } + } + } + } + + $status = proc_get_status($process); + if (!$status['running']) { + // check exit code + if ($status['exitcode'] !== 0) { + if ($this->enable_log_file) { + fwrite($file_res, "Command exited with non-zero code: {$status['exitcode']}\n"); + } + throw new ExecutionException( + cmd: $original_command ?? $cmd, + message: "Command exited with non-zero code: {$status['exitcode']}", + code: $status['exitcode'], + cd: $this->cd, + env: $this->env, + ); + } + break; + } + } + } finally { + fclose($pipes[1]); + fclose($pipes[2]); + fclose($file_res); + proc_close($process); + } + } + + /** + * Logs the command information to a log file. + */ + abstract protected function logCommandInfo(string $cmd): void; +} diff --git a/src/SPC/util/UnixShell.php b/src/SPC/util/shell/UnixShell.php similarity index 55% rename from src/SPC/util/UnixShell.php rename to src/SPC/util/shell/UnixShell.php index 3e96e4bd..75a0be3d 100644 --- a/src/SPC/util/UnixShell.php +++ b/src/SPC/util/shell/UnixShell.php @@ -2,53 +2,38 @@ declare(strict_types=1); -namespace SPC\util; +namespace SPC\util\shell; use SPC\builder\freebsd\library\BSDLibraryBase; use SPC\builder\linux\library\LinuxLibraryBase; use SPC\builder\macos\library\MacOSLibraryBase; -use SPC\exception\RuntimeException; +use SPC\exception\SPCInternalException; use ZM\Logger\ConsoleColor; -class UnixShell +/** + * Unix-like OS shell command executor. + * + * This class provides methods to execute shell commands in a Unix-like environment. + * It supports setting environment variables and changing the working directory. + */ +class UnixShell extends Shell { - private ?string $cd = null; - - private bool $debug; - - private array $env = []; - - /** - * @throws RuntimeException - */ public function __construct(?bool $debug = null) { if (PHP_OS_FAMILY === 'Windows') { - throw new RuntimeException('Windows cannot use UnixShell'); + throw new SPCInternalException('Windows cannot use UnixShell'); } - $this->debug = $debug ?? defined('DEBUG_MODE'); + parent::__construct($debug); } - public function cd(string $dir): UnixShell - { - logger()->info('Entering dir: ' . $dir); - $c = clone $this; - $c->cd = $dir; - return $c; - } - - /** - * @throws RuntimeException - */ - public function exec(string $cmd): UnixShell + public function exec(string $cmd): static { /* @phpstan-ignore-next-line */ logger()->info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); - $cmd = $this->getExecString($cmd); - if (!$this->debug) { - $cmd .= ' 1>/dev/null 2>&1'; - } - f_passthru($cmd); + $original_command = $cmd; + $this->logCommandInfo($original_command); + $this->last_cmd = $cmd = $this->getExecString($cmd); + $this->passthru($cmd, $this->debug, $original_command); return $this; } @@ -68,21 +53,6 @@ class UnixShell return $this; } - public function appendEnv(array $env): UnixShell - { - foreach ($env as $k => $v) { - if ($v === '') { - continue; - } - if (!isset($this->env[$k])) { - $this->env[$k] = $v; - } else { - $this->env[$k] = "{$v} {$this->env[$k]}"; - } - } - return $this; - } - public function execWithResult(string $cmd, bool $with_log = true): array { if ($with_log) { @@ -97,17 +67,9 @@ class UnixShell return [$code, $out]; } - public function setEnv(array $env): UnixShell - { - foreach ($env as $k => $v) { - if (trim($v) === '') { - continue; - } - $this->env[$k] = trim($v); - } - return $this; - } - + /** + * Returns unix-style environment variable string. + */ public function getEnvString(): string { $str = ''; @@ -117,9 +79,29 @@ class UnixShell return trim($str); } + protected function logCommandInfo(string $cmd): void + { + // write executed command to log file using fwrite + $log_file = fopen(SPC_SHELL_LOG, 'a'); + fwrite($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); + fwrite($log_file, "> Executing command: {$cmd}\n"); + // get the backtrace to find the file and line number + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + if (isset($backtrace[1]['file'], $backtrace[1]['line'])) { + $file = $backtrace[1]['file']; + $line = $backtrace[1]['line']; + fwrite($log_file, "> Called from: {$file} at line {$line}\n"); + } + fwrite($log_file, "> Environment variables: {$this->getEnvString()}\n"); + if ($this->cd !== null) { + fwrite($log_file, "> Working dir: {$this->cd}\n"); + } + fwrite($log_file, "\n"); + } + private function getExecString(string $cmd): string { - logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']); + // logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']); $env_str = $this->getEnvString(); if (!empty($env_str)) { $cmd = "{$env_str} {$cmd}"; diff --git a/src/SPC/util/shell/WindowsCmd.php b/src/SPC/util/shell/WindowsCmd.php new file mode 100644 index 00000000..651bd67a --- /dev/null +++ b/src/SPC/util/shell/WindowsCmd.php @@ -0,0 +1,87 @@ +info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); + + $original_command = $cmd; + $this->logCommandInfo($original_command); + $this->last_cmd = $cmd = $this->getExecString($cmd); + // echo $cmd . PHP_EOL; + + $this->passthru($cmd, $this->debug, $original_command); + return $this; + } + + public function execWithWrapper(string $wrapper, string $args): WindowsCmd + { + return $this->exec($wrapper . ' "' . str_replace('"', '^"', $args) . '"'); + } + + public function execWithResult(string $cmd, bool $with_log = true): array + { + if ($with_log) { + /* @phpstan-ignore-next-line */ + logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd)); + } else { + logger()->debug('Running command with result: ' . $cmd); + } + exec($cmd, $out, $code); + return [$code, $out]; + } + + public function setEnv(array $env): static + { + // windows currently does not support setting environment variables + throw new SPCInternalException('Windows does not support setting environment variables in shell commands.'); + } + + public function appendEnv(array $env): static + { + // windows currently does not support appending environment variables + throw new SPCInternalException('Windows does not support appending environment variables in shell commands.'); + } + + public function getLastCommand(): string + { + return $this->last_cmd; + } + + protected function logCommandInfo(string $cmd): void + { + // write executed command to the log file using fwrite + $log_file = fopen(SPC_SHELL_LOG, 'a'); + fwrite($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); + fwrite($log_file, "> Executing command: {$cmd}\n"); + if ($this->cd !== null) { + fwrite($log_file, "> Working dir: {$this->cd}\n"); + } + fwrite($log_file, "\n"); + } + + private function getExecString(string $cmd): string + { + if ($this->cd !== null) { + $cmd = 'cd /d ' . escapeshellarg($this->cd) . ' && ' . $cmd; + } + return $cmd; + } +} diff --git a/src/globals/functions.php b/src/globals/functions.php index ea3db9d9..9dd1c17c 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -8,8 +8,8 @@ use SPC\builder\BuilderProvider; use SPC\exception\InterruptException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; -use SPC\util\UnixShell; -use SPC\util\WindowsCmd; +use SPC\util\shell\UnixShell; +use SPC\util\shell\WindowsCmd; use ZM\Logger\ConsoleLogger; /** diff --git a/tests/SPC/util/TestBase.php b/tests/SPC/util/TestBase.php index c7e8b22d..fd82ccfb 100644 --- a/tests/SPC/util/TestBase.php +++ b/tests/SPC/util/TestBase.php @@ -48,17 +48,17 @@ abstract class TestBase extends TestCase /** * Create a UnixShell instance with debug disabled to suppress logs */ - protected function createUnixShell(): \SPC\util\UnixShell + protected function createUnixShell(): \SPC\util\shell\UnixShell { - return new \SPC\util\UnixShell(false); + return new \SPC\util\shell\UnixShell(false); } /** * Create a WindowsCmd instance with debug disabled to suppress logs */ - protected function createWindowsCmd(): \SPC\util\WindowsCmd + protected function createWindowsCmd(): \SPC\util\shell\WindowsCmd { - return new \SPC\util\WindowsCmd(false); + return new \SPC\util\shell\WindowsCmd(false); } /** diff --git a/tests/SPC/util/UnixShellTest.php b/tests/SPC/util/UnixShellTest.php index 0f16ead4..f65e5a40 100644 --- a/tests/SPC/util/UnixShellTest.php +++ b/tests/SPC/util/UnixShellTest.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace SPC\Tests\util; -use SPC\exception\RuntimeException; -use SPC\util\UnixShell; +use SPC\exception\EnvironmentException; +use SPC\util\shell\UnixShell; /** * @internal @@ -18,7 +18,7 @@ final class UnixShellTest extends TestBase $this->markTestSkipped('This test is for Windows systems only'); } - $this->expectException(RuntimeException::class); + $this->expectException(EnvironmentException::class); $this->expectExceptionMessage('Windows cannot use UnixShell'); new UnixShell(); diff --git a/tests/SPC/util/WindowsCmdTest.php b/tests/SPC/util/WindowsCmdTest.php index 7d64d7e3..fb4ee3f2 100644 --- a/tests/SPC/util/WindowsCmdTest.php +++ b/tests/SPC/util/WindowsCmdTest.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace SPC\Tests\util; -use SPC\exception\RuntimeException; -use SPC\util\WindowsCmd; +use SPC\exception\SPCInternalException; +use SPC\util\shell\WindowsCmd; /** * @internal @@ -18,7 +18,7 @@ final class WindowsCmdTest extends TestBase $this->markTestSkipped('This test is for Unix systems only'); } - $this->expectException(RuntimeException::class); + $this->expectException(SPCInternalException::class); $this->expectExceptionMessage('Only windows can use WindowsCmd'); new WindowsCmd();