Remote   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 279
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 97.2%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 33
c 1
b 0
f 0
lcom 1
cbo 5
dl 0
loc 279
ccs 139
cts 143
cp 0.972
rs 9.3999

11 Methods

Rating   Name   Duplication   Size   Complexity  
A validate() 0 6 3
B validateSsh() 0 18 5
A getSshOptionArgs() 0 11 2
A __toString() 0 14 2
A setProcessBuilder() 0 6 1
A getProcessBuilder() 0 17 3
B __construct() 0 44 4
B __destruct() 0 23 6
A isControlMasterEstablished() 0 20 1
A establishControlMaster() 0 23 1
B run() 0 31 5
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
        $input = array_replace_recursive(array(
61
            'ssh' => array(
62 10
                'host' => null,
63
                'options' => array(
64 10
                    'RequestTTY' => 'no', // Disable pseudo-tty allocation
65 10
                ),
66
                'control' => array(
67 10
                    'useControlMaster' => true,
68 10
                ),
69 10
            ),
70 10
        ), $input);
71
72 10
        if ($input['ssh']['control']['useControlMaster']) {
73 10
            $input['ssh']['control'] = array_merge(array(
74 10
                'ControlPath' => '~/.ssh/tempo_ctl_%r@%h:%p',
75 10
                'ControlPersist' => '5m',
76 10
                'closeOnDestruct' => false,
77 10
            ), $input['ssh']['control']);
78 10
        }
79
80 10
        parent::__construct($input, $flags, $iteratorClass);
81 8
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86 10
    protected function validate($index = null)
87
    {
88 10
        if ($index === null || $index === 'ssh') {
89 10
            $this->validateSsh();
90 8
        }
91 8
    }
92
93
    /**
94
     * @throws \InvalidArgumentException
95
     */
96 10
    protected function validateSsh()
97
    {
98 10
        if (!isset($this['ssh']['host']) || empty($this['ssh']['host'])) {
99 1
            throw new InvalidArgumentException('property: [ssh][host] is mandatory');
100
        }
101
102
        foreach (array(
103 9
            'ControlPath',
104 9
            'ControlPersist',
105 9
        ) as $controlOption) {
106 9
            if (isset($this['ssh']['options'][$controlOption])) {
107 1
                throw new InvalidArgumentException(sprintf(
108 1
                    'The ssh option %s can only be specified in the [ssh][control] section',
109
                    $controlOption
110 1
                ));
111
            }
112 8
        }
113 8
    }
114
115
    /**
116
     * @return array
117
     */
118 1
    protected function getSshOptionArgs()
119
    {
120 1
        $args = array();
121
122 1
        foreach ($this['ssh']['options'] as $option => $value) {
123 1
            $args[] = '-o';
124 1
            $args[] = $option.'='.$value;
125 1
        }
126
127 1
        return $args;
128
    }
129
130
    /**
131
     * Destroy ControlMaster if nessasary
132
     * @throws \RuntimeException
133
     */
134 10
    public function __destruct()
135
    {
136 10
        if (isset($this['ssh'])
137 10
            && isset($this['ssh']['control'])
138 10
            && $this['ssh']['control']['useControlMaster']
139 10
            && $this['ssh']['control']['closeOnDestruct']
140 10
            && $this->isControlMasterEstablished()
141 10
        ) {
142 2
            $process = $this->getProcessBuilder()
143 2
                ->setArguments(array(
144 2
                    '-O', // Control an active connection multiplexing master process
145 2
                    'exit',
146
                    (string)$this
147 2
                ))
148 2
                ->getProcess()
149 2
            ;
150
151
            $process
152 2
                ->disableOutput()
153 2
                ->mustRun()
154
            ;
155 2
        }
156 10
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161 5
    public function __toString()
162
    {
163 5
        $string = $this['ssh']['host'];
164
165 5
        if (isset($this['ssh']['user'])) {
166 1
            $string = sprintf(
167 1
                '%s@%s',
168 1
                $this['ssh']['user'],
169
                $string
170 1
            );
171 1
        }
172
173 5
        return $string;
174
    }
175
176
    /**
177
     * @param \Symfony\Component\Process\ProcessBuilder $processBuilder
178
     * @return self
179
     */
180 2
    public function setProcessBuilder(ProcessBuilder $processBuilder)
181
    {
182 2
        $this->processBuilder = $processBuilder;
183
184 2
        return $this;
185
    }
186
187
    /**
188
     * @return \Symfony\Component\Process\ProcessBuilder
189
     */
190 3
    public function getProcessBuilder()
191
    {
192 3
        if ($this->processBuilder === null) {
193 1
            $this->processBuilder = new ProcessBuilder();
194
195
            $processPrefix = array(
196 1
                'ssh',
197 1
            );
198 1
            if ($this['ssh']['control']['useControlMaster']) {
199 1
                $processPrefix[] = '-o';
200 1
                $processPrefix[] = 'ControlPath='.$this['ssh']['control']['ControlPath'];
201 1
            }
202 1
            $this->processBuilder->setPrefix($processPrefix);
203 1
        }
204
205 3
        return $this->processBuilder;
206
    }
207
208 3
    protected function isControlMasterEstablished()
209
    {
210 3
        $process = $this->getProcessBuilder()
211 3
            ->setArguments(array(
212 3
                '-O', // Control an active connection multiplexing master process
213 3
                'check',
214
                (string)$this
215 3
            ))
216 3
            ->getProcess()
217 3
        ;
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
        $args = array_merge(array(
235 1
            '-n', // Redirects stdin from /dev/null (actually, prevents reading from stdin)
236
237 1
            '-o',
238 1
            'ControlMaster=yes',
239
240 1
            '-o', // ControlPersist - How to persist the master socket
241 1
            'ControlPersist='.$this['ssh']['control']['ControlPersist'],
242 1
        ), $this->getSshOptionArgs());
243 1
        $args[] = (string)$this;
244
245 1
        $process = $this->getProcessBuilder()
246 1
            ->setArguments($args)
247 1
            ->getProcess()
248 1
        ;
249
250
        $process
251 1
            ->disableOutput()
252 1
            ->mustRun()
253
        ;
254 1
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 1
    public function run($command)
260
    {
261 1
        if ($this['ssh']['control']['useControlMaster'] && !$this->isControlMasterEstablished()) {
262 1
            $this->establishControlMaster();
263 1
        }
264
265 1
        $args = $this->getSshOptionArgs();
266 1
        $args[] = (string)$this;
267
268 1
        $process = $this->getProcessBuilder()
269 1
            ->setArguments($args)
270 1
            ->setInput($command)
271 1
            ->getProcess()
272 1
        ;
273
274 1
        $process->setTimeout(null);
275
276
        try {
277 1
            $process->mustRun();
278 1
        } catch (ProcessFailedException $e) {
279
            if ($process->getExitCode() !== 255) {
280
                // Rebuild the exception to expose actual failed command routed through ssh
281
                $process = $e->getProcess();
282
                throw new RemoteProcessFailedException($process);
283
            }
284
285
            throw $e;
286
        }
287
288 1
        return $process->getOutput();
289
    }
290
}
291