diff --git a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php index 10cc6b904e..16bb97948b 100644 --- a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php +++ b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php @@ -1,72 +1,98 @@ passphraseSSHKey !== null) { return; } $credential = id(new PassphraseCredentialQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($this->getConfig('credential'))) ->needSecrets(true) ->executeOne(); if ($credential->getProvidesType() !== PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE) { throw new Exception('Only private key credentials are supported.'); } $this->passphraseSSHKey = PassphraseSSHKey::loadFromPHID( $credential->getPHID(), PhabricatorUser::getOmnipotentUser()); } public function setConnectTimeout($timeout) { $this->connectTimeout = $timeout; return $this; } public function getExecFuture($command) { $this->openCredentialsIfNotOpen(); $argv = func_get_args(); - // This assumes there's a UNIX shell living at the other - // end of the connection, which isn't the case for Windows machines. - if ($this->getConfig('platform') !== 'windows') { - $argv = $this->applyWorkingDirectoryToArgv($argv); - } + if ($this->getConfig('platform') === 'windows') { + // Handle Windows by executing the command under PowerShell. + $command = id(new PhutilCommandString($argv)) + ->setEscapingMode(PhutilCommandString::MODE_POWERSHELL); - $full_command = call_user_func_array('csprintf', $argv); + $change_directory = ''; + if ($this->getWorkingDirectory() !== null) { + $change_directory .= 'cd '.$this->getWorkingDirectory(); + } - if ($this->getConfig('platform') === 'windows') { - // On Windows platforms we need to execute cmd.exe explicitly since - // most commands are not really executables. - $full_command = 'C:\\Windows\\system32\\cmd.exe /C '.$full_command; + $script = <<applyWorkingDirectoryToArgv($argv); + + $full_command = call_user_func_array('csprintf', $argv); } $command_timeout = ''; if ($this->connectTimeout !== null) { $command_timeout = csprintf( '-o %s', 'ConnectTimeout='.$this->connectTimeout); } return new ExecFuture( 'ssh '. '-o StrictHostKeyChecking=no '. '-o BatchMode=yes '. '%C -p %s -i %P %P@%s -- %s', $command_timeout, $this->getConfig('port'), $this->passphraseSSHKey->getKeyfileEnvelope(), $this->passphraseSSHKey->getUsernameEnvelope(), $this->getConfig('host'), $full_command); } } diff --git a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php index 552ee7f996..d16f11fcfc 100644 --- a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php @@ -1,100 +1,121 @@ formatSettingForDescription('command'), $this->formatSettingForDescription('hostartifact')); } + public function escapeCommand($pattern, array $args) { + array_unshift($args, $pattern); + + $mode = PhutilCommandString::MODE_DEFAULT; + if ($this->platform == 'windows') { + $mode = PhutilCommandString::MODE_POWERSHELL; + } + + return id(new PhutilCommandString($args)) + ->setEscapingMode($mode); + } + public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); $variables = $build_target->getVariables(); + $artifact = $build->loadArtifact($settings['hostartifact']); + + $lease = $artifact->loadDrydockLease(); + + $this->platform = $lease->getAttribute('platform'); + $command = $this->mergeVariables( - 'vcsprintf', + array($this, 'escapeCommand'), $settings['command'], $variables); - $artifact = $build->loadArtifact($settings['hostartifact']); - - $lease = $artifact->loadDrydockLease(); + $this->platform = null; $interface = $lease->getInterface('command'); $future = $interface->getExecFuture('%C', $command); $log_stdout = $build->createLog($build_target, 'remote', 'stdout'); $log_stderr = $build->createLog($build_target, 'remote', 'stderr'); $start_stdout = $log_stdout->start(); $start_stderr = $log_stderr->start(); // Read the next amount of available output every second. while (!$future->isReady()) { list($stdout, $stderr) = $future->read(); $log_stdout->append($stdout); $log_stderr->append($stderr); $future->discardBuffers(); // Wait one second before querying for more data. sleep(1); } // Get the return value so we can log that as well. list($err) = $future->resolve(); // Retrieve the last few bits of information. list($stdout, $stderr) = $future->read(); $log_stdout->append($stdout); $log_stderr->append($stderr); $future->discardBuffers(); $log_stdout->finalize($start_stdout); $log_stderr->finalize($start_stderr); if ($err) { throw new HarbormasterBuildFailureException(); } } public function getArtifactInputs() { return array( array( 'name' => pht('Run on Host'), 'key' => $this->getSetting('hostartifact'), 'type' => HarbormasterBuildArtifact::TYPE_HOST, ), ); } public function getFieldSpecifications() { return array( 'command' => array( 'name' => pht('Command'), 'type' => 'text', 'required' => true, + 'caption' => pht( + 'Under Windows, this is executed under PowerShell.'. + 'Under UNIX, this is executed using the user\'s shell.'), ), 'hostartifact' => array( 'name' => pht('Host'), 'type' => 'text', 'required' => true, ), ); } }