Skip to content

Commit 651d9d0

Browse files
committed
WIP
1 parent 9cc45ac commit 651d9d0

File tree

3 files changed

+130
-282
lines changed

3 files changed

+130
-282
lines changed

src/Platform.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,12 @@ public static function getBinaryPath(): string
212212
self::installBinary();
213213
}
214214

215+
if (PHP_OS_FAMILY === 'Windows' && !is_executable($binaryPath)) {
216+
@chmod($binaryPath, 0755);
217+
}
218+
219+
return $binaryPath;
220+
215221
return $binaryPath;
216222
}
217223
}

src/ProcessManager.php

Lines changed: 124 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,185 +1,179 @@
11
<?php
22

33
namespace VoltTest;
4+
45
use RuntimeException;
56

67
class 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

Comments
 (0)