Skip to content
This repository has been archived by the owner on Jun 7, 2023. It is now read-only.

Commit

Permalink
refactor shell command handling
Browse files Browse the repository at this point in the history
Summary:Needed to extend it to support environment variables. Fix some
annoyances with the existing API:

 - only able to get exit code if the exception is thrown (`git diff` often exits with 1 in non-error situations)
 - almost always specifying null stdin
 - almost always specifying DONT_VERBOSE
 - output was only printed at the end of a command (eg the slow git submodule command in HHVM looked like a hang)

Existing API is now a wrapper around the new one.

refs #12
refs #9
refs #5

Reviewed By: JoelMarcey

Differential Revision: D3186167

fb-gh-sync-id: d64a852facbc6e8ed167fe462ff51cd1e4bde621
fbshipit-source-id: d64a852facbc6e8ed167fe462ff51cd1e4bde621
  • Loading branch information
fredemmott authored and Facebook Github Bot 4 committed Apr 18, 2016
1 parent 7137ed4 commit f0c3dbd
Showing 5 changed files with 313 additions and 94 deletions.
146 changes: 146 additions & 0 deletions src/ShipItShellCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?hh
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
namespace Facebook\ShipIt;

class ShipItShellCommand {
private ImmVector<string> $command;

private Map<string, string> $environmentVariables = Map {};
private bool $throwForNonZeroExit = true;
private ?string $stdin = null;
private bool $outputToScreen = false;

public function __construct(
private string $path,
...$command
) {
$this->command = new ImmVector($command);
}

public function setStdIn(string $input): this {
$this->stdin = $input;
return $this;
}

public function setOutputToScreen(): this {
$this->outputToScreen = true;
return $this;
}

public function setEnvironmentVariables(
ImmMap<string, string> $vars,
): this {
$this->environmentVariables->setAll($vars);
return $this;
}

public function setNoExceptions(): this {
$this->throwForNonZeroExit = false;
return $this;
}

public function runSynchronously(): ShipItShellCommandResult {
$command = implode(' ', $this->command->map($str ==> escapeshellarg($str)));

$fds = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$stdin = $this->stdin;
if ($stdin === null) {
unset($fds[0]);
}

$pipes = null;
$fp = proc_open(
$command,
$fds,
$pipes,
$this->path,
$this->environmentVariables->toArray(),
);
if (!$fp || !is_array($pipes)) {
throw new \Exception("Failed executing $command");
}
if ($stdin !== null) {
while (strlen($stdin)) {
$written = fwrite($pipes[0], $stdin);
$stdin = substr($stdin, $written);
}
fclose($pipes[0]);
}

$stdout_stream = $pipes[1];
$stderr_stream = $pipes[2];
stream_set_blocking($stdout_stream, false);
stream_set_blocking($stderr_stream, false);
$stdout = '';
$stderr = '';
while (true) {
$ready_streams = [$stdout_stream, $stderr_stream];
$null_byref = null;
$result = stream_select(
$ready_streams,
/* write streams = */ $null_byref,
/* exception streams = */ $null_byref,
/* timeout = */ null,
);
if ($result === false) {
break;
}
$all_empty = true;
foreach ($ready_streams as $stream) {
$out = fread($stream, 1024);
if (strlen($out) === 0) {
continue;
}
$all_empty = false;

if ($stream === $stdout_stream) {
$stdout .= $out;
$this->maybeFwrite(STDOUT, $out);
continue;
}
if ($stream === $stderr_stream) {
$stderr .= $out;
$this->maybeFwrite(STDERR, $out);
continue;
}

invariant_violation('Unhandled stream!');
}

if ($all_empty) {
break;
}
}
$exitcode = proc_close($fp);

$result = new ShipItShellCommandResult(
$exitcode,
$stdout,
$stderr,
);

if ($exitcode !== 0 && $this->throwForNonZeroExit) {
throw new ShipItShellCommandException($command, $result);
}

return $result;
}

private function maybeFwrite(resource $stream, string $out): void {
if (!$this->outputToScreen) {
return;
}
fwrite($stream, $out);
}
}
37 changes: 37 additions & 0 deletions src/ShipItShellCommandException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?hh // strict
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
namespace Facebook\ShipIt;

