Refactor all command class exception handling

This commit is contained in:
crazywhalecc
2025-08-06 20:45:16 +08:00
committed by Jerry Ma
parent f68f060be2
commit 333b776e77
10 changed files with 691 additions and 643 deletions

View File

@@ -4,18 +4,15 @@ declare(strict_types=1);
namespace SPC\builder;
use PharIo\FileSystem\File;
use SPC\exception\ExceptionHandler;
use SPC\exception\FileSystemException;
use SPC\exception\BuildFailureException;
use SPC\exception\InterruptException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\LockFile;
use SPC\store\SourceManager;
use SPC\store\SourcePatcher;
use SPC\util\CustomExt;
use SPC\util\AttributeMapper;
abstract class BuilderBase
{
@@ -64,12 +61,11 @@ abstract class BuilderBase
match ($status) {
LIB_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] setup success, took ' . round(microtime(true) - $starttime, 2) . ' s'),
LIB_STATUS_ALREADY => logger()->notice('lib [' . $lib::NAME . '] already built'),
LIB_STATUS_BUILD_FAILED => logger()->error('lib [' . $lib::NAME . '] build failed'),
LIB_STATUS_INSTALL_FAILED => logger()->error('lib [' . $lib::NAME . '] install failed'),
default => logger()->warning('lib [' . $lib::NAME . '] build status unknown'),
};
if (in_array($status, [LIB_STATUS_BUILD_FAILED, LIB_STATUS_INSTALL_FAILED])) {
throw new RuntimeException('Library [' . $lib::NAME . '] setup failed.');
throw new BuildFailureException('Library [' . $lib::NAME . '] setup failed.');
}
}
}
@@ -254,9 +250,8 @@ abstract class BuilderBase
}
$ext->buildShared();
}
} catch (RuntimeException $e) {
} finally {
FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', $extension_dir_line);
throw $e;
}
FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', $extension_dir_line);
FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', '# test "[$]$1" = "no" && $1=yes', 'test "[$]$1" = "no" && $1=yes');
@@ -311,7 +306,7 @@ abstract class BuilderBase
return intval($match[1]);
}
throw new RuntimeException('PHP version file format is malformed, please remove it and download again');
throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again');
}
public function getPHPVersion(bool $exception_on_failure = true): string
@@ -329,7 +324,7 @@ abstract class BuilderBase
if (!$exception_on_failure) {
return 'unknown';
}
throw new RuntimeException('PHP version file format is malformed, please remove it and download again');
throw new WrongUsageException('PHP version file format is malformed, please remove it and download again');
}
/**
@@ -476,7 +471,7 @@ abstract class BuilderBase
foreach ($patches as $patch) {
try {
if (!file_exists($patch)) {
throw new RuntimeException("Additional patch script file {$patch} not found!");
throw new WrongUsageException("Additional patch script file {$patch} not found!");
}
logger()->debug('Running additional patch script: ' . $patch);
require $patch;
@@ -489,11 +484,6 @@ abstract class BuilderBase
exit($e->getCode());
} catch (\Throwable $e) {
logger()->critical('Patch script ' . $patch . ' failed to run.');
if ($this->getOption('debug')) {
ExceptionHandler::getInstance()->handle($e);
} else {
logger()->critical('Please check with --debug option to see more details.');
}
throw $e;
}
}

View File

@@ -9,8 +9,8 @@ use Laravel\Prompts\Prompt;
use Psr\Log\LogLevel;
use SPC\ConsoleApplication;
use SPC\exception\ExceptionHandler;
use SPC\exception\ValidationException;
use SPC\exception\WrongUsageException;
use SPC\exception\SPCException;
use SPC\util\AttributeMapper;
use SPC\util\GlobalEnvManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
@@ -32,6 +32,7 @@ abstract class BaseCommand extends Command
parent::__construct($name);
$this->addOption('debug', null, null, 'Enable debug mode');
$this->addOption('no-motd', null, null, 'Disable motd');
$this->addOption('preserve-log', null, null, 'Preserve log files, do not delete them on initialized');
}
public function initialize(InputInterface $input, OutputInterface $output): void
@@ -93,31 +94,21 @@ abstract class BaseCommand extends Command
GlobalEnvManager::init();
f_putenv('SPC_SKIP_TOOLCHAIN_CHECK=yes');
}
if ($this->shouldExecute()) {
try {
// show raw argv list for logger()->debug
logger()->debug('argv: ' . implode(' ', $_SERVER['argv']));
return $this->handle();
} catch (ValidationException|WrongUsageException $e) {
$msg = explode("\n", $e->getMessage());
foreach ($msg as $v) {
logger()->error($v);
}
} /* @noinspection PhpRedundantCatchClauseInspection */ catch (SPCException $e) {
// Handle SPCException and log it
ExceptionHandler::handleSPCException($e);
return static::FAILURE;
} catch (\Throwable $e) {
if ($this->getOption('debug')) {
ExceptionHandler::getInstance()->handle($e);
} else {
$msg = explode("\n", $e->getMessage());
foreach ($msg as $v) {
logger()->error($v);
}
}
// Handle any other exceptions
ExceptionHandler::handleDefaultException($e);
return static::FAILURE;
}
}
return static::SUCCESS;
}
protected function getOption(string $name): mixed
{
@@ -129,11 +120,6 @@ abstract class BaseCommand extends Command
return $this->input->getArgument($name);
}
protected function shouldExecute(): bool
{
return true;
}
protected function logWithResult(bool $result, string $success_msg, string $fail_msg): int
{
if ($result) {

View File

@@ -50,7 +50,6 @@ class BuildLibsCommand extends BuildCommand
}
}
try {
// 构建对象
$builder = BuilderProvider::makeBuilderByInput($this->input);
// 只编译 library 的情况下,标记
@@ -68,14 +67,5 @@ class BuildLibsCommand extends BuildCommand
$time = round(microtime(true) - START_TIME, 3);
logger()->info('Build libs complete, used ' . $time . ' s !');
return static::SUCCESS;
} catch (\Throwable $e) {
if ($this->getOption('debug')) {
ExceptionHandler::getInstance()->handle($e);
} else {
logger()->critical('Build failed with ' . get_class($e) . ': ' . $e->getMessage());
logger()->critical('Please check with --debug option to see more details.');
}
return static::FAILURE;
}
}
}

