2020class Process extends EventEmitter
2121{
2222 /**
23- * @var ? WritableStreamInterface
23+ * @var WritableStreamInterface|null|ReadableStreamInterface
2424 */
2525 public $ stdin ;
2626
2727 /**
28- * @var ? ReadableStreamInterface
28+ * @var ReadableStreamInterface|null|WritableStreamInterface
2929 */
3030 public $ stdout ;
3131
3232 /**
33- * @var ? ReadableStreamInterface
33+ * @var ReadableStreamInterface|null|WritableStreamInterface
3434 */
3535 public $ stderr ;
3636
3737 /**
3838 * Array with all process pipes (once started)
39+ *
40+ * Unless explicitly configured otherwise during construction, the following
41+ * standard I/O pipes will be assigned by default:
3942 * - 0: STDIN (`WritableStreamInterface`)
4043 * - 1: STDOUT (`ReadableStreamInterface`)
4144 * - 2: STDERR (`ReadableStreamInterface`)
@@ -47,6 +50,8 @@ class Process extends EventEmitter
4750 private $ cmd ;
4851 private $ cwd ;
4952 private $ env ;
53+ private $ fds ;
54+
5055 private $ enhanceSigchildCompatibility ;
5156 private $ sigchildPipe ;
5257
@@ -65,9 +70,10 @@ class Process extends EventEmitter
6570 * @param string $cmd Command line to run
6671 * @param null|string $cwd Current working directory or null to inherit
6772 * @param null|array $env Environment variables or null to inherit
73+ * @param null|array $fds File descriptors to allocate for this process (or null = default STDIO streams)
6874 * @throws \LogicException On windows or when proc_open() is not installed
6975 */
70- public function __construct ($ cmd , $ cwd = null , array $ env = null )
76+ public function __construct ($ cmd , $ cwd = null , array $ env = null , array $ fds = null )
7177 {
7278 if (substr (strtolower (PHP_OS ), 0 , 3 ) === 'win ' ) {
7379 throw new \LogicException ('Windows isn \'t supported due to the blocking nature of STDIN/STDOUT/STDERR pipes. ' );
@@ -87,18 +93,27 @@ public function __construct($cmd, $cwd = null, array $env = null)
8793 }
8894 }
8995
96+ if ($ fds === null ) {
97+ $ fds = array (
98+ array ('pipe ' , 'r ' ), // stdin
99+ array ('pipe ' , 'w ' ), // stdout
100+ array ('pipe ' , 'w ' ), // stderr
101+ );
102+ }
103+
104+ $ this ->fds = $ fds ;
90105 $ this ->enhanceSigchildCompatibility = self ::isSigchildEnabled ();
91106 }
92107
93108 /**
94109 * Start the process.
95110 *
96- * After the process is started, the standard IO streams will be constructed
97- * and available via public properties. STDIN will be paused upon creation.
111+ * After the process is started, the standard I/O streams will be constructed
112+ * and available via public properties.
98113 *
99114 * @param LoopInterface $loop Loop interface for stream construction
100115 * @param float $interval Interval to periodically monitor process state (seconds)
101- * @throws RuntimeException If the process is already running or fails to start
116+ * @throws \ RuntimeException If the process is already running or fails to start
102117 */
103118 public function start (LoopInterface $ loop , $ interval = 0.1 )
104119 {
@@ -107,17 +122,22 @@ public function start(LoopInterface $loop, $interval = 0.1)
107122 }
108123
109124 $ cmd = $ this ->cmd ;
110- $ fdSpec = array (
111- array ('pipe ' , 'r ' ), // stdin
112- array ('pipe ' , 'w ' ), // stdout
113- array ('pipe ' , 'w ' ), // stderr
114- );
115-
125+ $ fdSpec = $ this ->fds ;
116126 $ sigchild = null ;
127+
117128 // Read exit code through fourth pipe to work around --enable-sigchild
118129 if ($ this ->enhanceSigchildCompatibility ) {
119130 $ fdSpec [] = array ('pipe ' , 'w ' );
120- $ sigchild = 3 ;
131+ \end ($ fdSpec );
132+ $ sigchild = \key ($ fdSpec );
133+
134+ // make sure this is fourth or higher (do not mess with STDIO)
135+ if ($ sigchild < 3 ) {
136+ $ fdSpec [3 ] = $ fdSpec [$ sigchild ];
137+ unset($ fdSpec [$ sigchild ]);
138+ $ sigchild = 3 ;
139+ }
140+
121141 $ cmd = sprintf ('(%s) ' . $ sigchild . '>/dev/null; code=$?; echo $code >& ' . $ sigchild . '; exit $code ' , $ cmd );
122142 }
123143
@@ -127,13 +147,13 @@ public function start(LoopInterface $loop, $interval = 0.1)
127147 throw new \RuntimeException ('Unable to launch a new process. ' );
128148 }
129149
130- $ closeCount = 0 ;
131-
150+ // count open process pipes and await close event for each to drain buffers before detecting exit
132151 $ that = $ this ;
152+ $ closeCount = 0 ;
133153 $ streamCloseHandler = function () use (&$ closeCount , $ loop , $ interval , $ that ) {
134- $ closeCount++ ;
154+ $ closeCount-- ;
135155
136- if ($ closeCount < 2 ) {
156+ if ($ closeCount > 0 ) {
137157 return ;
138158 }
139159
@@ -160,18 +180,25 @@ public function start(LoopInterface $loop, $interval = 0.1)
160180 }
161181
162182 foreach ($ pipes as $ n => $ fd ) {
163- if ($ n === 0 ) {
183+ $ meta = \stream_get_meta_data ($ fd );
184+ if (\strpos ($ meta ['mode ' ], 'w ' ) !== false ) {
164185 $ stream = new WritableResourceStream ($ fd , $ loop );
165186 } else {
166187 $ stream = new ReadableResourceStream ($ fd , $ loop );
167188 $ stream ->on ('close ' , $ streamCloseHandler );
189+ $ closeCount ++;
168190 }
169191 $ this ->pipes [$ n ] = $ stream ;
170192 }
171193
172- $ this ->stdin = $ this ->pipes [0 ];
173- $ this ->stdout = $ this ->pipes [1 ];
174- $ this ->stderr = $ this ->pipes [2 ];
194+ $ this ->stdin = isset ($ this ->pipes [0 ]) ? $ this ->pipes [0 ] : null ;
195+ $ this ->stdout = isset ($ this ->pipes [1 ]) ? $ this ->pipes [1 ] : null ;
196+ $ this ->stderr = isset ($ this ->pipes [2 ]) ? $ this ->pipes [2 ] : null ;
197+
198+ // immediately start checking for process exit when started without any I/O pipes
199+ if (!$ closeCount ) {
200+ $ streamCloseHandler ();
201+ }
175202 }
176203
177204 /**
@@ -186,9 +213,9 @@ public function close()
186213 return ;
187214 }
188215
189- $ this ->stdin -> close ();
190- $ this -> stdout ->close ();
191- $ this -> stderr -> close ();
216+ foreach ( $ this ->pipes as $ pipe ) {
217+ $ pipe ->close ();
218+ }
192219
193220 if ($ this ->enhanceSigchildCompatibility ) {
194221 $ this ->pollExitCodePipe ();
0 commit comments