add doctor command

This commit is contained in:
crazywhalecc
2023-04-22 21:23:12 +08:00
parent 4c0d35c723
commit 528ad1199a
11 changed files with 334 additions and 27 deletions

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace SPC\doctor;
#[\Attribute(\Attribute::TARGET_METHOD)]
class AsCheckItem
{
public mixed $callback = null;
public function __construct(
public string $item_name,
public ?string $limit_os = null,
public int $level = 100
) {
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace SPC\doctor;
#[\Attribute(\Attribute::TARGET_METHOD)]
class AsFixItem
{
public function __construct(public string $name)
{
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace SPC\doctor;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\store\FileSystem;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class CheckListHandler
{
/** @var AsCheckItem[] */
private array $check_list = [];
private array $fix_map = [];
/**
* @throws \ReflectionException
* @throws FileSystemException
* @throws RuntimeException
*/
public function __construct(private InputInterface $input, private OutputInterface $output)
{
$this->loadCheckList();
}
/**
* @throws RuntimeException
*/
public function runCheck(int $fix_policy = FIX_POLICY_DIE): void
{
foreach ($this->check_list as $item) {
if ($item->limit_os !== null && $item->limit_os !== PHP_OS_FAMILY) {
continue;
}
$this->output->write('Checking <comment>' . $item->item_name . '</comment> ... ');
$result = call_user_func($item->callback);
if ($result === null) {
$this->output->writeln('skipped');
} elseif ($result instanceof CheckResult) {
if ($result->isOK()) {
$this->output->writeln('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() !== '') {
$helper = new QuestionHelper();
$question = new ConfirmationQuestion('Do you want to fix it? [Y/n] ', true);
if ($helper->ask($this->input, $this->output, $question)) {
$this->emitFix($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() . ' ...');
$this->emitFix($result);
} else {
throw new RuntimeException('Some check items can not be fixed !');
}
break;
}
}
}
}
/**
* Load Doctor check item list
*
* @throws \ReflectionException
* @throws RuntimeException
* @throws FileSystemException
*/
private function loadCheckList(): void
{
foreach (FileSystem::getClassesPsr4(__DIR__ . '/item', 'SPC\\doctor\\item') as $class) {
$ref = new \ReflectionClass($class);
foreach ($ref->getMethods() as $method) {
$attr = $method->getAttributes(AsCheckItem::class);
if (isset($attr[0])) {
/** @var AsCheckItem $instance */
$instance = $attr[0]->newInstance();
$instance->callback = [new $class(), $method->getName()];
$this->check_list[] = $instance;
continue;
}
$attr = $method->getAttributes(AsFixItem::class);
if (isset($attr[0])) {
/** @var AsFixItem $instance */
$instance = $attr[0]->newInstance();
// Redundant fix item
if (isset($this->fix_map[$instance->name])) {
throw new RuntimeException('Redundant doctor fix item: ' . $instance->name);
}
$this->fix_map[$instance->name] = [new $class(), $method->getName()];
}
}
}
// sort check list by level
usort($this->check_list, fn ($a, $b) => $a->level > $b->level ? -1 : ($a->level == $b->level ? 0 : 1));
}
private function emitFix(CheckResult $result)
{
$fix = $this->fix_map[$result->getFixItem()];
$fix_result = call_user_func($fix, ...$result->getFixParams());
if ($fix_result) {
$this->output->writeln('<info>Fix done</info>');
} else {
$this->output->writeln('<error>Fix failed</error>');
throw new RuntimeException('Some check item are not fixed');
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace SPC\doctor;
class CheckResult
{
public function __construct(private string $message = '', private string $fix_item = '', private array $fix_params = [])
{
}
public static function fail(string $message, string $fix_item = '', array $fix_params = []): CheckResult
{
return new static($message, $fix_item, $fix_params);
}
public static function ok(): CheckResult
{
return new static();
}
public function getMessage(): string
{
return $this->message;
}
public function getFixItem(): string
{
return $this->fix_item;
}
public function getFixParams(): array
{
return $this->fix_params;
}
public function isOK(): bool
{
return empty($this->message);
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace SPC\doctor\item;
use SPC\builder\traits\UnixSystemUtilTrait;
use SPC\doctor\AsCheckItem;
use SPC\doctor\AsFixItem;
use SPC\doctor\CheckResult;
use SPC\exception\RuntimeException;
class MacOSToolCheckList
{
use UnixSystemUtilTrait;
/** @var string[] MacOS 环境下编译依赖的命令 */
public const REQUIRED_COMMANDS = [
'curl',
'make',
'bison',
'flex',
'pkg-config',
'git',
'autoconf',
'automake',
'tar',
'unzip',
'xz',
'gzip',
'bzip2',
'gotop',
'cmake',
];
#[AsCheckItem('if homebrew has installed', limit_os: 'Darwin', level: 998)]
public function checkBrew(): ?CheckResult
{
// 检查 homebrew 是否已经安装
if ($this->findCommand('brew') === null) {
return CheckResult::fail('Homebrew is not installed', 'brew');
}
return CheckResult::ok();
}
#[AsCheckItem('if necessary tools are installed', limit_os: 'Darwin')]
public function checkCliTools(): ?CheckResult
{
$missing = [];
foreach (self::REQUIRED_COMMANDS as $cmd) {
if ($this->findCommand($cmd) === null) {
$missing[] = $cmd;
}
}
if (!empty($missing)) {
return CheckResult::fail('missing system commands: ' . implode(', ', $missing), 'build-tools', [$missing]);
}
return CheckResult::ok();
}
#[AsFixItem('brew')]
public function fixBrew(): bool
{
try {
shell(true)->exec('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"');
} catch (RuntimeException) {
return false;
}
return true;
}
#[AsFixItem('build-tools')]
public function fixBuildTools(array $missing): bool
{
foreach ($missing as $cmd) {
try {
shell(true)->exec('brew install ' . escapeshellarg($cmd));
} catch (RuntimeException) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace SPC\doctor\item;
use SPC\builder\traits\UnixSystemUtilTrait;
use SPC\doctor\AsCheckItem;
use SPC\doctor\CheckResult;
class OSCheckList
{
use UnixSystemUtilTrait;
#[AsCheckItem('if current os are supported', level: 999)]
public function checkOS(): ?CheckResult
{
if (!in_array(PHP_OS_FAMILY, ['Darwin', 'Linux'])) {
return CheckResult::fail('Current OS is not supported');
}
return CheckResult::ok();
}
}