View File

@@ -5,8 +5,7 @@ declare(strict_types=1);
namespace SPC\command;
use SPC\builder\BuilderProvider;
use SPC\exception\ExceptionHandler;
use SPC\exception\WrongUsageException;
use SPC\exception\SPCException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\SourcePatcher;
@@ -119,7 +118,6 @@ class BuildPHPCommand extends BuildCommand
logger()->warning('Some cases micro.sfx cannot be packed via UPX due to dynamic size bug, be aware!');
}
}
try {
// create builder
$builder = BuilderProvider::makeBuilderByInput($this->input);
$include_suggest_ext = $this->getOption('with-suggested-exts');
@@ -167,6 +165,8 @@ class BuildPHPCommand extends BuildCommand
}
$this->printFormatInfo($this->getDefinedEnvs(), true);
$this->printFormatInfo($indent_texts);
// bind extra info to SPCException
SPCException::bindBuildPHPExtraInfo($indent_texts);
logger()->notice('Build will start after 2s ...');
sleep(2);
@@ -266,20 +266,6 @@ class BuildPHPCommand extends BuildCommand
$path = FileSystem::convertPath("{$build_root_path}/license/");
logger()->info("License path{$fixed}: {$path}");
return static::SUCCESS;
} catch (WrongUsageException $e) {
// WrongUsageException is not an exception, it's a user error, so we just print the error message
logger()->critical($e->getMessage());
logger()->error($e->getTraceAsString());
return static::FAILURE;
} catch (\Throwable $e) {
if ($this->getOption('debug')) {
ExceptionHandler::getInstance()->handle($e);
} else {
logger()->critical('Build failed with ' . get_class($e) . ': ' . $e->getMessage());
logger()->critical('Please check with --debug option to see more details.');
}
return static::FAILURE;
}
}
/**

View File

@@ -4,9 +4,6 @@ declare(strict_types=1);
namespace SPC\command;
use SPC\exception\DownloaderException;
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
use SPC\store\Downloader;
use SPC\store\FileSystem;
use SPC\store\LockFile;
@@ -36,7 +33,6 @@ class DeleteDownloadCommand extends BaseCommand
public function handle(): int
{
try {
// get source list that will be downloaded
$sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources'))));
if (empty($sources)) {
@@ -81,12 +77,5 @@ class DeleteDownloadCommand extends BaseCommand
}
logger()->info('Delete success!');
return static::SUCCESS;
} catch (DownloaderException $e) {
logger()->error($e->getMessage());
return static::FAILURE;
} catch (WrongUsageException $e) {
logger()->critical($e->getMessage());
return static::FAILURE;
}
}
}

View File

@@ -6,9 +6,7 @@ namespace SPC\command;
use SPC\builder\traits\UnixSystemUtilTrait;
use SPC\exception\DownloaderException;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\exception\SPCException;
use SPC\store\Config;
use SPC\store\Downloader;
use SPC\store\LockFile;
@@ -87,7 +85,6 @@ class DownloadCommand extends BaseCommand
public function handle(): int
{
try {
if ($this->getOption('clean')) {
return $this->_clean();
}
@@ -227,7 +224,7 @@ class DownloadCommand extends BaseCommand
logger()->info("[{$ni}/{$cnt}] Downloading source {$source}");
try {
Downloader::downloadSource($source, $config, $force_all || in_array($source, $force_list));
} /* @noinspection PhpRedundantCatchClauseInspection */ catch (DownloaderException|RuntimeException $e) {
} catch (SPCException $e) {
// if `--no-alt` option is set, we will not download alternative sources
if ($this->getOption('no-alt')) {
throw $e;
@@ -251,13 +248,6 @@ class DownloadCommand extends BaseCommand
$time = round(microtime(true) - START_TIME, 3);
logger()->info('Download complete, used ' . $time . ' s !');
return static::SUCCESS;
} catch (DownloaderException $e) {
logger()->error($e->getMessage());
return static::FAILURE;
} catch (WrongUsageException $e) {
logger()->critical($e->getMessage());
return static::FAILURE;
}
}
private function downloadFromZip(string $path): int
@@ -276,28 +266,24 @@ class DownloadCommand extends BaseCommand
}
// unzip command check
if (PHP_OS_FAMILY !== 'Windows' && !$this->findCommand('unzip')) {
logger()->critical('Missing unzip command, you need to install it first !');
logger()->critical('You can use "bin/spc doctor" command to check and install required tools');
$this->output->writeln('Missing unzip command, you need to install it first !');
$this->output->writeln('You can use "bin/spc doctor" command to check and install required tools');
return static::FAILURE;
}
// create downloads
try {
if (PHP_OS_FAMILY !== 'Windows') {
$abs_path = realpath($path);
f_passthru('mkdir ' . DOWNLOAD_PATH . ' && cd ' . DOWNLOAD_PATH . ' && unzip ' . escapeshellarg($abs_path));
} else {
if (PHP_OS_FAMILY === 'Windows') {
// Windows TODO
throw new WrongUsageException('Windows currently does not support --from-zip !');
}
if (!file_exists(LockFile::LOCK_FILE)) {
throw new RuntimeException('.lock.json not exist in "downloads/"');
}
} catch (RuntimeException $e) {
logger()->critical('Extract failed: ' . $e->getMessage());
$this->output->writeln('<error>Windows currently does not support --from-zip !</error>');
return static::FAILURE;
}
logger()->info('Extract success');
$abs_path = realpath($path);
f_passthru('mkdir ' . DOWNLOAD_PATH . ' && cd ' . DOWNLOAD_PATH . ' && unzip ' . escapeshellarg($abs_path));
if (!file_exists(LockFile::LOCK_FILE)) {
$this->output->writeln('<error>.lock.json not exist in "downloads/", please run "bin/spc download" first !</error>');
return static::FAILURE;
}
$this->output->writeln('<info>Extract success</info>');
return static::SUCCESS;
}

