Skip to content

Commit

Permalink
Merge pull request #51 from gabloe/master
Browse files Browse the repository at this point in the history
Fixed JENKINS-46876, JENKINS-46508, JENKINS-46496, JENKINS-48057, and JENKINS-47797
  • Loading branch information
svanoort committed Jan 29, 2018
2 parents 7c12b3a + ceb03d0 commit 4a06f3c
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 102 deletions.
112 changes: 43 additions & 69 deletions src/main/java/org/jenkinsci/plugins/durabletask/PowershellScript.java
Expand Up @@ -69,85 +69,59 @@ public String getScript() {

String cmd;
if (capturingOutput) {
cmd = String.format(". \"%s\"; Execute-AndWriteOutput -MainScript \"%s\" -OutputFile \"%s\" -LogFile \"%s\" -ResultFile \"%s\" -CaptureOutput;",
cmd = String.format(". '%s'; Execute-AndWriteOutput -MainScript '%s' -OutputFile '%s' -LogFile '%s' -ResultFile '%s' -CaptureOutput;",
quote(c.getPowerShellHelperFile(ws)),
quote(c.getPowerShellWrapperFile(ws)),
quote(c.getOutputFile(ws)),
quote(c.getLogFile(ws)),
quote(c.getResultFile(ws)));
} else {
cmd = String.format(". \"%s\"; Execute-AndWriteOutput -MainScript \"%s\" -LogFile \"%s\" -ResultFile \"%s\";",
cmd = String.format(". '%s'; Execute-AndWriteOutput -MainScript '%s' -LogFile '%s' -ResultFile '%s';",
quote(c.getPowerShellHelperFile(ws)),
quote(c.getPowerShellWrapperFile(ws)),
quote(c.getLogFile(ws)),
quote(c.getResultFile(ws)));
}

// By default PowerShell adds a byte order mark (BOM) to the beginning of a file when using Out-File with a unicode encoding such as UTF8.
// This causes the Jenkins output to contain bogus characters because Java does not handle the BOM characters by default.
// This code mimics Out-File, but does not write a BOM. Hopefully PowerShell will provide a non-BOM option for writing files in future releases.
String helperScript = "Function Out-FileNoBom {\n" +
"[CmdletBinding()]\n" +
"param(\n" +
" [Parameter(Mandatory=$true, Position=0)] [string] $FilePath,\n" +
" [Parameter(ValueFromPipeline=$true)] $InputObject\n" +
")\n" +
" $out = New-Object IO.StreamWriter $FilePath, $false\n" +
" try {\n" +
" $Input | Out-String -Stream | % { $out.WriteLine($_) }\n" +
" } finally {\n" +
" $out.Dispose()\n" +
" }\n" +
"}\n" +
"Function Execute-AndWriteOutput {\n" +
"[CmdletBinding()]\n" +
"param(\n" +
" [Parameter(Mandatory=$true)] [string]$MainScript,\n" +
" [Parameter(Mandatory=$false)] [string]$OutputFile,\n" +
" [Parameter(Mandatory=$true)] [string]$LogFile,\n" +
" [Parameter(Mandatory=$true)] [string]$ResultFile,\n" +
" [Parameter(Mandatory=$false)] [switch]$CaptureOutput\n" +
")\n" +
" if ($CaptureOutput -eq $true) {\n" +
" if ($PSVersionTable.PSVersion.Major -ge 5) {\n" +
" $(& $MainScript | Out-FileNoBom -FilePath $OutputFile) 2>&1 3>&1 4>&1 5>&1 6>&1 | Out-FileNoBom -FilePath $LogFile; $LastExitCode | Out-File -FilePath $ResultFile -Encoding ASCII;\n" +
" } else {\n" +
" $(& $MainScript | Out-FileNoBom -FilePath $OutputFile) 2>&1 3>&1 4>&1 5>&1 | Out-FileNoBom -FilePath $LogFile; $LastExitCode | Out-File -FilePath $ResultFile -Encoding ASCII;\n" +
" }\n" +
" } else {\n" +
" & $MainScript *>&1 | Out-FileNoBom -FilePath $LogFile; $LastExitCode | Out-File -FilePath $ResultFile -Encoding ASCII;\n" +
" }\n" +
"}";

// Note: PowerShell core is now named pwsh. Workaround this issue on *nix systems by creating a symlink that maps 'powershell' to 'pwsh'.
String powershellBinary = "powershell";
String powershellArgs;
if (launcher.isUnix()) {
powershellArgs = "-NoProfile -NonInteractive";
} else {
powershellArgs = "-NoProfile -NonInteractive -ExecutionPolicy Bypass";
}
args.add(powershellBinary);
args.addAll(Arrays.asList(powershellArgs.split(" ")));
args.addAll(Arrays.asList("-Command", cmd));

// Exception propagation does not occur in legacy PowerShell versions. This means that an exception thrown in an inner PowerShell process
// does not propagate to the outer process regardless of $ErrorActionPreference selection. This ensures the exit code is non-zero if an error occurs regardless of PowerShell version.
String scriptWrapper = String.format("[CmdletBinding()]\r\n" +
"param()\r\n" +
"& %s %s -File '%s';\r\n" +
"if ($Error) {\r\n" +
" exit 1;\r\n" +
"} else {\r\n" +
" exit $LASTEXITCODE;\r\n" +
"}", powershellBinary, powershellArgs, quote(c.getPowerShellScriptFile(ws)));

// Add an explicit exit to the end of the script so that exit codes are propagated
String scriptWithExit = script + "\r\nexit $LASTEXITCODE;";

// Copy the helper script from the resources directory into the workspace
c.getPowerShellHelperFile(ws).copyFrom(getClass().getResource("powershellHelper.ps1"));

// Execute the script, and catch any errors in order to properly set the jenkins build status. $LastExitCode cannot be solely responsible for determining build status because
// there are several instances in which it is not set, e.g. thrown exceptions, and errors that aren't caused by native executables.
String wrapperScriptContent = "try {\r\n" +
" & '" + quote(c.getPowerShellScriptFile(ws)) + "'\r\n" +
"} catch {\r\n" +
" Write-Error $_;" +
" exit 1;\r\n" +
"} finally {\r\n" +
" if ($LastExitCode -ne $null) {\r\n" +
" exit $LastExitCode;\r\n" +
" } elseif ($error.Count -gt 0 -or !$?) {\r\n" +
" exit 1;\r\n" +
" } else {\r\n" +
" exit 0;\r\n" +
" }\r\n" +
"}";

// Write the PowerShell scripts out with a UTF8 BOM
writeWithBom(c.getPowerShellHelperFile(ws), helperScript);
writeWithBom(c.getPowerShellScriptFile(ws), script);
writeWithBom(c.getPowerShellWrapperFile(ws), wrapperScriptContent);

if (launcher.isUnix()) {
// Open-Powershell does not support ExecutionPolicy
args.addAll(Arrays.asList("powershell", "-NonInteractive", "-Command", cmd));
// There is no need to add a BOM with Open PowerShell
c.getPowerShellScriptFile(ws).write(scriptWithExit, "UTF-8");
c.getPowerShellWrapperFile(ws).write(scriptWrapper, "UTF-8");
} else {
args.addAll(Arrays.asList("powershell.exe", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", cmd));
// Write the Windows PowerShell scripts out with a UTF8 BOM
writeWithBom(c.getPowerShellScriptFile(ws), scriptWithExit);
writeWithBom(c.getPowerShellWrapperFile(ws), scriptWrapper);
}

Launcher.ProcStarter ps = launcher.launch().cmds(args).envs(escape(envVars)).pwd(ws).quiet(true);
listener.getLogger().println("[" + ws.getRemote().replaceFirst("^.+(\\\\|/)", "") + "] Running PowerShell script");
ps.readStdout().readStderr();
Expand All @@ -157,7 +131,7 @@ public String getScript() {
}

private static String quote(FilePath f) {
return f.getRemote().replace("$", "`$");
return f.getRemote().replace("'", "''");
}

// In order for PowerShell to properly read a script that contains unicode characters the script should have a BOM, but there is no built in support for
Expand All @@ -179,13 +153,13 @@ public FilePath getPowerShellScriptFile(FilePath ws) throws IOException, Interru
return controlDir(ws).child("powershellScript.ps1");
}

public FilePath getPowerShellWrapperFile(FilePath ws) throws IOException, InterruptedException {
return controlDir(ws).child("powershellWrapper.ps1");
}

public FilePath getPowerShellHelperFile(FilePath ws) throws IOException, InterruptedException {
return controlDir(ws).child("powershellHelper.ps1");
}

public FilePath getPowerShellWrapperFile(FilePath ws) throws IOException, InterruptedException {
return controlDir(ws).child("powershellWrapper.ps1");
}

private static final long serialVersionUID = 1L;
}
Expand Down
@@ -0,0 +1,85 @@
# By default PowerShell adds a byte order mark (BOM) to the beginning of a file when using Out-File with a unicode encoding such as UTF8.
# This causes the Jenkins output to contain bogus characters because Java does not handle the BOM characters by default.
# This code mimics Out-File, but does not write a BOM. Hopefully PowerShell will provide a non-BOM option for writing files in future releases.

function New-StreamWriter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)] [string] $FilePath,
[Parameter(Mandatory=$true)] [System.Text.Encoding] $Encoding
)
[string]$FullFilePath = [IO.Path]::GetFullPath( $FilePath );
[System.IO.StreamWriter]$writer = New-Object System.IO.StreamWriter( $FullFilePath, $true, $Encoding );
$writer.AutoFlush = $true;
return $writer;
}

