11<?php
22
33namespace VoltTest ;
4+
45use RuntimeException ;
56
67class ProcessManager
78{
89 private string $ binaryPath ;
10+
911 private $ currentProcess = null ;
10- private bool $ debug ;
11- private int $ timeout = 30 ;
1212
13- public function __construct (string $ binaryPath, bool $ debug = true )
13+ public function __construct (string $ binaryPath )
1414 {
15- $ this ->binaryPath = str_replace ( ' / ' , '\\' , $ binaryPath) ;
16- $ this -> debug = $ debug ;
17-
18- if (! file_exists ( $ this -> binaryPath )) {
19- throw new RuntimeException ( " Binary not found at: { $ this -> binaryPath }" );
15+ $ this ->binaryPath = $ binaryPath ;
16+ if ( DIRECTORY_SEPARATOR !== '\\' && function_exists ( ' pcntl_async_signals ' )) {
17+ pcntl_async_signals ( true );
18+ pcntl_signal ( SIGINT , [ $ this , ' handleSignal ' ]);
19+ pcntl_signal ( SIGTERM , [ $ this , ' handleSignal ' ] );
2020 }
21-
22- $ this ->debugLog ("ProcessManager initialized with binary: {$ this ->binaryPath }" );
2321 }
2422
25- private function debugLog ( string $ message ): void
23+ private function handleSignal ( int $ signal ): void
2624 {
27- if ($ this ->debug ) {
28- fwrite ( STDERR , " [DEBUG] " . date ( ' Y-m-d H:i:s ' ) . " - $ message \n" );
29- flush ( );
25+ if ($ this ->currentProcess && is_resource ( $ this -> currentProcess ) ) {
26+ proc_terminate ( $ this -> currentProcess );
27+ proc_close ( $ this -> currentProcess );
3028 }
29+ exit (0 );
3130 }
3231
3332 public function execute (array $ config , bool $ streamOutput ): string
3433 {
35- $ this ->debugLog ("Starting execution " );
36-
37- // Create temporary directory for test files
38- $ tempDir = rtrim (sys_get_temp_dir (), '/ \\' ) . '\\volt_ ' . uniqid ();
39- mkdir ($ tempDir );
40- $ this ->debugLog ("Created temp directory: $ tempDir " );
41-
42- // Create config file
43- $ configFile = $ tempDir . '\\config.json ' ;
44- file_put_contents ($ configFile , json_encode ($ config , JSON_PRETTY_PRINT ));
45- $ this ->debugLog ("Wrote config to file: $ configFile " );
46-
47- $ this ->debugLog ("config file contain: " . file_get_contents ($ configFile ));
48-
49- // Change to temp directory and prepare command
50- $ currentDir = getcwd ();
51- chdir ($ tempDir );
52-
53- // Prepare command without any flags - the binary should read config.json by default
54- $ cmd = sprintf ('"%s" ' , $ this ->binaryPath );
55- $ this ->debugLog ("Command: $ cmd " );
56-
57- // Start process
58- $ descriptorspec = [
59- 1 => ['pipe ' , 'w ' ], // stdout
60- 2 => ['pipe ' , 'w ' ] // stderr
61- ];
62-
63- $ this ->debugLog ("Opening process in directory: " . getcwd ());
64- $ process = proc_open ($ cmd , $ descriptorspec , $ pipes , $ tempDir , null , [
65- 'bypass_shell ' => false
66- ]);
34+ [$ success , $ process , $ pipes ] = $ this ->openProcess ();
35+ $ this ->currentProcess = $ process ;
6736
68- if (!is_resource ($ process )) {
69- $ this ->cleanup ($ tempDir , $ currentDir );
70- throw new RuntimeException ("Failed to start process " );
37+ if (! $ success || ! is_array ($ pipes )) {
38+ throw new RuntimeException ('Failed to start process of volt test ' );
7139 }
7240
73- $ this ->currentProcess = $ process ;
74- $ this ->debugLog ("Process started " );
75-
7641 try {
77- // Set streams to non-blocking mode
78- foreach ($ pipes as $ pipe ) {
79- stream_set_blocking ($ pipe , false );
80- }
42+ $ this ->writeInput ($ pipes [0 ], json_encode ($ config , JSON_PRETTY_PRINT ));
43+ fclose ($ pipes [0 ]);
8144
82- $ output = '' ;
83- $ startTime = time ();
84- $ lastDataTime = time ();
45+ $ output = $ this ->handleProcess ($ pipes , $ streamOutput );
8546
86- while ( true ) {
87- $ status = proc_get_status ( $ process ) ;
88- if (! $ status [ ' running ' ] ) {
89- $ this -> debugLog ( " Process has finished " );
90- break ;
91- }
47+ // Store stderr content before closing
48+ $ stderrContent = '' ;
49+ if ( isset ( $ pipes [ 2 ]) && is_resource ( $ pipes [ 2 ]) ) {
50+ rewind ( $ pipes [ 2 ] );
51+ $ stderrContent = stream_get_contents ( $ pipes [ 2 ]) ;
52+ }
9253
93- // Check timeout
94- if (time () - $ startTime > $ this ->timeout ) {
95- throw new RuntimeException ("Process timed out after {$ this ->timeout } seconds " );
54+ // Clean up pipes
55+ foreach ($ pipes as $ pipe ) {
56+ if (is_resource ($ pipe )) {
57+ fclose ($ pipe );
9658 }
59+ }
9760
98- $ read = $ pipes ;
99- $ write = null ;
100- $ except = null ;
101-
102- if (stream_select ($ read , $ write , $ except , 0 , 200000 )) {
103- foreach ($ read as $ pipe ) {
104- $ data = fread ($ pipe , 8192 );
105- if ($ data === false ) {
106- continue ;
107- }
108- if ($ data !== '' ) {
109- $ lastDataTime = time ();
110- if ($ pipe === $ pipes [1 ]) {
111- $ output .= $ data ;
112- if ($ streamOutput ) {
113- fwrite (STDOUT , $ data );
114- flush ();
115- }
116- } else {
117- fwrite (STDERR , $ data );
118- flush ();
119- }
120- }
121- }
61+ if (is_resource ($ process )) {
62+ $ exitCode = $ this ->closeProcess ($ process );
63+ $ this ->currentProcess = null ;
64+ if ($ exitCode !== 0 ) {
65+ echo "\nError: " . trim ($ stderrContent ) . "\n" ;
66+
67+ return '' ;
12268 }
12369 }
12470
125- // Close pipes
71+ return $ output ;
72+ } finally {
12673 foreach ($ pipes as $ pipe ) {
12774 if (is_resource ($ pipe )) {
12875 fclose ($ pipe );
12976 }
13077 }
78+ if (is_resource ($ process )) {
79+ $ this ->closeProcess ($ process );
80+ $ this ->currentProcess = null ;
81+ }
82+ }
83+ }
13184
132- $ exitCode = proc_close ($ process );
133- $ this ->debugLog ("Process closed with exit code: $ exitCode " );
85+ protected function openProcess (): array
86+ {
87+ $ pipes = [];
88+ $ descriptors = [
89+ 0 => ['pipe ' , 'r ' ],
90+ 1 => ['pipe ' , 'w ' ],
91+ 2 => ['pipe ' , 'w ' ],
92+ ];
13493
135- // Restore original directory and cleanup
136- $ this ->cleanup ($ tempDir , $ currentDir );
94+ // Windows-specific: Remove bypass_shell to allow proper execution
95+ $ options = DIRECTORY_SEPARATOR === '\\'
96+ ? []
97+ : ['bypass_shell ' => true ];
13798
138- if ($ exitCode !== 0 ) {
139- throw new RuntimeException ("Process failed with exit code $ exitCode " );
140- }
99+ $ process = proc_open (
100+ escapeshellcmd ($ this ->binaryPath ),
101+ $ descriptors ,
102+ $ pipes ,
103+ null ,
104+ null ,
105+ $ options
106+ );
141107
142- return $ output ;
108+ if (!is_resource ($ process )) {
109+ return [false , null , []];
110+ }
143111
144- } catch ( \ Exception $ e ) {
145- $ this -> debugLog ( " Error occurred: " . $ e -> getMessage ());
112+ return [ true , $ process , $ pipes ];
113+ }
146114
147- // Clean up
148- foreach ($ pipes as $ pipe ) {
149- if (is_resource ($ pipe )) {
150- fclose ($ pipe );
115+ private function handleProcess (array $ pipes , bool $ streamOutput ): string
116+ {
117+ $ output = '' ;
118+
119+ while (true ) {
120+ $ read = array_filter ($ pipes , 'is_resource ' );
121+ if (empty ($ read )) break ;
122+
123+ $ write = $ except = null ;
124+
125+ // Windows: Add timeout to prevent infinite blocking
126+ $ timeout = DIRECTORY_SEPARATOR === '\\' ? 0 : 1 ;
127+ $ result = stream_select ($ read , $ write , $ except , $ timeout );
128+
129+ if ($ result === false ) break ;
130+
131+ foreach ($ read as $ pipe ) {
132+ $ type = array_search ($ pipe , $ pipes , true );
133+ $ data = fread ($ pipe , 4096 );
134+
135+ if ($ data === false || $ data === '' ) {
136+ if (feof ($ pipe )) {
137+ fclose ($ pipe );
138+ unset($ pipes [$ type ]);
139+ continue ;
140+ }
151141 }
152- }
153142
154- if (is_resource ( $ process )) {
155- $ status = proc_get_status ( $ process ) ;
156- if ($ status [ ' running ' ]) {
157- exec ( " taskkill /F /T /PID { $ status [ ' pid ' ]} 2>&1 " , $ killOutput , $ resultCode );
158- $ this -> debugLog ( " Taskkill result code: $ resultCode " );
143+ if ($ type === 1 ) { // stdout
144+ $ output .= $ data ;
145+ if ($ streamOutput ) echo $ data ;
146+ } elseif ( $ type === 2 && $ streamOutput ) { // stderr
147+ fwrite ( STDERR , $ data );
159148 }
160- proc_close ($ process );
161149 }
162150
163- // Restore directory and cleanup
164- $ this ->cleanup ($ tempDir , $ currentDir );
151+ // Windows: Add small delay to prevent CPU spike
152+ if (DIRECTORY_SEPARATOR === '\\' ) usleep (100000 );
153+ }
165154
166- throw $ e ;
155+ return $ output ;
156+ }
157+
158+ protected function writeInput ($ pipe , string $ input ): void
159+ {
160+ if (is_resource ($ pipe )) {
161+ fwrite ($ pipe , $ input );
167162 }
168163 }
169164
170- private function cleanup ( string $ tempDir , string $ currentDir ): void
165+ protected function closeProcess ( $ process ): int
171166 {
172- // Restore original directory
173- chdir ($ currentDir );
174-
175- // Clean up temp directory
176- if (file_exists ($ tempDir )) {
177- $ files = glob ($ tempDir . '/* ' );
178- foreach ($ files as $ file ) {
179- unlink ($ file );
180- }
181- rmdir ($ tempDir );
182- $ this ->debugLog ("Cleaned up temp directory " );
167+ if (! is_resource ($ process )) {
168+ return -1 ;
183169 }
170+
171+ $ status = proc_get_status ($ process );
172+ if ($ status ['running ' ]) {
173+ proc_terminate ($ process );
174+ }
175+
176+
177+ return proc_close ($ process );
184178 }
185- }
179+ }
0 commit comments