Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 94 additions & 32 deletions src/main/java/hudson/plugins/powershell/PowerShell.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
package hudson.plugins.powershell;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.*;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Computer;
import hudson.model.Item;
import hudson.model.Node;
import hudson.model.TaskListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.tasks.CommandInterpreter;
import hudson.util.ListBoxModel;
import jenkins.model.Jenkins;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import org.apache.commons.lang.SystemUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.verb.POST;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
* Invokes PowerShell from Jenkins.
Expand All @@ -31,6 +44,8 @@

private transient TaskListener listener;

private String installation;

@DataBoundConstructor
public PowerShell(String command, boolean stopOnError, boolean useProfile, Integer unstableReturn) {
super(command);
Expand All @@ -43,14 +58,7 @@
public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws InterruptedException
{
this.listener = listener;
try
{
return super.perform(build, launcher, listener);
}
catch (InterruptedException e)
{
throw e;
}
return super.perform(build, launcher, listener);
}

public boolean isStopOnError() {
Expand Down Expand Up @@ -81,50 +89,87 @@
return this.unstableReturn != null && exitCode != 0 && this.unstableReturn.equals(exitCode);
}

@DataBoundSetter
public void setInstallation(String installation) {
this.installation = Util.fixEmptyAndTrim(installation);
}

public String getInstallation() {
return installation;
}

@Override
public String[] buildCommandLine(FilePath script) {
String powerShellExecutable = null;
PowerShellInstallation installation = null;
if (isRunningOnWindows(script)) {
installation = Jenkins.get().getDescriptorByType(PowerShellInstallation.DescriptorImpl.class).getAnyInstallation(PowerShellInstallation.DEFAULTWINDOWS);

final var installation = getPowerShellInstallation(script);
final var powerShellExecutable = getPowerShellExecutable(script, installation);

List<String> args = new ArrayList<>();
args.add(powerShellExecutable);
args.add("-NonInteractive");
if (!useProfile) {
args.add("-NoProfile");
}
else {
installation = Jenkins.get().getDescriptorByType(PowerShellInstallation.DescriptorImpl.class).getAnyInstallation(PowerShellInstallation.DEFAULTLINUX);
if (isRunningOnWindows(script)) {
// ExecutionPolicy option does not work (and is not required) for non-Windows platforms
// See https://github.com/PowerShell/PowerShell/issues/2742
args.add("-ExecutionPolicy");
args.add("Bypass");
}
args.add("-File");
args.add(script.getRemote());
return args.toArray(new String[0]);
}

@NonNull
private String getPowerShellExecutable(FilePath script, PowerShellInstallation installation) {
String powerShellExecutable = null;

if (installation != null) {
Node node = filePathToNode(script);
try {
if (node != null && installation.forNode(node, listener) != null) {
powerShellExecutable = installation.forNode(node, listener).getPowerShellBinary();
installation = installation.forNode(node, listener);
}
else {

final var home = installation.getPowershellHome();
if (home != null) {
final var separator = isRunningOnWindows(script) ? "\\" : "/";
powerShellExecutable = home + separator + installation.getPowerShellBinary();
} else {
powerShellExecutable = installation.getPowerShellBinary();
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}

// fallback to installed version on agent
if (powerShellExecutable == null)
{
powerShellExecutable = PowerShellInstallation.getDefaultPowershellWhenNoConfiguration(isRunningOnWindows(script));
}

if (isRunningOnWindows(script)) {
if (useProfile){
return new String[] { powerShellExecutable, "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script.getRemote()};
} else {
return new String[] { powerShellExecutable, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script.getRemote()};
return powerShellExecutable;
}

@Nullable
private PowerShellInstallation getPowerShellInstallation(FilePath script) {
PowerShellInstallation powerShellInstallation;

final var descriptor = Jenkins.get().getDescriptorByType(PowerShellInstallation.DescriptorImpl.class);

powerShellInstallation = descriptor.getInstallation(this.installation);
if (powerShellInstallation == null) {
if (isRunningOnWindows(script)) {
powerShellInstallation = descriptor.getAnyInstallation(PowerShellInstallation.DEFAULT_WINDOWS_NAME);
}
} else {
// ExecutionPolicy option does not work (and is not required) for non-Windows platforms
// See https://github.com/PowerShell/PowerShell/issues/2742
if (useProfile){
return new String[] { powerShellExecutable, "-NonInteractive", "-File", script.getRemote()};
} else {
return new String[] { powerShellExecutable, "-NonInteractive", "-NoProfile", "-File", script.getRemote()};
else {
powerShellInstallation = descriptor.getAnyInstallation(PowerShellInstallation.DEFAULT_LINUX_NAME);
}
}

return powerShellInstallation;

Check warning on line 172 in src/main/java/hudson/plugins/powershell/PowerShell.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 104-172 are not covered by tests
}

@Override
Expand Down Expand Up @@ -181,9 +226,26 @@
return true;
}

@NonNull
@Override
public String getDisplayName() {
return "PowerShell";
}

@POST
public ListBoxModel doFillInstallationItems() {
Jenkins.get().checkPermission(Item.CONFIGURE);

ListBoxModel model = new ListBoxModel();

PowerShellInstallation.DescriptorImpl descriptor =
Jenkins.get().getDescriptorByType(PowerShellInstallation.DescriptorImpl.class);

model.add(Messages.none(), "");
for (PowerShellInstallation tool : descriptor.getInstallations()) {
model.add(tool.getName());
}
return model;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.EnvVars;
import hudson.Extension;
import hudson.Util;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.model.EnvironmentSpecific;
Expand All @@ -19,40 +20,60 @@
import org.kohsuke.stapler.StaplerRequest2;

import java.io.IOException;
import java.io.Serial;
import java.lang.reflect.Array;
import java.util.List;

public class PowerShellInstallation extends ToolInstallation implements NodeSpecific<PowerShellInstallation>,
EnvironmentSpecific<PowerShellInstallation> {

public static transient final String DEFAULTWINDOWS = "DefaultWindows";
static final String DEFAULT_WINDOWS_NAME = "DefaultWindows";

public static transient final String DEFAULTLINUX = "DefaultLinux";
static final String DEFAULT_LINUX_NAME = "DefaultLinux";

private static final String DEFAULT_WINDOWS_EXECUTABLE = "powershell.exe";

private static final String DEFAULT_LINUX_EXECUTABLE = "pwsh";

@Serial
private static final long serialVersionUID = 1;

/**
* Originally, {@link hudson.tools.ToolInstallation#home} was the only field used to indicate both installation
* directory and executable file.
* <p>
* This was split into two fields, but {@link hudson.tools.ToolInstallation#home} is private with no setter and
* could not be reused, leading to {@link PowerShellInstallation#powershellHome}.
* <p>
* In a future version, this field could be migrated back into {@link hudson.tools.ToolInstallation#home}, removing
* {@link PowerShellInstallation#powershellHome}.
*/
private /*almost final*/ String powershellHome;
private /*almost final*/ String executable;

@DataBoundConstructor
public PowerShellInstallation(String name, String home, List<? extends ToolProperty<?>> properties) {
super(name, home, properties);
public PowerShellInstallation(String name, String powershellHome, String executable, List<? extends ToolProperty<?>> properties) {
super(name, null, properties);
this.powershellHome = Util.fixEmptyAndTrim(powershellHome);
this.executable = executable;
}

@Override
public PowerShellInstallation forNode(@NonNull Node node, TaskListener log) throws IOException, InterruptedException {
return new PowerShellInstallation(getName(), translateFor(node, log), getProperties());
return new PowerShellInstallation(getName(), translateFor(node, log), executable, getProperties());
}

@Override
public PowerShellInstallation forEnvironment(EnvVars environment) {
return new PowerShellInstallation(getName(), environment.expand(getHome()), getProperties());
return new PowerShellInstallation(getName(), environment.expand(getHome()), executable, getProperties());
}

public static String getDefaultPowershellWhenNoConfiguration(Boolean isRunningOnWindows) {
if (isRunningOnWindows) {
return "powershell.exe";
return DEFAULT_WINDOWS_EXECUTABLE;
}
else {
return "pwsh";
return DEFAULT_LINUX_EXECUTABLE;

Check warning on line 76 in src/main/java/hudson/plugins/powershell/PowerShellInstallation.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 63-76 are not covered by tests
}
}

Expand All @@ -65,15 +86,55 @@
return;
}

PowerShellInstallation windowsInstallation = new PowerShellInstallation(DEFAULTWINDOWS, "powershell.exe", null);
PowerShellInstallation linuxInstallation = new PowerShellInstallation(DEFAULTLINUX, "pwsh", null);
PowerShellInstallation[] defaultInstallations = { windowsInstallation, linuxInstallation};
descriptor.setInstallations(defaultInstallations);
PowerShellInstallation windowsInstallation = new PowerShellInstallation(DEFAULT_WINDOWS_NAME, null, DEFAULT_WINDOWS_EXECUTABLE, null);
PowerShellInstallation linuxInstallation = new PowerShellInstallation(DEFAULT_LINUX_NAME, null, DEFAULT_LINUX_EXECUTABLE, null);
descriptor.setInstallations(windowsInstallation, linuxInstallation);
descriptor.save();
}

public String getPowershellHome() {
return powershellHome;
}

@Override
public String getHome() {
return powershellHome;
}

public String getPowerShellBinary() {
return getHome();
return executable;
}

public String getExecutable() {
return executable;

Check warning on line 109 in src/main/java/hudson/plugins/powershell/PowerShellInstallation.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 96-109 are not covered by tests
}

@Serial
@Override
protected Object readResolve() {
if (this.executable == null) {

Check warning on line 115 in src/main/java/hudson/plugins/powershell/PowerShellInstallation.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 115 is only partially covered, one branch is missing
final var home = super.getHome();
if (home == null) {
this.executable = DEFAULT_LINUX_EXECUTABLE;
this.powershellHome = null;
} else if (DEFAULT_LINUX_EXECUTABLE.equals(home) || DEFAULT_WINDOWS_EXECUTABLE.equals(home)) {
this.executable = home;
this.powershellHome = null;
} else if (home.endsWith(DEFAULT_LINUX_EXECUTABLE)) {
this.executable = DEFAULT_LINUX_EXECUTABLE;
this.powershellHome = home.substring(0, home.length() - DEFAULT_LINUX_EXECUTABLE.length() - 1);
} else if (home.endsWith(DEFAULT_WINDOWS_EXECUTABLE)) {
this.executable = DEFAULT_WINDOWS_EXECUTABLE;
this.powershellHome = home.substring(0, home.length() - DEFAULT_WINDOWS_EXECUTABLE.length() - 1);
} else {
this.executable = DEFAULT_LINUX_EXECUTABLE;
this.powershellHome = home;
}
this.executable = Util.fixEmptyAndTrim(this.executable);
this.powershellHome = Util.fixEmptyAndTrim(this.powershellHome);

Check warning on line 134 in src/main/java/hudson/plugins/powershell/PowerShellInstallation.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 116-134 are not covered by tests
}

return super.readResolve();
}

@Extension
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
none=(None)
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,20 @@ THE SOFTWARE.
<f:entry title="${%Command}" description="${%description(rootURL)}">
<f:textarea name="command" value="${instance.command}" checkMethod="post" codemirror-mode="clike"
codemirror-config='"mode": "text/x-csharp", "lineNumbers": true, "matchBrackets": true'/>
</f:entry>
</f:entry>

<f:entry field="stopOnError" title="Stop On Errors">
<f:checkbox name="stopOnError" checked="${instance.stopOnError}" default="true" />
<f:checkbox name="stopOnError" checked="${instance.stopOnError}" default="true" />
</f:entry>

<f:entry field="useProfile" title="${%Use PowerShell profile}">
<f:entry field="useProfile" title="${%Use PowerShell profile}">
<f:checkbox default="true" />
</f:entry>

<f:entry field="installation" title="${%PowerShell tool}">
<f:select/>
</f:entry>

<f:advanced>
<f:entry title="${%ERRORLEVEL to set build unstable}" field="unstableReturn" >
<f:number value="${instance.unstableReturn}" min="-2147483648" max="2147483647" step="1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
<f:entry title="${%Name}" field="name">
<f:textbox/>
</f:entry>
<f:entry title="${%Path to Powershell}" field="home">
<f:textbox/>
<f:entry title="${%Powershell home}" field="powershellHome">
<f:textbox name="home" />
</f:entry>
<f:entry title="${%Powershell executable}" field="executable">
<f:textbox default="pwsh" />
</f:entry>
</j:jelly>
3 changes: 2 additions & 1 deletion src/main/webapp/help.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
By default, PowerShell processes profile scripts at startup.
This can be disabled to improve startup time, avoid any potential conflict and provide a clean shell.

On Windows it uses PowerShell.exe and on Linux pwsh (PowerShell Core)
The used PowerShell binary is defined by the tool configuration.
(DefaultWindows is used under Windows and DefaultLinux under Linux)

<p>
If you already have a batch file in SCM, you can just type in the path of that PowerShell file
Expand Down
Loading