function Out-FileNoBom {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)][AllowNull()] [System.IO.StreamWriter] $Writer,
[Parameter(ValueFromPipeline = $true)] [object] $InputObject
)
Process {
if ($Writer) {
$Input | Out-String -Stream -Width 192 | ForEach-Object { $Writer.WriteLine( $_ ); }
} else {
$Input;
}
}
}

function Execute-AndWriteOutput {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)] [string]$MainScript,
[Parameter(Mandatory=$false)] [string]$OutputFile,
[Parameter(Mandatory=$true)] [string]$LogFile,
[Parameter(Mandatory=$true)] [string]$ResultFile,
[Parameter(Mandatory=$false)] [switch]$CaptureOutput
)
try {
$errorCaught = $null;
[System.Text.Encoding] $encoding = New-Object System.Text.UTF8Encoding( $false );
[System.Console]::OutputEncoding = [System.Console]::InputEncoding = $encoding;
[System.IO.Directory]::SetCurrentDirectory( $PWD );
$null = New-Item $LogFile -ItemType File -Force;
[System.IO.StreamWriter] $LogWriter = New-StreamWriter -FilePath $LogFile -Encoding $encoding;
$OutputWriter = $null;
if ($CaptureOutput -eq $true) {
$null = New-Item $OutputFile -ItemType File -Force;
[System.IO.StreamWriter]$OutputWriter = New-StreamWriter -FilePath $OutputFile -Encoding $encoding;
}
& { & $MainScript | Out-FileNoBom -Writer $OutputWriter } *>&1 | Out-FileNoBom -Writer $LogWriter;
} catch {
$errorCaught = $_;
$errorCaught | Out-String -Width 192 | Out-FileNoBom -Writer $LogWriter;
} finally {
$exitCode = 0;
if ($LASTEXITCODE -ne $null) {
if ($LASTEXITCODE -eq 0 -and $errorCaught -ne $null) {
$exitCode = 1;
} else {
$exitCode = $LASTEXITCODE;
}
} elseif ($errorCaught -ne $null) {
$exitCode = 1;
}
$exitCode | Out-File -FilePath $ResultFile -Encoding ASCII;
if ($CaptureOutput -eq $true -and !(Test-Path $OutputFile)) {
$null = New-Item $OutputFile -ItemType File -Force;
}
if (!(Test-Path $LogFile)) {
$null = New-Item $LogFile -ItemType File -Force;
}
if ($CaptureOutput -eq $true -and $OutputWriter -ne $null) {
$OutputWriter.Flush();
$OutputWriter.Dispose();
}
if ($LogWriter -ne $null) {
$LogWriter.Flush();
$LogWriter.Dispose();
}
exit $exitCode;
}
}
Expand Up @@ -56,7 +56,8 @@ public class PowershellScriptTest {
private StreamTaskListener listener;
private FilePath ws;
private Launcher launcher;

private int psVersion;

@Before public void vars() throws IOException, InterruptedException {
listener = StreamTaskListener.fromStdout();
ws = j.jenkins.getRootPath().child("ws");
Expand All @@ -65,10 +66,13 @@ public class PowershellScriptTest {
String pathSeparator = properties.getProperty("path.separator");
String[] paths = System.getenv("PATH").split(pathSeparator);
boolean powershellExists = false;
String cmd = launcher.isUnix()?"powershell":"powershell.exe";
// Note: This prevents this set of tests from running on PowerShell core unless a symlink is created that maps 'powershell' to 'pwsh' on *nix systems
String cmd = "powershell";
for (String p : paths) {
File f = new File(p, cmd);
if (f.exists()) {
// If running on *nix then the binary does not have an extension. Check for both variants to ensure *nix and windows+cygwin are both supported.
File withoutExtension = new File(p, cmd);
File withExtension = new File(p, cmd + ".exe");
if (withoutExtension.exists() || withExtension.exists()) {
powershellExists = true;
break;
}
Expand All @@ -81,12 +85,11 @@ public class PowershellScriptTest {
if (!launcher.isUnix()) {
args.addAll(Arrays.asList("-ExecutionPolicy", "Bypass"));
}
args.add("$PSVersionTable.PSVersion.Major");
args.addAll(Arrays.asList("-Command", "& {Write-Output $PSVersionTable.PSVersion.Major}"));
Launcher.ProcStarter ps = launcher.launch().cmds(args).quiet(true);
ps.readStdout();
Proc proc = ps.start();
String psVersionStr = IOUtils.toString(proc.getStdout());
int psVersion;
try {
psVersion = Integer.parseInt(psVersionStr.trim());
} catch (NumberFormatException x) {
Expand All @@ -98,44 +101,46 @@ public class PowershellScriptTest {

@Test public void explicitExit() throws Exception {
Controller c = new PowershellScript("Write-Output \"Hello, World!\"; exit 1;").launch(new EnvVars(), ws, launcher, listener);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
TeeOutputStream tos = new TeeOutputStream(baos, System.err);
while (c.exitStatus(ws, launcher, listener) == null) {
c.writeLog(ws, tos);
Thread.sleep(100);
}
c.writeLog(ws, tos);
assertEquals(Integer.valueOf(1), c.exitStatus(ws, launcher, listener));
String log = baos.toString();
assertTrue(log, log.contains("Hello, World!"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
c.writeLog(ws, baos);
assertEquals(Integer.valueOf(1), c.exitStatus(ws, launcher));
assertThat(baos.toString(), containsString("Hello, World!"));
c.cleanup(ws);
}

@Test public void implicitExit() throws Exception {
Controller c = new PowershellScript("Write-Output \"Success!\";").launch(new EnvVars(), ws, launcher, listener);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
TeeOutputStream tos = new TeeOutputStream(baos, System.err);
while (c.exitStatus(ws, launcher, listener) == null) {
c.writeLog(ws, tos);
Thread.sleep(100);
}
c.writeLog(ws, tos);
assertEquals(Integer.valueOf(0), c.exitStatus(ws, launcher, listener));
String log = baos.toString();
assertTrue(log, log.contains("Success!"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
c.writeLog(ws, baos);
assertEquals(Integer.valueOf(0), c.exitStatus(ws, launcher));
assertThat(baos.toString(), containsString("Success!"));
c.cleanup(ws);
}

@Test public void implicitError() throws Exception {
Controller c = new PowershellScript("MyBogus-Cmdlet").launch(new EnvVars(), ws, launcher, listener);
while (c.exitStatus(ws, launcher, listener) == null) {
Thread.sleep(100);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
TeeOutputStream tos = new TeeOutputStream(baos, System.err);
c.writeLog(ws, baos);
assertTrue(c.exitStatus(ws, launcher).intValue() != 0);
assertThat(baos.toString(), containsString("MyBogus-Cmdlet"));
c.cleanup(ws);
}

@Test public void implicitErrorNegativeTest() throws Exception {
Controller c = new PowershellScript("$ErrorActionPreference = 'SilentlyContinue'; MyBogus-Cmdlet").launch(new EnvVars(), ws, launcher, listener);
while (c.exitStatus(ws, launcher, listener) == null) {
c.writeLog(ws, tos);
Thread.sleep(100);
}
c.writeLog(ws, tos);
assertTrue(c.exitStatus(ws, launcher, listener).intValue() != 0);
assertTrue(c.exitStatus(ws, launcher).intValue() == 0);
c.cleanup(ws);
}

Expand All @@ -150,6 +155,7 @@ public class PowershellScriptTest {
c.writeLog(ws, baos);
assertTrue(c.exitStatus(ws, launcher, listener).intValue() != 0);
assertThat(baos.toString(), containsString("explicit error"));
assertEquals("Hello, World!\r\n", new String(c.getOutput(ws, launcher)));
c.cleanup(ws);
}

Expand All @@ -160,10 +166,18 @@ public class PowershellScriptTest {
while (c.exitStatus(ws, launcher, listener) == null) {
Thread.sleep(100);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
c.writeLog(ws, baos);
assertEquals(0, c.exitStatus(ws, launcher, listener).intValue());
assertThat(baos.toString(), containsString("Hello, World!"));
assertEquals(0, c.exitStatus(ws, launcher).intValue());
assertEquals("VERBOSE: Hello, World!\r\n", new String(c.getOutput(ws, launcher)));
c.cleanup(ws);
}

@Test public void spacesInWorkspace() throws Exception {
final FilePath newWs = new FilePath(ws, "subdirectory with spaces");
Controller c = new PowershellScript("Write-Host 'Running in a workspace with spaces in the path'").launch(new EnvVars(), newWs, launcher, listener);
while (c.exitStatus(newWs, launcher, listener) == null) {
Thread.sleep(100);
}
assertEquals(0, c.exitStatus(newWs, launcher).intValue());
c.cleanup(ws);
}

Expand All @@ -181,14 +195,12 @@ public class PowershellScriptTest {

@Test public void unicodeChars() throws Exception {
Controller c = new PowershellScript("Write-Output \"Helló, Wõrld ®\";").launch(new EnvVars(), ws, launcher, listener);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
TeeOutputStream tos = new TeeOutputStream(baos, System.err);
while (c.exitStatus(ws, launcher, listener) == null) {
c.writeLog(ws, tos);
Thread.sleep(100);
}
c.writeLog(ws, tos);
assertEquals(Integer.valueOf(0), c.exitStatus(ws, launcher, listener));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
c.writeLog(ws, baos);
assertEquals(Integer.valueOf(0), c.exitStatus(ws, launcher));
String log = baos.toString("UTF-8");
assertTrue(log, log.contains("Helló, Wõrld ®"));
c.cleanup(ws);
Expand Down

0 comments on commit 4a06f3c

Please sign in to comment.