Refactor shell utilities: reorganize namespaces and introduce Shell base class

This commit is contained in:
crazywhalecc 2025-08-06 20:28:25 +08:00 committed by Jerry Ma
parent cc447a089a
commit e28580de00
10 changed files with 314 additions and 150 deletions

View File

@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\util;
use SPC\exception\RuntimeException;
use ZM\Logger\ConsoleColor;
class WindowsCmd
{
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('Only windows can use WindowsCmd');
}
$this->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;
}
}

View File

@ -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
{

View File

@ -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.

View File

@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace SPC\util\shell;
use SPC\exception\ExecutionException;
abstract class Shell
{
protected ?string $cd = null;
protected bool $debug;
protected array $env = [];
protected string $last_cmd = '';
protected bool $enable_log_file = true;
public function __construct(?bool $debug = null, bool $enable_log_file = true)
{
$this->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;
}

View File

@ -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}";

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace SPC\util\shell;
use SPC\exception\SPCInternalException;
use ZM\Logger\ConsoleColor;
class WindowsCmd extends Shell
{
public function __construct(?bool $debug = null)
{
if (PHP_OS_FAMILY !== 'Windows') {
throw new SPCInternalException('Only windows can use WindowsCmd');
}
parent::__construct($debug);
}
public function exec(string $cmd): static
{
/* @phpstan-ignore-next-line */
logger()->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;
}
}

View File

@ -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;
/**

View File

@ -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);
}
/**

View File

@ -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();

View File

@ -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();