diff --git a/.gitignore b/.gitignore index bfabc21..20f3cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ work .idea +/target/ \ No newline at end of file diff --git a/pom.xml b/pom.xml index aa65ca5..f447f68 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.plugins plugin - 2.10 + 3.25 org.jenkinsci.plugins @@ -18,7 +18,8 @@ UTF-8 true - 1.642.3 + 2.7.3 + 8 https://wiki.jenkins-ci.org/display/JENKINS/Managed+Script+Plugin @@ -50,7 +51,22 @@ org.jenkins-ci.plugins token-macro - 1.12.1 + 2.0 + + + org.jenkins-ci.plugins + structs + 1.14 + + + org.jenkins-ci.plugins + durable-task + 1.25 + + + org.jenkins-ci.plugins.workflow + workflow-durable-task-step + 2.16 @@ -80,11 +96,10 @@ - - org.jenkins-ci.tools - maven-hpi-plugin - 1.106 - true + + org.jenkins-ci.tools + maven-hpi-plugin + true maven-release-plugin diff --git a/src/main/java/org/jenkinsci/plugins/managedscripts/ManagedBatchScript.java b/src/main/java/org/jenkinsci/plugins/managedscripts/ManagedBatchScript.java new file mode 100644 index 0000000..da98044 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/managedscripts/ManagedBatchScript.java @@ -0,0 +1,160 @@ +/* + * The MIT License + * + * Copyright 2014 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.jenkinsci.plugins.managedscripts; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.EnvVars; +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.TaskListener; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.model.Jenkins; +import org.jenkinsci.lib.configprovider.model.Config; +import org.jenkinsci.plugins.configfiles.ConfigFiles; +import org.jenkinsci.plugins.durabletask.DurableTaskDescriptor; +import org.jenkinsci.plugins.durabletask.FileMonitoringTask; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Runs a Windows batch script. + */ +public final class ManagedBatchScript extends FileMonitoringTask { + + private static final Logger LOGGER = Logger.getLogger(ManagedBatchScriptStep.class.getName()); + private final String script; + private boolean capturingOutput; + private final String[] buildStepArgs; + + @DataBoundConstructor + public ManagedBatchScript(String script, String[] buildStepArgs) { + this.script = script; + this.buildStepArgs = buildStepArgs.clone(); + } + + public String getScript() { + return script; + } + + @Override + public void captureOutput() { + capturingOutput = true; + } + + @SuppressFBWarnings(value = "VA_FORMAT_STRING_USES_NEWLINE", justification = "%n from master might be \\n") + @Override + protected FileMonitoringController doLaunch(FilePath ws, Launcher launcher, TaskListener listener, EnvVars envVars) throws IOException, InterruptedException { + if (launcher.isUnix()) { + throw new IOException("Batch scripts can only be run on Windows nodes"); + } + BatchController c = new BatchController(ws); + + String cmd; + if (capturingOutput) { + cmd = String.format("@echo off \r\ncmd /c \"\"%s\"\" > \"%s\" 2> \"%s\"\r\necho %%ERRORLEVEL%% > \"%s\"\r\n", + quote(c.getBatchFile2(ws)), + quote(c.getOutputFile(ws)), + quote(c.getLogFile(ws)), + quote(c.getResultFile(ws))); + } else { + cmd = String.format("@echo off \r\ncmd /c \"\"%s\"\" > \"%s\" 2>&1\r\necho %%ERRORLEVEL%% > \"%s\"\r\n", + quote(c.getBatchFile2(ws)), + quote(c.getLogFile(ws)), + quote(c.getResultFile(ws))); + } + c.getBatchFile1(ws).write(cmd, "UTF-8"); + c.getBatchFile2(ws).write(getMangedScriptCommandLine(ws), "UTF-8"); + + Launcher.ProcStarter ps = launcher.launch().cmds("cmd", "/c", "\"\"" + c.getBatchFile1(ws) + "\"\"").envs(escape(envVars)).pwd(ws).quiet(true); + listener.getLogger().println("[" + ws.getRemote().replaceFirst("^.+\\\\", "") + "] Running batch script"); // details printed by cmd + ps.readStdout().readStderr(); // TODO see BourneShellScript + ps.start(); + return c; + } + + private String getMangedScriptCommandLine(FilePath ws) { + String modifiedScript; + Config buildStepConfig = ConfigFiles.getByIdOrNull(Jenkins.getInstance(), script); + if (buildStepConfig == null) { + throw new IllegalStateException(org.jenkinsci.plugins.managedscripts.Messages.config_does_not_exist(script)); + } + modifiedScript = buildStepConfig.content + "\r\nexit %ERRORLEVEL%"; + try { + FilePath scriptPath = ws.createTextTempFile("managedbatch", ".bat", modifiedScript); + return buildCommandLine(scriptPath); + } catch (IOException | InterruptedException ex) { + LOGGER.log(Level.SEVERE, "Error creating tmp file"); + throw new RuntimeException(ex); + } + } + + private String buildCommandLine(FilePath script) { + StringBuilder commandline = new StringBuilder("cmd /c call "); + commandline.append(script.getRemote()); + + // Add additional parameters set by user + if (buildStepArgs != null) { + for (String arg : buildStepArgs) { + commandline.append(" "); + commandline.append(arg); + } + } + + return commandline.toString(); + } + + private static String quote(FilePath f) { + return f.getRemote().replace("%", "%%"); + } + + private static final class BatchController extends FileMonitoringController { + + private BatchController(FilePath ws) throws IOException, InterruptedException { + super(ws); + } + + public FilePath getBatchFile1(FilePath ws) throws IOException, InterruptedException { + return controlDir(ws).child("jenkins-wrap.bat"); + } + + public FilePath getBatchFile2(FilePath ws) throws IOException, InterruptedException { + return controlDir(ws).child("jenkins-main.bat"); + } + + private static final long serialVersionUID = 1L; + } + + @Extension + public static final class DescriptorImpl extends DurableTaskDescriptor { + + @Override + public String getDisplayName() { + return "Managed Windows batch"; + } + + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/managedscripts/ManagedBatchScriptStep.java b/src/main/java/org/jenkinsci/plugins/managedscripts/ManagedBatchScriptStep.java new file mode 100644 index 0000000..ae3f54b --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/managedscripts/ManagedBatchScriptStep.java @@ -0,0 +1,213 @@ +package org.jenkinsci.plugins.managedscripts; + +import hudson.Extension; +import hudson.model.Item; +import hudson.model.ItemGroup; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import org.jenkinsci.Symbol; +import org.jenkinsci.lib.configprovider.model.Config; +import org.jenkinsci.plugins.configfiles.ConfigFiles; +import org.jenkinsci.plugins.durabletask.DurableTask; +import org.jenkinsci.plugins.managedscripts.WinBatchConfig.Arg; +import org.jenkinsci.plugins.workflow.steps.durable_task.DurableTaskStep; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; + +/** + * A project that uses this builder can choose a build step from a list of + * predefined windows batch files that are used as command line scripts. + *

