diff --git a/README.md b/README.md index b254357..964a19f 100644 --- a/README.md +++ b/README.md @@ -201,14 +201,16 @@ Whenever there is output that closure will get called with two parameters: - `type`: this can be `Symfony\Component\Process\Process::OUT` for regular output and `Symfony\Component\Process\Process::ERR` for error output - `line`: the output itself -### Windows Target +### Windows Target -If your target is a Windows machine, you can use the `removeBash` method to remove the bash command from the command line. +If your target is a Windows machine, use the `onWindows` method. ```php -Ssh::create('user', 'host')->removeBash(); +Ssh::create('user', 'host')->onWindows()->execute('dir'); ``` +By default this package pipes your commands to `bash` on the remote host. On Windows the remote shell is `cmd.exe`, which handles input differently and always reports a successful exit code, even when a command fails. The `onWindows` method passes your commands as an argument to `ssh` instead, so `cmd.exe` runs them through `cmd.exe /c` and their real exit code (and thus `isSuccessful()`) is preserved. + ## Testing ``` bash diff --git a/src/Ssh.php b/src/Ssh.php index ce53d77..20a4716 100755 --- a/src/Ssh.php +++ b/src/Ssh.php @@ -16,6 +16,8 @@ class Ssh protected bool $addBash; + protected bool $onWindows; + protected Closure $processConfigurationClosure; protected Closure $onOutput; @@ -38,6 +40,8 @@ public function __construct(?string $user, string $host, ?int $port = null, ?str $this->addBash = true; + $this->onWindows = false; + $this->processConfigurationClosure = fn (Process $process) => null; $this->onOutput = fn ($type, $line) => null; @@ -163,6 +167,13 @@ public function removeBash(): self return $this; } + public function onWindows(): self + { + $this->onWindows = true; + + return $this; + } + protected function getPasswordCommand(): string { if ($this->password !== null) { @@ -181,26 +192,62 @@ public function getExecuteCommand($command): string { $commands = $this->wrapArray($command); + if ($this->onWindows) { + return $this->getWindowsExecuteCommand($commands); + } + + return $this->getBashExecuteCommand($commands); + } + + /** + * @param array $commands + */ + protected function getBashExecuteCommand(array $commands): string + { $commandString = implode(PHP_EOL, $commands); - if (in_array($this->host, ['local', 'localhost', '127.0.0.1'])) { + if ($this->isLocalHost()) { return $commandString; } $passwordCommand = $this->getPasswordCommand(); $extraOptions = implode(' ', $this->getExtraOptions()); - $target = $this->getTargetForSsh(); - - $delimiter = 'EOF-SPATIE-SSH'; - $bash = $this->addBash ? "'bash -se'" : ''; + $delimiter = 'EOF-SPATIE-SSH'; - return "{$passwordCommand}ssh {$extraOptions} {$target} {$bash} << \\$delimiter".PHP_EOL + return "{$passwordCommand}ssh {$extraOptions} {$target} {$bash} << \\{$delimiter}".PHP_EOL .$commandString.PHP_EOL .$delimiter; } + /** + * On Windows the remote shell is cmd.exe, which cannot read commands from stdin the way + * `bash -se` does. Passing the commands as an ssh argument runs them through `cmd.exe /c`, + * which returns the real exit code instead of always reporting success. + * + * @param array $commands + */ + protected function getWindowsExecuteCommand(array $commands): string + { + $commandString = implode(' && ', $commands); + + if ($this->isLocalHost()) { + return $commandString; + } + + $passwordCommand = $this->getPasswordCommand(); + $extraOptions = implode(' ', $this->getExtraOptions()); + $target = $this->getTargetForSsh(); + + return "{$passwordCommand}ssh {$extraOptions} {$target} \"{$commandString}\""; + } + + protected function isLocalHost(): bool + { + return in_array($this->host, ['local', 'localhost', '127.0.0.1']); + } + /** * @param string|array $command * diff --git a/tests/SshTest.php b/tests/SshTest.php index 7e2f37d..711a7fc 100644 --- a/tests/SshTest.php +++ b/tests/SshTest.php @@ -146,6 +146,18 @@ assertMatchesSnapshot($command); }); +it('can run a single command on windows', function () { + $command = $this->ssh->onWindows()->getExecuteCommand('whoami'); + + assertMatchesSnapshot($command); +}); + +it('can run multiple commands on windows', function () { + $command = $this->ssh->onWindows()->getExecuteCommand(['whoami', 'cd C:\\Windows']); + + assertMatchesSnapshot($command); +}); + it('does not alter ssh command when setting timeout', function () { $command = $this->ssh->setTimeout(10)->getExecuteCommand('whoami'); diff --git a/tests/__snapshots__/SshTest__it_can_run_a_single_command_on_windows__1.txt b/tests/__snapshots__/SshTest__it_can_run_a_single_command_on_windows__1.txt new file mode 100644 index 0000000..c827a2d --- /dev/null +++ b/tests/__snapshots__/SshTest__it_can_run_a_single_command_on_windows__1.txt @@ -0,0 +1 @@ +ssh user@example.com "whoami" \ No newline at end of file diff --git a/tests/__snapshots__/SshTest__it_can_run_multiple_commands_on_windows__1.txt b/tests/__snapshots__/SshTest__it_can_run_multiple_commands_on_windows__1.txt new file mode 100644 index 0000000..8d32564 --- /dev/null +++ b/tests/__snapshots__/SshTest__it_can_run_multiple_commands_on_windows__1.txt @@ -0,0 +1 @@ +ssh user@example.com "whoami && cd C:\Windows" \ No newline at end of file