Introduce AttributeMapper for managing extensions and doctor attributes

This commit is contained in:
crazywhalecc
2025-08-06 20:35:52 +08:00
committed by Jerry Ma
parent e28580de00
commit 722bb31815
14 changed files with 338 additions and 265 deletions

View File

@@ -69,9 +69,6 @@ abstract class BaseCommand extends Command
}
}
/**
* @throws WrongUsageException
*/
abstract public function handle(): int;
protected function execute(InputInterface $input, OutputInterface $output): int
@@ -79,22 +76,17 @@ abstract class BaseCommand extends Command
$this->input = $input;
$this->output = $output;
global $ob_logger;
if ($input->getOption('debug') || $output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) {
$ob_logger = new ConsoleLogger(LogLevel::DEBUG, decorated: !$input->getOption('no-ansi'));
define('DEBUG_MODE', true);
} else {
$ob_logger = new ConsoleLogger(decorated: !$input->getOption('no-ansi'));
}
// init log
$this->initLogFiles();
// windows fallback
Prompt::fallbackWhen(PHP_OS_FAMILY === 'Windows');
ConfirmPrompt::fallbackUsing(function (ConfirmPrompt $prompt) use ($input, $output) {
$helper = new QuestionHelper();
$case = $prompt->default ? ' [Y/n] ' : ' [y/N] ';
$question = new ConfirmationQuestion($prompt->label . $case, $prompt->default);
return $helper->ask($input, $output, $question);
});
// init logger
$this->initConsoleLogger();
// load attribute maps
AttributeMapper::init();
// init windows fallback
$this->initWindowsPromptFallback($input, $output);
// init GlobalEnv
if (!$this instanceof BuildCommand) {
@@ -178,4 +170,58 @@ abstract class BaseCommand extends Command
return true;
}));
}
/**
* Initialize spc log files.
*/
private function initLogFiles(): void
{
$log_dir = SPC_LOGS_DIR;
if (!file_exists($log_dir)) {
mkdir($log_dir, 0755, true);
} elseif (!$this->getOption('preserve-log')) {
// Clean up old log files
$files = glob($log_dir . '/*.log');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
}
}
/**
* Initialize console logger.
*/
private function initConsoleLogger(): void
{
global $ob_logger;
if ($this->input->getOption('debug') || $this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) {
$ob_logger = new ConsoleLogger(LogLevel::DEBUG, decorated: !$this->input->getOption('no-ansi'));
define('DEBUG_MODE', true);
} else {
$ob_logger = new ConsoleLogger(decorated: !$this->input->getOption('no-ansi'));
}
$log_file_fd = fopen(SPC_OUTPUT_LOG, 'a');
$ob_logger->addLogCallback(function ($level, $output) use ($log_file_fd) {
if ($log_file_fd) {
fwrite($log_file_fd, strip_ansi_colors($output) . "\n");
}
return true;
});
}
/**
* Initialize Windows prompt fallback for laravel-prompts.
*/
private function initWindowsPromptFallback(InputInterface $input, OutputInterface $output): void
{
Prompt::fallbackWhen(PHP_OS_FAMILY === 'Windows');
ConfirmPrompt::fallbackUsing(function (ConfirmPrompt $prompt) use ($input, $output) {
$helper = new QuestionHelper();
$case = $prompt->default ? ' [Y/n] ' : ' [y/N] ';
$question = new ConfirmationQuestion($prompt->label . $case, $prompt->default);
return $helper->ask($input, $output, $question);
});
}
}

View File

