@@ -104,13 +104,20 @@ private static function run_via_proc_open( string $command, array $options, int
104104 stream_set_blocking ($ pipes [1 ], false );
105105 stream_set_blocking ($ pipes [2 ], false );
106106
107- $ output = '' ;
107+ $ separate_streams = ! empty ($ options ['separate_streams ' ]);
108+ $ stdout = '' ;
109+ $ stderr = '' ;
110+ $ output = '' ;
108111 $ deadline = $ timeout_seconds > 0 ? microtime (true ) + $ timeout_seconds : null ;
109112 $ exit_code = null ;
110113
111114 while ( true ) {
112- $ chunk = (string ) stream_get_contents ($ pipes [1 ]) . (string ) stream_get_contents ($ pipes [2 ]);
115+ $ stdout_chunk = (string ) stream_get_contents ($ pipes [1 ]);
116+ $ stderr_chunk = (string ) stream_get_contents ($ pipes [2 ]);
117+ $ chunk = $ stdout_chunk . $ stderr_chunk ;
113118 if ( '' !== $ chunk ) {
119+ $ stdout .= $ stdout_chunk ;
120+ $ stderr .= $ stderr_chunk ;
114121 $ output .= $ chunk ;
115122 if ( null !== $ on_output ) {
116123 $ on_output ($ chunk );
@@ -124,21 +131,27 @@ private static function run_via_proc_open( string $command, array $options, int
124131 }
125132
126133 if ( null !== $ deadline && microtime (true ) >= $ deadline ) {
127- $ output = self ::terminate_timed_out_process ($ process , $ pipes , $ output );
134+ $ remaining = self ::terminate_timed_out_process ($ process , $ pipes , $ output, $ stdout , $ stderr );
128135 return self ::error (
129136 $ options ,
130137 sprintf ('Process command timed out after %d second(s). ' , $ timeout_seconds ),
131138 array (
132139 'timeout ' => $ timeout_seconds ,
133- 'output ' => self ::cap_output (trim ($ output ), $ output_cap ),
140+ 'output ' => self ::cap_output (trim ($ remaining ['output ' ]), $ output_cap ),
141+ 'stdout ' => self ::cap_output (trim ($ remaining ['stdout ' ]), $ output_cap ),
142+ 'stderr ' => self ::cap_output (trim ($ remaining ['stderr ' ]), $ output_cap ),
134143 )
135144 );
136145 }
137146
138147 usleep ( (int ) ( $ options ['poll_interval_microseconds ' ] ?? 50000 ) );
139148 }
140149
141- $ output .= (string ) stream_get_contents ($ pipes [1 ]) . (string ) stream_get_contents ($ pipes [2 ]);
150+ $ stdout_tail = (string ) stream_get_contents ($ pipes [1 ]);
151+ $ stderr_tail = (string ) stream_get_contents ($ pipes [2 ]);
152+ $ stdout .= $ stdout_tail ;
153+ $ stderr .= $ stderr_tail ;
154+ $ output .= $ stdout_tail . $ stderr_tail ;
142155 foreach ( $ pipes as $ pipe ) {
143156 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Process pipes are not WordPress filesystem paths.
144157 fclose ($ pipe );
@@ -150,44 +163,66 @@ private static function run_via_proc_open( string $command, array $options, int
150163 }
151164
152165 $ output = self ::cap_output (trim (str_replace ("\r" , "\n" , $ output )), $ output_cap );
166+ $ stdout = self ::cap_output (trim (str_replace ("\r" , "\n" , $ stdout )), $ output_cap );
167+ $ stderr = self ::cap_output (trim (str_replace ("\r" , "\n" , $ stderr )), $ output_cap );
153168 if ( 0 !== $ exit_code ) {
169+ $ data = array (
170+ 'exit_code ' => $ exit_code ,
171+ 'output ' => $ output ,
172+ );
173+ if ( $ separate_streams ) {
174+ $ data ['stdout ' ] = $ stdout ;
175+ $ data ['stderr ' ] = $ stderr ;
176+ }
177+
154178 return self ::error (
155179 $ options ,
156180 sprintf ('Process command failed (exit %d): %s ' , $ exit_code , $ output ),
157- array (
158- 'exit_code ' => $ exit_code ,
159- 'output ' => $ output ,
160- )
181+ $ data
161182 );
162183 }
163184
164- return array (
185+ $ result = array (
165186 'success ' => true ,
166187 'output ' => $ output ,
167188 'exit_code ' => 0 ,
168189 );
190+ if ( $ separate_streams ) {
191+ $ result ['stdout ' ] = $ stdout ;
192+ $ result ['stderr ' ] = $ stderr ;
193+ }
194+
195+ return $ result ;
169196 }
170197
171198 /**
172199 * @param resource $process
173200 * @param array<int,resource> $pipes
174201 */
175- private static function terminate_timed_out_process ( $ process , array $ pipes , string $ output ): string {
202+ private static function terminate_timed_out_process ( $ process , array $ pipes , string $ output, string $ stdout = '' , string $ stderr = '' ): array {
176203 proc_terminate ($ process );
177204 usleep (100000 );
178205 $ status = proc_get_status ($ process );
179206 if ( ! empty ($ status ['running ' ]) ) {
180207 proc_terminate ($ process , 9 );
181208 }
182209
183- $ output .= (string ) stream_get_contents ($ pipes [1 ]) . (string ) stream_get_contents ($ pipes [2 ]);
210+ $ stdout_tail = (string ) stream_get_contents ($ pipes [1 ]);
211+ $ stderr_tail = (string ) stream_get_contents ($ pipes [2 ]);
212+ $ stdout .= $ stdout_tail ;
213+ $ stderr .= $ stderr_tail ;
214+ $ output .= $ stdout_tail . $ stderr_tail ;
184215 foreach ( $ pipes as $ pipe ) {
185216 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Process pipes are not WordPress filesystem paths.
186217 fclose ($ pipe );
187218 }
188219 proc_close ($ process );
189220
190- return $ output ;
221+ return array (
222+ 'output ' => $ output ,
223+ 'stdout ' => $ stdout ,
224+ 'stderr ' => $ stderr ,
225+ );
191226 }
192227
193228 private static function cap_output ( string $ output , int $ output_cap ): string {
@@ -204,11 +239,19 @@ private static function cap_output( string $output, int $output_cap ): string {
204239 */
205240 private static function error ( array $ options , string $ message , array $ data = array () ): array |\WP_Error {
206241 if ( ! empty ($ options ['error_as_result ' ]) ) {
207- return array (
242+ $ result = array (
208243 'success ' => false ,
209244 'output ' => (string ) ( $ data ['output ' ] ?? $ message ),
210245 'exit_code ' => (int ) ( $ data ['exit_code ' ] ?? 1 ),
211246 );
247+ if ( array_key_exists ('stdout ' , $ data ) ) {
248+ $ result ['stdout ' ] = (string ) $ data ['stdout ' ];
249+ }
250+ if ( array_key_exists ('stderr ' , $ data ) ) {
251+ $ result ['stderr ' ] = (string ) $ data ['stderr ' ];
252+ }
253+
254+ return $ result ;
212255 }
213256
214257 $ code = isset ($ options ['error_code ' ]) && is_string ($ options ['error_code ' ]) && '' !== $ options ['error_code ' ] ? $ options ['error_code ' ] : 'process_command_failed ' ;
0 commit comments