diff --git a/pom.xml b/pom.xml
index 99e80f0..241de91 100644
--- a/pom.xml
+++ b/pom.xml
@@ -89,6 +89,10 @@
org.jenkins-ci.plugins
ssh-credentials
+
+ org.jenkins-ci.plugins
+ git-client
+
diff --git a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgent.java b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgent.java
index 3ec9f44..f25edf4 100644
--- a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgent.java
+++ b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgent.java
@@ -24,19 +24,26 @@
package com.cloudbees.jenkins.plugins.sshagent.exec;
-import hudson.AbortException;
-import hudson.FilePath;
-import hudson.Launcher;
-import hudson.model.TaskListener;
-import hudson.slaves.WorkspaceList;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.TimeUnit;
+import hudson.AbortException;
+import hudson.FilePath;
+import hudson.Functions;
+import hudson.Launcher;
+import hudson.model.TaskListener;
+import hudson.plugins.git.GitTool;
+import hudson.slaves.WorkspaceList;
+
/**
* Runs a native SSH agent installed on a system.
*/
@@ -46,15 +53,46 @@ public final class ExecRemoteAgent implements Serializable {
/** Agent environment used for {@code ssh-add} and {@code ssh-agent -k}. */
private final Map agentEnv;
+ private final String sshAgentExe;
public ExecRemoteAgent(Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
+ sshAgentExe = !Functions.isWindows() ? "ssh-agent" : searchSSHAgentExeForWindows(launcher, listener);
+ String agentOutput = executeCommand(launcher, listener, sshAgentExe);
+ agentEnv = parseAgentEnv(agentOutput, listener); // TODO could include local filenames, better to look up remote charset
+ }
+
+ private static String executeCommand(Launcher launcher, TaskListener listener, String... cmd)
+ throws IOException, InterruptedException, AbortException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
- if (launcher.launch().cmds("ssh-agent").stdout(baos).start()
- .joinWithTimeout(1, TimeUnit.MINUTES, listener) != 0) {
+ if (launcher.launch().cmds(cmd).stdout(baos).start().joinWithTimeout(1, TimeUnit.MINUTES, listener) != 0) {
String reason = new String(baos.toByteArray(), StandardCharsets.US_ASCII);
throw new AbortException("Failed to run ssh-agent: " + reason);
}
- agentEnv = parseAgentEnv(new String(baos.toByteArray(), StandardCharsets.US_ASCII), listener); // TODO could include local filenames, better to look up remote charset
+ return new String(baos.toByteArray(), StandardCharsets.US_ASCII);
+ }
+
+ private static final Path GIT_SSH_AGENT_PATH_WINDOWS = Path.of("usr", "bin", "ssh-agent.exe");
+
+ private static String searchSSHAgentExeForWindows(Launcher launcher, TaskListener listener)
+ throws IOException, InterruptedException {
+ Optional defaultGitHome = Optional.ofNullable(GitTool.getDefaultInstallation()) //
+ .map(GitTool::getHome).map(Path::of);
+ if (defaultGitHome.isPresent()) {
+ Path sshAgentExe = defaultGitHome.get().resolve(GIT_SSH_AGENT_PATH_WINDOWS);
+ if (Files.isRegularFile(sshAgentExe)) {
+ return sshAgentExe.toString();
+ }
+ }
+ String gitPaths = executeCommand(launcher, listener, "where", "git");
+ List paths = gitPaths.lines().map(Path::of).toList();
+ for (Path gitExe : paths) {
+ Path sshAgentExe = gitExe.getParent().getParent().resolve(GIT_SSH_AGENT_PATH_WINDOWS);
+ if (Files.exists(sshAgentExe)) {
+ return sshAgentExe.toString();
+ }
+ }
+ throw new IllegalStateException(
+ "Executing with default ssh-agent on Windows is not supported and an alternative implementation from a git installation is not available.");
}
/**
@@ -84,10 +122,10 @@ public void addIdentity(String privateKey, final String passphrase, String comme
env.put("DISPLAY", "bogus"); // just to force using SSH_ASKPASS
env.put("SSH_ASKPASS", askpass.getRemote());
}
-
+
// as the next command is in quiet mode, we just add a message to the log
listener.getLogger().println("Running ssh-add (command line suppressed)");
-
+
if (launcher.launch().quiet(true).cmds("ssh-add", keyFile.getRemote()).envs(env)
.stdout(listener).start().joinWithTimeout(1, TimeUnit.MINUTES, listener) != 0) {
throw new AbortException("Failed to run ssh-add");
@@ -117,7 +155,7 @@ public void stop(Launcher launcher, TaskListener listener) throws IOException, I
throw new AbortException("Failed to run ssh-agent -k");
}
}
-
+
/**
* Parses ssh-agent output.
*/
@@ -133,10 +171,10 @@ private Map parseAgentEnv(String agentOutput, TaskListener listen
// get SSH_AGENT_PID
env.put(AgentPidVar, getAgentValue(agentOutput, AgentPidVar));
listener.getLogger().println(AgentPidVar + "=" + env.get(AgentPidVar));
-
+
return env;
}
-
+
/**
* Parses a value from ssh-agent output.
*/
@@ -145,16 +183,16 @@ private String getAgentValue(String agentOutput, String envVar) {
int end = agentOutput.indexOf(';', pos);
return agentOutput.substring(pos, end);
}
-
+
/**
- * Creates a self-deleting script for SSH_ASKPASS. Self-deleting to be able to detect a wrong passphrase.
+ * Creates a self-deleting script for SSH_ASKPASS. Self-deleting to be able to detect a wrong passphrase.
*/
private FilePath createAskpassScript(FilePath temp) throws IOException, InterruptedException {
// TODO: assuming that ssh-add runs the script in shell even on Windows, not cmd
// for cmd following could work
// suffix = ".bat";
// script = "@ECHO %SSH_PASSPHRASE%\nDEL \"" + askpass.getAbsolutePath() + "\"\n";
-
+
FilePath askpass = temp.createTextTempFile("askpass_", ".sh", "#!/bin/sh\necho \"$SSH_PASSPHRASE\"\nrm \"$0\"\n");
// executable only for a current user