class ShipItShellCommandException extends \Exception {
public function __construct(
private string $command,
private ShipItShellCommandResult $result,
) {
$exitCode = $result->getExitCode();
$error = $result->getStdErr();
parent::__construct("$command returned exit code $exitCode: $error");
}

public function getError(): string {
return $this->result->getStdErr();
}

public function getExitCode(): int {
return $this->result->getExitCode();
}

public function getOutput(): string {
return $this->result->getStdOut();
}

public function getResult(): ShipItShellCommandResult {
return $this->result;
}
}
31 changes: 31 additions & 0 deletions src/ShipItShellCommandResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?hh // strict
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
namespace Facebook\ShipIt;

final class ShipItShellCommandResult {
public function __construct(
private int $exitCode,
private string $stdout,
private string $stderr,
) {
}

public function getExitCode(): int {
return $this->exitCode;
}

public function getStdOut(): string {
return $this->stdout;
}

public function getStdErr(): string {
return $this->stderr;
}
}
107 changes: 13 additions & 94 deletions src/ShipItUtil.php
Original file line number Diff line number Diff line change
@@ -12,29 +12,6 @@
type ShipItAffectedFile = string;
type ShipItDiffAsString = string;

class ShipItShellCommandException extends \Exception {
public function __construct(
private string $command,
private int $exitCode,
private string $output,
private string $error,
) {
parent::__construct("$command returned exit code $exitCode: $error");
}

public function getError(): string {
return $this->error;
}

public function getExitCode(): int {
return $this->exitCode;
}

public function getOutput(): string {
return $this->output;
}
}

abstract class ShipItUtil {
const SHORT_REV_LENGTH = 7;
// flags for shellExec, no flag equal to 1
@@ -164,98 +141,40 @@ public static function isFileRemoval(string $body): bool {
return (bool) preg_match('@^deleted file@m', $body);
}

// readStreams reads from multiple streams in "parallel"
// (by using stream_select) which ensures that
// reading process won't block waiting for data in one stream when it
// appears in other
private static function readStreams(
array<resource> $streams,
): array<string> {
$outs = array_fill(0, count($streams), '');
$reverse_map = array();

for ($i = 0; $i < count($streams); ++$i) {
stream_set_blocking($streams[$i], 0);
$reverse_map[$streams[$i]] = $i;
}
$stop = false;
while (!$stop) {
$null = null;
$ready_streams = $streams;
if (!stream_select($ready_streams, $null, $null, null)) {
$stop = true;
} else {
$all_empty = true;
foreach ($ready_streams as $stream) {
$out = fread($stream, 1024);
if ($out !== false && strlen($out) !== 0) {
$all_empty = false;
$outs[$reverse_map[$stream]] .= $out;
}
}
$stop = $all_empty;
}
}
return $outs;
}

public static function shellExec(
string $path,
?string $stdin,
int $flags,
...$args
): string {
$fds = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
if ($stdin === null) {
unset($fds[0]);
}
$command = new ShipItShellCommand($path, ...$args);

$argn = null;
foreach ($args as &$argn) {
$argn = escapeshellarg($argn);
}
unset($argn);

$cmd = implode(' ', $args);
if ($flags & self::VERBOSE_SHELL) {
$cmd = implode(' ', $args);
fwrite(STDERR, "\$ $cmd\n");
}
$pipes = null;
$fp = proc_open($cmd, $fds, $pipes, $path);
if (!$fp || !is_array($pipes)) {
throw new \Exception("Failed executing $cmd");
}


if ($stdin !== null) {
if ($flags & self::VERBOSE_SHELL_INPUT) {
fwrite(STDERR, "--STDIN--\n$stdin\n");
}
while (strlen($stdin)) {
$written = fwrite($pipes[0], $stdin);
$stdin = substr($stdin, $written);
}
fclose($pipes[0]);
$command->setStdIn($stdin);
}
list($output, $error) = self::readStreams(array($pipes[1], $pipes[2]));

if ($flags & self::VERBOSE_SHELL_OUTPUT) {
if ($error) {
fwrite(STDERR, "--STDERR--\n$error\n");
}
if ($output) {
fwrite(STDERR, "--STDOUT--\n$output\n");
}
$command->setOutputToScreen();
}
$exitcode = proc_close($fp);

if ($exitcode && !($flags & self::NO_THROW)) {
throw new ShipItShellCommandException($cmd, $exitcode, $output, $error);
if ($flags && self::NO_THROW) {
$command->setNoExceptions();
}

$result = $command->runSynchronously();

$output = $result->getStdOut();
if ($flags & self::RETURN_STDERR) {
return $output."\n".$error;
$output .= "\n".$result->getStdErr();
}
return $output;
}
Loading

0 comments on commit f0c3dbd

Please sign in to comment.