1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* Responsible for executing commands. |
5
|
|
|
* |
6
|
|
|
* This could probably be replaced with something from Symfony, but right now this simple implementation works. |
7
|
|
|
*/ |
8
|
|
|
class Executor { |
9
|
|
|
protected $defaultOptions = array( |
10
|
|
|
'throwException' => true, |
11
|
|
|
'inputContent' => null, |
12
|
|
|
'inputFile' => null, |
13
|
|
|
'inputStream' => null, |
14
|
|
|
'outputFile' => null, |
15
|
|
|
'outputFileAppend' => false, |
16
|
|
|
'outputStream' => null, |
17
|
|
|
); |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* @param string $command The command |
21
|
|
|
* @param boolean $throwException If true, an Exception will be thrown on a nonzero error code |
|
|
|
|
22
|
|
|
* @param boolean $returnOutput If true, output will be captured |
|
|
|
|
23
|
|
|
* @param boolean $inputContent Content for STDIN. Otherwise the parent script's STDIN is used |
|
|
|
|
24
|
|
|
* @return A map containing 'return', 'output', and 'error' |
25
|
|
|
*/ |
26
|
|
|
public function execLocal($command, $options = array()) { |
27
|
|
|
$process = $this->createLocal($command, $options); |
28
|
|
|
return $process->exec(); |
29
|
|
|
} |
30
|
|
|
|
31
|
|
|
public function execRemote($command, $options = array()) { |
32
|
|
|
$process = $this->createRemote($command, $options); |
33
|
|
|
return $process->exec(); |
34
|
|
|
} |
35
|
|
|
|
36
|
|
|
public function createLocal($command, $options) { |
37
|
|
|
$options = array_merge($this->defaultOptions, $options); |
38
|
|
|
if(is_array($command)) $command = $this->commandArrayToString($command); |
39
|
|
|
|
40
|
|
|
return new Process($command, $options); |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
public function createRemote($server, $command, $options = array()) { |
44
|
|
|
$process = $this->createLocal($command, $options); |
45
|
|
|
$process->setRemoteServer($server); |
46
|
|
|
return $process; |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Turn an array command in a string, escaping and concatenating each item |
51
|
|
|
* @param array $command Command array. First element is the command and all remaining are the arguments. |
52
|
|
|
* @return string String command |
53
|
|
|
*/ |
54
|
|
|
public function commandArrayToString($command) { |
55
|
|
|
$string = escapeshellcmd(array_shift($command)); |
56
|
|
|
foreach($command as $arg) { |
57
|
|
|
$string .= ' ' . escapeshellarg($arg); |
58
|
|
|
} |
59
|
|
|
return $string; |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
|
65
|
|
|
class Process { |
66
|
|
|
protected $command; |
67
|
|
|
protected $options; |
68
|
|
|
protected $remoteServer = null; |
69
|
|
|
|
70
|
|
|
public function __construct($command, $options = array()) { |
71
|
|
|
$this->command = $command; |
72
|
|
|
$this->options = $options; |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
public function setRemoteServer($remoteServer) { |
76
|
|
|
$this->remoteServer = $remoteServer; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
public function exec($options = array()) { |
80
|
|
|
$options = array_merge($this->options, $options); |
81
|
|
|
|
82
|
|
|
// Modify command for remote execution, if necessary. |
83
|
|
|
if($this->remoteServer) { |
84
|
|
|
if(!empty($options['outputFile']) || !empty($options['outputStream'])) $ssh = "ssh -T "; |
85
|
|
|
else $ssh = "ssh -t "; |
86
|
|
|
if (!empty($options['identity'])) { |
87
|
|
|
$ssh .= '-i ' . escapeshellarg($options['identity']) . ' '; |
88
|
|
|
} |
89
|
|
|
$command = $ssh . escapeshellarg($this->remoteServer) . ' ' . escapeshellarg($this->command); |
90
|
|
|
} else { |
91
|
|
|
$command = $this->command; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
$pipes = array(); |
95
|
|
|
$pipeSpec = array( |
96
|
|
|
0 => STDIN, |
97
|
|
|
1 => array('pipe', 'w'), |
98
|
|
|
2 => STDERR, |
99
|
|
|
); |
100
|
|
|
|
101
|
|
|
// Alternatives |
102
|
|
|
if($options['inputContent'] || $options['inputStream']) $pipeSpec[0] = array('pipe', 'r'); |
103
|
|
|
|
104
|
|
|
if($options['outputFile']) { |
105
|
|
|
$pipeSpec[1] = array('file', |
106
|
|
|
$options['outputFile'], |
107
|
|
|
$options['outputFileAppend'] ? 'a' : 'w'); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
$process = proc_open($command, $pipeSpec, $pipes); |
111
|
|
|
|
112
|
|
View Code Duplication |
if($options['inputContent']) { |
|
|
|
|
113
|
|
|
fwrite($pipes[0], $options['inputContent']); |
114
|
|
|
|
115
|
|
|
} else if($options['inputStream']) { |
116
|
|
|
while($content = fread($options['inputStream'], 8192)) { |
117
|
|
|
fwrite($pipes[0], $content); |
118
|
|
|
} |
119
|
|
|
} |
120
|
|
|
if(isset($pipes[0])) fclose($pipes[0]); |
121
|
|
|
|
122
|
|
|
$result = array(); |
123
|
|
|
|
124
|
|
|
if(isset($pipes[1])) { |
125
|
|
|
// If a stream was provided, then pipe all the content |
126
|
|
|
// Doing it this way rather than passing outputStream to $pipeSpec |
127
|
|
|
// Means that streams as well as simple FDs can be used |
128
|
|
View Code Duplication |
if($options['outputStream']) { |
|
|
|
|
129
|
|
|
while($content = fread($pipes[1], 8192)) { |
130
|
|
|
fwrite($options['outputStream'], $content); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
// Otherwise save to a string |
134
|
|
|
} else { |
135
|
|
|
$result['output'] = stream_get_contents($pipes[1]); |
136
|
|
|
} |
137
|
|
|
fclose($pipes[1]); |
138
|
|
|
} |
139
|
|
|
if(isset($pipes[2])) { |
140
|
|
|
$result['error'] = stream_get_contents($pipes[2]); |
141
|
|
|
fclose($pipes[2]); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
$result['return'] = proc_close($process); |
145
|
|
|
|
146
|
|
|
if($options['throwException'] && $result['return'] != 0) { |
147
|
|
|
throw new Exception("Command: $command\nExecution failed: returned {$result['return']}.\n" |
148
|
|
|
. (empty($result['output']) ? "" : "Output:\n{$result['output']}")); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
return $result; |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
|
This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.
Consider the following example. The parameter
$italy
is not defined by the methodfinale(...)
.The most likely cause is that the parameter was removed, but the annotation was not.