+ * + * @author Michael DK Fowler + */ +public class ManagedBatchScriptStep extends DurableTaskStep { + + private final String scriptId; + private String[] buildStepArgs; + private ScriptBuildStepArgs scriptBuildStepArgs; + + public static class ArgValue implements Serializable { + + public final String arg; + + @DataBoundConstructor + public ArgValue(String arg) { + this.arg = arg; + } + } + + public static class ScriptBuildStepArgs { + + public final boolean defineArgs; + public final ArgValue[] buildStepArgs; + + @DataBoundConstructor + public ScriptBuildStepArgs(boolean defineArgs, ArgValue[] buildStepArgs) { + this.defineArgs = defineArgs; + this.buildStepArgs = buildStepArgs == null ? new ArgValue[0] : Arrays.copyOf(buildStepArgs, buildStepArgs.length); + } + } + + /** + * The constructor used at form submission + * + * @param buildStepId the Id of the config file + * @param scriptBuildStepArgs whether to save the args and arg values (the + * boolean is required because of html form submission, which also sends + * hidden values) + */ + @DataBoundConstructor + public ManagedBatchScriptStep(String buildStepId) { + if (buildStepId == null) { + throw new IllegalArgumentException(); + } + this.scriptId = buildStepId; + } + + public ManagedBatchScriptStep(WinBatchBuildStep step) { + if (step == null) { + throw new IllegalArgumentException(); + } + this.scriptId = step.getBuildStepId(); + this.buildStepArgs = step.getBuildStepArgs(); + if (this.buildStepArgs != null && this.buildStepArgs.length > 0) { + ArgValue[] args = new ArgValue[buildStepArgs.length]; + for (int c = 0; c < buildStepArgs.length; c++) { + args[c] = new ArgValue(buildStepArgs[c]); + } + this.scriptBuildStepArgs = new ScriptBuildStepArgs(true, args); + } + + } + + public String getBuildStepId() { + return scriptId; + } + + public String[] getBuildStepArgs() { + String[] args = buildStepArgs == null ? new String[0] : buildStepArgs; + return Arrays.copyOf(args, args.length); + } + + public ScriptBuildStepArgs getScriptBuildStepArgs() { + return scriptBuildStepArgs; + } + + @DataBoundSetter + public void setScriptBuildStepArgs(ScriptBuildStepArgs scriptBuildStepArgs) { + this.scriptBuildStepArgs = scriptBuildStepArgs; + List l = null; + if (scriptBuildStepArgs != null && scriptBuildStepArgs.defineArgs + && scriptBuildStepArgs.buildStepArgs != null) { + l = new ArrayList<>(); + for (ArgValue arg : scriptBuildStepArgs.buildStepArgs) { + l.add(arg.arg); + } + } + this.buildStepArgs = l == null ? null : l.toArray(new String[l.size()]); + } + + // Overridden for better type safety. + @Override + protected DurableTask task() { + return new ManagedBatchScript(scriptId, buildStepArgs); + } + + /** + * Descriptor for {@link ManagedBatchScriptStep}. + */ + @Symbol("managedbat") + @Extension + public static final class DescriptorImpl extends DurableTaskStepDescriptor { + + @Override + public String getFunctionName() { + return "managedbat"; + } + + /** + * This human readable name is used in the configuration screen. + */ + @Override + public String getDisplayName() { + return "Managed Windows Batch Script"; + } + + /** + * Return all batch files (templates) that the user can choose from when + * creating a build step. Ordered by name. + * + * @return A collection of batch files of type {@link WinBatchConfig}. + */ + public ListBoxModel doFillBuildStepIdItems(@AncestorInPath ItemGroup context) { + List configsInContext = ConfigFiles.getConfigsInContext(context, WinBatchConfig.WinBatchConfigProvider.class); + Collections.sort(configsInContext, new Comparator() { + @Override + public int compare(Config o1, Config o2) { + return o1.name.compareTo(o2.name); + } + }); + ListBoxModel items = new ListBoxModel(); + items.add("please select", ""); + for (Config config : configsInContext) { + items.add(config.name, config.id); + } + return items; + } + } + + /** + * gets the argument description to be displayed on the screen when + * selecting a config in the dropdown + * + * @param configId the config id to get the arguments description for + * @return the description + */ + private String getArgsDescription(@AncestorInPath Item context, String configId) { + final WinBatchConfig config = ConfigFiles.getByIdOrNull(context, configId); + if (config != null) { + if (config.args != null && !config.args.isEmpty()) { + StringBuilder sb = new StringBuilder("Required arguments: "); + int i = 1; + for (Iterator iterator = config.args.iterator(); iterator.hasNext(); i++) { + Arg arg = iterator.next(); + sb.append(i).append(". ").append(arg.name); + if (iterator.hasNext()) { + sb.append(" | "); + } + } + return sb.toString(); + } else { + return "No arguments required"; + } + } + return "please select a valid script!"; + } + + /** + * validate that an existing config was chosen + * + * @param buildStepId the buildStepId + * @return + */ + public HttpResponse doCheckBuildStepId(StaplerRequest req, @AncestorInPath Item context, @QueryParameter String buildStepId) { + final WinBatchConfig config = ConfigFiles.getByIdOrNull(context, buildStepId); + if (config != null) { + return DetailLinkDescription.getDescription(req, context, buildStepId, getArgsDescription(context, buildStepId)); + } else { + return FormValidation.error("you must select a valid batch file"); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/managedscripts/ManagedPowerShellScript.java b/src/main/java/org/jenkinsci/plugins/managedscripts/ManagedPowerShellScript.java new file mode 100644 index 0000000..4b615ed --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/managedscripts/ManagedPowerShellScript.java @@ -0,0 +1,180 @@ +/* + * The MIT License + * + * Copyright 2014 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.jenkinsci.plugins.managedscripts; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.EnvVars; +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.TaskListener; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import jenkins.model.Jenkins; +import org.jenkinsci.lib.configprovider.model.Config; +import org.jenkinsci.plugins.configfiles.ConfigFiles; +import org.jenkinsci.plugins.durabletask.DurableTaskDescriptor; +import org.jenkinsci.plugins.durabletask.FileMonitoringTask; +import org.jenkinsci.plugins.managedscripts.ManagedPowerShellScriptStep.ArgValue; +import org.jenkinsci.plugins.managedscripts.ManagedPowerShellScriptStep.ScriptBuildStepArgs; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Runs a Windows batch script. + */ +public final class ManagedPowerShellScript extends FileMonitoringTask { + + private final String script; + private final String[] buildStepArgs; + + @DataBoundConstructor + public ManagedPowerShellScript(String script, ScriptBuildStepArgs scriptBuildStepArgs) { + this.script = script; + List l = null; + if (scriptBuildStepArgs != null && scriptBuildStepArgs.defineArgs + && scriptBuildStepArgs.buildStepArgs != null) { + l = new ArrayList<>(); + for (ArgValue arg : scriptBuildStepArgs.buildStepArgs) { + l.add(arg.arg); + } + } + this.buildStepArgs = l == null ? null : l.toArray(new String[l.size()]); + } + + public String getScript() { + return script; + } + + public String[] getBuildStepArgs() { + String[] args = buildStepArgs == null ? new String[0] : buildStepArgs; + return Arrays.copyOf(args, args.length); + } + + @SuppressFBWarnings(value = "VA_FORMAT_STRING_USES_NEWLINE", justification = "%n from master might be \\n") + @Override + protected FileMonitoringController doLaunch(FilePath ws, Launcher launcher, TaskListener listener, EnvVars envVars) throws IOException, InterruptedException { + List args = new ArrayList<>(); + PowershellController c = new PowershellController(ws); + + String cmd; + 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))); + + // 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)); + // powershell.exe -ExecutionPolicy ByPass "& 'D:\JenkinsAgent\tmp\jenkins4006833840543187830.ps1'" g ggg + String scriptWrapper = String.format("[CmdletBinding()]\r\n" + + "param()\r\n" + + "& %s %s -Command \"& '%s'\";\r\n" + + "exit $LASTEXITCODE;", powershellBinary, powershellArgs, quote(c.getPowerShellScriptFile(ws))); + + // Add an explicit exit to the end of the script so that exit codes are propagated + Config buildStepConfig = ConfigFiles.getByIdOrNull(Jenkins.getInstance(), script); + if (buildStepConfig == null) { + throw new IllegalStateException(org.jenkinsci.plugins.managedscripts.Messages.config_does_not_exist(script)); + } + String scriptWithExit = buildStepConfig.content + "\r\nexit $LASTEXITCODE;"; + // Copy the helper script from the resources directory into the workspace + c.getPowerShellHelperFile(ws).copyFrom(getClass().getResource("powershellHelper.ps1")); + + if (launcher.isUnix()) { + // 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 { + // 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(); + ps.start(); + + return c; + } + + private static String quote(FilePath f) { + 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 + // writing UTF-8 with BOM in Java. This code writes a UTF-8 BOM before writing the file contents. + private static void writeWithBom(FilePath f, String contents) throws IOException, InterruptedException { + OutputStream out = f.write(); + out.write(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}); + out.write(contents.getBytes(Charset.forName("UTF-8"))); + out.flush(); + out.close(); + } + + private static final class PowershellController extends FileMonitoringController { + + private PowershellController(FilePath ws) throws IOException, InterruptedException { + super(ws); + } + + public FilePath getPowerShellScriptFile(FilePath ws) throws IOException, InterruptedException { + return controlDir(ws).child("powershellScript.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; + } + + @Extension + public static final class DescriptorImpl extends DurableTaskDescriptor { + + @Override + public String getDisplayName() { + return "Managed PowerShell script"; + } + + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/managedscripts/ManagedPowerShellScriptStep.java b/src/main/java/org/jenkinsci/plugins/managedscripts/ManagedPowerShellScriptStep.java new file mode 100644 index 0000000..3cfc7f1 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/managedscripts/ManagedPowerShellScriptStep.java @@ -0,0 +1,212 @@ +package org.jenkinsci.plugins.managedscripts; + +import hudson.Extension; +import hudson.model.Item; +import hudson.model.ItemGroup; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import org.jenkinsci.Symbol; +import org.jenkinsci.lib.configprovider.model.Config; +import org.jenkinsci.plugins.configfiles.ConfigFiles; +import org.jenkinsci.plugins.durabletask.DurableTask; +import org.jenkinsci.plugins.managedscripts.PowerShellConfig.Arg; +import org.jenkinsci.plugins.workflow.steps.durable_task.DurableTaskStep; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; + +/** + * A project that uses this builder can choose a build step from a list of + * predefined powershell files that are used as command line scripts. + *

+ * + * @author Michael DK Fowler + */ +public class ManagedPowerShellScriptStep extends DurableTaskStep { + + private final String scriptId; + private String[] buildStepArgs; + private ScriptBuildStepArgs scriptBuildStepArgs; + + public static class ArgValue implements Serializable { + + public final String arg; + + @DataBoundConstructor + public ArgValue(String arg) { + this.arg = arg; + } + } + + public static class ScriptBuildStepArgs { + + public final boolean defineArgs; + public final ArgValue[] buildStepArgs; + + @DataBoundConstructor + public ScriptBuildStepArgs(boolean defineArgs, ArgValue[] buildStepArgs) { + this.defineArgs = defineArgs; + this.buildStepArgs = buildStepArgs == null ? new ArgValue[0] : Arrays.copyOf(buildStepArgs, buildStepArgs.length); + } + } + + /** + * The constructor used at form submission + * + * @param buildStepId the Id of the config file + */ + @DataBoundConstructor + public ManagedPowerShellScriptStep(String buildStepId) { + if (buildStepId == null) { + throw new IllegalArgumentException(); + } + this.scriptId = buildStepId; + } + + public ManagedPowerShellScriptStep(PowerShellBuildStep step) { + if (step == null) { + throw new IllegalArgumentException(); + } + this.scriptId = step.getBuildStepId(); + this.buildStepArgs = step.getBuildStepArgs(); + if (this.buildStepArgs != null && this.buildStepArgs.length > 0) { + ArgValue[] args = new ArgValue[buildStepArgs.length]; + for (int c = 0; c < buildStepArgs.length; c++) { + args[c] = new ArgValue(buildStepArgs[c]); + } + this.scriptBuildStepArgs = new ScriptBuildStepArgs(true, args); + } + + } + + public String getBuildStepId() { + return scriptId; + } + + public String[] getBuildStepArgs() { + String[] args = buildStepArgs == null ? new String[0] : buildStepArgs; + return Arrays.copyOf(args, args.length); + } + + public ScriptBuildStepArgs getScriptBuildStepArgs() { + return scriptBuildStepArgs; + } + + @DataBoundSetter + public void setScriptBuildStepArgs(ScriptBuildStepArgs scriptBuildStepArgs) { + this.scriptBuildStepArgs = scriptBuildStepArgs; + List l = null; + if (scriptBuildStepArgs != null && scriptBuildStepArgs.defineArgs + && scriptBuildStepArgs.buildStepArgs != null) { + l = new ArrayList<>(); + for (ArgValue arg : scriptBuildStepArgs.buildStepArgs) { + l.add(arg.arg); + } + } + this.buildStepArgs = l == null ? null : l.toArray(new String[l.size()]); + } + + // Overridden for better type safety. + @Override + protected DurableTask task() { + return new ManagedPowerShellScript(scriptId, this.scriptBuildStepArgs); + } + + /** + * Descriptor for {@link ManagedPowerShellScriptStep}. + */ + @Symbol("managedbat") + @Extension + public static final class DescriptorImpl extends DurableTaskStepDescriptor { + + @Override + public String getFunctionName() { + return "managedpowershell"; + } + + /** + * This human readable name is used in the configuration screen. + */ + @Override + public String getDisplayName() { + return "Managed PowerShell Script"; + } + + /** + * gets the argument description to be displayed on the screen when + * selecting a config in the dropdown + * + * @param configId the config id to get the arguments description for + * @return the description + */ + private String getArgsDescription(@AncestorInPath Item context, String configId) { + final PowerShellConfig config = ConfigFiles.getByIdOrNull(context, configId); + if (config != null) { + if (config.args != null && !config.args.isEmpty()) { + StringBuilder sb = new StringBuilder("Required arguments: "); + int i = 1; + for (Iterator iterator = config.args.iterator(); iterator.hasNext(); i++) { + Arg arg = iterator.next(); + sb.append(i).append(". ").append(arg.name); + if (iterator.hasNext()) { + sb.append(" | "); + } + } + return sb.toString(); + } else { + return "No arguments required"; + } + } + return "please select a valid script!"; + } + + /** + * validate that an existing config was chosen + * + * @param buildStepId the buildStepId + * @return + */ + public HttpResponse doCheckBuildStepId(StaplerRequest req, @AncestorInPath Item context, @QueryParameter String buildStepId) { + final PowerShellConfig config = ConfigFiles.getByIdOrNull(context, buildStepId); + if (config != null) { + return DetailLinkDescription.getDescription(req, context, buildStepId, getArgsDescription(context, buildStepId)); + } else { + return FormValidation.error("you must select a valid powershell file"); + } + } + + /** + * Return all powershell files (templates) that the user can choose from + * when creating a build step. Ordered by name. + * + * @param context + * @return A collection of powershell files of type + * {@link PowerShellConfig}. + */ + public ListBoxModel doFillBuildStepIdItems(@AncestorInPath ItemGroup context) { + List configsInContext = ConfigFiles.getConfigsInContext(context, PowerShellConfig.PowerShellConfigProvider.class); + Collections.sort(configsInContext, new Comparator() { + @Override + public int compare(Config o1, Config o2) { + return o1.name.compareTo(o2.name); + } + }); + ListBoxModel items = new ListBoxModel(); + items.add("please select", ""); + for (Config config : configsInContext) { + items.add(config.name, config.id); + } + return items; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/managedscripts/PowerShellBuildStep.java b/src/main/java/org/jenkinsci/plugins/managedscripts/PowerShellBuildStep.java index 0b60de8..9d6bc31 100644 --- a/src/main/java/org/jenkinsci/plugins/managedscripts/PowerShellBuildStep.java +++ b/src/main/java/org/jenkinsci/plugins/managedscripts/PowerShellBuildStep.java @@ -58,7 +58,7 @@ public ScriptBuildStepArgs(boolean defineArgs, ArgValue[] buildStepArgs) { /** * The constructor used at form submission * - * @param buildStepId the Id of the config file + * @param buildStepId the Id of the config file * @param scriptBuildStepArgs whether to save the args and arg values (the boolean is required because of html form submission, which also sends hidden values) */ @DataBoundConstructor @@ -78,7 +78,7 @@ public PowerShellBuildStep(String buildStepId, ScriptBuildStepArgs scriptBuildSt /** * The constructor * - * @param buildStepId the Id of the config file + * @param buildStepId the Id of the config file * @param buildStepArgs list of arguments specified as buildStepargs */ public PowerShellBuildStep(String buildStepId, String[] buildStepArgs) { @@ -105,12 +105,10 @@ public String[] buildCommandLine(FilePath script) { // Add additional parameters set by user if (buildStepArgs != null) { - for (String arg : buildStepArgs) { - cml.add(arg); - } + cml.addAll(Arrays.asList(buildStepArgs)); } - return (String[]) cml.toArray(new String[cml.size()]); + return cml.toArray(new String[cml.size()]); } @Override @@ -222,6 +220,7 @@ public HttpResponse doCheckBuildStepId(StaplerRequest req, @AncestorInPath Item public ListBoxModel doFillBuildStepIdItems(@AncestorInPath ItemGroup context) { List configsInContext = ConfigFiles.getConfigsInContext(context, PowerShellConfig.PowerShellConfigProvider.class); Collections.sort(configsInContext, new Comparator() { + @Override public int compare(Config o1, Config o2) { return o1.name.compareTo(o2.name); } @@ -233,10 +232,5 @@ public int compare(Config o1, Config o2) { } return items; } - - private ConfigProvider getBuildStepConfigProvider() { - ExtensionList providers = ConfigProvider.all(); - return providers.get(PowerShellConfig.PowerShellConfigProvider.class); - } } } diff --git a/src/main/java/org/jenkinsci/plugins/managedscripts/WinBatchBuildStep.java b/src/main/java/org/jenkinsci/plugins/managedscripts/WinBatchBuildStep.java index 9c2c568..431eeb6 100644 --- a/src/main/java/org/jenkinsci/plugins/managedscripts/WinBatchBuildStep.java +++ b/src/main/java/org/jenkinsci/plugins/managedscripts/WinBatchBuildStep.java @@ -57,7 +57,7 @@ public ScriptBuildStepArgs(boolean defineArgs, ArgValue[] buildStepArgs) { /** * The constructor used at form submission * - * @param buildStepId the Id of the config file + * @param buildStepId the Id of the config file * @param scriptBuildStepArgs whether to save the args and arg values (the boolean is required because of html form submission, which also sends hidden values) */ @DataBoundConstructor @@ -77,7 +77,7 @@ public WinBatchBuildStep(String buildStepId, ScriptBuildStepArgs scriptBuildStep /** * The constructor * - * @param buildStepId the Id of the config file + * @param buildStepId the Id of the config file * @param buildStepArgs list of arguments specified as buildStepargs */ public WinBatchBuildStep(String buildStepId, String[] buildStepArgs) { @@ -181,6 +181,7 @@ public String getDisplayName() { public ListBoxModel doFillBuildStepIdItems(@AncestorInPath ItemGroup context) { List configsInContext = ConfigFiles.getConfigsInContext(context, WinBatchConfig.WinBatchConfigProvider.class); Collections.sort(configsInContext, new Comparator() { + @Override public int compare(Config o1, Config o2) { return o1.name.compareTo(o2.name); } @@ -192,54 +193,47 @@ public int compare(Config o1, Config o2) { } return items; } + } - /** + /** * gets the argument description to be displayed on the screen when selecting a config in the dropdown - * - * @param configId the config id to get the arguments description for - * @return the description - */ - private String getArgsDescription(@AncestorInPath Item context, String configId) { - final WinBatchConfig config = ConfigFiles.getByIdOrNull(context, configId); - if (config != null) { - if (config.args != null && !config.args.isEmpty()) { - StringBuilder sb = new StringBuilder("Required arguments: "); - int i = 1; - for (Iterator iterator = config.args.iterator(); iterator.hasNext(); i++) { - Arg arg = iterator.next(); - sb.append(i).append(". ").append(arg.name); - if (iterator.hasNext()) { - sb.append(" | "); - } + * + * @param configId the config id to get the arguments description for + * @return the description + */ + private String getArgsDescription(@AncestorInPath Item context, String configId) { + final WinBatchConfig config = ConfigFiles.getByIdOrNull(context, configId); + if (config != null) { + if (config.args != null && !config.args.isEmpty()) { + StringBuilder sb = new StringBuilder("Required arguments: "); + int i = 1; + for (Iterator iterator = config.args.iterator(); iterator.hasNext(); i++) { + Arg arg = iterator.next(); + sb.append(i).append(". ").append(arg.name); + if (iterator.hasNext()) { + sb.append(" | "); } - return sb.toString(); - } else { - return "No arguments required"; } - } - return "please select a valid script!"; - } - - /** - * validate that an existing config was chosen - * - * @param buildStepId the buildStepId - * @return - */ - public HttpResponse doCheckBuildStepId(StaplerRequest req, @AncestorInPath Item context, @QueryParameter String buildStepId) { - final WinBatchConfig config = ConfigFiles.getByIdOrNull(context, buildStepId); - if (config != null) { - return DetailLinkDescription.getDescription(req, context, buildStepId, getArgsDescription(context, buildStepId)); + return sb.toString(); } else { - return FormValidation.error("you must select a valid batch file"); + return "No arguments required"; } } + return "please select a valid script!"; + } - private ConfigProvider getBuildStepConfigProvider() { - ExtensionList providers = ConfigProvider.all(); - return providers.get(WinBatchConfig.WinBatchConfigProvider.class); + /** + * validate that an existing config was chosen + * + * @param buildStepId the buildStepId + * @return + */ + public HttpResponse doCheckBuildStepId(StaplerRequest req, @AncestorInPath Item context, @QueryParameter String buildStepId) { + final WinBatchConfig config = ConfigFiles.getByIdOrNull(context, buildStepId); + if (config != null) { + return DetailLinkDescription.getDescription(req, context, buildStepId, getArgsDescription(context, buildStepId)); + } else { + return FormValidation.error("you must select a valid batch file"); } - } - } diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly index a5ce6b6..c986c3f 100644 --- a/src/main/resources/index.jelly +++ b/src/main/resources/index.jelly @@ -1,3 +1,4 @@ +

This plugin allows to centrally manage shell scripts and reference these as build steps in your builds.
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/managedscripts/ManagedBatchScriptStep/config-details.jelly b/src/main/resources/org/jenkinsci/plugins/managedscripts/ManagedBatchScriptStep/config-details.jelly new file mode 100644 index 0000000..9deb917 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/managedscripts/ManagedBatchScriptStep/config-details.jelly @@ -0,0 +1,35 @@ + + + + + + + + + + + view selected script + + + + + + + + +
+ +
+ + + +
+
+ +
+ + + + + +
diff --git a/src/main/resources/org/jenkinsci/plugins/managedscripts/ManagedPowershellScriptStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/managedscripts/ManagedPowershellScriptStep/config.jelly new file mode 100644 index 0000000..266525a --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/managedscripts/ManagedPowershellScriptStep/config.jelly @@ -0,0 +1,36 @@ + + + + + + + + + + + + view selected script + + + + + + + + +
+ +
+ + + +
+
+ +
+ + + + + +
diff --git a/src/main/resources/org/jenkinsci/plugins/managedscripts/ManagedPowershellScriptStep/help.jelly b/src/main/resources/org/jenkinsci/plugins/managedscripts/ManagedPowershellScriptStep/help.jelly new file mode 100644 index 0000000..29ae03d --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/managedscripts/ManagedPowershellScriptStep/help.jelly @@ -0,0 +1,5 @@ + +
+ This step allows to reference and execute a centrally managed powershell script within your build. + New files can be added in the global configuration. +
diff --git a/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellBuildStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellBuildStep/config.jelly index 31689c2..266525a 100644 --- a/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellBuildStep/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellBuildStep/config.jelly @@ -1,3 +1,4 @@ + diff --git a/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellBuildStep/help.jelly b/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellBuildStep/help.jelly index dfdcfd3..29ae03d 100644 --- a/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellBuildStep/help.jelly +++ b/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellBuildStep/help.jelly @@ -1,3 +1,4 @@ +
This step allows to reference and execute a centrally managed powershell script within your build. New files can be added in the global configuration. diff --git a/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellConfig/PowerShellConfigProvider/newInstanceDetail.jelly b/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellConfig/PowerShellConfigProvider/newInstanceDetail.jelly index 87c0209..565f67f 100644 --- a/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellConfig/PowerShellConfigProvider/newInstanceDetail.jelly +++ b/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellConfig/PowerShellConfigProvider/newInstanceDetail.jelly @@ -23,7 +23,7 @@ THE SOFTWARE. --> - + ${%buildstep_provider_description} \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellConfig/edit-config.jelly b/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellConfig/edit-config.jelly index eea99b6..e59b5ea 100644 --- a/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellConfig/edit-config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/managedscripts/PowerShellConfig/edit-config.jelly @@ -1,3 +1,4 @@ + - + ${%buildstep_provider_description} \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/managedscripts/ScriptConfig/edit-config.jelly b/src/main/resources/org/jenkinsci/plugins/managedscripts/ScriptConfig/edit-config.jelly index 864f177..9cb6354 100644 --- a/src/main/resources/org/jenkinsci/plugins/managedscripts/ScriptConfig/edit-config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/managedscripts/ScriptConfig/edit-config.jelly @@ -1,3 +1,4 @@ + - + ${%buildstep_provider_description} \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/managedscripts/WinBatchConfig/edit-config.jelly b/src/main/resources/org/jenkinsci/plugins/managedscripts/WinBatchConfig/edit-config.jelly index 36cda9e..332c921 100644 --- a/src/main/resources/org/jenkinsci/plugins/managedscripts/WinBatchConfig/edit-config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/managedscripts/WinBatchConfig/edit-config.jelly @@ -23,7 +23,7 @@ THE SOFTWARE. --> - + diff --git a/src/main/resources/org/jenkinsci/plugins/managedscripts/WinBatchConfig/show-config.jelly b/src/main/resources/org/jenkinsci/plugins/managedscripts/WinBatchConfig/show-config.jelly index de9f4bd..722bf47 100644 --- a/src/main/resources/org/jenkinsci/plugins/managedscripts/WinBatchConfig/show-config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/managedscripts/WinBatchConfig/show-config.jelly @@ -7,7 +7,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> - + diff --git a/src/main/resources/org/jenkinsci/plugins/managedscripts/powershellHelper.ps1 b/src/main/resources/org/jenkinsci/plugins/managedscripts/powershellHelper.ps1 new file mode 100644 index 0000000..cf3863c --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/managedscripts/powershellHelper.ps1 @@ -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 { + $exitCode = 0; + $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 { + 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; + } +} \ No newline at end of file