diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -634,7 +634,9 @@ 'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php', 'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php', 'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php', + 'DrydockSetupCheckWinRM' => 'applications/drydock/check/DrydockSetupCheckWinRM.php', 'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php', + 'DrydockWinRMCommandInterface' => 'applications/drydock/interface/command/DrydockWinRMCommandInterface.php', 'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php', 'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php', 'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php', @@ -3428,7 +3430,9 @@ 'DrydockResourceViewController' => 'DrydockResourceController', 'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface', 'DrydockSSHCommandInterface' => 'DrydockCommandInterface', + 'DrydockSetupCheckWinRM' => 'PhabricatorSetupCheck', 'DrydockWebrootInterface' => 'DrydockInterface', + 'DrydockWinRMCommandInterface' => 'DrydockCommandInterface', 'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation', 'FeedConduitAPIMethod' => 'ConduitAPIMethod', 'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod', diff --git a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php --- a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php @@ -75,19 +75,7 @@ $full_path = $base_path.$separator.$lease_id; $cmd = $lease->getInterface('command'); - - if ($v_platform !== 'windows') { - $cmd->execx('mkdir %s', $full_path); - } else { - // Windows is terrible. The mkdir command doesn't even support putting - // the path in quotes. IN QUOTES. ARGUHRGHUGHHGG!! Do some terribly - // inaccurate sanity checking since we can't safely escape the path. - if (preg_match('/^[A-Z]\\:\\\\[a-zA-Z0-9\\\\\\ ]/', $full_path) === 0) { - throw new Exception( - 'Unsafe path detected for Windows platform: "'.$full_path.'".'); - } - $cmd->execx('mkdir %C', $full_path); - } + $cmd->execx('mkdir %s', $full_path); $lease->setAttribute('path', $full_path); } @@ -103,7 +91,12 @@ switch ($type) { case 'command': - return id(new DrydockSSHCommandInterface()) + $interface = new DrydockSSHCommandInterface(); + if ($resource->getAttribute('platform') === 'windows') { + $interface = new DrydockWinRMCommandInterface(); + } + + return $interface ->setConfiguration(array( 'host' => $resource->getAttribute('host'), 'port' => $resource->getAttribute('port'), diff --git a/src/applications/drydock/check/DrydockSetupCheckWinRM.php b/src/applications/drydock/check/DrydockSetupCheckWinRM.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/check/DrydockSetupCheckWinRM.php @@ -0,0 +1,41 @@ + 'https://github.com/masterzen/winrm'), + 'https://github.com/masterzen/winrm'), + phutil_tag('tt', array(), 'PATH')); + + $message = pht( + 'You only need this binary if you are leasing Windows hosts in '. + 'Drydock or Harbormaster. If you don\'t need to run commands on '. + 'Windows machines, you can safely ignore this message.'); + + $this->newIssue('bin.winrm') + ->setShortName(pht("'%s' Missing", 'winrm')) + ->setName(pht("Missing '%s' Binary", 'winrm')) + ->setSummary( + pht("The '%s' binary could not be located or executed.", 'winrm')) + ->setMessage(pht("%s\n\n%s", $preamble, $message)); + } + + } + +} diff --git a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php --- a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php +++ b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php @@ -36,45 +36,10 @@ $argv = func_get_args(); - if ($this->getConfig('platform') === 'windows') { - // Handle Windows by executing the command under PowerShell. - $command = id(new PhutilCommandString($argv)) - ->setEscapingMode(PhutilCommandString::MODE_POWERSHELL); + // Handle UNIX by executing under the native shell. + $argv = $this->applyWorkingDirectoryToArgv($argv); - $change_directory = ''; - if ($this->getWorkingDirectory() !== null) { - $change_directory .= 'cd '.$this->getWorkingDirectory(); - } - - $script = <<applyWorkingDirectoryToArgv($argv); - - $full_command = call_user_func_array('csprintf', $argv); - } + $full_command = call_user_func_array('csprintf', $argv); $command_timeout = ''; if ($this->connectTimeout !== null) { diff --git a/src/applications/drydock/interface/command/DrydockWinRMCommandInterface.php b/src/applications/drydock/interface/command/DrydockWinRMCommandInterface.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/interface/command/DrydockWinRMCommandInterface.php @@ -0,0 +1,69 @@ +passphraseWinRMPassword !== null) { + return; + } + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIDs(array($this->getConfig('credential'))) + ->needSecrets(true) + ->executeOne(); + + if ($credential->getProvidesType() !== + PassphraseCredentialTypePassword::PROVIDES_TYPE) { + throw new Exception('Only password credentials are supported.'); + } + + $this->passphraseWinRMPassword = PassphrasePasswordKey::loadFromPHID( + $credential->getPHID(), + PhabricatorUser::getOmnipotentUser()); + } + + public function getExecFuture($command) { + $this->openCredentialsIfNotOpen(); + + $argv = func_get_args(); + + $change_directory = ''; + if ($this->getWorkingDirectory() !== null) { + $change_directory .= 'cd '.$this->getWorkingDirectory().' & '; + } + + // Encode the command to run under Powershell. + $command = id(new PhutilCommandString($argv)) + ->setEscapingMode(PhutilCommandString::MODE_POWERSHELL); + + // When Microsoft says "Unicode" they don't mean UTF-8. + $command = mb_convert_encoding($command, 'UTF-16LE'); + $command = base64_encode($command); + + $powershell = + 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'; + $powershell .= + ' -ExecutionPolicy Bypass'. + ' -NonInteractive'. + ' -InputFormat Text'. + ' -OutputFormat Text'. + ' -EncodedCommand '.$command; + + return new ExecFuture( + 'winrm '. + '-hostname=%s '. + '-username=%P '. + '-password=%P '. + '-port=%s '. + '%s', + $this->getConfig('host'), + $this->passphraseWinRMPassword->getUsernameEnvelope(), + $this->passphraseWinRMPassword->getPasswordEnvelope(), + $this->getConfig('port'), + $change_directory.$powershell); + } +}