View File

@@ -5,9 +5,6 @@ declare(strict_types=1);
namespace SPC\command;
use SPC\builder\traits\UnixSystemUtilTrait;
use SPC\exception\DownloaderException;
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\PackageManager;
use Symfony\Component\Console\Attribute\AsCommand;
@@ -30,7 +27,6 @@ class InstallPkgCommand extends BaseCommand
public function handle(): int
{
try {
// Use shallow-clone can reduce git resource download
if ($this->getOption('shallow-clone')) {
define('GIT_SHALLOW_CLONE', true);
@@ -84,12 +80,5 @@ class InstallPkgCommand extends BaseCommand
$time = round(microtime(true) - START_TIME, 3);
logger()->info('Install packages complete, used ' . $time . ' s !');
return static::SUCCESS;
} catch (DownloaderException $e) {
logger()->error($e->getMessage());
return static::FAILURE;
} catch (WrongUsageException $e) {
logger()->critical($e->getMessage());
return static::FAILURE;
}
}
}

View File

@@ -7,10 +7,7 @@ namespace SPC\command\dev;
use SPC\builder\BuilderProvider;
use SPC\builder\LibraryBase;
use SPC\command\BuildCommand;
use SPC\exception\ExceptionHandler;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\exception\ValidationException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\LockFile;
@@ -30,7 +27,6 @@ class PackLibCommand extends BuildCommand
public function handle(): int
{
try {
$lib_name = $this->getArgument('library');
$builder = BuilderProvider::makeBuilderByInput($this->input);
$builder->setLibsOnly();
@@ -135,15 +131,6 @@ class PackLibCommand extends BuildCommand
$time = round(microtime(true) - START_TIME, 3);
logger()->info('Build libs complete, used ' . $time . ' s !');
return static::SUCCESS;
} catch (\Throwable $e) {
if ($this->getOption('debug')) {
ExceptionHandler::getInstance()->handle($e);
} else {
logger()->critical('Build failed with ' . get_class($e) . ': ' . $e->getMessage());
logger()->critical('Please check with --debug option to see more details.');
}
return static::FAILURE;
}
}
private function sanityCheckLib(LibraryBase $lib): void
@@ -152,7 +139,10 @@ class PackLibCommand extends BuildCommand
// config
foreach ($lib->getStaticLibs() as $static_lib) {
if (!file_exists(FileSystem::convertPath(BUILD_LIB_PATH . '/' . $static_lib))) {
throw new RuntimeException('Static library ' . $static_lib . ' not found in ' . BUILD_LIB_PATH);
throw new ValidationException(
'Static library ' . $static_lib . ' not found in ' . BUILD_LIB_PATH,
validation_module: "Static library {$static_lib} existence check"
);
}
}
}

