Completed
Pull Request — master (#234)
by Дмитрий
07:51
created

ShellCommand::buildArgs()   C

Complexity

Conditions 9
Paths 12

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 9
dl 0
loc 22
rs 6.412
eloc 15
c 1
b 1
f 0
nc 12
nop 1
1
<?php
2
namespace PHPDaemon\Core;
3
4
use PHPDaemon\Core\CallbackWrapper;
5
use PHPDaemon\Core\Daemon;
6
use PHPDaemon\FS\File;
7
use PHPDaemon\Network\IOStream;
8
9
/**
10
 * Process
11
 * @package PHPDaemon\Core
12
 * @author  Vasily Zorin <[email protected]>
13
 */
14
class ShellCommand extends IOStream
15
{
16
17
    protected $finishWrite;
18
19
    /**
20
     * @var string Command string
21
     */
22
    protected $cmd;
23
24
    /**
25
     * @var string Executable path
26
     */
27
    public $binPath;
28
29
    /**
30
     * @var array Opened pipes
31
     */
32
    protected $pipes;
33
34
    /**
35
     * @var resource Process descriptor
36
     */
37
    protected $pd;
38
39
    /**
40
     * @var resource FD write
41
     */
42
    protected $fdWrite;
43
44
    /**
45
     * @var boolean Output errors?
46
     */
47
    protected $outputErrors = true;
48
49
    /**
50
     * @var string SUID
51
     */
52
    public $setUser;
53
54
    /**
55
     * @var string SGID
56
     */
57
    public $setGroup;
58
59
    /**
60
     * @var string Chroot
61
     */
62
    public $chroot = '/';
63
64
    /**
65
     * @var array Hash of environment's variables
66
     */
67
    protected $env = []; // 
68
69
    /**
70
     * @var string Chdir
71
     */
72
    public $cwd;
73
74
    /**
75
     * @var string Path to error logfile
76
     */
77
    protected $errlogfile = null;
78
79
    /**
80
     * @var array Array of arguments
81
     */
82
    protected $args;
83
84
    /**
85
     * @var integer Process priority
86
     */
87
    protected $nice;
88
89
    /**
90
     * @var \EventBufferEvent
91
     */
92
    protected $bevWrite;
93
94
    /**
95
     * @var \EventBufferEvent
96
     */
97
    protected $bevErr;
98
99
    /**
100
     * @var boolean Got EOF?
101
     */
102
    protected $EOF = false;
103
104
    /**
105
     * Get command string
106
     * @return string
107
     */
108
    public function getCmd()
109
    {
110
        return $this->cmd;
111
    }
112
113
    /**
114
     * Set group
115
     * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be ShellCommand?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
116
     */
117
    public function setGroup($val)
118
    {
119
        $this->setGroup = $val;
120
        return $this;
121
    }
122
123
    /**
124
     * Set cwd
125
     * @param  string $dir
126
     * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be ShellCommand?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
127
     */
128
    public function setCwd($dir)
129
    {
130
        $this->cwd = $dir;
131
        return $this;
132
    }
133
134
    /**
135
     * Set group
136
     * @param  string $val
137
     * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be ShellCommand?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
138
     */
139
    public function setUser($val)
140
    {
141
        $this->setUser = $val;
142
        return $this;
143
    }
144
145
    /**
146
     * Set chroot
147
     * @param  string $dir
148
     * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be ShellCommand?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
149
     */
150
    public function setChroot($dir)
151
    {
152
        $this->chroot = $dir;
153
        return $this;
154
    }
155
156
    /**
157
     * Execute
158
     * @param  string   $binPath Binpath
0 ignored issues
show
Documentation introduced by
Should the type for parameter $binPath not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
159
     * @param  callable $cb 	 Callback
0 ignored issues
show
Documentation introduced by
Should the type for parameter $cb not be callable|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
160
     * @param  array    $args    Optional. Arguments
0 ignored issues
show
Documentation introduced by
Should the type for parameter $args not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
161
     * @param  array    $env     Optional. Hash of environment's variables
0 ignored issues
show
Documentation introduced by
Should the type for parameter $env not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
162
     */
163
    public static function exec($binPath = null, $cb = null, $args = null, $env = null)
164
    {
165
        $o = new static;
166
        $data = '';
167
        $o->bind('read', function ($o) use (&$data, $o) {
0 ignored issues
show
Bug Best Practice introduced by
The parameter name $o conflicts with one of the imported variables.
Loading history...
168
            $data .= $o->readUnlimited();
169
        });
170
        $o->bind('eof', function ($o) use (&$data, $cb) {
171
            $cb($o, $data);
172
            $o->close();
173
        });
174
        $o->execute($binPath, $args, $env);
175
    }
176
177
178
    /**
179
     * Sets fd
180
     * @param  resource          $fd File descriptor
181
     * @param  \EventBufferEvent $bev
0 ignored issues
show
Documentation introduced by
Should the type for parameter $bev not be \EventBufferEvent|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
182
     * @return void
183
     */
184
    public function setFd($fd, $bev = null)
185
    {
186
        $this->fd = $fd;
187
        if ($fd === false) {
188
            $this->finish();
189
            return;
190
        }
191
        $this->fdWrite = $this->pipes[0];
192
        $flags         = !is_resource($this->fd) ? \EventBufferEvent::OPT_CLOSE_ON_FREE : 0;
193
        $flags |= \EventBufferEvent::OPT_DEFER_CALLBACKS; /* buggy option */
194
        $this->bev      = new \EventBufferEvent(Daemon::$process->eventBase, $this->fd, 0, [$this, 'onReadEv'], null, [$this, 'onStateEv']);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 140 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
195
        $this->bevWrite = new \EventBufferEvent(Daemon::$process->eventBase, $this->fdWrite, 0, null, [$this, 'onWriteEv'], null);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 130 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
196
        if (!$this->bev || !$this->bevWrite) {
197
            $this->finish();
198
            return;
199
        }
200
        if ($this->priority !== null) {
201
            $this->bev->priority = $this->priority;
202
        }
203
        if ($this->timeout !== null) {
204
            $this->setTimeout($this->timeout);
205
        }
206 View Code Duplication
        if (!$this->bev->enable(\Event::READ | \Event::TIMEOUT | \Event::PERSIST)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
207
            $this->finish();
208
            return;
209
        }
210 View Code Duplication
        if (!$this->bevWrite->enable(\Event::WRITE | \Event::TIMEOUT | \Event::PERSIST)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
211
            $this->finish();
212
            return;
213
        }
214
        $this->bev->setWatermark(\Event::READ, $this->lowMark, $this->highMark);
215
216
        init:
217
        if (!$this->inited) {
218
            $this->inited = true;
219
            $this->init();
220
        }
221
    }
222
223
    /**
224
     * Sets an array of arguments
225
     * @param  array Arguments
226
     * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be ShellCommand?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
227
     */
228
    public function setArgs($args = null)
229
    {
230
        $this->args = $args;
231
232
        return $this;
233
    }
234
235
    /**
236
     * Set a hash of environment's variables
237
     * @param  array Hash of environment's variables
238
     * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be ShellCommand?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
239
     */
240
    public function setEnv($env = null)
241
    {
242
        $this->env = $env;
243
244
        return $this;
245
    }
246
247
    /**
248
     * Called when got EOF
249
     * @return void
250
     */
251
    public function onEofEvent()
252
    {
253
        if ($this->EOF) {
254
            return;
255
        }
256
        $this->EOF = true;
257
258
        $this->event('eof');
259
    }
260
261
    /**
262
     * Set priority
263
     * @param  integer $nice Priority
0 ignored issues
show
Documentation introduced by
Should the type for parameter $nice not be integer|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
264
     * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be ShellCommand?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
265
     */
266
    public function nice($nice = null)
267
    {
268
        $this->nice = $nice;
269
270
        return $this;
271
    }
272
273
    /**
274
     * Called when new data received
275
     * @return this|null
0 ignored issues
show
Documentation introduced by
Should the return type not be ShellCommand|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
276
     */
277
    protected function onRead()
278
    {
279
        if (func_num_args() === 1) {
280
            $this->onRead = func_get_arg(0);
0 ignored issues
show
Documentation introduced by
The property onRead does not exist on object<PHPDaemon\Core\ShellCommand>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
281
            return $this;
282
        }
283
        $this->event('read');
284
    }
285
286
    /**
287
     * Build arguments string from associative/enumerated array (may be mixed)
288
     * @param  array $args
289
     * @return string
290
     */
291
    public static function buildArgs($args)
292
    {
293
        if (!is_array($args)) {
294
            return '';
295
        }
296
        $ret = '';
297
        foreach ($args as $k => $v) {
298
            if (!is_int($v) && ($v !== null)) {
299
                $v = escapeshellarg($v);
300
            }
301
            if (is_int($k)) {
302
                $ret .= ' ' . $v;
303
            } else {
304
                if ($k{0} !== '-') {
305
                    $ret .= ' --' . $k . ($v !== null ? '=' . $v : '');
306
                } else {
307
                    $ret .= ' ' . $k . ($v !== null ? '=' . $v : '');
308
                }
309
            }
310
        }
311
        return $ret;
312
    }
313
314
    /**
315
     * Execute
316
     * @param  string $binPath Optional. Binpath
0 ignored issues
show
Documentation introduced by
Should the type for parameter $binPath not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
317
     * @param  array  $args    Optional. Arguments
0 ignored issues
show
Documentation introduced by
Should the type for parameter $args not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
318
     * @param  array  $env     Optional. Hash of environment's variables
0 ignored issues
show
Documentation introduced by
Should the type for parameter $env not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
319
     * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be ShellCommand?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
320
     */
321
    public function execute($binPath = null, $args = null, $env = null)
322
    {
323
        if ($binPath !== null) {
324
            $this->binPath = $binPath;
325
        }
326
327
        if ($env !== null) {
328
            $this->env = $env;
329
        }
330
331
        if ($args !== null) {
332
            $this->args = $args;
333
        }
334
        $this->cmd = $this->binPath . static::buildArgs($this->args) . ($this->outputErrors ? ' 2>&1' : '');
335
336
        if (
337
                isset($this->setUser)
338
                || isset($this->setGroup)
339
        ) {
340
            if (
341
                    isset($this->setUser)
342
                    && isset($this->setGroup)
343
                    && ($this->setUser !== $this->setGroup)
344
            ) {
345
                $this->cmd = 'sudo -g ' . escapeshellarg($this->setGroup) . '  -u ' . escapeshellarg($this->setUser) . ' ' . $this->cmd;
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 136 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
346
            } else {
347
                $this->cmd = 'su ' . escapeshellarg($this->setGroup) . ' -c ' . escapeshellarg($this->cmd);
348
            }
349
        }
350
351
        if ($this->chroot !== '/') {
352
            $this->cmd = 'chroot ' . escapeshellarg($this->chroot) . ' ' . $this->cmd;
353
        }
354
355
        if ($this->nice !== null) {
356
            $this->cmd = 'nice -n ' . ((int)$this->nice) . ' ' . $this->cmd;
357
        }
358
359
        $pipesDescr = [
360
            0 => ['pipe', 'r'], // stdin is a pipe that the child will read from
361
            1 => ['pipe', 'w'] // stdout is a pipe that the child will write to
362
        ];
363
364
        if (
365
                ($this->errlogfile !== null)
366
                && !$this->outputErrors
367
        ) {
368
            $pipesDescr[2] = ['file', $this->errlogfile, 'a']; // @TODO: refactoring
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
369
        }
370
371
        $this->pd = proc_open($this->cmd, $pipesDescr, $this->pipes, $this->cwd, $this->env);
372
        if ($this->pd) {
373
            $this->setFd($this->pipes[1]);
374
        }
375
376
        return $this;
377
    }
378
379
    /**
380
     * Finish write stream
381
     * @return boolean
382
     */
383
    public function finishWrite()
384
    {
385
        if (!$this->writing) {
386
            $this->closeWrite();
387
        }
388
389
        $this->finishWrite = true;
390
391
        return true;
392
    }
393
394
    /**
395
     * Close the process
396
     * @return void
397
     */
398
    public function close()
399
    {
400
        parent::close();
401
        $this->closeWrite();
402
        if (is_resource($this->pd)) {
403
            proc_close($this->pd);
404
        }
405
    }
406
407
    /**
408
     * Called when stream is finished
409
     */
410
    public function onFinish()
411
    {
412
        $this->onEofEvent();
413
    }
414
415
    /**
416
     * Close write stream
417
     * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be ShellCommand?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
418
     */
419
    public function closeWrite()
420
    {
421
        if ($this->bevWrite) {
422
            if (isset($this->bevWrite)) {
423
                $this->bevWrite->free();
424
            }
425
            $this->bevWrite = null;
426
        }
427
428
        if ($this->fdWrite) {
429
            fclose($this->fdWrite);
430
            $this->fdWrite = null;
431
        }
432
433
        return $this;
434
    }
435
436
    /**
437
     * Got EOF?
438
     * @return boolean
439
     */
440
    public function eof()
441
    {
442
        return $this->EOF;
443
    }
444
445
    /**
446
     * Send data to the connection. Note that it just writes to buffer that flushes at every baseloop
447
     * @param  string $data Data to send
448
     * @return boolean Success
449
     */
450 View Code Duplication
    public function write($data)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
451
    {
452
        if (!$this->alive) {
453
            Daemon::log('Attempt to write to dead IOStream (' . get_class($this) . ')');
454
            return false;
455
        }
456
        if (!isset($this->bevWrite)) {
457
            return false;
458
        }
459
        if (!mb_orig_strlen($data)) {
460
            return true;
461
        }
462
        $this->writing   = true;
463
        Daemon::$noError = true;
464
        if (!$this->bevWrite->write($data) || !Daemon::$noError) {
465
            $this->close();
466
            return false;
467
        }
468
        return true;
469
    }
470
471
    /**
472
     * Send data and appending \n to connection. Note that it just writes to buffer flushed at every baseloop
473
     * @param  string Data to send
474
     * @return boolean Success
475
     */
476 View Code Duplication
    public function writeln($data)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
477
    {
478
        if (!$this->alive) {
479
            Daemon::log('Attempt to write to dead IOStream (' . get_class($this) . ')');
480
            return false;
481
        }
482
        if (!isset($this->bevWrite)) {
483
            return false;
484
        }
485
        if (!mb_orig_strlen($data) && !mb_orig_strlen($this->EOL)) {
486
            return true;
487
        }
488
        $this->writing = true;
489
        $this->bevWrite->write($data);
490
        $this->bevWrite->write($this->EOL);
491
        return true;
492
    }
493
494
    /**
495
     * Sets callback which will be called once when got EOF
496
     * @param  callable $cb
0 ignored issues
show
Documentation introduced by
Should the type for parameter $cb not be callable|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
497
     * @return this
0 ignored issues
show
Documentation introduced by
Should the return type not be ShellCommand?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
498
     */
499
    public function onEOF($cb = null)
500
    {
501
        $this->onEOF = CallbackWrapper::wrap($cb);
0 ignored issues
show
Documentation introduced by
The property onEOF does not exist on object<PHPDaemon\Core\ShellCommand>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Bug introduced by
It seems like $cb defined by parameter $cb on line 499 can also be of type null; however, PHPDaemon\Core\CallbackWrapper::wrap() does only seem to accept callable, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
502
        return $this;
503
    }
504
505
    public function getStatus()
506
    {
507
        return !empty($this->pd) ? proc_get_status($this->pd) : null;
508
    }
509
}
510