Completed
Push — master ( 2476a6...86cf37 )
by Joe
02:41
created

Remote::run()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5.2742

Importance

Changes 7
Bugs 0 Features 2
Metric Value
c 7
b 0
f 2
dl 0
loc 30
ccs 14
cts 18
cp 0.7778
rs 8.439
cc 5
eloc 19
nc 6
nop 1
crap 5.2742
1
<?php
2
3
namespace Assimtech\Tempo\Node;
4
5
use Assimtech\Tempo\ArrayObject\ValidatableArrayObject;
6
use InvalidArgumentException;
7
use Symfony\Component\Process\ProcessBuilder;
8
use Symfony\Component\Process\Exception\ProcessFailedException;
9
use Assimtech\Tempo\Process\Exception\RemoteProcessFailedException;
10
use Assimtech\Sysexits;
11
12
class Remote extends ValidatableArrayObject implements NodeInterface
13
{
14
    /**
15
     * @var \Symfony\Component\Process\ProcessBuilder $sshProcessBuilder
16
     */
17
    protected $processBuilder;
18
19
    /**
20
     * {@inheritdoc}
21
     *
22
     * Built-in properties are:
23
     *  [Mandatory]
24
     *  [ssh][host] - The hostname or IP address for the ssh connection
25
     *
26
     *  [Optional]
27
     *  [ssh][user] - The user to use for the ssh connection
28
     *
29
     *  [ssh][options] - An associative array of ssh options, see -o option in ssh(1)
30
     *
31
     *  [ssh][control][useControlMaster] - Use control master connection?
32
     *  [ssh][control][ControlPath] - see ControlPath in ssh_config(5)
33
     *  [ssh][control][ControlPersist] - see ControlPersist in ssh_config(5)
34
     *  [ssh][control][closeOnDestruct] - Should the control master connection be destroyed when this node is?
35
     *
36
     * @throws \InvalidArgumentException
37
     */
38 10
    public function __construct($input = array(), $flags = 0, $iteratorClass = 'ArrayIterator')
39
    {
40
        // Handle string shortcut setup
41 10
        if (is_string($input)) {
42 6
            $userHost = explode('@', $input);
43 6
            if (count($userHost) === 2) {
44
                $input = array(
45
                    'ssh' => array(
46 2
                        'user' => $userHost[0],
47 2
                        'host' => $userHost[1],
48 2
                    ),
49 2
                );
50 2
            } else {
51
                $input = array(
52
                    'ssh' => array(
53 4
                        'host' => $input,
54 4
                    ),
55 4
                );
56
            }
57 6
        }
58
59
        // Defaults
60 10
        if (is_array($input)) {
61 10
            $input = array_replace_recursive(array(
62
                'ssh' => array(
63 10
                    'host' => null,
64
                    'options' => array(
65 10
                        'RequestTTY' => 'no', // Disable pseudo-tty allocation
66 10
                    ),
67
                    'control' => array(
68 10
                        'useControlMaster' => true,
69 10
                    ),
70 10
                ),
71 10
            ), $input);
72
73 10
            if ($input['ssh']['control']['useControlMaster']) {
74 10
                $input['ssh']['control'] = array_merge(array(
75 10
                    'ControlPath' => '~/.ssh/tempo_' . $input['ssh']['host'],
76 10
                    'ControlPersist' => '5m',
77 10
                    'closeOnDestruct' => false,
78 10
                ), $input['ssh']['control']);
79 10
            }
80 10
        }
81
82 10
        parent::__construct($input, $flags, $iteratorClass);
83 8
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88 10
    protected function validate($index = null)
89
    {
90 10
        if ($index === null || $index === 'ssh') {
91 10
            $this->validateSsh();
92 8
        }
93 8
    }
94
95
    /**
96
     * @throws \InvalidArgumentException
97
     */
98 10
    protected function validateSsh()
99
    {
100 10
        if (!isset($this['ssh']['host']) || empty($this['ssh']['host'])) {
101 1
            throw new InvalidArgumentException('property: [ssh][host] is mandatory');
102
        }
103
104
        foreach (array(
105 9
            'ControlPath',
106 9
            'ControlPersist',
107 9
        ) as $controlOption) {
108 9
            if (isset($this['ssh']['options'][$controlOption])) {
109 1
                throw new InvalidArgumentException(sprintf(
110 1
                    'The ssh option %s can only be specified in the [ssh][control] section',
111
                    $controlOption
112 1
                ));
113
            }
114 8
        }
115 8
    }
116
117
    /**
118
     * @return array
119
     */
120 1
    protected function getSshOptionArgs()
121
    {
122 1
        $args = array();
123
124 1
        foreach ($this['ssh']['options'] as $option => $value) {
125 1
            $args[] = '-o';
126 1
            $args[] = $option.'='.$value;
127 1
        }
128
129 1
        return $args;
130
    }
131
132
    /**
133
     * Destroy ControlMaster if nessasary
134
     * @throws \RuntimeException
135
     */
136 10
    public function __destruct()
137
    {
138 10
        if (isset($this['ssh'])
139 10
            && isset($this['ssh']['control'])
140 10
            && $this['ssh']['control']['useControlMaster']
141 10
            && $this['ssh']['control']['closeOnDestruct']
142 10
            && $this->isControlMasterEstablished()
143 10
        ) {
144 2
            $processBuilder = $this->getProcessBuilder();
145 2
            $processBuilder->setArguments(array(
146 2
                '-O', // Control an active connection multiplexing master process
147 2
                'exit',
148
                (string)$this
149 2
            ));
150 2
            $process = $processBuilder->getProcess();
151
152
            $process
153 2
                ->disableOutput()
154 2
                ->mustRun()
155
            ;
156 2
        }
157 10
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 5
    public function __toString()
163
    {
164 5
        $string = $this['ssh']['host'];
165
166 5
        if (isset($this['ssh']['user'])) {
167 1
            $string = sprintf(
168 1
                '%s@%s',
169 1
                $this['ssh']['user'],
170
                $string
171 1
            );
172 1
        }
173
174 5
        return $string;
175
    }
176
177
    /**
178
     * @param \Symfony\Component\Process\ProcessBuilder $processBuilder
179
     * @return self
180
     */
181 2
    public function setProcessBuilder(ProcessBuilder $processBuilder)
182
    {
183 2
        $this->processBuilder = $processBuilder;
184
185 2
        return $this;
186
    }
187
188
    /**
189
     * @return \Symfony\Component\Process\ProcessBuilder
190
     */
191 3
    public function getProcessBuilder()
192
    {
193 3
        if ($this->processBuilder === null) {
194 1
            $this->processBuilder = new ProcessBuilder();
195
196
            $processPrefix = array(
197 1
                'ssh',
198 1
            );
199 1
            if ($this['ssh']['control']['useControlMaster']) {
200 1
                $processPrefix[] = '-o';
201 1
                $processPrefix[] = 'ControlPath='.$this['ssh']['control']['ControlPath'];
202 1
            }
203 1
            $this->processBuilder->setPrefix($processPrefix);
204 1
        }
205
206 3
        return $this->processBuilder;
207
    }
208
209 3
    protected function isControlMasterEstablished()
210
    {
211 3
        $processBuilder = $this->getProcessBuilder();
212 3
        $processBuilder->setArguments(array(
213 3
            '-O', // Control an active connection multiplexing master process
214 3
            'check',
215
            (string)$this
216 3
        ));
217 3
        $process = $processBuilder->getProcess();
218
219
        $process
220 3
            ->disableOutput()
221 3
            ->run()
222
        ;
223
224 3
        $ret = $process->getExitCode();
225
226 3
        return ($ret === Sysexits::EX_OK);
227
    }
228
229
    /**
230
     * @throws \RuntimeException
231
     */
232 1
    protected function establishControlMaster()
233
    {
234 1
        $processBuilder = $this->getProcessBuilder();
235 1
        $args = array_merge(array(
236 1
            '-n', // Redirects stdin from /dev/null (actually, prevents reading from stdin)
237
238 1
            '-o',
239 1
            'ControlMaster=yes',
240
241 1
            '-o', // ControlPersist - How to persist the master socket
242 1
            'ControlPersist='.$this['ssh']['control']['ControlPersist'],
243 1
        ), $this->getSshOptionArgs());
244 1
        $args[] = (string)$this;
245 1
        $processBuilder->setArguments($args);
246 1
        $process = $processBuilder->getProcess();
247
248
        $process
249 1
            ->disableOutput()
250 1
            ->mustRun()
251
        ;
252 1
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257 1
    public function run($command)
258
    {
259 1
        if ($this['ssh']['control']['useControlMaster'] && !$this->isControlMasterEstablished()) {
260 1
            $this->establishControlMaster();
261 1
        }
262
263 1
        $processBuilder = $this->getProcessBuilder();
264 1
        $args = $this->getSshOptionArgs();
265 1
        $args[] = (string)$this;
266 1
        $processBuilder->setArguments($args);
267
        $processBuilder
268 1
            ->setInput($command)
269
        ;
270 1
        $process = $processBuilder->getProcess();
271
272 1
        $process->setTimeout(null);
273
        try {
274 1
            $process->mustRun();
275 1
        } catch (ProcessFailedException $e) {
276
            if ($process->getExitCode() !== 255) {
277
                // Rebuild the exception to expose actual failed command routed through ssh
278
                $process = $e->getProcess();
279
                throw new RemoteProcessFailedException($process);
280
            }
281
282
            throw $e;
283
        }
284
285 1
        return $process->getOutput();
286
    }
287
}
288