View File

@@ -4,24 +4,167 @@ declare(strict_types=1);
namespace SPC\exception;
use ZM\Logger\ConsoleColor;
class ExceptionHandler
{
protected mixed $whoops = null;
public const array KNOWN_EXCEPTIONS = [
BuildFailureException::class,
DownloaderException::class,
EnvironmentException::class,
ExecutionException::class,
FileSystemException::class,
InterruptException::class,
PatchException::class,
SPCInternalException::class,
ValidationException::class,
WrongUsageException::class,
];
private static ?ExceptionHandler $obj = null;
public const array MINOR_LOG_EXCEPTIONS = [
InterruptException::class,
WrongUsageException::class,
];
public static function getInstance(): ExceptionHandler
public static function handleSPCException(SPCException $e): void
{
if (self::$obj === null) {
self::$obj = new self();
}
return self::$obj;
// XXX error: yyy
$head_msg = match ($class = get_class($e)) {
BuildFailureException::class => "Build failed: {$e->getMessage()}",
DownloaderException::class => "Download failed: {$e->getMessage()}",
EnvironmentException::class => "Environment check failed: {$e->getMessage()}",
ExecutionException::class => "Command execution failed: {$e->getMessage()}",
FileSystemException::class => "File system error: {$e->getMessage()}",
InterruptException::class => "⚠ Build interrupted by user: {$e->getMessage()}",
PatchException::class => "Patch apply failed: {$e->getMessage()}",
SPCInternalException::class => "SPC internal error: {$e->getMessage()}",
ValidationException::class => "Validation failed: {$e->getMessage()}",
WrongUsageException::class => $e->getMessage(),
default => "Unknown SPC exception {$class}: {$e->getMessage()}",
};
self::logError($head_msg);
// ----------------------------------------
$minor_logs = in_array($class, self::MINOR_LOG_EXCEPTIONS, true);
if ($minor_logs) {
return;
}
public function handle(\Throwable $e): void
self::logError("----------------------------------------\n");
// get the SPCException module
if ($php_info = $e->getBuildPHPInfo()) {
self::logError('✗ Failed module: ' . ConsoleColor::yellow("PHP builder {$php_info['builder_class']} for {$php_info['os']}"));
} elseif ($lib_info = $e->getLibraryInfo()) {
self::logError('✗ Failed module: ' . ConsoleColor::yellow("library {$lib_info['library_name']} builder for {$lib_info['os']}"));
} elseif ($ext_info = $e->getExtensionInfo()) {
self::logError('✗ Failed module: ' . ConsoleColor::yellow("shared extension {$ext_info['extension_name']} builder"));
} elseif (!in_array($class, self::KNOWN_EXCEPTIONS)) {
self::logError('✗ Failed From: ' . ConsoleColor::yellow('Unknown SPC module ' . $class));
}
self::logError('');
// get command execution info
if ($e instanceof ExecutionException) {
self::logError('✗ Failed command: ' . ConsoleColor::yellow($e->getExecutionCommand()));
if ($cd = $e->getCd()) {
self::logError('✗ Command executed in: ' . ConsoleColor::yellow($cd));
}
if ($env = $e->getEnv()) {
self::logError('✗ Command inline env variables:');
foreach ($env as $k => $v) {
self::logError(ConsoleColor::yellow("{$k}={$v}"), 4);
}
}
}
// validation error
if ($e instanceof ValidationException) {
self::logError('✗ Failed validation module: ' . ConsoleColor::yellow($e->getValidationModuleString()));
}
// environment error
if ($e instanceof EnvironmentException) {
self::logError('✗ Failed environment check: ' . ConsoleColor::yellow($e->getMessage()));
if (($solution = $e->getSolution()) !== null) {
self::logError('✗ Solution: ' . ConsoleColor::yellow($solution));
}
}
// get patch info
if ($e instanceof PatchException) {
self::logError("✗ Failed patch module: {$e->getPatchModule()}");
}
// get internal trace
if ($e instanceof SPCInternalException) {
self::logError('✗ Internal trace:');
self::logError(ConsoleColor::gray("{$e->getTraceAsString()}\n"), 4);
}
// get the full build info if possible
if (($info = $e->getBuildPHPExtraInfo()) && defined('DEBUG_MODE')) {
self::logError('✗ Build PHP extra info:');
$maxlen = 0;
foreach ($info as $k => $v) {
$maxlen = max(strlen($k), $maxlen);
}
foreach ($info as $k => $v) {
if (is_string($v)) {
self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($v), 4);
} elseif (is_array($v) && !is_assoc_array($v)) {
$first = array_shift($v);
self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($first), 4);
foreach ($v as $vs) {
self::logError(str_pad('', $maxlen + 2) . ConsoleColor::yellow($vs), 4);
}
}
}
}
self::logError("\n----------------------------------------\n");
// put getenv info to log
$env_log = fopen(SPC_ENV_LOG, 'a');
$env_info = getenv();
if ($env_info) {
foreach ($env_info as $k => $v) {
fwrite($env_log, $k . ' = ' . $v . PHP_EOL);
}
}
self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none(SPC_OUTPUT_LOG));
if (file_exists(SPC_SHELL_LOG)) {
self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none(SPC_SHELL_LOG));
}
if ($e->getExtraLogFiles() !== []) {
foreach ($e->getExtraLogFiles() as $key => $file) {
self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::none(SPC_LOGS_DIR . "/{$file}"));
}
}
if (!defined('DEBUG_MODE')) {
self::logError('⚠ If you want to see more details in console, use `--debug` option.');
}
}
public static function handleDefaultException(\Throwable $e): void
{
logger()->error('Uncaught ' . get_class($e) . ': ' . $e->getMessage() . ' at ' . $e->getFile() . '(' . $e->getLine() . ')');
logger()->error($e->getTraceAsString());
logger()->critical('You can report this exception to static-php-cli GitHub repo.');
$class = get_class($e);
self::logError("Unhandled exception {$class}: {$e->getMessage()}\n\t{$e->getMessage()}\n");
self::logError('Stack trace:');
self::logError(ConsoleColor::gray($e->getTraceAsString()), 4);
self::logError('Please report this exception to: https://github.com/crazywhalecc/static-php-cli/issues');
}
private static function logError($message, int $indent_space = 0): void
{
$spc_log = fopen(SPC_OUTPUT_LOG, 'a');
$msg = explode("\n", (string) $message);
foreach ($msg as $v) {
$line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT);
fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL);
echo ConsoleColor::red($line) . PHP_EOL;
}
}
}

