20
20
class Process extends EventEmitter
21
21
{
22
22
/**
23
- * @var ? WritableStreamInterface
23
+ * @var WritableStreamInterface|null|ReadableStreamInterface
24
24
*/
25
25
public $ stdin ;
26
26
27
27
/**
28
- * @var ? ReadableStreamInterface
28
+ * @var ReadableStreamInterface|null|WritableStreamInterface
29
29
*/
30
30
public $ stdout ;
31
31
32
32
/**
33
- * @var ? ReadableStreamInterface
33
+ * @var ReadableStreamInterface|null|WritableStreamInterface
34
34
*/
35
35
public $ stderr ;
36
36
37
37
/**
38
38
* 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:
39
42
* - 0: STDIN (`WritableStreamInterface`)
40
43
* - 1: STDOUT (`ReadableStreamInterface`)
41
44
* - 2: STDERR (`ReadableStreamInterface`)
@@ -47,6 +50,8 @@ class Process extends EventEmitter
47
50
private $ cmd ;
48
51
private $ cwd ;
49
52
private $ env ;
53
+ private $ fds ;
54
+
50
55
private $ enhanceSigchildCompatibility ;
51
56
private $ sigchildPipe ;
52
57
@@ -65,9 +70,10 @@ class Process extends EventEmitter
65
70
* @param string $cmd Command line to run
66
71
* @param null|string $cwd Current working directory or null to inherit
67
72
* @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)
68
74
* @throws \LogicException On windows or when proc_open() is not installed
69
75
*/
70
- public function __construct ($ cmd , $ cwd = null , array $ env = null )
76
+ public function __construct ($ cmd , $ cwd = null , array $ env = null , array $ fds = null )
71
77
{
72
78
if (substr (strtolower (PHP_OS ), 0 , 3 ) === 'win ' ) {
73
79
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)
87
93
}
88
94
}
89
95
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 ;
90
105
$ this ->enhanceSigchildCompatibility = self ::isSigchildEnabled ();
91
106
}
92
107
93
108
/**
94
109
* Start the process.
95
110
*
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.
98
113
*
99
114
* @param LoopInterface $loop Loop interface for stream construction
100
115
* @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
102
117
*/
103
118
public function start (LoopInterface $ loop , $ interval = 0.1 )
104
119
{
@@ -107,17 +122,22 @@ public function start(LoopInterface $loop, $interval = 0.1)
107
122
}
108
123
109
124
$ 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 ;
116
126
$ sigchild = null ;
127
+
117
128
// Read exit code through fourth pipe to work around --enable-sigchild
118
129
if ($ this ->enhanceSigchildCompatibility ) {
119
130
$ 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
+
121
141
$ cmd = sprintf ('(%s) ' . $ sigchild . '>/dev/null; code=$?; echo $code >& ' . $ sigchild . '; exit $code ' , $ cmd );
122
142
}
123
143
@@ -127,13 +147,13 @@ public function start(LoopInterface $loop, $interval = 0.1)
127
147
throw new \RuntimeException ('Unable to launch a new process. ' );
128
148
}
129
149
130
- $ closeCount = 0 ;
131
-
150
+ // count open process pipes and await close event for each to drain buffers before detecting exit
132
151
$ that = $ this ;
152
+ $ closeCount = 0 ;
133
153
$ streamCloseHandler = function () use (&$ closeCount , $ loop , $ interval , $ that ) {
134
- $ closeCount++ ;
154
+ $ closeCount-- ;
135
155
136
- if ($ closeCount < 2 ) {
156
+ if ($ closeCount > 0 ) {
137
157
return ;
138
158
}
139
159
@@ -160,18 +180,25 @@ public function start(LoopInterface $loop, $interval = 0.1)
160
180
}
161
181
162
182
foreach ($ pipes as $ n => $ fd ) {
163
- if ($ n === 0 ) {
183
+ $ meta = \stream_get_meta_data ($ fd );
184
+ if (\strpos ($ meta ['mode ' ], 'w ' ) !== false ) {
164
185
$ stream = new WritableResourceStream ($ fd , $ loop );
165
186
} else {
166
187
$ stream = new ReadableResourceStream ($ fd , $ loop );
167
188
$ stream ->on ('close ' , $ streamCloseHandler );
189
+ $ closeCount ++;
168
190
}
169
191
$ this ->pipes [$ n ] = $ stream ;
170
192
}
171
193
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
+ }
175
202
}
176
203
177
204
/**
@@ -186,9 +213,9 @@ public function close()
186
213
return ;
187
214
}
188
215
189
- $ this ->stdin -> close ();
190
- $ this -> stdout ->close ();
191
- $ this -> stderr -> close ();
216
+ foreach ( $ this ->pipes as $ pipe ) {
217
+ $ pipe ->close ();
218
+ }
192
219
193
220
if ($ this ->enhanceSigchildCompatibility ) {
194
221
$ this ->pollExitCodePipe ();
0 commit comments