Skip to content

Commit 0776101

Browse files
committed
Simplified ExecRemoteAgent to use Launcher & FilePath for all remote calls.
1 parent 20d2aeb commit 0776101

File tree

7 files changed

+70
-208
lines changed

7 files changed

+70
-208
lines changed

src/main/java/com/cloudbees/jenkins/plugins/sshagent/RemoteAgent.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ public interface RemoteAgent {
4545
* @param comment the comment to give to the key.
4646
* @throws java.io.IOException if something went wrong.
4747
*/
48-
void addIdentity(String privateKey, String passphrase, String comment) throws IOException;
48+
void addIdentity(String privateKey, String passphrase, String comment) throws IOException, InterruptedException;
4949

5050
/**
5151
* Stops the agent.
5252
*/
53-
void stop();
53+
void stop() throws IOException, InterruptedException;
5454
}

src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ public SSHAgentEnvironment(Launcher launcher, BuildListener listener, @CheckForN
391391
* @throws IOException if the key cannot be added.
392392
* @since 1.9
393393
*/
394-
public void add(SSHUserPrivateKey key) throws IOException {
394+
public void add(SSHUserPrivateKey key) throws IOException, InterruptedException {
395395
final Secret passphrase = key.getPassphrase();
396396
final String effectivePassphrase = passphrase == null ? null : passphrase.getPlainText();
397397
for (String privateKey : key.getPrivateKeys()) {

src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStepExecution.java

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ public void onResume() {
8282
try {
8383
purgeSockets();
8484
initRemoteAgent();
85-
} catch (IOException io) {
85+
} catch (IOException | InterruptedException x) {
8686
listener.getLogger().println(Messages.SSHAgentBuildWrapper_CouldNotStartAgent());
87+
x.printStackTrace(listener.getLogger());
8788
}
8889
}
8990

@@ -92,7 +93,7 @@ static FilePath tempDir(FilePath ws) {
9293
return ws.sibling(ws.getName() + System.getProperty(WorkspaceList.class.getName(), "@") + "tmp");
9394
}
9495

95-
private static class Callback extends BodyExecutionCallback {
96+
private static class Callback extends BodyExecutionCallback.TailCall {
9697

9798
private static final long serialVersionUID = 1L;
9899

@@ -103,15 +104,8 @@ private static class Callback extends BodyExecutionCallback {
103104
}
104105

105106
@Override
106-
public void onSuccess(StepContext context, Object result) {
107+
protected void finished(StepContext context) throws Exception {
107108
execution.cleanUp();
108-
context.onSuccess(result);
109-
}
110-
111-
@Override
112-
public void onFailure(StepContext context, Throwable t) {
113-
execution.cleanUp();
114-
context.onFailure(t);
115109
}
116110

117111
}
@@ -137,7 +131,7 @@ public void expand(EnvVars env) throws IOException, InterruptedException {
137131
*
138132
* @throws IOException
139133
*/
140-
private void initRemoteAgent() throws IOException {
134+
private void initRemoteAgent() throws IOException, InterruptedException {
141135

142136
List<SSHUserPrivateKey> userPrivateKeys = new ArrayList<SSHUserPrivateKey>();
143137
for (String id : new LinkedHashSet<String>(step.getCredentials())) {
@@ -197,17 +191,16 @@ private void initRemoteAgent() throws IOException {
197191
/**
198192
* Shuts down the current SSH Agent and purges socket files.
199193
*/
200-
private void cleanUp() {
194+
private void cleanUp() throws Exception {
201195
try {
202196
TaskListener listener = getContext().get(TaskListener.class);
203197
if (agent != null) {
204198
agent.stop();
205199
listener.getLogger().println(Messages.SSHAgentBuildWrapper_Stopped());
206200
}
207-
} catch (Throwable th) {
208-
getContext().onFailure(th);
201+
} finally {
202+
purgeSockets();
209203
}
210-
purgeSockets();
211204
}
212205

213206
/**
Lines changed: 55 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* The MIT License
33
*
4-
* Copyright (c) 2014, Eccam s.r.o., Milan Kriz
4+
* Copyright (c) 2014, Eccam s.r.o., Milan Kriz, CloudBees Inc.
55
*
66
* Permission is hereby granted, free of charge, to any person obtaining a copy
77
* of this software and associated documentation files (the "Software"), to deal
@@ -25,27 +25,16 @@
2525
package com.cloudbees.jenkins.plugins.sshagent.exec;
2626

2727
import com.cloudbees.jenkins.plugins.sshagent.RemoteAgent;
28-
28+
import hudson.AbortException;
29+
import hudson.FilePath;
30+
import hudson.Launcher;
2931
import hudson.model.TaskListener;
30-
31-
import java.lang.Process;
32-
import java.lang.ProcessBuilder;
33-
34-
import java.util.Map;
35-
import java.util.HashMap;
36-
import java.util.List;
37-
import java.util.Arrays;
38-
32+
import java.io.ByteArrayOutputStream;
3933
import java.io.IOException;
40-
import java.io.File;
41-
import java.io.FileOutputStream;
42-
import java.io.OutputStream;
43-
import java.io.OutputStreamWriter;
44-
import java.io.Writer;
45-
import java.nio.charset.Charset;
46-
import java.nio.charset.StandardCharsets;
34+
import java.util.HashMap;
35+
import java.util.Map;
36+
import java.util.concurrent.TimeUnit;
4737

48-
import org.apache.commons.io.IOUtils;
4938

5039
/**
5140
* An implementation that uses native SSH agent installed on a system.
@@ -54,49 +43,44 @@ public class ExecRemoteAgent implements RemoteAgent {
5443
private static final String AuthSocketVar = "SSH_AUTH_SOCK";
5544
private static final String AgentPidVar = "SSH_AGENT_PID";
5645

57-
/** Process builder keeping environment for all ExecRemoteAgent related processes. */
58-
private final ProcessBuilder processBuilder;
46+
private final Launcher launcher;
5947

6048
/**
6149
* The listener in case we need to report exceptions
6250
*/
6351
private final TaskListener listener;
64-
65-
/**
66-
* Process in which the ssh-agent is running.
67-
*/
68-
private final Process agent;
52+
53+
private final FilePath temp;
6954

7055
/**
7156
* The socket bound by the agent.
7257
*/
7358
private final String socket;
7459

75-
/**
76-
* Constructor.
77-
*
78-
* @param listener the listener.
79-
* @throws Exception if the agent could not start.
80-
*/
81-
public ExecRemoteAgent(TaskListener listener) throws Exception {
82-
this.processBuilder = new ProcessBuilder();
60+
/** Agent environment used for {@code ssh-add} and {@code ssh-agent -k}. */
61+
private final Map<String, String> agentEnv;
62+
63+
ExecRemoteAgent(Launcher launcher, TaskListener listener, FilePath temp) throws Exception {
64+
this.launcher = launcher;
8365
this.listener = listener;
84-
85-
this.agent = execProcess("ssh-agent");
86-
Map<String, String> agentEnv = parseAgentEnv(this.agent);
66+
this.temp = temp;
67+
68+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
69+
if (launcher.launch().cmds("ssh-agent").stdout(baos).start().joinWithTimeout(1, TimeUnit.MINUTES, listener) != 0) {
70+
throw new AbortException("Failed to run ssh-agent");
71+
}
72+
agentEnv = parseAgentEnv(baos.toString()); // TODO could include local filenames, better to look up remote charset
8773

8874
if (agentEnv.containsKey(AuthSocketVar))
8975
socket = agentEnv.get(AuthSocketVar);
9076
else
9177
socket = ""; // socket is not set
92-
93-
// set agent environment to the process builder to provide it to ssh-add which will be executed later
94-
processBuilder.environment().putAll(agentEnv);
9578
}
9679

9780
/**
9881
* {@inheritDoc}
9982
*/
83+
@Override
10084
public String getSocket() {
10185
return socket;
10286
}
@@ -105,80 +89,46 @@ public String getSocket() {
10589
* {@inheritDoc}
10690
*/
10791
@Override
108-
public void addIdentity(String privateKey, final String passphrase, String comment) throws IOException {
109-
File keyFile = File.createTempFile("private_key_", ".key");
110-
try (FileOutputStream os = new FileOutputStream(keyFile); Writer keyWriter = new OutputStreamWriter(os, StandardCharsets.US_ASCII)) {
111-
keyWriter.write(privateKey);
112-
}
113-
setReadOnlyForOwner(keyFile);
114-
115-
File askpass = createAskpassScript();
116-
117-
processBuilder.environment().put("SSH_PASSPHRASE", passphrase);
118-
processBuilder.environment().put("DISPLAY", ":0"); // just to force using SSH_ASKPASS
119-
processBuilder.environment().put("SSH_ASKPASS", askpass.getPath());
120-
121-
final Process sshAdd = execProcess("ssh-add " + keyFile.getPath());
122-
123-
String errorString = IOUtils.toString(sshAdd.getErrorStream());
124-
if (!errorString.isEmpty()) {
125-
errorString += "Check the passphrase for the private key.";
126-
listener.getLogger().println(errorString);
127-
}
128-
IOUtils.copy(sshAdd.getInputStream(), listener.getLogger()); // default encoding appropriate here: local process output
129-
92+
public void addIdentity(String privateKey, final String passphrase, String comment) throws IOException, InterruptedException {
93+
FilePath keyFile = temp.createTextTempFile("private_key_", ".key", privateKey);
13094
try {
131-
sshAdd.waitFor();
132-
}
133-
catch (InterruptedException e) {
134-
// waiting or process somehow interrupted
135-
}
136-
137-
processBuilder.environment().remove("SSH_ASKPASS");
138-
processBuilder.environment().remove("DISPLAY");
139-
processBuilder.environment().remove("SSH_PASSPHRASE");
140-
141-
if (askpass.isFile() && !askpass.delete()) { // the ASKPASS script is self-deleting, anyway rather try to delete it in case of some error
142-
listener.getLogger().println("ExecRemoteAgent::addIdentity - failed to delete " + askpass);
143-
}
144-
145-
if (!keyFile.delete()) {
146-
listener.getLogger().println("ExecRemoteAgent::addIdentity - could NOT delete a temp file with a private key!");
95+
keyFile.chmod(0600);
96+
97+
FilePath askpass = createAskpassScript();
98+
try {
99+
100+
Map<String,String> env = new HashMap<>(agentEnv);
101+
env.put("SSH_PASSPHRASE", passphrase);
102+
env.put("DISPLAY", ":0"); // just to force using SSH_ASKPASS
103+
env.put("SSH_ASKPASS", askpass.getRemote());
104+
if (launcher.launch().cmds("ssh-add", keyFile.getRemote()).envs(env).stdout(listener).start().joinWithTimeout(1, TimeUnit.MINUTES, listener) != 0) {
105+
throw new AbortException("Failed to run ssh-add");
106+
}
107+
} finally {
108+
if (askpass.exists()) { // the ASKPASS script is self-deleting, anyway rather try to delete it in case of some error
109+
askpass.delete();
110+
}
111+
}
112+
} finally {
113+
keyFile.delete();
147114
}
148115
}
149116

150117
/**
151118
* {@inheritDoc}
152119
*/
153-
public void stop() {
154-
try {
155-
execProcess("ssh-agent -k");
156-
}
157-
catch (IOException e) {
158-
listener.error("ExecRemoteAgent::stop - " + e.getCause());
120+
@Override
121+
public void stop() throws IOException, InterruptedException {
122+
if (launcher.launch().cmds("ssh-agent", "-k").envs(agentEnv).stdout(listener).start().joinWithTimeout(1, TimeUnit.MINUTES, listener) != 0) {
123+
throw new AbortException("Failed to run ssh-agent -k");
159124
}
160-
agent.destroy();
161-
}
162-
163-
/**
164-
* Executes a new process using ProcessBuilder with custom environment variables.
165-
*/
166-
private Process execProcess(String command) throws IOException {
167-
listener.getLogger().println("ExecRemoteAgent::execProcess - " + command);
168-
List<String> command_list = Arrays.asList(command.split(" "));
169-
processBuilder.command(command_list);
170-
Process process = processBuilder.start();
171-
processBuilder.command("");
172-
return process;
173125
}
174126

175127
/**
176128
* Parses ssh-agent output.
177129
*/
178-
private Map<String,String> parseAgentEnv(Process agent) throws Exception{
179-
Map<String, String> env = new HashMap<String, String>();
180-
181-
String agentOutput = IOUtils.toString(agent.getInputStream()); // default encoding appropriate here: local filenames
130+
private Map<String,String> parseAgentEnv(String agentOutput) throws Exception{
131+
Map<String, String> env = new HashMap<>();
182132

183133
// get SSH_AUTH_SOCK
184134
env.put(AuthSocketVar, getAgentValue(agentOutput, AuthSocketVar));
@@ -196,46 +146,23 @@ private Map<String,String> parseAgentEnv(Process agent) throws Exception{
196146
*/
197147
private String getAgentValue(String agentOutput, String envVar) {
198148
int pos = agentOutput.indexOf(envVar) + envVar.length() + 1; // +1 for '='
199-
int end = agentOutput.indexOf(";", pos);
149+
int end = agentOutput.indexOf(';', pos);
200150
return agentOutput.substring(pos, end);
201151
}
202152

203-
/**
204-
* Sets file's permissions to readable only for an owner.
205-
*/
206-
private boolean setReadOnlyForOwner(File file) {
207-
boolean ok = file.setExecutable(false, false);
208-
ok &= file.setWritable(false, false);
209-
ok &= file.setReadable(false, false);
210-
ok &= file.setReadable(true, true);
211-
return ok;
212-
}
213-
214153
/**
215154
* Creates a self-deleting script for SSH_ASKPASS. Self-deleting to be able to detect a wrong passphrase.
216155
*/
217-
private File createAskpassScript() throws IOException {
156+
private FilePath createAskpassScript() throws IOException, InterruptedException {
218157
// TODO: assuming that ssh-add runs the script in shell even on Windows, not cmd
219158
// for cmd following could work
220159
// suffix = ".bat";
221160
// script = "@ECHO %SSH_PASSPHRASE%\nDEL \"" + askpass.getAbsolutePath() + "\"\n";
222161

223-
final String suffix;
224-
final String script;
225-
226-
suffix = ".sh";
227-
228-
File askpass = File.createTempFile("askpass_", suffix);
162+
FilePath askpass = temp.createTextTempFile("askpass_", ".sh", "#!/bin/sh\necho $SSH_PASSPHRASE\nrm $0\n");
229163

230-
script = "#!/bin/sh\necho $SSH_PASSPHRASE\nrm " + askpass.getAbsolutePath().replace("\\", "\\\\") + "\n"; // TODO try using `rm $0` instead
231-
232-
try (OutputStream os = new FileOutputStream(askpass); Writer askpassWriter = new OutputStreamWriter(os, /* due to presence of a local filename */Charset.defaultCharset())) {
233-
askpassWriter.write(script);
234-
}
235-
236164
// executable only for a current user
237-
askpass.setExecutable(false, false);
238-
askpass.setExecutable(true, true);
165+
askpass.chmod(0700);
239166
return askpass;
240167
}
241168
}

src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgentFactory.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import hudson.model.TaskListener;
3333

3434
import java.io.IOException;
35+
import java.util.concurrent.TimeUnit;
3536

3637

3738

@@ -55,7 +56,7 @@ public String getDisplayName() {
5556
@Override
5657
public boolean isSupported(Launcher launcher, final TaskListener listener) {
5758
try {
58-
int status = launcher.launch().cmds("ssh-agent", "-k").join();
59+
int status = launcher.launch().cmds("ssh-agent", "-k").quiet(true).start().joinWithTimeout(1, TimeUnit.MINUTES, listener);
5960
/*
6061
* `ssh-agent -k` returns 0 if terminates running agent or 1 if
6162
* it fails to terminate it. On Linux,
@@ -78,6 +79,6 @@ public boolean isSupported(Launcher launcher, final TaskListener listener) {
7879
*/
7980
@Override
8081
public RemoteAgent start(Launcher launcher, final TaskListener listener, FilePath temp) throws Throwable {
81-
return launcher.getChannel().call(new ExecRemoteAgentStarter(listener));
82+
return new ExecRemoteAgent(launcher, listener, temp);
8283
}
8384
}

0 commit comments

Comments
 (0)