-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProcessExecutor.php
More file actions
110 lines (94 loc) · 3.09 KB
/
ProcessExecutor.php
File metadata and controls
110 lines (94 loc) · 3.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<?php
declare(strict_types=1);
namespace KaririCode\Devkit\Core;
use KaririCode\Devkit\ValueObject\ToolResult;
/**
* Generic process executor used by all tool runners via composition.
*
* Handles process spawning, stdout/stderr capture, timing, and
* exit-code collection. Binary resolution follows a three-tier
* strategy tailored for PHAR distribution:
*
* 1. PHAR-internal vendor/bin/ (self-contained distribution)
* 2. Project-local vendor/bin/ (composer-installed devkit)
* 3. Global PATH (system-wide tool installations)
*
* @since 1.0.0
*/
final readonly class ProcessExecutor
{
public function __construct(
private string $workingDirectory,
) {
}
/**
* Execute a command and capture the result.
*
* @param list<string> $command Full command with arguments.
*/
public function execute(string $toolName, array $command): ToolResult
{
$start = hrtime(true);
$process = proc_open(
$command,
[
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
],
$pipes,
$this->workingDirectory,
);
if (! \is_resource($process)) {
return new ToolResult(
toolName: $toolName,
exitCode: 127,
stdout: '',
stderr: 'Failed to spawn process: ' . implode(' ', $command),
elapsedSeconds: 0.0,
);
}
fclose($pipes[0]);
$stdout = (string) stream_get_contents($pipes[1]);
$stderr = (string) stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($process);
$elapsed = (hrtime(true) - $start) / 1_000_000_000;
return new ToolResult(
toolName: $toolName,
exitCode: $exitCode,
stdout: $stdout,
stderr: $stderr,
elapsedSeconds: round($elapsed, 3),
);
}
/**
* Resolve a tool binary path using the three-tier strategy.
*
* @param string $vendorBin Relative path like "vendor/bin/phpunit"
*/
public function resolveBinary(string $vendorBin): ?string
{
// Tier 1: PHAR-internal binary
if ('' !== \Phar::running(false)) {
$pharBin = \Phar::running(true) . '/' . $vendorBin;
if (file_exists($pharBin)) {
return $pharBin;
}
}
// Tier 2: Project-local vendor binary
$localBin = $this->workingDirectory . '/' . $vendorBin;
if (is_file($localBin) && is_executable($localBin)) {
return $localBin;
}
// Tier 3: Global PATH
$basename = basename($vendorBin);
/** @psalm-suppress ForbiddenCode — shell_exec is intentional for binary resolution; input is escaped */
$globalBin = trim((string) shell_exec('command -v ' . escapeshellarg($basename) . ' 2>/dev/null'));
if ('' !== $globalBin && is_executable($globalBin)) {
return $globalBin;
}
return null;
}
}