]> BookStack Code Mirror - system-cli/blob - src/Services/ProgramRunner.php
Bumped version
[system-cli] / src / Services / ProgramRunner.php
1 <?php declare(strict_types=1);
2
3 namespace Cli\Services;
4
5 use Exception;
6 use Symfony\Component\Process\ExecutableFinder;
7 use Symfony\Component\Process\Process;
8
9 class ProgramRunner
10 {
11     /**
12      * Names of the program to search for.
13      * @var string[]
14      */
15     protected array $programNames = [];
16
17     protected int $timeout = 240;
18     protected int $idleTimeout = 15;
19     protected array $environment = [];
20     protected array $additionalProgramDirectories = [];
21
22     public function __construct(
23         string|array $program,
24         protected string $defaultPath
25     ) {
26         $this->programNames = is_string($program) ? [$program] : $program;
27     }
28
29     public function withTimeout(int $timeoutSeconds): static
30     {
31         $this->timeout = $timeoutSeconds;
32         return $this;
33     }
34
35     public function withIdleTimeout(int $idleTimeoutSeconds): static
36     {
37         $this->idleTimeout = $idleTimeoutSeconds;
38         return $this;
39     }
40
41     public function withEnvironment(array $environment): static
42     {
43         $this->environment = $environment;
44         return $this;
45     }
46
47     public function withAdditionalPathLocation(string $directoryPath): static
48     {
49         $this->additionalProgramDirectories[] = $directoryPath;
50         return $this;
51     }
52
53     public function runCapturingAllOutput(array $args): string
54     {
55         $output = '';
56         $callable = function ($data) use (&$output) {
57             $output .= $data;
58         };
59
60         $this->runWithoutOutputCallbacks($args, $callable, $callable);
61         return $output;
62     }
63
64     public function runCapturingStdErr(array $args): string
65     {
66         $err = '';
67         $this->runWithoutOutputCallbacks($args, fn() => '', function ($data) use (&$err) {
68             $err .= $data;
69         });
70         return $err;
71     }
72
73     public function runWithoutOutputCallbacks(array $args, callable $stdOutCallback = null, callable $stdErrCallback = null): int
74     {
75         $process = $this->startProcess($args);
76         foreach ($process as $type => $data) {
77             if ($type === $process::ERR) {
78                 if ($stdErrCallback) {
79                     $stdErrCallback($data);
80                 }
81             } else {
82                 if ($stdOutCallback) {
83                     $stdOutCallback($data);
84                 }
85             }
86         }
87
88         return $process->getExitCode() ?? 1;
89     }
90
91     /**
92      * @throws Exception
93      */
94     public function ensureFound(): void
95     {
96         $this->resolveProgramPath();
97     }
98
99     public function isFound(): bool
100     {
101         try {
102             $this->ensureFound();
103             return true;
104         } catch (Exception $exception) {
105             return false;
106         }
107     }
108
109     protected function startProcess(array $args): Process
110     {
111         $programPath = $this->resolveProgramPath();
112         $process = new Process([$programPath, ...$args], null, $this->environment);
113         $process->setTimeout($this->timeout);
114         $process->setIdleTimeout($this->idleTimeout);
115         $process->start();
116         return $process;
117     }
118
119     /**
120      * @throws Exception
121      */
122     protected function resolveProgramPath(): string
123     {
124         foreach ($this->programNames as $programName) {
125             $executableFinder = new ExecutableFinder();
126             $path = $executableFinder->find($programName, $this->defaultPath, $this->additionalProgramDirectories);
127
128             if (!is_null($path) && is_file($path)) {
129                 return $path;
130             }
131         }
132
133         $combinedProgram = implode('|', $this->programNames);
134         throw new Exception("Could not locate \"{$combinedProgram}\" program.");
135     }
136 }