@@ -273,34 +273,117 @@ function_exists('pcntl_fork')
273273 ;
274274 }
275275
276+ // IPC inspired from https://github.com/barracudanetworks/forkdaemon-php
277+ private const SOCKET_HEADER_SIZE = 4 ;
278+
279+ private function ipc_init ()
280+ {
281+ // windows needs AF_INET
282+ $ domain = strtoupper (substr (PHP_OS , 0 , 3 )) == 'WIN ' ? AF_INET : AF_UNIX ;
283+
284+ // create a socket pair for IPC
285+ $ sockets = array ();
286+ if (socket_create_pair ($ domain , SOCK_STREAM , 0 , $ sockets ) === false )
287+ {
288+ throw new \RuntimeException ('socket_create_pair failed: ' . socket_strerror (socket_last_error ()));
289+ }
290+
291+ return $ sockets ;
292+ }
293+
294+ private function socket_receive ($ socket )
295+ {
296+ // initially read to the length of the header size, then
297+ // expand to read more
298+ $ bytes_total = self ::SOCKET_HEADER_SIZE ;
299+ $ bytes_read = 0 ;
300+ $ have_header = false ;
301+ $ socket_message = '' ;
302+ while ($ bytes_read < $ bytes_total )
303+ {
304+ $ read = @socket_read ($ socket , $ bytes_total - $ bytes_read );
305+ if ($ read === false )
306+ {
307+ throw new \RuntimeException ('socket_receive error: ' . socket_strerror (socket_last_error ()));
308+ }
309+
310+ // blank socket_read means done
311+ if ($ read == '' )
312+ {
313+ break ;
314+ }
315+
316+ $ bytes_read += strlen ($ read );
317+ $ socket_message .= $ read ;
318+
319+ if (!$ have_header && $ bytes_read >= self ::SOCKET_HEADER_SIZE )
320+ {
321+ $ have_header = true ;
322+ list ($ bytes_total ) = array_values (unpack ('N ' , $ socket_message ));
323+ $ bytes_read = 0 ;
324+ $ socket_message = '' ;
325+ }
326+ }
327+
328+ return @unserialize ($ socket_message );
329+ }
330+
331+ private function socket_send ($ socket , $ message )
332+ {
333+ $ serialized_message = @serialize ($ message );
334+ if ($ serialized_message == false )
335+ {
336+ throw new \RuntimeException ('socket_send failed to serialize message ' );
337+ }
338+
339+ $ header = pack ('N ' , strlen ($ serialized_message ));
340+ $ data = $ header . $ serialized_message ;
341+ $ bytes_left = strlen ($ data );
342+ while ($ bytes_left > 0 )
343+ {
344+ $ bytes_sent = @socket_write ($ socket , $ data );
345+ if ($ bytes_sent === false )
346+ {
347+ throw new \RuntimeException ('socket_send failed to write to socket ' );
348+ }
349+
350+ $ bytes_left -= $ bytes_sent ;
351+ $ data = substr ($ data , $ bytes_sent );
352+ }
353+ }
354+
276355 private function runInFork (TestCase $ test ): void
277356 {
278- if (socket_create_pair (AF_UNIX , SOCK_STREAM , 0 , $ sockets ) === false ) {
279- throw new \Exception ('could not create socket pair ' );
280- }
357+ list ($ socket_child , $ socket_parent ) = $ this ->ipc_init ();
281358
282359 $ pid = pcntl_fork ();
283- // pcntl_fork may return NULL if the function is disabled in php.ini.
284- if ($ pid === -1 || $ pid === null ) {
360+
361+ if ($ pid === -1 ) {
285362 throw new \Exception ('could not fork ' );
286363 } else if ($ pid ) {
287364 // we are the parent
288365
289- pcntl_waitpid ($ pid , $ status ); // protect against zombie children
366+ socket_close ($ socket_parent );
367+
368+ // read child stdout, stderr
369+ $ result = $ this ->socket_receive ($ socket_child );
290370
291- // read child output
292- $ output = '' ;
293- while (($ read = socket_read ($ sockets [1 ], 2048 , PHP_BINARY_READ )) !== false ) {
294- $ output .= $ read ;
371+ $ stderr = '' ;
372+ $ stdout = '' ;
373+ if (is_array ($ result ) && array_key_exists ('error ' , $ result )) {
374+ $ stderr = $ result ['error ' ];
375+ } else {
376+ $ stdout = $ result ;
295377 }
296- socket_close ($ sockets [1 ]);
297378
298379 $ php = AbstractPhpProcess::factory ();
299- $ php ->processChildResult ($ test , $ output , '' ); // TODO stderr
380+ $ php ->processChildResult ($ test , $ stdout , $ stderr );
300381
301382 } else {
302383 // we are the child
303384
385+ socket_close ($ socket_child );
386+
304387 $ offset = hrtime ();
305388 $ dispatcher = Event \Facade::instance ()->initForIsolation (
306389 \PHPUnit \Event \Telemetry \HRTime::fromSecondsAndNanoseconds (
@@ -310,22 +393,27 @@ private function runInFork(TestCase $test): void
310393 );
311394
312395 $ test ->setInIsolation (true );
313- $ test ->runBare ();
396+ try {
397+ $ test ->run ();
398+ } catch (Throwable $ e ) {
399+ $ this ->socket_send ($ socket_parent , ['error ' => $ e ->getMessage ()]);
400+ exit ();
401+ }
314402
315- // send result into parent
316- socket_write ($ sockets [0 ],
317- serialize (
318- [
319- 'testResult ' => $ test ->result (),
320- 'codeCoverage ' => CodeCoverage::instance ()->isActive () ? CodeCoverage::instance ()->codeCoverage () : null ,
321- 'numAssertions ' => $ test ->numberOfAssertionsPerformed (),
322- 'output ' => !$ test ->expectsOutput () ? $ output : '' ,
323- 'events ' => $ dispatcher ->flush (),
324- 'passedTests ' => PassedTests::instance ()
325- ]
326- )
403+ $ result = serialize (
404+ [
405+ 'testResult ' => $ test ->result (),
406+ 'codeCoverage ' => CodeCoverage::instance ()->isActive () ? CodeCoverage::instance ()->codeCoverage () : null ,
407+ 'numAssertions ' => $ test ->numberOfAssertionsPerformed (),
408+ 'output ' => !$ test ->expectsOutput () ? $ test ->output () : '' ,
409+ 'events ' => $ dispatcher ->flush (),
410+ 'passedTests ' => PassedTests::instance ()
411+ ]
327412 );
328- socket_close ($ sockets [0 ]);
413+
414+ // send result into parent
415+ $ this ->socket_send ($ socket_parent , $ result );
416+ exit ();
329417 }
330418 }
331419
0 commit comments