Skip to content

Commit 3acd661

Browse files
committed
Fixed NUL descriptor in windows + safer pipes handling + improved isWindows + support for custom pipes descriptor passed to execute method
1 parent 4cb09b4 commit 3acd661

File tree

1 file changed

+67
-14
lines changed

1 file changed

+67
-14
lines changed

src/Helper/Shell.php

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ class Shell
4646
const STATE_CLOSED = 'closed';
4747
const STATE_TERMINATED = 'terminated';
4848

49+
const DEFAULT_STDIN_WIN = ['pipe', 'r'];
50+
const DEFAULT_STDIN_NIX = ['pipe', 'r'];
51+
52+
const DEFAULT_STDOUT_WIN = ['pipe', 'w'];
53+
const DEFAULT_STDOUT_NIX = ['pipe', 'w'];
54+
55+
const DEFAULT_STDERR_WIN = ['pipe', 'w'];
56+
const DEFAULT_STDERR_NIX = ['pipe', 'w'];
57+
4958
/** @var bool Whether to wait for the process to finish or return instantly */
5059
protected bool $async = false;
5160

@@ -98,25 +107,40 @@ public function __construct(protected string $command, protected ?string $input
98107
$this->input = $input;
99108
}
100109

101-
protected function getDescriptors(): array
110+
protected function prepareDescriptors(?array $stdin = null, ?array $stdout = null, ?array $stderr = null): array
102111
{
103-
$out = $this->isWindows() ? ['file', 'NUL', 'w'] : ['pipe', 'w'];
104-
112+
$win = $this->isWindows();
113+
if (!$stdin) {
114+
$stdin = $win ? self::DEFAULT_STDIN_WIN : self::DEFAULT_STDIN_NIX;
115+
}
116+
if (!$stdout) {
117+
$stdout = $win ? self::DEFAULT_STDOUT_WIN : self::DEFAULT_STDOUT_NIX;
118+
}
119+
if (!$stderr) {
120+
$stderr = $win ? self::DEFAULT_STDERR_WIN : self::DEFAULT_STDERR_NIX;
121+
}
105122
return [
106-
self::STDIN_DESCRIPTOR_KEY => ['pipe', 'r'],
107-
self::STDOUT_DESCRIPTOR_KEY => $out,
108-
self::STDERR_DESCRIPTOR_KEY => $out,
123+
self::STDIN_DESCRIPTOR_KEY => $stdin,
124+
self::STDOUT_DESCRIPTOR_KEY => $stdout,
125+
self::STDERR_DESCRIPTOR_KEY => $stderr,
109126
];
110127
}
111128

112129
protected function isWindows(): bool
113130
{
114-
return '\\' === DIRECTORY_SEPARATOR;
131+
// If PHP_OS is defined, use it - More reliable:
132+
if (defined('PHP_OS')) {
133+
return 'WIN' === strtoupper(substr(PHP_OS, 0, 3)); // May be 'WINNT' or 'WIN32' or 'Windows'
134+
}
135+
return '\\' === DIRECTORY_SEPARATOR; // Fallback - Less reliable (Windows 7...)
115136
}
116137

117138
protected function setInput(): void
118139
{
119-
fwrite($this->pipes[self::STDIN_DESCRIPTOR_KEY], $this->input ?? '');
140+
//Make sure the pipe is a stream resource before writing to it to avoid a warning
141+
if (is_resource($this->pipes[self::STDIN_DESCRIPTOR_KEY])) {
142+
fwrite($this->pipes[self::STDIN_DESCRIPTOR_KEY], $this->input ?? '');
143+
}
120144
}
121145

122146
protected function updateProcessStatus(): void
@@ -132,9 +156,16 @@ protected function updateProcessStatus(): void
132156

133157
protected function closePipes(): void
134158
{
135-
fclose($this->pipes[self::STDIN_DESCRIPTOR_KEY]);
136-
fclose($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
137-
fclose($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
159+
//Make sure the pipe are a stream resource before closing them to avoid a warning
160+
if (is_resource($this->pipes[self::STDIN_DESCRIPTOR_KEY])) {
161+
fclose($this->pipes[self::STDIN_DESCRIPTOR_KEY]);
162+
}
163+
if (is_resource($this->pipes[self::STDOUT_DESCRIPTOR_KEY])) {
164+
fclose($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
165+
}
166+
if (is_resource($this->pipes[self::STDERR_DESCRIPTOR_KEY])) {
167+
fclose($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
168+
}
138169
}
139170

140171
protected function wait(): ?int
@@ -177,14 +208,25 @@ public function setOptions(
177208

178209
return $this;
179210
}
180-
181-
public function execute(bool $async = false): self
211+
212+
/**
213+
* execute
214+
* Execute the command with optional stdin, stdout and stderr which override the defaults
215+
* If async is set to true, the process will be executed in the background
216+
*
217+
* @param bool $async - default false
218+
* @param ?array $stdin - default null (loads default descriptor)
219+
* @param ?array $stdout - default null (loads default descriptor)
220+
* @param ?array $stderr - default null (loads default descriptor)
221+
* @return self
222+
*/
223+
public function execute(bool $async = false, ?array $stdin = null, ?array $stdout = null, ?array $stderr = null): self
182224
{
183225
if ($this->isRunning()) {
184226
throw new RuntimeException('Process is already running.');
185227
}
186228

187-
$this->descriptors = $this->getDescriptors();
229+
$this->descriptors = $this->prepareDescriptors($stdin, $stdout, $stderr);
188230
$this->processStartTime = microtime(true);
189231

190232
$this->process = proc_open(
@@ -218,6 +260,10 @@ public function execute(bool $async = false): self
218260

219261
private function setOutputStreamNonBlocking(): bool
220262
{
263+
// Make sure the pipe is a stream resource before setting it to non-blocking to avoid a warning
264+
if (!is_resource($this->pipes[self::STDOUT_DESCRIPTOR_KEY])) {
265+
return false;
266+
}
221267
return stream_set_blocking($this->pipes[self::STDOUT_DESCRIPTOR_KEY], false);
222268
}
223269

@@ -228,11 +274,18 @@ public function getState(): string
228274

229275
public function getOutput(): string
230276
{
277+
// Make sure the pipe is a stream resource before reading it to avoid a warning
278+
if (!is_resource($this->pipes[self::STDOUT_DESCRIPTOR_KEY])) {
279+
return '';
280+
}
231281
return stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
232282
}
233283

234284
public function getErrorOutput(): string
235285
{
286+
if (!is_resource($this->pipes[self::STDERR_DESCRIPTOR_KEY])) {
287+
return '';
288+
}
236289
return stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
237290
}
238291

0 commit comments

Comments
 (0)