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