View File

@@ -9,7 +9,6 @@ use SPC\builder\BuilderBase;
use SPC\builder\BuilderProvider;
use SPC\builder\Extension;
use SPC\builder\LibraryBase;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\FileSystem;
use SPC\store\LockFile;
@@ -88,7 +87,7 @@ class BuilderTest extends TestCase
if ($cnt !== 0) {
$this->assertEquals(intval($match[1]), $this->builder->getPHPVersionID());
} else {
$this->expectException(RuntimeException::class);
$this->expectException(WrongUsageException::class);
$this->builder->getPHPVersionID();
}
} else {
@@ -105,7 +104,7 @@ class BuilderTest extends TestCase
if ($cnt !== 0) {
$this->assertEquals($match[1], $this->builder->getPHPVersion());
} else {
$this->expectException(RuntimeException::class);
$this->expectException(WrongUsageException::class);
$this->builder->getPHPVersion();
}
} else {
@@ -246,7 +245,7 @@ class BuilderTest extends TestCase
public function testEmitPatchPointNotExists()
{
$this->expectOutputRegex('/failed to run/');
$this->expectException(RuntimeException::class);
$this->expectException(WrongUsageException::class);
$this->builder->setOption('with-added-patch', ['/tmp/patch-point.not_exsssists.php']);
$this->builder->emitPatchPoint('not-exists');
}