@@ -25,7 +25,7 @@ class Command
25
25
protected $ _readbuffer = 16536 ;
26
26
protected $ _separator = ' ' ;
27
27
protected $ _cmd ;
28
- protected $ _args = array () ;
28
+ protected $ _args = [] ;
29
29
protected $ _exitcode ;
30
30
protected $ _stdout ;
31
31
protected $ _stderr ;
@@ -35,7 +35,7 @@ class Command
35
35
protected $ _timeend ;
36
36
protected $ _cwd ;
37
37
protected $ _env ;
38
- protected $ _conf = array () ;
38
+ protected $ _conf = [] ;
39
39
40
40
/**
41
41
* Creates a new Command object
@@ -47,7 +47,7 @@ class Command
47
47
static public function factory ($ cmd = null , $ noescape = false )
48
48
{
49
49
$ obj = new self ();
50
- if ($ cmd !== NULL ) {
50
+ if ($ cmd !== null ) {
51
51
$ obj ->command ($ cmd , $ noescape );
52
52
}
53
53
return $ obj ;
@@ -105,7 +105,7 @@ public function setDirectory($cwd)
105
105
* @param array $env
106
106
* @return Command - Fluent
107
107
*/
108
- public function setEnv ($ env = array () )
108
+ public function setEnv ($ env = [] )
109
109
{
110
110
$ this ->_env = $ env ;
111
111
return $ this ;
@@ -158,12 +158,12 @@ public function command($cmd, $noescape = false)
158
158
*/
159
159
public function option ($ left , $ right = null , $ sep = null )
160
160
{
161
- if ($ right !== NULL ) {
161
+ if ($ right !== null ) {
162
162
$ right = escapeshellarg ($ right );
163
163
if (empty ($ right )) {
164
164
$ right = "'' " ;
165
165
}
166
- $ left .= ($ sep === NULL ? $ this ->_separator : $ sep ) . $ right ;
166
+ $ left .= ($ sep === null ? $ this ->_separator : $ sep ) . $ right ;
167
167
}
168
168
169
169
$ this ->_args [] = $ left ;
@@ -198,11 +198,11 @@ public function run($stdin = null, $throw_exceptions = true)
198
198
$ this ->_timestart = microtime (true );
199
199
200
200
// Prepare the buffers structure
201
- $ buffers = array (
201
+ $ buffers = [
202
202
0 => $ stdin ,
203
203
1 => &$ this ->_stdout ,
204
204
2 => &$ this ->_stderr ,
205
- ) ;
205
+ ] ;
206
206
$ this ->_exitcode = self ::exec ($ this ->getFullCommand (), $ buffers , $ this ->_callback , $ this ->_callbacklines , $ this ->_readbuffer , $ this ->_cwd , $ this ->_env , $ this ->_conf );
207
207
$ this ->_timeend = microtime (true );
208
208
@@ -225,7 +225,7 @@ public function __toString()
225
225
*/
226
226
public function getFullCommand ()
227
227
{
228
- $ parts = array_merge (array ( $ this ->_cmd ) , $ this ->_args );
228
+ $ parts = array_merge ([ $ this ->_cmd ] , $ this ->_args );
229
229
return implode ($ this ->_separator , $ parts );
230
230
}
231
231
@@ -295,74 +295,124 @@ public static function echoStdErr($content)
295
295
public static function exec ($ cmd , &$ buffers , $ callback = null , $ callbacklines = false , $ readbuffer = 16536 , $ cwd = null , $ env = null , $ conf = null )
296
296
{
297
297
if (!is_array ($ buffers )) {
298
- $ buffers = array () ;
298
+ $ buffers = [] ;
299
299
}
300
300
301
301
// Define the pipes to configure for the process
302
- $ descriptors = array (
303
- self ::STDIN => array ( 'pipe ' , 'r ' ) ,
304
- self ::STDOUT => array ( 'pipe ' , 'w ' ) ,
305
- self ::STDERR => array ( 'pipe ' , 'w ' ) ,
306
- ) ;
302
+ $ descriptors = [
303
+ self ::STDIN => [ 'pipe ' , 'r ' ] ,
304
+ self ::STDOUT => [ 'pipe ' , 'w ' ] ,
305
+ self ::STDERR => [ 'pipe ' , 'w ' ] ,
306
+ ] ;
307
307
308
308
// Start the process
309
309
$ ph = proc_open ($ cmd , $ descriptors , $ pipes , $ cwd , $ env , $ conf );
310
310
if (!is_resource ($ ph )) {
311
311
return null ;
312
312
}
313
313
314
- // Feed the process with the stdin if any and close it
314
+ // Prepare STDIN
315
+ $ stdin_position = 0 ;
315
316
$ stdin = $ buffers [self ::STDIN ];
316
- if (is_resource ($ stdin )) {
317
- // It seems this method is less memory-intensive that the stream copying builtin:
318
- // stream_copy_to_stream(resource $source, resource $dest)
319
- while (!feof ($ stdin )) {
320
- fwrite ($ pipes [self ::STDIN ], fread ($ stdin , $ readbuffer ));
321
- }
322
-
323
- } else if (!empty ($ stdin )) {
324
- fwrite ($ pipes [self ::STDIN ], $ stdin );
317
+ $ stdin_is_stream = is_resource ($ stdin );
318
+ $ use_stdin = $ stdin_is_stream || !empty ($ stdin );
319
+ $ stdin_length = null ;
320
+
321
+ if (!$ use_stdin ) {
322
+ fclose ($ pipes [self ::STDIN ]);
323
+ } else if (!$ stdin_is_stream ) {
324
+ $ stdin_length = strlen ($ stdin );
325
325
}
326
326
327
- fclose ($ pipes [self ::STDIN ]);
327
+ // Setup all streams to non-blocking mode
328
+ stream_set_blocking ($ pipes [self ::STDIN ], false );
329
+ stream_set_blocking ($ pipes [self ::STDOUT ], false );
330
+ stream_set_blocking ($ pipes [self ::STDERR ], false );
328
331
329
- // Setup non-blocking behaviour for stdout and stderr
330
- stream_set_blocking ($ pipes [self ::STDOUT ], 0 );
331
- stream_set_blocking ($ pipes [self ::STDERR ], 0 );
332
+ $ stream_select_timeout_sec = null ;
333
+ $ stream_select_timeout_usec = null ;
332
334
333
335
$ delay = 0 ;
334
336
$ code = null ;
335
- $ open = array (self ::STDOUT , self ::STDERR );
337
+
338
+ $ buffers [self ::STDIN ] = '' ;
336
339
$ buffers [self ::STDOUT ] = empty ($ buffers [self ::STDOUT ]) ? '' : $ buffers [self ::STDOUT ];
337
340
$ buffers [self ::STDERR ] = empty ($ buffers [self ::STDERR ]) ? '' : $ buffers [self ::STDERR ];
338
341
339
- while (!empty ($ open )) {
342
+ // Read from the process' STDOUT and STDERR
343
+ $ reads = [
344
+ $ pipes [self ::STDOUT ],
345
+ $ pipes [self ::STDERR ],
346
+ ];
347
+
348
+ // Write to the process' STDIN
349
+ $ writes = [
350
+ $ pipes [self ::STDIN ],
351
+ ];
352
+
353
+ $ stream_id_map = [
354
+ self ::STDIN => $ pipes [self ::STDIN ],
355
+ self ::STDOUT => $ pipes [self ::STDOUT ],
356
+ self ::STDERR => $ pipes [self ::STDERR ],
357
+ ];
358
+
359
+ // Read/write loop
360
+ while (true ) {
361
+
362
+ // Setup streams before each iteration since they are changed by stream_select()
363
+ $ streams = [
364
+ 'read ' => array_filter ($ reads , 'is_resource ' ),
365
+ 'write ' => array_filter ($ writes , 'is_resource ' ),
366
+ 'except ' => [],
367
+ ];
368
+
369
+ // This line will block until a stream is ready for input or output
370
+ $ ready_streams = stream_select (
371
+ $ streams ['read ' ],
372
+ $ streams ['write ' ],
373
+ $ streams ['except ' ],
374
+ $ stream_select_timeout_sec ,
375
+ $ stream_select_timeout_usec
376
+ );
377
+
340
378
// Try to find the exit code of the command before buggy proc_close()
341
- if ($ code === NULL ) {
379
+ if ($ code === null ) {
342
380
$ status = proc_get_status ($ ph );
343
381
if (!$ status ['running ' ]) {
344
382
$ code = $ status ['exitcode ' ];
383
+ break ;
345
384
}
346
385
}
347
386
348
- // Go thru all open pipes and check for data
349
- foreach ($ open as $ i =>$ pipe ) {
350
- // Try to get some data
351
- $ str = fread ($ pipes [$ pipe ], $ readbuffer );
352
- if (strlen ($ str )) {
353
- $ buffers [$ pipe ] .= $ str ;
387
+ if ($ ready_streams === 0 ) {
388
+ // Stream timeout; no streams ready, retry stream_select
389
+ continue ;
390
+ }
391
+
392
+ if ($ ready_streams === false ) {
393
+ throw new \Exception ("stream_select() failed while waiting for I/O on command " );
394
+ }
395
+
396
+ // Read from all ready streams
397
+ foreach ($ streams ['read ' ] as $ stream ) {
398
+
399
+ $ stream_id = array_search ($ stream , $ stream_id_map , true );
400
+
401
+ $ str = stream_get_contents ($ stream , $ readbuffer );
402
+ if (strlen ($ str ) !== 0 ) {
403
+ $ buffers [$ stream_id ] .= $ str ;
354
404
355
405
if ($ callback ) {
356
406
if ($ callbacklines ) {
357
407
// Note: \r will be left in the line in case of CRLF,
358
408
// and we will need to add \n to the end of each line
359
- $ lines = explode ("\n" , $ buffers [$ pipe ]);
409
+ $ lines = explode ("\n" , $ buffers [$ stream_id ]);
360
410
361
411
// This is left over and does not end with the delimiter
362
- $ buffers [$ pipe ] = array_pop ($ lines );
412
+ $ buffers [$ stream_id ] = array_pop ($ lines );
363
413
364
414
foreach ($ lines as $ line ) {
365
- $ callback_return = call_user_func ($ callback , $ pipe , "$ line \n" );
415
+ $ callback_return = call_user_func ($ callback , $ stream_id , "$ line \n" );
366
416
if ($ callback_return === false ) {
367
417
// We killed the proc early, set code to 0
368
418
$ code = 0 ;
@@ -371,8 +421,8 @@ public static function exec($cmd, &$buffers, $callback = null, $callbacklines =
371
421
}
372
422
373
423
} else {
374
- $ callback_return = call_user_func ($ callback , $ pipe , $ buffers [$ pipe ]);
375
- $ buffers [$ pipe ] = '' ;
424
+ $ callback_return = call_user_func ($ callback , $ stream_id , $ buffers [$ stream_id ]);
425
+ $ buffers [$ stream_id ] = '' ;
376
426
if ($ callback_return === false ) {
377
427
// We killed the proc early, set code to 0
378
428
$ code = 0 ;
@@ -384,28 +434,56 @@ public static function exec($cmd, &$buffers, $callback = null, $callbacklines =
384
434
// Since we've got some data we don't need to sleep :)
385
435
$ delay = 0 ;
386
436
// Check if we have consumed all the data in the current pipe
387
- } else if (feof ($ pipes [$ pipe ])) {
388
- if ($ callback ) {
389
- if (call_user_func ($ callback , $ pipe , null ) === false ) {
390
- break 2 ;
437
+ }
438
+ }
439
+
440
+ // Write to all write ready streams (STDIN of the process)
441
+ foreach ($ streams ['write ' ] as $ stream ) {
442
+
443
+ $ stream_id = array_search ($ stream , $ stream_id_map , true );
444
+
445
+ if ($ stdin_is_stream ) {
446
+ // It seems this method is less memory-intensive that the stream copying builtin:
447
+ // stream_copy_to_stream(resource $source, resource $dest)
448
+ if (feof ($ stdin )) {
449
+ fclose ($ stream );
450
+ } else {
451
+ if (strlen ($ buffers [$ stream_id ]) < $ readbuffer ) {
452
+ // The STDIN buffer is running low
453
+ $ buffers [$ stream_id ] .= stream_get_contents ($ stdin , $ readbuffer );
454
+ }
455
+
456
+ $ bytes_written = fwrite ($ stream , $ buffers [$ stream_id ]);
457
+
458
+ if ($ bytes_written === false ) {
459
+ continue ;
460
+ }
461
+
462
+ $ buffer_length = strlen ($ buffers [$ stream_id ]);
463
+
464
+ if ($ bytes_written === $ buffer_length ) {
465
+ $ buffers [$ stream_id ] = '' ;
466
+ } else {
467
+ // Only part of the buffer was written so we remove that part
468
+ $ buffers [$ stream_id ] = substr ($ buffers [$ stream_id ], $ bytes_written );
391
469
}
392
470
}
393
- unset($ open [$ i ]);
394
- continue 2 ;
471
+
472
+ } else {
473
+ if ($ stdin_position >= $ stdin_length ) {
474
+ fclose ($ stream );
475
+ } else {
476
+ $ chunk = substr ($ stdin , $ stdin_position , $ readbuffer );
477
+ $ bytes_written = fwrite ($ stream , $ chunk );
478
+ $ stdin_position += $ bytes_written ;
479
+ }
395
480
}
396
481
}
397
482
398
- // Check if we have to sleep for a bit to be nice on the CPU
399
- if ($ delay ) {
400
- usleep ($ delay * 1000 );
401
- $ delay = ceil (min (self ::SLEEP_MAX , $ delay *self ::SLEEP_FACTOR ));
402
- } else {
403
- $ delay = self ::SLEEP_START ;
404
- }
405
- }
483
+ } // End read/write loop
406
484
407
485
// Make sure all pipes are closed
408
- foreach ($ pipes as $ pipe=> $ desc ) {
486
+ foreach ($ pipes as $ pipe => $ desc ) {
409
487
if (is_resource ($ desc )) {
410
488
if ($ callback ) {
411
489
call_user_func ($ callback , $ pipe , null );
@@ -421,7 +499,7 @@ public static function exec($cmd, &$buffers, $callback = null, $callbacklines =
421
499
}
422
500
423
501
// Find out the exit code
424
- if ($ code === NULL ) {
502
+ if ($ code === null ) {
425
503
$ code = proc_close ($ ph );
426
504
} else {
427
505
proc_close ($ ph );
0 commit comments