Lines 100.00% 25 / 25
Functions and Methods 100.00% 5 / 5
Classes and Traits 100.00% 1 / 1
Name Lines Functions and Methods CRAP Classes and Traits
StatusCommand 100.00% 25 / 25 100.00% 5 / 5 11 100.00% 1 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 configure 100.00% 4 / 4 100.00% 1 / 1 1
 execute 100.00% 12 / 12 100.00% 1 / 1 4
 reportProbe 100.00% 6 / 6 100.00% 1 / 1 3
 findProbeOption 100.00% 2 / 2 100.00% 1 / 1 2
1<?php
2
3declare(strict_types=1);
4
5namespace Stewart\Runtime\Console;
6
7use Amp\ByteStream\StreamException;
8use Amp\CancelledException;
9use Stewart\Contracts\Exception\StewartException;
10use Stewart\Runtime\Config\ConfigLoader;
11use Stewart\Runtime\Control\Client\ControlClient;
12use Stewart\Runtime\Control\Client\ControlTargetResolver;
13use Stewart\Runtime\Control\Protocol\Codec\FrameCodec;
14use Stewart\Runtime\Control\Protocol\Frame\SnapshotFrame;
15use Stewart\Runtime\Control\Protocol\Status\RuntimeSnapshot;
16use Stewart\Runtime\Exception\ConfigurationException;
17use Stewart\Runtime\Health\ProbeKind;
18use Stewart\Runtime\Health\ReadinessCheck;
19use Symfony\Component\Console\Attribute\AsCommand;
20use Symfony\Component\Console\Command\Command;
21use Symfony\Component\Console\Input\InputInterface;
22use Symfony\Component\Console\Input\InputOption;
23use Symfony\Component\Console\Output\OutputInterface;
24
25#[AsCommand(name: self::NAME, description: 'Show what the running daemon is doing, once')]
26final class StatusCommand extends ControlCommand
27{
28    public const string NAME = 'status';
29
30    public function __construct(
31        ConfigLoader $config,
32        ControlTargetResolver $targets,
33        private readonly ControlClient $client,
34        private readonly StatusRenderer $renderer,
35        private readonly FrameCodec $codec,
36        private readonly ReadinessCheck $readiness,
37    ) {
38        parent::__construct($config, $targets);
39    }
40
41    protected function configure(): void
42    {
43        parent::configure();
44        $this
45            ->addOption('json', null, InputOption::VALUE_NONE, 'Print the snapshot as the wire JSON instead of tables')
46            ->addOption('probe', null, InputOption::VALUE_REQUIRED, 'liveness or readiness: print one line and exit 0 or 1, for health checks');
47    }
48
49    protected function execute(InputInterface $input, OutputInterface $output): int
50    {
51        try {
52            $probe = $this->findProbeOption($input);
53            $snapshot = $this->client->fetchSnapshot($this->resolveControlTarget($input), $this->parseTimeoutOption($input));
54        } catch (StewartException|StreamException|CancelledException $e) {
55            $output->writeln('<error>' . $e->getMessage() . '</error>');
56
57            return Command::FAILURE;
58        }
59
60        if ($probe !== null) {
61            return $this->reportProbe($output, $probe, $snapshot);
62        }
63
64        if ((bool) $input->getOption('json')) {
65            $output->write($this->codec->encodeFrame(new SnapshotFrame($snapshot)));
66
67            return Command::SUCCESS;
68        }
69
70        $this->renderer->render($output, $snapshot);
71
72        return Command::SUCCESS;
73    }
74
75    private function reportProbe(OutputInterface $output, ProbeKind $probe, RuntimeSnapshot $snapshot): int
76    {
77        if ($probe === ProbeKind::Liveness) {
78            $output->writeln('alive');
79
80            return Command::SUCCESS;
81        }
82
83        $verdict = $this->readiness->assessSnapshot($snapshot);
84        $output->writeln($verdict->describeVerdict());
85
86        return $verdict->isReady() ? Command::SUCCESS : Command::FAILURE;
87    }
88
89    /** @throws ConfigurationException */
90    private function findProbeOption(InputInterface $input): ?ProbeKind
91    {
92        $probe = $this->nonEmptyStringOption($input, 'probe');
93
94        return $probe === null ? null : ProbeKind::parse($probe);
95    }
96}