Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F18047950
D10363.id.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
D10363.id.diff
View Options
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
@@ -45,6 +45,7 @@
'ExecFuture' => 'future/exec/ExecFuture.php',
'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php',
'ExecPassthruTestCase' => 'future/exec/__tests__/ExecPassthruTestCase.php',
+ 'ExecPowershellTestCase' => 'future/exec/__tests__/ExecPowershellTestCase.php',
'FileFinder' => 'filesystem/FileFinder.php',
'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php',
'FileList' => 'filesystem/FileList.php',
@@ -497,6 +498,7 @@
'ExecFuture' => 'Future',
'ExecFutureTestCase' => 'PhutilTestCase',
'ExecPassthruTestCase' => 'PhutilTestCase',
+ 'ExecPowershellTestCase' => 'PhutilTestCase',
'FileFinderTestCase' => 'PhutilTestCase',
'FilesystemException' => 'Exception',
'FilesystemTestCase' => 'PhutilTestCase',
diff --git a/src/future/exec/ExecFuture.php b/src/future/exec/ExecFuture.php
--- a/src/future/exec/ExecFuture.php
+++ b/src/future/exec/ExecFuture.php
@@ -32,6 +32,9 @@
private $stdin = null;
private $closePipe = true;
+ private $powershellXml = false;
+ private $powershellStderr = null;
+
private $stdoutPos = 0;
private $stderrPos = 0;
private $command = null;
@@ -207,6 +210,19 @@
/**
+ * Enable parsing the nonsense XML crap that Powershell outputs to stderr,
+ * which wraps all standard error output as CLIXML.
+ *
+ * @param bool Whether this future should parse standard error as CLIXML.
+ * @return this
+ */
+ public function setPowershellXML($powershell_xml) {
+ $this->powershellXml = $powershell_xml;
+ return $this;
+ }
+
+
+ /**
* Set the value of a specific environmental variable for this command.
*
* @param string Environmental variable name.
@@ -728,11 +744,27 @@
}
if ($max_stderr_read_bytes > 0) {
- $this->stderr .= $this->readAndDiscard(
- $stderr,
- $this->getStderrSizeLimit() - strlen($this->stderr),
- 'stderr',
- $max_stderr_read_bytes);
+ if ($this->powershellXml) {
+ $this->powershellStderr .= $this->readAndDiscard(
+ $stderr,
+ $this->getStderrSizeLimit() - strlen($this->powershellStderr),
+ 'stderr',
+ $max_stderr_read_bytes);
+
+ list($parsed_stderr, $stderr_taken) = $this->parsePowershellXML(
+ $this->powershellStderr);
+
+ $this->stderr .= $parsed_stderr;
+ $this->powershellStderr = substr(
+ $this->powershellStderr,
+ $stderr_taken);
+ } else {
+ $this->stderr .= $this->readAndDiscard(
+ $stderr,
+ $this->getStderrSizeLimit() - strlen($this->stderr),
+ 'stderr',
+ $max_stderr_read_bytes);
+ }
}
if (!$status['running']) {
@@ -756,6 +788,73 @@
/**
+ * Parse the powershell XML and convert it into output for CLIXML.
+ */
+ public function parsePowershellXML($powershell_xml) {
+ $lines = phutil_split_lines($powershell_xml);
+
+ $result = '';
+ $consumed_characters = 0;
+ $previous_characters = 0;
+
+ // With CLIXML, each line contains XML, or the CLIXML start line.
+ for ($i = 0; $i < count($lines); $i++) {
+ $line = $lines[$i];
+
+ // Calculate how many characters we're consuming.
+ $previous_characters = $consumed_characters;
+ $consumed_characters += strlen($line);
+ while (
+ $consumed_characters < strlen($powershell_xml) && (
+ $powershell_xml[$consumed_characters] === "\r" ||
+ $powershell_xml[$consumed_characters] === "\n")) {
+
+ $consumed_characters += 1;
+ }
+
+ // CLIXML outputs "#< CLIXML" on the first line, so we discard it.
+ if (trim($line) === '#< CLIXML') {
+ continue;
+ }
+
+ // Try and load the line as XML. Because other processes can write to
+ // standard error, it can be random output from other processes (yay!)
+ $xml = @simplexml_load_string($line);
+ if ($xml === false) {
+
+ // Check to see if this is the last line; if it is, we might not be
+ // able to parse it because it's not fully read yet.
+ if ($i === count($lines) - 1) {
+ return array($result, $previous_characters);
+ }
+
+ // If we've fully read this line, and we still can't read it, then it
+ // might be output from another program, so just return it as part
+ // of the results.
+ $result .= $line;
+ continue;
+ }
+
+ $xml->registerXPathNamespace(
+ 'ns',
+ 'http://schemas.microsoft.com/powershell/2004/04');
+
+ $error_lines = $xml->xpath('//ns:S[@S=\'Error\']');
+ foreach ($error_lines as $error) {
+
+ // Microsoft; land of the completely made-up standards.
+ $error = str_replace('_x000D_', "\r", $error);
+ $error = str_replace('_x000A_', "\n", $error);
+
+ $result .= $error;
+ }
+ }
+
+ return array($result, $consumed_characters);
+ }
+
+
+ /**
* @return void
* @task internal
*/
diff --git a/src/future/exec/__tests__/ExecPowershellTestCase.php b/src/future/exec/__tests__/ExecPowershellTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/future/exec/__tests__/ExecPowershellTestCase.php
@@ -0,0 +1,165 @@
+<?php
+
+final class ExecPowershellTestCase extends PhutilTestCase {
+
+ private function getOnlyPowershellXML() {
+ return
+ '#< CLIXML'."\r\n".
+ '<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/'.
+ '2004/04"><S S="Error">cd Z:\366_x000D__x000A_</S><S S="Error">trap_x000'.
+ 'D__x000A_</S><S S="Error">{_x000D__x000A_</S><S S="Error"> #try {_x000'.
+ 'D__x000A_</S><S S="Error"> # #$Host.UI.WriteErrorLine($_)_x000D__x000'.
+ 'A_</S><S S="Error"> #} catch {_x000D__x000A_</S><S S="Error"> # #[Mi'.
+ 'crosoft.PowerShell.Commands.WriteErrorException]_x000D__x000A_</S><S S='.
+ '"Error"> # #Write-Host $__x000D__x000A_</S><S S="Error"> #}_x000D__x'.
+ '000A_</S><S S="Error"> _x000D__x000A_</S><S S="Error"> #exit 1_x000'.
+ 'D__x000A_</S><S S="Error">}_x000D__x000A_</S><S S="Error">try {_x000D__'.
+ 'x000A_</S><S S="Error"> Write-Host "TEST"; Write-Error _x000D__x000A_<'.
+ '/S><S S="Error">"<xml>microsoft<you>better<not>suck&l'.
+ 't;/not></you></xml><S WHOOOOPS>"_x000D__x000A_</S>'.
+ '<S S="Error">} catch {_x000D__x000A_</S><S S="Error"> Write-Host "exce'.
+ 'ption occurred"_x000D__x000A_</S><S S="Error"> exit 1_x000D__x000A_</S'.
+ '><S S="Error">}_x000D__x000A_</S><S S="Error">if ($LastExitCode -ne 0) '.
+ '{_x000D__x000A_</S><S S="Error"> exit $LastExitCode_x000D__x000A_</S><'.
+ 'S S="Error">} : <xml>microsoft<you>better<not>suck<'.
+ ';/not></you></xml><S WHOOOOPS>_x000D__x000A_</S><S'.
+ ' S="Error"> + CategoryInfo : NotSpecified: (:) [Write-Error'.
+ '], WriteErrorExcep _x000D__x000A_</S><S S="Error"> tion_x000D__x000A_'.
+ '</S><S S="Error"> + FullyQualifiedErrorId : Microsoft.PowerShell.Com'.
+ 'mands.WriteErrorExceptio _x000D__x000A_</S><S S="Error"> n_x000D__x00'.
+ '0A_</S><S S="Error"> _x000D__x000A_</S></Objs>';
+ }
+
+ private function getIntermixedPowershellXML() {
+ return
+ 'whatever blah blah blah blah.'."\r\n".
+ 'some other lines here.'."\r\n".
+ 'microsoft y u do dis'."\r\n".
+ '#< CLIXML'."\r\n".
+ '<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/'.
+ '2004/04"><S S="Error">abcdefghi_x000D__x000A_</S><S S="Error">At line:1'.
+ ' char:1_x000D__x000A_</S><S S="Error">+ abcdefghi _x000D__x000A_</S><S '.
+ 'S="Error">-Force_x000D__x000A_</S><S S="Error">+ _x000D__x000A_</S><S S'.
+ '="Error">~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'.
+ '~~~~~~~~~~~~~~~~_x000D__x000A_</S><S S="Error"> + CategoryInfo '.
+ ' : NotSpecified: (:) [Stop-EC2Instance], AmazonEC2E _x000D__x000A_</'.
+ 'S><S S="Error"> xception_x000D__x000A_</S><S S="Error"> + FullyQua'.
+ 'lifiedErrorId : Amazon.EC2.AmazonEC2Exception,Amazon.PowerShell. _x000D'.
+ '__x000A_</S><S S="Error"> Cmdlets.EC2.StopEC2InstanceCmdlet_x000D__x0'.
+ '00A_</S><S S="Error"> _x000D__x000A_</S></Objs>'."\r\n";
+ }
+
+ private function getOnlyPowershellExpected() {
+ return
+ 'cd Z:\366'."\r\n".
+ 'trap'."\r\n".
+ '{'."\r\n".
+ ' #try {'."\r\n".
+ ' # #$Host.UI.WriteErrorLine($_)'."\r\n".
+ ' #} catch {'."\r\n".
+ ' # #[Microsoft.PowerShell.Commands.WriteErrorException]'."\r\n".
+ ' # #Write-Host $_'."\r\n".
+ ' #}'."\r\n".
+ ' '."\r\n".
+ ' #exit 1'."\r\n".
+ '}'."\r\n".
+ 'try {'."\r\n".
+ ' Write-Host "TEST"; Write-Error '."\r\n".
+ '"<xml>microsoft<you>better<not>suck</not></you></xml><S WHOOOOPS>"'.
+ "\r\n".
+ '} catch {'."\r\n".
+ ' Write-Host "exception occurred"'."\r\n".
+ ' exit 1'."\r\n".
+ '}'."\r\n".
+ 'if ($LastExitCode -ne 0) {'."\r\n".
+ ' exit $LastExitCode'."\r\n".
+ '} : <xml>microsoft<you>better<not>suck</not></you></xml><S WHOOOOPS>'.
+ "\r\n".
+ ' + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErr'.
+ 'orExcep '."\r\n".
+ ' tion'."\r\n".
+ ' + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorE'.
+ 'xceptio '."\r\n".
+ ' n'."\r\n".
+ ' '."\r\n";
+ }
+
+ private function getIntermixedPowershellExpected() {
+ return
+ 'whatever blah blah blah blah.'."\r\n".
+ 'some other lines here.'."\r\n".
+ 'microsoft y u do dis'."\r\n".
+ 'abcdefghi'."\r\n".
+ 'At line:1 char:1'."\r\n".
+ '+ abcdefghi '."\r\n".
+ '-Force'."\r\n".
+ '+ '."\r\n".
+ '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'.
+ '~~~~~~~'."\r\n".
+ ' + CategoryInfo : NotSpecified: (:) [Stop-EC2Instance], Ama'.
+ 'zonEC2E '."\r\n".
+ ' xception'."\r\n".
+ ' + FullyQualifiedErrorId : Amazon.EC2.AmazonEC2Exception,Amazon.Powe'.
+ 'rShell. '."\r\n".
+ ' Cmdlets.EC2.StopEC2InstanceCmdlet'."\r\n".
+ ' '."\r\n";
+ }
+
+ public function testParseOnlyPowershellXML() {
+ $powershell = $this->getOnlyPowershellXML();
+
+ list($parsed, $consumed) =
+ id(new ExecFuture(''))->parsePowershellXML($powershell);
+ $expected = $this->getOnlyPowershellExpected();
+
+ $parsed = str_replace("\r", '', $parsed);
+ $expected = str_replace("\r", '', $expected);
+
+ $this->assertEqual($expected, $parsed);
+ }
+
+ public function testParseIntermixedPowershellXML() {
+ $powershell = $this->getIntermixedPowershellXML();
+
+ list($parsed, $consumed) =
+ id(new ExecFuture(''))->parsePowershellXML($powershell);
+ $expected = $this->getIntermixedPowershellExpected();
+
+ $parsed = str_replace("\r", '', $parsed);
+ $expected = str_replace("\r", '', $expected);
+
+ $this->assertEqual($expected, $parsed);
+ }
+
+ public function testExecOnlyPowershellXML() {
+ $file = id(new TempFile());
+ Filesystem::writeFile($file, $this->getOnlyPowershellXML());
+
+ list($stdout, $stderr) = id(new ExecFuture('cat %s >&2', $file))
+ ->setPowershellXML(true)
+ ->resolvex();
+
+ $expected = $this->getOnlyPowershellExpected();
+
+ $stderr = str_replace("\r", '', $stderr);
+ $expected = str_replace("\r", '', $expected);
+
+ $this->assertEqual($expected, $stderr);
+ }
+
+ public function testExecIntermixedPowershellXML() {
+ $file = id(new TempFile());
+ Filesystem::writeFile($file, $this->getIntermixedPowershellXML());
+
+ list($stdout, $stderr) = id(new ExecFuture('cat %s >&2', $file))
+ ->setPowershellXML(true)
+ ->resolvex();
+
+ $expected = $this->getIntermixedPowershellExpected();
+
+ $stderr = str_replace("\r", '', $stderr);
+ $expected = str_replace("\r", '', $expected);
+
+ $this->assertEqual($expected, $stderr);
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Aug 4, 1:15 PM (3 w, 4 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
8490037
Default Alt Text
D10363.id.diff (12 KB)
Attached To
Mode
D10363: Parse Powershell output on standard error
Attached
Detach File
Event Timeline
Log In to Comment