Fix JSchProcess.waitFor() with time-out
SshSupport.runSshCommand() had a comment that wait with time-out could not be used because JSchProcess.exitValue() threw the wrong unchecked exception when the process was still running. Fix this and make JSchProcess.exitValue() throw the right exception, then wait with a time-out in SshSupport. The Apache sshd client's SshdExecProcess has always used the correct IllegalThreadStateException. Add tests for SshSupport.runCommand(). Change-Id: Id30893174ae8be3b9a16119674049337b0cf4381 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
This commit is contained in:
parent
cc9975ff68
commit
24fdc1d039
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
|
* Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
|
||||||
*
|
*
|
||||||
* This program and the accompanying materials are made available under the
|
* This program and the accompanying materials are made available under the
|
||||||
* terms of the Eclipse Distribution License v. 1.0 which is available at
|
* terms of the Eclipse Distribution License v. 1.0 which is available at
|
||||||
|
@ -14,6 +14,7 @@
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assume.assumeTrue;
|
import static org.junit.Assume.assumeTrue;
|
||||||
|
|
||||||
|
@ -22,9 +23,14 @@
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.eclipse.jgit.api.errors.TransportException;
|
import org.eclipse.jgit.api.errors.TransportException;
|
||||||
|
import org.eclipse.jgit.errors.CommandFailedException;
|
||||||
import org.eclipse.jgit.transport.CredentialItem;
|
import org.eclipse.jgit.transport.CredentialItem;
|
||||||
|
import org.eclipse.jgit.transport.URIish;
|
||||||
|
import org.eclipse.jgit.util.FS;
|
||||||
|
import org.eclipse.jgit.util.SshSupport;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.experimental.theories.DataPoints;
|
import org.junit.experimental.theories.DataPoints;
|
||||||
import org.junit.experimental.theories.Theory;
|
import org.junit.experimental.theories.Theory;
|
||||||
|
@ -67,10 +73,45 @@ public void setUp() throws Exception {
|
||||||
defaultCloneDir = new File(getTemporaryDirectory(), "cloned");
|
defaultCloneDir = new File(getTemporaryDirectory(), "cloned");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = TransportException.class)
|
@Test
|
||||||
public void testSshWithoutConfig() throws Exception {
|
public void testSshWithoutConfig() throws Exception {
|
||||||
cloneWith("ssh://" + TEST_USER + "@localhost:" + testPort
|
assertThrows(TransportException.class,
|
||||||
+ "/doesntmatter", defaultCloneDir, null);
|
() -> cloneWith("ssh://" + TEST_USER + "@localhost:" + testPort
|
||||||
|
+ "/doesntmatter", defaultCloneDir, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSingleCommand() throws Exception {
|
||||||
|
installConfig("IdentityFile " + privateKey1.getAbsolutePath());
|
||||||
|
String command = SshTestGitServer.ECHO_COMMAND + " 1 without timeout";
|
||||||
|
long start = System.nanoTime();
|
||||||
|
String reply = SshSupport.runSshCommand(
|
||||||
|
new URIish("ssh://" + TEST_USER + "@localhost:" + testPort),
|
||||||
|
null, FS.DETECTED, command, 0); // 0 == no timeout
|
||||||
|
long elapsed = System.nanoTime() - start;
|
||||||
|
assertEquals(command, reply);
|
||||||
|
// Now that we have an idea how long this takes on the test
|
||||||
|
// infrastructure, try again with a timeout.
|
||||||
|
command = SshTestGitServer.ECHO_COMMAND + " 1 expecting no timeout";
|
||||||
|
// Still use a generous timeout.
|
||||||
|
int timeout = 10 * ((int) TimeUnit.NANOSECONDS.toSeconds(elapsed) + 1);
|
||||||
|
reply = SshSupport.runSshCommand(
|
||||||
|
new URIish("ssh://" + TEST_USER + "@localhost:" + testPort),
|
||||||
|
null, FS.DETECTED, command, timeout);
|
||||||
|
assertEquals(command, reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSingleCommandWithTimeoutExpired() throws Exception {
|
||||||
|
installConfig("IdentityFile " + privateKey1.getAbsolutePath());
|
||||||
|
String command = SshTestGitServer.ECHO_COMMAND + " 2 EXPECTING TIMEOUT";
|
||||||
|
|
||||||
|
CommandFailedException e = assertThrows(CommandFailedException.class,
|
||||||
|
() -> SshSupport.runSshCommand(new URIish(
|
||||||
|
"ssh://" + TEST_USER + "@localhost:" + testPort), null,
|
||||||
|
FS.DETECTED, command, 1));
|
||||||
|
assertTrue(e.getMessage().contains(command));
|
||||||
|
assertTrue(e.getMessage().contains("time"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
@ -22,6 +24,7 @@
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.apache.sshd.common.NamedResource;
|
import org.apache.sshd.common.NamedResource;
|
||||||
import org.apache.sshd.common.PropertyResolver;
|
import org.apache.sshd.common.PropertyResolver;
|
||||||
|
@ -67,6 +70,16 @@
|
||||||
*/
|
*/
|
||||||
public class SshTestGitServer {
|
public class SshTestGitServer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple echo test command. Replies with the command string as passed. If
|
||||||
|
* of the form "echo [int] anything", takes the integer value as a delay in
|
||||||
|
* seconds before replying, which may be useful to test various
|
||||||
|
* timeout-related things.
|
||||||
|
*
|
||||||
|
* @since 5.9
|
||||||
|
*/
|
||||||
|
public static final String ECHO_COMMAND = "echo";
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
protected final String testUser;
|
protected final String testUser;
|
||||||
|
|
||||||
|
@ -166,6 +179,8 @@ public SshTestGitServer(@NonNull String testUser,
|
||||||
return new GitUploadPackCommand(command, executorService);
|
return new GitUploadPackCommand(command, executorService);
|
||||||
} else if (command.startsWith(RemoteConfig.DEFAULT_RECEIVE_PACK)) {
|
} else if (command.startsWith(RemoteConfig.DEFAULT_RECEIVE_PACK)) {
|
||||||
return new GitReceivePackCommand(command, executorService);
|
return new GitReceivePackCommand(command, executorService);
|
||||||
|
} else if (command.startsWith(ECHO_COMMAND)) {
|
||||||
|
return new EchoCommand(command, executorService);
|
||||||
}
|
}
|
||||||
return new UnknownCommand(command);
|
return new UnknownCommand(command);
|
||||||
});
|
});
|
||||||
|
@ -475,4 +490,52 @@ public void run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple echo command that echoes back the command string. If the first
|
||||||
|
* argument is a positive integer, it's taken as a delay (in seconds) before
|
||||||
|
* replying. Assumes UTF-8 character encoding.
|
||||||
|
*/
|
||||||
|
private static class EchoCommand extends AbstractCommandSupport {
|
||||||
|
|
||||||
|
protected EchoCommand(String command,
|
||||||
|
CloseableExecutorService executorService) {
|
||||||
|
super(command, ThreadUtils.noClose(executorService));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
String[] parts = getCommand().split(" ");
|
||||||
|
int timeout = 0;
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
try {
|
||||||
|
timeout = Integer.parseInt(parts[1]);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// No timeout.
|
||||||
|
}
|
||||||
|
if (timeout > 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(TimeUnit.SECONDS.toMillis(timeout));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
doEcho(getCommand(), getOutputStream());
|
||||||
|
onExit(0);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn(
|
||||||
|
MessageFormat.format("Could not run {0}", getCommand()),
|
||||||
|
e);
|
||||||
|
onExit(-1, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doEcho(String text, OutputStream stream)
|
||||||
|
throws IOException {
|
||||||
|
stream.write(text.getBytes(StandardCharsets.UTF_8));
|
||||||
|
stream.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -198,7 +198,7 @@ public InputStream getErrorStream() {
|
||||||
@Override
|
@Override
|
||||||
public int exitValue() {
|
public int exitValue() {
|
||||||
if (isRunning())
|
if (isRunning())
|
||||||
throw new IllegalStateException();
|
throw new IllegalThreadStateException();
|
||||||
return channel.getExitStatus();
|
return channel.getExitStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -621,6 +621,7 @@ sourceRefDoesntResolveToAnyObject=Source ref {0} doesn''t resolve to any object.
|
||||||
sourceRefNotSpecifiedForRefspec=Source ref not specified for refspec: {0}
|
sourceRefNotSpecifiedForRefspec=Source ref not specified for refspec: {0}
|
||||||
squashCommitNotUpdatingHEAD=Squash commit -- not updating HEAD
|
squashCommitNotUpdatingHEAD=Squash commit -- not updating HEAD
|
||||||
sshCommandFailed=Execution of ssh command ''{0}'' failed with error ''{1}''
|
sshCommandFailed=Execution of ssh command ''{0}'' failed with error ''{1}''
|
||||||
|
sshCommandTimeout=Execution of ssh command ''{0}'' timed out after {1} seconds
|
||||||
sslFailureExceptionMessage=Secure connection to {0} could not be established because of SSL problems
|
sslFailureExceptionMessage=Secure connection to {0} could not be established because of SSL problems
|
||||||
sslFailureInfo=A secure connection to {0} could not be established because the server''s certificate could not be validated.
|
sslFailureInfo=A secure connection to {0} could not be established because the server''s certificate could not be validated.
|
||||||
sslFailureCause=SSL reported: {0}
|
sslFailureCause=SSL reported: {0}
|
||||||
|
|
|
@ -649,6 +649,7 @@ public static JGitText get() {
|
||||||
/***/ public String sourceRefNotSpecifiedForRefspec;
|
/***/ public String sourceRefNotSpecifiedForRefspec;
|
||||||
/***/ public String squashCommitNotUpdatingHEAD;
|
/***/ public String squashCommitNotUpdatingHEAD;
|
||||||
/***/ public String sshCommandFailed;
|
/***/ public String sshCommandFailed;
|
||||||
|
/***/ public String sshCommandTimeout;
|
||||||
/***/ public String sslFailureExceptionMessage;
|
/***/ public String sslFailureExceptionMessage;
|
||||||
/***/ public String sslFailureInfo;
|
/***/ public String sslFailureInfo;
|
||||||
/***/ public String sslFailureCause;
|
/***/ public String sslFailureCause;
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.eclipse.jgit.annotations.Nullable;
|
import org.eclipse.jgit.annotations.Nullable;
|
||||||
import org.eclipse.jgit.errors.CommandFailedException;
|
import org.eclipse.jgit.errors.CommandFailedException;
|
||||||
|
@ -61,11 +62,21 @@ public static String runSshCommand(URIish sshUri,
|
||||||
CommandFailedException failure = null;
|
CommandFailedException failure = null;
|
||||||
@SuppressWarnings("resource")
|
@SuppressWarnings("resource")
|
||||||
MessageWriter stderr = new MessageWriter();
|
MessageWriter stderr = new MessageWriter();
|
||||||
|
@SuppressWarnings("resource")
|
||||||
|
MessageWriter stdout = new MessageWriter();
|
||||||
String out;
|
String out;
|
||||||
try (MessageWriter stdout = new MessageWriter()) {
|
try {
|
||||||
|
long start = System.nanoTime();
|
||||||
session = SshSessionFactory.getInstance().getSession(sshUri,
|
session = SshSessionFactory.getInstance().getSession(sshUri,
|
||||||
provider, fs, 1000 * timeout);
|
provider, fs, 1000 * timeout);
|
||||||
process = session.exec(command, 0);
|
int commandTimeout = timeout;
|
||||||
|
if (timeout > 0) {
|
||||||
|
commandTimeout = checkTimeout(command, timeout, start);
|
||||||
|
}
|
||||||
|
process = session.exec(command, commandTimeout);
|
||||||
|
if (timeout > 0) {
|
||||||
|
commandTimeout = checkTimeout(command, timeout, start);
|
||||||
|
}
|
||||||
errorThread = new StreamCopyThread(process.getErrorStream(),
|
errorThread = new StreamCopyThread(process.getErrorStream(),
|
||||||
stderr.getRawStream());
|
stderr.getRawStream());
|
||||||
errorThread.start();
|
errorThread.start();
|
||||||
|
@ -73,9 +84,15 @@ public static String runSshCommand(URIish sshUri,
|
||||||
stdout.getRawStream());
|
stdout.getRawStream());
|
||||||
outThread.start();
|
outThread.start();
|
||||||
try {
|
try {
|
||||||
// waitFor with timeout has a bug - JSch' exitValue() throws the
|
boolean finished = false;
|
||||||
// wrong exception type :(
|
if (timeout <= 0) {
|
||||||
if (process.waitFor() == 0) {
|
process.waitFor();
|
||||||
|
finished = true;
|
||||||
|
} else {
|
||||||
|
finished = process.waitFor(commandTimeout,
|
||||||
|
TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
if (finished) {
|
||||||
out = stdout.toString();
|
out = stdout.toString();
|
||||||
} else {
|
} else {
|
||||||
out = null; // still running after timeout
|
out = null; // still running after timeout
|
||||||
|
@ -103,15 +120,26 @@ public static String runSshCommand(URIish sshUri,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (process != null) {
|
if (process != null) {
|
||||||
|
try {
|
||||||
if (process.exitValue() != 0) {
|
if (process.exitValue() != 0) {
|
||||||
failure = new CommandFailedException(process.exitValue(),
|
failure = new CommandFailedException(
|
||||||
|
process.exitValue(),
|
||||||
MessageFormat.format(
|
MessageFormat.format(
|
||||||
JGitText.get().sshCommandFailed, command,
|
JGitText.get().sshCommandFailed,
|
||||||
stderr.toString()));
|
command, stderr.toString()));
|
||||||
|
}
|
||||||
|
// It was successful after all
|
||||||
|
out = stdout.toString();
|
||||||
|
} catch (IllegalThreadStateException e) {
|
||||||
|
failure = new CommandFailedException(0,
|
||||||
|
MessageFormat.format(
|
||||||
|
JGitText.get().sshCommandTimeout, command,
|
||||||
|
Integer.valueOf(timeout)));
|
||||||
}
|
}
|
||||||
process.destroy();
|
process.destroy();
|
||||||
}
|
}
|
||||||
stderr.close();
|
stderr.close();
|
||||||
|
stdout.close();
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
SshSessionFactory.getInstance().releaseSession(session);
|
SshSessionFactory.getInstance().releaseSession(session);
|
||||||
}
|
}
|
||||||
|
@ -122,4 +150,17 @@ public static String runSshCommand(URIish sshUri,
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int checkTimeout(String command, int timeout, long since)
|
||||||
|
throws CommandFailedException {
|
||||||
|
long elapsed = System.nanoTime() - since;
|
||||||
|
int newTimeout = timeout
|
||||||
|
- (int) TimeUnit.NANOSECONDS.toSeconds(elapsed);
|
||||||
|
if (newTimeout <= 0) {
|
||||||
|
// All time used up for connecting the session
|
||||||
|
throw new CommandFailedException(0,
|
||||||
|
MessageFormat.format(JGitText.get().sshCommandTimeout,
|
||||||
|
command, Integer.valueOf(timeout)));
|
||||||
|
}
|
||||||
|
return newTimeout;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue