This repository has been archived by the owner on Mar 9, 2021. It is now read-only.
/
SystemProcess.php
692 lines (639 loc) · 20.4 KB
/
SystemProcess.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
<?php
/**
* systemProcess base class
*
* This file is part of systemProcess.
*
* systemProcess is free software; you can redistribute it and/or modify it
* under the terms of the Lesser GNU General Public License as published by the
* Free Software Foundation; version 3 of the License.
*
* systemProcess is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the Lesser GNU General Public License
* for more details.
*
* You should have received a copy of the Lesser GNU General Public License
* along with systemProcess; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPL
*/
namespace SystemProcess;
use SystemProcess\Argument;
use SystemProcess\Argument\EscapedArgument;
use SystemProcess\Argument\UnescapedArgument;
use \SystemProcess\InvalidCustomDescriptorException;
/**
* Management facility for any external system process.
*
* SystemProcess is general purpose proc_open wrapper which provides all means
* to easily specify and execute external commands from your php script.
*
* It was designed providing great flexibility combined a maximum of comfort.
* The fluent interface pattern is used to provide an easy and readable way of
* defining complex commandstrings as well as simple ones. There is no need to
* handle the escaping of your arguments as this will be done automatically.
*
* The constructor takes the executable to run as an argument. The following
* example will execute the command "echo" with the two arguments "foo" and
* "bar":
* <code>
* <?php
* $p = new SystemProcess( 'echo' );
* $p->argument( 'foo' )->argument( 'bar' );
* $returnCode = $p->execute();
* ?>
* </code>
* As you can see the fluent interface is used to combine the argument calls in
* a readable way.
*
* Quite complex constructs containing redirects, pipes or even custom
* file descriptors are possible too. They can be realized with nearly no
* effort.
* <code>
* <?php
* $consumer = new SystemProcess( 'cat' );
* $consumer->redirect( SystemProcess::STDOUT, SystemProcess::STDERR );
*
* $provider = new SystemProcess( 'echo' );
* $provider->nonZeroExitCodeException = true;
* $provider->argument( 'foobar' )
* ->pipe( $consumer )
* ->execute();
*
* var_dump( $provider->stderrOutput );
* ?>
* </code>
* As you can see even complex commands are still quite readable. If the
* attribute "nonZeroExitCodeException" is set to true an exception will be
* thrown instead of just returning a non zero exit code. This exception will
* contain the stdout- and stderrOutput as well as the executed command string.
*
* In case you need asyncronous execution call the execute function with the
* first argument set to "true". You will get a set of pipes in return which you
* can work with like any other stream in php.
*
* If you just want to use this classes api to generate the shell commands, but
* do have no intention to actually execute it you can use the __toString()
* functionallity of SystemProcess. An explicit conversion of this object to
* string will give you the string context as well, as a use in any string
* context like printf.
* <code>
* <?php
* $p = new SystemProcess( 'echo' );
* $p->argument( 'foo' )
* ->argument( 'bar' )
*
* // Store command to a variable
* $command = (string)$p;
* // Or print it out
* echo $p, "\n";
* ?>
* </code>
*
* More advanced functionallity like sending signals to running processes is
* available also. Take a look at the api documentation for these type of
* methods.
*
* @version //autogen//
* @copyright Copyright (C) 2008 Jakob Westhoff. All rights reserved.
* @author Jakob Westhoff <jakob@php.net>
* @license LGPLv3
*/
class SystemProcess
{
/*
* Types of command parts
*/
const EXECUTABLE = 1;
const ARGUMENT = 2;
const UNESCAPEDARGUMENT = 3;
const SYSTEMPROCESS = 4;
const STDOUT_REDIRECT = 5;
const STDERR_REDIRECT = 6;
/*
* Constants to represent the stdin, out and err handles
*/
const STDIN = 0;
const STDOUT = 1;
const STDERR = 2;
/*
* Constants defining descriptor types
*/
const PIPE = 'pipe';
const FILE = 'file';
/*
* Constants of signals, which can be send to the running process
*/
const SIGHUP = 1;
const SIGINT = 2;
const SIGQUIT = 3;
const SIGILL = 4;
const SIGTRAP = 5;
const SIGABRT = 6;
const SIGFPE = 8;
const SIGKILL = 9;
const SIGUSR1 = 10;
const SIGSEGV = 11;
const SIGUSR2 = 12;
const SIGPIPE = 13;
const SIGALRM = 14;
const SIGTERM = 15;
const SIGSTKFLT = 16;
const SIGCHLD = 17;
const SIGCONT = 18;
const SIGSTOP = 19;
const SIGTSTP = 20;
const SIGTTIN = 21;
const SIGTTOU = 22;
const SIGIO = 23;
const SIGXCPU = 24;
const SIGXFSZ = 25;
const SIGVTALRM = 26;
const SIGPROF = 27;
const SIGWINCH = 28;
/**
* Attributes of the class
*
* @var array
*/
protected $attributes = array();
/**
* Array containing all parts of the constructed command including their
* type
*
* @var array( array )
*/
protected $commandParts = array();
/**
* Environment to be set for execution
*
* @var array
*/
protected $environment = null;
/**
* Working directory to be used for execution
*
* @var string
*/
protected $workingDirectory = null;
/**
* Pipes from and to the running process
*
* @var array
*/
protected $pipes = null;
/**
* Custom file descriptors which can be created and used
*
* @var array
*/
protected $customDescriptors = array();
/**
* The process handle of the currently running process
*
* @var resource
*/
protected $processHandle = null;
/**
* Class constructor taking the executable
*
* @param string $executable Executable to create system process for;
* @return void
*/
public function __construct( $executable )
{
$this->attributes = array(
'stdoutOutput' => '',
'stderrOutput' => '',
'nonZeroExitCodeException' => false,
);
$this->commandParts[] = array( self::EXECUTABLE, $executable );
}
/**
* Interceptor method to handle writable attributes
*
* @param mixed $k
* @param mixed $v
* @return void
*/
public function __set( $k, $v )
{
if ( array_key_exists( $k, $this->attributes ) !== true )
{
throw new pbsAttributeException( pbsAttributeException::NON_EXISTANT, $k );
}
// None of the attributes are writeable
switch( $k )
{
case 'nonZeroExitCodeException':
$this->attributes['nonZeroExitCodeException'] = (bool)$v;
break;
default:
throw new pbsAttributeException( pbsAttributeException::WRITE, $k );
}
}
/**
* Interceptor method to handle readable attributes
*
* @param mixed $k
* @return mixed
*/
public function __get( $k )
{
if ( array_key_exists( $k, $this->attributes ) !== true )
{
throw new pbsAttributeException( pbsAttributeException::NON_EXISTANT, $k );
}
// All existant attributes are readable
switch( $k )
{
default:
return $this->attributes[$k];
}
}
/**
* Convert the systemProcess object to a useful string representation.
*
* In this case the command string which would be executed, if the
* exececute function is called, will be returned
*
* @return string
*/
public function __toString()
{
return $this->buildCommand( $this->commandParts );
}
/**
* Add an argument to the system process
*
* Accepts {@link \SystemProcess\Argument} objects, or any scalar, which is
* then wrapped into an argument object for BC.
*
* @param mixed $argument Argument to add to the commandline
* @param bool $alreadyEscaped The given argument will not be escaped. If
* you decide to pass true here, you need to make sure the argument
* supplied is not harmful and treated as one argument. Therfore you may
* need to enclose it in single or double quotes.
* @return \SystemProcess\SystemProcess The object this method was called on (fluent
* interface)
*/
public function argument( $argument, $alreadyEscaped = false )
{
if ( !$argument instanceof Argument )
{
if ( $alreadyEscaped )
{
$argument = new UnescapedArgument( $argument );
}
else
{
$argument = new EscapedArgument( $argument );
}
}
$this->commandParts[] = array( self::ARGUMENT, $argument );
return $this;
}
/**
* Pipe the output of the executed command to another system process
*
* @param \SystemProcess\SystemProcess $process Process to pipe the output to
* @return \SystemProcess\SystemProcess The object this method was called on (fluent
* interface)
*/
public function pipe( SystemProcess $process )
{
if ( $process === $this )
{
throw new RecursivePipeException();
}
$this->commandParts[] = array( self::SYSTEMPROCESS, &$process->commandParts );
return $this;
}
/**
* Redirect one of the streams to a file or another stream
*
* @param int $stream The stream to redirect (one of the class constants
* STDOUT or STDERR)
* @param mixed $target The target to redirect the given stream to. This
* may be a filename or a another stream
* @return \SystemProcess\SystemProcess The object this method was called on
* (fluent interface)
*/
public function redirect( $stream, $target )
{
$this->commandParts[] = array(
( $stream == self::STDOUT )
? ( self::STDOUT_REDIRECT )
: ( self::STDERR_REDIRECT ),
$target
);
return $this;
}
/**
* Set a special environment for the process.
*
* If none is set the environment of the php process is used.
*
* @param array $env The environment to be used defined as associative
* array. The array key is the variable name and the value is the
* corresponding value for this variable.
* @return \SystemProcess\SystemProcess The object this method was called on
* (fluent interface)
*/
public function environment( $env )
{
if ( $this->environment === null )
{
$this->environment = array();
}
$this->environment = array_merge( $this->environment, $env );
return $this;
}
/**
* Set the working directory to be used.
*
* If this function is not called the working dir of the php process will
* be used.
*
* @param string $cwd Working directory to be set
* @return \SystemProcess\SystemProcess The object this method was called on
* (fluent interface)
*/
public function workingDirectory( $cwd )
{
if ( strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN' )
{
$cwd = str_replace( '/', '\\', $cwd );
}
$this->workingDirectory = $cwd;
return $this;
}
/**
* Add a custom file descriptor which will be attached to the process.
*
* After the process is created you can use the the supplied pipes to
* interact with your custom descriptor. Custom file descriptor pipes can
* only be used with asyncronous executions.
*
* @param int $fd File descriptor id
* @param string $type PIPE or FILE constant
* @param string $target If the PIPE type is used this is whether "w" to
* allow the process writing to the pipe or "r" to allow the process
* reading from the pipe
* @param string filemode If the type is FILE this is the mode to open the
* file with, e.g. "a"
* @return \SystemProcess\SystemProcess The object this method was called on
* (fluent interface)
*/
public function descriptor( $fd, $type, $target, $filemode = null )
{
if ( $fd < 3 )
{
throw new InvalidCustomDescriptorException( $fd );
}
if ( $filemode === null )
{
$this->customDescriptors[(int)$fd] = array( $type, $target );
}
else
{
$this->customDescriptors[(int)$fd] = array( $type, $target, $filemode );
}
return $this;
}
/**
* Execute the system process
*
* @param bool $asyncronous Whether the execution is asynronous or not.
* @return mixed If asyncronous is true an array with all the pipes will be
* returned. If asyncronous is false the exitcode of the application will
* be returned after the process has finished its execution. If
* nonZeroExitCodeException is set to true a pbsNonZeroExitCodeException will
* be thrown.
*/
public function execute( $asyncronous = false )
{
$this->prepareExecution();
$command = $this->buildCommand( $this->commandParts );
$ds = $this->prepareDescriptorSpecification();
$this->processHandle = proc_open(
$command,
$ds,
$this->pipes,
$this->workingDirectory,
$this->environment
);
if ( $asyncronous === true )
{
return $this->pipes;
}
// Handle all the data until the streams are closed
$readablePipes = array( 1 => true, 2 => true );
foreach( $this->customDescriptors as $k => $cd )
{
if ( $cd[0] === self::PIPE && $cd[1] === 'w' )
{
$readablePipes[$k] = true;
}
}
while ( true )
{
// Read all the given data
$r = array( $this->pipes[1], $this->pipes[2] );
$w = null;
$e = null;
$num = stream_select( $r, $w, $e, null );
// Map the handles to their fd index
$readableHandles = array();
foreach( $r as $handle )
{
foreach( $this->pipes as $k => $pipe )
{
if ( $handle === $pipe )
{
$readableHandles[$k] = $handle;
}
}
}
// Read all the provided data
foreach( $readableHandles as $k => $handle )
{
switch( $k )
{
case 1:
$this->attributes['stdoutOutput'] .= fread( $handle, 4096 );
break;
case 2:
$this->attributes['stderrOutput'] .= fread( $handle, 4096 );
break;
default:
// A custom descriptor has data. We don't handle this here.
// Therefore the data is just read and discarded.
fread( $handle, 4096 );
}
if ( feof( $handle ) === true )
{
$readablePipes[$k] = false;
}
}
$finished = true;
foreach( $readablePipes as $pipe )
{
if ( $pipe === true )
{
$finished = false;
}
}
if ( $finished === true )
{
break;
}
}
// Wait until the process is finished and close it
$retVal = $this->close();
if ( $retVal !== 0 && $this->attributes['nonZeroExitCodeException'] === true )
{
throw new NonZeroExitCodeException(
$retVal,
$this->attributes['stdoutOutput'],
$this->attributes['stderrOutput'],
$command
);
}
return $retVal;
}
/**
* Close the currently running asyncronous process.
*
* This function will block until the process is finished.
*
* @return int errorcode
*/
public function close()
{
if ( $this->processHandle === null )
{
throw new NotRunningException();
}
// Close all pipes
foreach( $this->pipes as $pipe )
{
if ( is_resource( $pipe ) )
{
fclose( $pipe );
}
}
// Close the process
$retVal = proc_close( $this->processHandle );
$this->processHandle = null;
return $retVal;
}
/**
* Send an arbitrary POSIX signal to the running process
*
* @param int $signal Signal to be send to the process.
* @return void
*/
public function signal( $signal )
{
if ( $this->processHandle === null )
{
throw new NotRunningException();
}
proc_terminate( $this->processHandle, $signal );
}
/**
* Prepare the execution of the defined process
*
* @return void
*/
protected function prepareExecution()
{
// If there is a asyncronously running process still opened close it
if ( $this->processHandle !== null )
{
// Send TERM signal
$this->signal( self::SIGTERM );
sleep( 0.5 );
// Check if the process has terminated
$status = proc_get_status( $this->processHandle );
if ( $status['running'] === false )
{
// Send a KILL signal
$this->signal( self::SIGKILL );
}
$this->close();
}
// Clean the output buffers
$this->attributes['stdoutOutput'] = '';
$this->attributes['stderrOutput'] = '';
// Remove all remaining pipe references
$this->pipes = null;
}
/**
* Prepare the descriptor specification and create it
*
* @return array The descriptor specification array to create a new process
*/
protected function prepareDescriptorSpecification()
{
$ds = $this->customDescriptors;
// Make sure the default output descriptors are set correctly
$ds[self::STDIN] = array( 'pipe', 'r' );
$ds[self::STDOUT] = array( 'pipe', 'w' );
$ds[self::STDERR] = array( 'pipe', 'w' );
return $ds;
}
/**
* Build the commandline to execute
*
* @param array $parts Array containing the commandline parts
* @return string The constructed commandline
*/
public function buildCommand( $parts )
{
$cmd = '';
foreach( $parts as $part )
{
if ( $cmd !== '' )
{
$cmd .= ' ';
}
switch( $part[0] )
{
case self::EXECUTABLE:
$cmd .= escapeshellcmd( $part[1] );
break;
case self::ARGUMENT:
$cmd .= $part[1]->getPrepared();
break;
case self::SYSTEMPROCESS:
$cmd .= '| ' . $this->buildCommand( $part[1] );
break;
case self::STDOUT_REDIRECT:
if ( is_int( $part[1] ) === true )
{
$cmd .= '1>&' . $part[1];
}
else
{
$cmd .= '1>' . escapeshellarg( $part[1] );
}
break;
case self::STDERR_REDIRECT:
if ( is_int( $part[1] ) === true )
{
$cmd .= '2>&' . $part[1];
}
else
{
$cmd .= '2>' . escapeshellarg( $part[1] );
}
break;
}
}
return $cmd;
}
}