diff --git a/src/main/java/org/jenkinsci/plugins/durabletask/BourneShellScript.java b/src/main/java/org/jenkinsci/plugins/durabletask/BourneShellScript.java index 47915215..0031a340 100644 --- a/src/main/java/org/jenkinsci/plugins/durabletask/BourneShellScript.java +++ b/src/main/java/org/jenkinsci/plugins/durabletask/BourneShellScript.java @@ -34,6 +34,7 @@ import hudson.model.TaskListener; import hudson.tasks.Shell; import java.io.IOException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -42,10 +43,18 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import java.io.File; import javax.annotation.Nonnull; import jenkins.model.Jenkins; import jenkins.security.MasterToSlaveCallable; +import hudson.remoting.VirtualChannel; import org.kohsuke.stapler.DataBoundConstructor; +import javax.annotation.CheckForNull; +import jenkins.MasterToSlaveFileCallable; +import org.apache.commons.io.FileUtils; +import com.google.common.io.Files; + + /** * Runs a Bourne shell script on a Unix node using {@code nohup}. @@ -54,7 +63,9 @@ public final class BourneShellScript extends FileMonitoringTask { private static final Logger LOGGER = Logger.getLogger(BourneShellScript.class.getName()); - private static enum OsType {DARWIN, UNIX, WINDOWS} + private static enum OsType {DARWIN, UNIX, WINDOWS, ZOS} + + private static final String SYSTEM_DEFAULT_CHARSET = "SYSTEM_DEFAULT"; /** Number of times we will show launch diagnostics in a newly encountered workspace before going mute to save resources. */ @SuppressWarnings("FieldMayBeFinal") @@ -106,12 +117,21 @@ public String getScript() { if (script.isEmpty()) { listener.getLogger().println("Warning: was asked to run an empty script"); } - - ShellController c = new ShellController(ws); - + OsType os = ws.act(new getOsType()); + String scriptEncodingCharset = "UTF-8"; + if(os == OsType.ZOS) { + Charset zOSSystemEncodingCharset = Charset.forName(ws.act(new getIBMzOsEncoding())); + if(SYSTEM_DEFAULT_CHARSET.equals(getCharset())) { + // Setting default charset to IBM z/OS default EBCDIC charset on z/OS if no encoding specified on sh step + charset(zOSSystemEncodingCharset); + } + scriptEncodingCharset = zOSSystemEncodingCharset.name(); + } + + ShellController c = new ShellController(ws,(os == OsType.ZOS)); FilePath shf = c.getScriptFile(ws); - shf.write(script, "UTF-8"); + shf.write(script, scriptEncodingCharset); final Jenkins jenkins = Jenkins.getInstance(); String interpreter = ""; @@ -125,8 +145,6 @@ public String getScript() { String scriptPath = shf.getRemote(); List args = new ArrayList<>(); - OsType os = ws.act(new getOsType()); - if (os != OsType.DARWIN) { // JENKINS-25848 args.add("nohup"); } @@ -196,8 +214,12 @@ public String getScript() { /** Last-observed modification time of {@link getLogFile} on remote computer, in milliseconds. */ private transient long checkedTimestamp; - private ShellController(FilePath ws) throws IOException, InterruptedException { + /** Caching zOS flag to avoid round trip calls in exitStatus() */ + private final boolean isZos; + + private ShellController(FilePath ws, boolean zOsFlag) throws IOException, InterruptedException { super(ws); + this.isZos = zOsFlag; } public FilePath getScriptFile(FilePath ws) throws IOException, InterruptedException { @@ -210,7 +232,15 @@ private FilePath pidFile(FilePath ws) throws IOException, InterruptedException { } @Override protected Integer exitStatus(FilePath workspace, TaskListener listener) throws IOException, InterruptedException { - Integer status = super.exitStatus(workspace, listener); + Integer status; + if(isZos) { + // We need to transcode status file from EBCDIC only on z/OS platform + FilePath statusFile = getResultFile(workspace); + status = statusFile.act(new StatusCheckWithEncoding(getCharset())); + } + else { + status = super.exitStatus(workspace, listener); + } if (status != null) { LOGGER.log(Level.FINE, "found exit code {0} in {1}", new Object[] {status, controlDir}); return status; @@ -268,10 +298,50 @@ private static final class getOsType extends MasterToSlaveCallable { + @Override public String call() throws RuntimeException { + // Not null on z/OS systems + return System.getProperty("ibm.system.encoding"); + } + private static final long serialVersionUID = 1L; + } + + /* Local copy of StatusCheck to run on z/OS */ + static class StatusCheckWithEncoding extends MasterToSlaveFileCallable { + private final String charset; + StatusCheckWithEncoding(String charset) { + this.charset = charset; + } + @Override + @CheckForNull + public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + if (f.exists() && f.length() > 0) { + try { + String fileString = Files.readFirstLine(f, Charset.forName(charset)); + if (fileString == null || fileString.isEmpty()) { + return null; + } else { + fileString = fileString.trim(); + if (fileString.isEmpty()) { + return null; + } else { + return Integer.parseInt(fileString); + } + } + } catch (NumberFormatException x) { + throw new IOException("corrupted content in " + f + ": " + x, x); + } + } + return null; + } + } } diff --git a/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java b/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java index dd88a3ef..8e2af8cb 100644 --- a/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java +++ b/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java @@ -114,6 +114,11 @@ protected FileMonitoringController launchWithCookie(FilePath workspace, Launcher charset = SYSTEM_DEFAULT_CHARSET; } + @CheckForNull final String getCharset() + { + return charset; + } + /** * Should start a process which sends output to {@linkplain FileMonitoringController#getLogFile(FilePath) log file} * in the workspace and finally writes its exit code to {@linkplain FileMonitoringController#getResultFile(FilePath) result file}. @@ -161,6 +166,10 @@ protected static class FileMonitoringController extends Controller { // TODO imp /** @see FileMonitoringTask#charset */ private @CheckForNull String charset; + String getCharset() { + return charset; + } + /** * {@link #transcodingCharset} on the remote side when using {@link #writeLog}. * May be a wrapper for null; initialized on demand.