Chris@5
|
1 <?php
|
Chris@5
|
2
|
Chris@5
|
3 namespace Consolidation\SiteProcess;
|
Chris@5
|
4
|
Chris@5
|
5 use Psr\Log\LoggerInterface;
|
Chris@5
|
6 use Symfony\Component\Console\Style\OutputStyle;
|
Chris@5
|
7 use Symfony\Component\Process\Process;
|
Chris@5
|
8 use Consolidation\SiteProcess\Util\RealtimeOutputHandler;
|
Chris@5
|
9 use Consolidation\SiteProcess\Util\Escape;
|
Chris@5
|
10 use Symfony\Component\Console\Output\OutputInterface;
|
Chris@5
|
11 use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
Chris@5
|
12
|
Chris@5
|
13 /**
|
Chris@5
|
14 * A wrapper around Symfony Process.
|
Chris@5
|
15 *
|
Chris@5
|
16 * - Supports simulated mode. Typically enabled via a --simulate option.
|
Chris@5
|
17 * - Supports verbose mode - logs all runs.
|
Chris@5
|
18 * - Can convert output json data into php array (convenience method)
|
Chris@5
|
19 * - Provides a "realtime output" helper
|
Chris@5
|
20 */
|
Chris@5
|
21 class ProcessBase extends Process
|
Chris@5
|
22 {
|
Chris@5
|
23 /**
|
Chris@5
|
24 * @var OutputStyle
|
Chris@5
|
25 */
|
Chris@5
|
26 protected $output;
|
Chris@5
|
27
|
Chris@5
|
28 /**
|
Chris@5
|
29 * @var OutputInterface
|
Chris@5
|
30 */
|
Chris@5
|
31 protected $stderr;
|
Chris@5
|
32
|
Chris@5
|
33 private $simulated = false;
|
Chris@5
|
34
|
Chris@5
|
35 private $verbose = false;
|
Chris@5
|
36
|
Chris@5
|
37 /**
|
Chris@5
|
38 * @var LoggerInterface
|
Chris@5
|
39 */
|
Chris@5
|
40 private $logger;
|
Chris@5
|
41
|
Chris@5
|
42 /**
|
Chris@5
|
43 * Symfony 4 style constructor for creating Process instances from strings.
|
Chris@5
|
44 * @param string $command The commandline string to run
|
Chris@5
|
45 * @param string|null $cwd The working directory or null to use the working dir of the current PHP process
|
Chris@5
|
46 * @param array|null $env The environment variables or null to use the same environment as the current PHP process
|
Chris@5
|
47 * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input
|
Chris@5
|
48 * @param int|float|null $timeout The timeout in seconds or null to disable
|
Chris@5
|
49 * @return Process
|
Chris@5
|
50 */
|
Chris@5
|
51 public static function fromShellCommandline($command, $cwd = null, array $env = null, $input = null, $timeout = 60)
|
Chris@5
|
52 {
|
Chris@5
|
53 if (method_exists('\Symfony\Component\Process\Process', 'fromShellCommandline')) {
|
Chris@5
|
54 return Process::fromShellCommandline($command, $cwd, $env, $input, $timeout);
|
Chris@5
|
55 }
|
Chris@5
|
56 return new self($command, $cwd, $env, $input, $timeout);
|
Chris@5
|
57 }
|
Chris@5
|
58
|
Chris@5
|
59 /**
|
Chris@5
|
60 * realtimeStdout returns the output stream that realtime output
|
Chris@5
|
61 * should be sent to (if applicable)
|
Chris@5
|
62 *
|
Chris@5
|
63 * @return OutputStyle $output
|
Chris@5
|
64 */
|
Chris@5
|
65 public function realtimeStdout()
|
Chris@5
|
66 {
|
Chris@5
|
67 return $this->output;
|
Chris@5
|
68 }
|
Chris@5
|
69
|
Chris@5
|
70 protected function realtimeStderr()
|
Chris@5
|
71 {
|
Chris@5
|
72 if ($this->stderr) {
|
Chris@5
|
73 return $this->stderr;
|
Chris@5
|
74 }
|
Chris@5
|
75 if (method_exists($this->output, 'getErrorStyle')) {
|
Chris@5
|
76 return $this->output->getErrorStyle();
|
Chris@5
|
77 }
|
Chris@5
|
78
|
Chris@5
|
79 return $this->realtimeStdout();
|
Chris@5
|
80 }
|
Chris@5
|
81
|
Chris@5
|
82 /**
|
Chris@5
|
83 * setRealtimeOutput allows the caller to inject an OutputStyle object
|
Chris@5
|
84 * that will be used to stream realtime output if applicable.
|
Chris@5
|
85 *
|
Chris@5
|
86 * @param OutputStyle $output
|
Chris@5
|
87 */
|
Chris@5
|
88 public function setRealtimeOutput(OutputInterface $output, $stderr = null)
|
Chris@5
|
89 {
|
Chris@5
|
90 $this->output = $output;
|
Chris@5
|
91 $this->stderr = $stderr instanceof ConsoleOutputInterface ? $stderr->getErrorOutput() : $stderr;
|
Chris@5
|
92 }
|
Chris@5
|
93
|
Chris@5
|
94 /**
|
Chris@5
|
95 * @return bool
|
Chris@5
|
96 */
|
Chris@5
|
97 public function isVerbose()
|
Chris@5
|
98 {
|
Chris@5
|
99 return $this->verbose;
|
Chris@5
|
100 }
|
Chris@5
|
101
|
Chris@5
|
102 /**
|
Chris@5
|
103 * @param bool $verbose
|
Chris@5
|
104 */
|
Chris@5
|
105 public function setVerbose($verbose)
|
Chris@5
|
106 {
|
Chris@5
|
107 $this->verbose = $verbose;
|
Chris@5
|
108 }
|
Chris@5
|
109
|
Chris@5
|
110 /**
|
Chris@5
|
111 * @return bool
|
Chris@5
|
112 */
|
Chris@5
|
113 public function isSimulated()
|
Chris@5
|
114 {
|
Chris@5
|
115 return $this->simulated;
|
Chris@5
|
116 }
|
Chris@5
|
117
|
Chris@5
|
118 /**
|
Chris@5
|
119 * @param bool $simulated
|
Chris@5
|
120 */
|
Chris@5
|
121 public function setSimulated($simulated)
|
Chris@5
|
122 {
|
Chris@5
|
123 $this->simulated = $simulated;
|
Chris@5
|
124 }
|
Chris@5
|
125
|
Chris@5
|
126 /**
|
Chris@5
|
127 * @return LoggerInterface
|
Chris@5
|
128 */
|
Chris@5
|
129 public function getLogger()
|
Chris@5
|
130 {
|
Chris@5
|
131 return $this->logger;
|
Chris@5
|
132 }
|
Chris@5
|
133
|
Chris@5
|
134 /**
|
Chris@5
|
135 * @param LoggerInterface $logger
|
Chris@5
|
136 */
|
Chris@5
|
137 public function setLogger($logger)
|
Chris@5
|
138 {
|
Chris@5
|
139 $this->logger = $logger;
|
Chris@5
|
140 }
|
Chris@5
|
141
|
Chris@5
|
142 /**
|
Chris@5
|
143 * @inheritDoc
|
Chris@5
|
144 */
|
Chris@5
|
145 public function start(callable $callback = null, $env = array())
|
Chris@5
|
146 {
|
Chris@5
|
147 $cmd = $this->getCommandLine();
|
Chris@5
|
148 if ($this->isSimulated()) {
|
Chris@5
|
149 $this->getLogger()->notice('Simulating: ' . $cmd);
|
Chris@5
|
150 // Run a command that always succeeds (on Linux and Windows).
|
Chris@5
|
151 $this->setCommandLine('true');
|
Chris@5
|
152 } elseif ($this->isVerbose()) {
|
Chris@5
|
153 $this->getLogger()->info('Executing: ' . $cmd);
|
Chris@5
|
154 }
|
Chris@5
|
155 parent::start($callback, $env);
|
Chris@5
|
156 // Set command back to original value in case anyone asks.
|
Chris@5
|
157 if ($this->isSimulated()) {
|
Chris@5
|
158 $this->setCommandLine($cmd);
|
Chris@5
|
159 }
|
Chris@5
|
160 }
|
Chris@5
|
161
|
Chris@5
|
162 /**
|
Chris@5
|
163 * Get Process output and decode its JSON.
|
Chris@5
|
164 *
|
Chris@5
|
165 * @return array
|
Chris@5
|
166 * An associative array.
|
Chris@5
|
167 */
|
Chris@5
|
168 public function getOutputAsJson()
|
Chris@5
|
169 {
|
Chris@5
|
170 $output = trim($this->getOutput());
|
Chris@5
|
171 if (empty($output)) {
|
Chris@5
|
172 throw new \InvalidArgumentException('Output is empty.');
|
Chris@5
|
173 }
|
Chris@5
|
174 if (Escape::isWindows()) {
|
Chris@5
|
175 // Doubled double quotes were converted to \\".
|
Chris@5
|
176 // Revert to double quote.
|
Chris@5
|
177 $output = str_replace('\\"', '"', $output);
|
Chris@5
|
178 // Revert of doubled backslashes.
|
Chris@5
|
179 $output = preg_replace('#\\\\{2}#', '\\', $output);
|
Chris@5
|
180 }
|
Chris@5
|
181 $output = $this->removeNonJsonJunk($output);
|
Chris@5
|
182 $json = json_decode($output, true);
|
Chris@5
|
183 if (!isset($json)) {
|
Chris@5
|
184 throw new \InvalidArgumentException('Unable to decode output into JSON.');
|
Chris@5
|
185 }
|
Chris@5
|
186 return $json;
|
Chris@5
|
187 }
|
Chris@5
|
188
|
Chris@5
|
189 /**
|
Chris@5
|
190 * Allow for a certain amount of resiliancy in the output received when
|
Chris@5
|
191 * json is expected.
|
Chris@5
|
192 *
|
Chris@5
|
193 * @param string $data
|
Chris@5
|
194 * @return string
|
Chris@5
|
195 */
|
Chris@5
|
196 protected function removeNonJsonJunk($data)
|
Chris@5
|
197 {
|
Chris@5
|
198 // Exit early if we have no output.
|
Chris@5
|
199 $data = trim($data);
|
Chris@5
|
200 if (empty($data)) {
|
Chris@5
|
201 return $data;
|
Chris@5
|
202 }
|
Chris@5
|
203 // If the data is a simple quoted string, or an array, then exit.
|
Chris@5
|
204 if ((($data[0] == '"') && ($data[strlen($data) - 1] == '"')) ||
|
Chris@5
|
205 (($data[0] == "[") && ($data[strlen($data) - 1] == "]"))
|
Chris@5
|
206 ) {
|
Chris@5
|
207 return $data;
|
Chris@5
|
208 }
|
Chris@5
|
209 // If the json is not a simple string or a simple array, then is must
|
Chris@5
|
210 // be an associative array. We will remove non-json garbage characters
|
Chris@5
|
211 // before and after the enclosing curley-braces.
|
Chris@5
|
212 $start = strpos($data, '{');
|
Chris@5
|
213 $end = strrpos($data, '}') + 1;
|
Chris@5
|
214 $data = substr($data, $start, $end - $start);
|
Chris@5
|
215 return $data;
|
Chris@5
|
216 }
|
Chris@5
|
217
|
Chris@5
|
218 /**
|
Chris@5
|
219 * Return a realTime output object.
|
Chris@5
|
220 *
|
Chris@5
|
221 * @return callable
|
Chris@5
|
222 */
|
Chris@5
|
223 public function showRealtime()
|
Chris@5
|
224 {
|
Chris@5
|
225 $realTimeOutput = new RealtimeOutputHandler($this->realtimeStdout(), $this->realtimeStderr());
|
Chris@5
|
226 $realTimeOutput->configure($this);
|
Chris@5
|
227 return $realTimeOutput;
|
Chris@5
|
228 }
|
Chris@5
|
229 }
|