@@ -4,10 +4,12 @@ declare(strict_types=1);
namespace SPC\command;
use SPC\doctor\CheckListHandler;
use SPC\doctor\CheckResult;
use SPC\exception\RuntimeException;
use SPC\doctor\DoctorHandler;
use SPC\util\AttributeMapper;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
use ZM\Logger\ConsoleColor;
use function Laravel\Prompts\confirm;
@@ -16,72 +18,78 @@ class DoctorCommand extends BaseCommand
{
public function configure(): void
{
$this->addOption('auto-fix', null, null, 'Automatically fix failed items (if possible)');
$this->addOption('auto-fix', null, InputOption::VALUE_OPTIONAL, 'Automatically fix failed items (if possible)', false);
}
public function handle(): int
{
try {
$checker = new CheckListHandler();
// skipped items
$skip_items = array_filter(explode(',', getenv('SPC_SKIP_DOCTOR_CHECK_ITEMS') ?: ''));
$fix_policy = match ($this->input->getOption('auto-fix')) {
'never' => FIX_POLICY_DIE,
true, null => FIX_POLICY_AUTOFIX,
default => FIX_POLICY_PROMPT,
};
$fix_map = AttributeMapper::getDoctorFixMap();
$fix_policy = $this->input->getOption('auto-fix') ? FIX_POLICY_AUTOFIX : FIX_POLICY_PROMPT;
foreach ($checker->runChecks() as $check) {
if ($check->limit_os !== null && $check->limit_os !== PHP_OS_FAMILY) {
continue;
foreach (DoctorHandler::getValidCheckList() as $check) {
// output
$this->output->write("Checking <comment>{$check->item_name}</comment> ... ");
// null => skipped
if (($result = call_user_func($check->callback)) === null) {
$this->output->writeln('skipped');
continue;
}
// invalid return value => skipped
if (!$result instanceof CheckResult) {
$this->output->writeln('<error>Skipped due to invalid return value</error>');
continue;
}
// true => OK
if ($result->isOK()) {
/* @phpstan-ignore-next-line */
$this->output->writeln($result->getMessage() ?? (string) ConsoleColor::green('✓'));
continue;
}
// Failed => output error message
$this->output->writeln('<error>' . $result->getMessage() . '</error>');
// If the result is not fixable, fail immediately
if ($result->getFixItem() === '') {
$this->output->writeln('This check item can not be fixed !');
return static::FAILURE;
}
if (!isset($fix_map[$result->getFixItem()])) {
$this->output->writeln("<error>Internal error: Unknown fix item: {$result->getFixItem()}</error>");
return static::FAILURE;
}
// prompt for fix
if ($fix_policy === FIX_POLICY_PROMPT) {
if (!confirm('Do you want to fix it?')) {
$this->output->writeln('<comment>You canceled fix.</comment>');
return static::FAILURE;
}
$this->output->write('Checking <comment>' . $check->item_name . '</comment> ... ');
// check if this item is skipped
if (in_array($check->item_name, $skip_items) || ($result = call_user_func($check->callback)) === null) {
$this->output->writeln('skipped');
} elseif ($result instanceof CheckResult) {
if ($result->isOK()) {
$this->output->writeln($result->getMessage() ?? 'ok');
continue;
}
// Failed
$this->output->writeln('<error>' . $result->getMessage() . '</error>');
switch ($fix_policy) {
case FIX_POLICY_DIE:
throw new RuntimeException('Some check items can not be fixed !');
case FIX_POLICY_PROMPT:
if ($result->getFixItem() !== '') {
$question = confirm('Do you want to fix it?');
if ($question) {
$checker->emitFix($this->output, $result);
} else {
throw new RuntimeException('You cancelled fix');
}
} else {
throw new RuntimeException('Some check items can not be fixed !');
}
break;
case FIX_POLICY_AUTOFIX:
if ($result->getFixItem() !== '') {
$this->output->writeln('Automatically fixing ' . $result->getFixItem() . ' ...');
$checker->emitFix($this->output, $result);
} else {
throw new RuntimeException('Some check items can not be fixed !');
}
break;
}
if (DoctorHandler::emitFix($this->output, $result)) {
$this->output->writeln('<info>Fix applied successfully!</info>');
} else {
$this->output->writeln('<error>Failed to apply fix!</error>');
return static::FAILURE;
}
}
$this->output->writeln('<info>Doctor check complete !</info>');
} catch (\Throwable $e) {
$this->output->writeln('<error>' . $e->getMessage() . '</error>');
if (extension_loaded('pcntl')) {
pcntl_signal(SIGINT, SIG_IGN);
// auto fix
if ($fix_policy === FIX_POLICY_AUTOFIX) {
$this->output->writeln('Automatically fixing ' . $result->getFixItem() . ' ...');
if (DoctorHandler::emitFix($this->output, $result)) {
$this->output->writeln('<info>Fix applied successfully!</info>');
} else {
$this->output->writeln('<error>Failed to apply fix!</error>');
return static::FAILURE;
}
}
return static::FAILURE;
}
$this->output->writeln('<info>Doctor check complete !</info>');
return static::SUCCESS;
}
}