Add timeout to ssh client
authorDmitry Neverov <dmitry.neverov@gmail.com>
Tue, 23 Jan 2018 16:01:36 +0000 (17:01 +0100)
committerDmitry Neverov <dmitry.neverov@gmail.com>
Tue, 23 Jan 2018 16:01:36 +0000 (17:01 +0100)
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/BuildContext.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/Context.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/JSchClient.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/NoBuildContext.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/impl/SshHandler.java
git-agent/src/org/jetbrains/git4idea/ssh/GitSSHHandler.java

index b1e94103efb59c27ee832c1328138fc5bb293ea7..fe6751ebc15d9ace6ac252ca3a8418fe28ee6fe7 100644 (file)
@@ -60,4 +60,10 @@ public class BuildContext implements Context {
   public boolean isProvideCredHelper() {
     return myConfig.isProvideCredHelper();
   }
+
+  @Nullable
+  @Override
+  public AgentPluginConfig getConfig() {
+    return myConfig;
+  }
 }
index 926d365264da081c869b09fd50b6d2881634d644..698f1ca5abe4763ab0d2b8c92965e1f5ad83f23e 100644 (file)
@@ -32,4 +32,7 @@ public interface Context {
 
   boolean isProvideCredHelper();
 
+  @Nullable
+  AgentPluginConfig getConfig();
+
 }
index 8304e19f4327c795fe03beb65ed2f17cbe193149..cdb92adf5a705a0cdca23dd3b39b47effd2e5f9e 100644 (file)
@@ -33,10 +33,9 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.security.Security;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
 
 public class JSchClient {
 
@@ -170,17 +169,25 @@ public class JSchClient {
       channel.setInputStream(System.in);
       channel.setErrStream(System.err);
       InputStream input = channel.getInputStream();
-      channel.connect();
+      Integer timeoutSeconds = getTimeoutSeconds();
+      if (timeoutSeconds != null) {
+        channel.connect(timeoutSeconds * 1000);
+      } else {
+        channel.connect();
+      }
+
 
       if (!channel.isConnected()) {
         throw new IOException("Connection failed");
       }
 
-      byte[] buffer = new byte[BUF_SIZE];
-      int count;
-      while (channel.isConnected() && !channel.isClosed() && (count = input.read(buffer)) != -1) {
-        System.out.write(buffer, 0, count);
+      Copy copyThread = new Copy(channel, input);
+      if (timeoutSeconds != null) {
+        new Timer(copyThread, timeoutSeconds * 1000).start();
       }
+      copyThread.start();
+      copyThread.join();
+      copyThread.rethrowError();
     } finally {
       if (channel != null)
         channel.disconnect();
@@ -190,6 +197,94 @@ public class JSchClient {
   }
 
 
+  @Nullable
+  private Integer getTimeoutSeconds() {
+    String timeout = System.getenv(GitSSHHandler.TEAMCITY_SSH_IDLE_TIMEOUT_SECONDS);
+    if (timeout == null)
+      return null;
+    try {
+      return Integer.parseInt(timeout);
+    } catch (NumberFormatException e) {
+      myLogger.log(Logger.WARN, "Failed to parse idle timeout: '" + timeout + "'");
+      return null;
+    }
+  }
+
+
+  private class Timer extends Thread {
+    private final long myThresholdNanos;
+    private volatile Copy myCopyThread;
+    Timer(@NotNull Copy copyThread, long timeoutSeconds) {
+      myCopyThread = copyThread;
+      myThresholdNanos = TimeUnit.SECONDS.toNanos(timeoutSeconds);
+      setDaemon(true);
+      setName("Timer");
+    }
+
+    @Override
+    public void run() {
+      boolean logged = false;
+      long sleepInterval = Math.min(TimeUnit.SECONDS.toMillis(10), TimeUnit.NANOSECONDS.toMillis(myThresholdNanos));
+      //noinspection InfiniteLoopStatement: it is a daemon thread and doesn't prevent process from termination
+      while (true) {
+        if (System.nanoTime() - myCopyThread.getTimestamp() > myThresholdNanos) {
+          if (!logged) {
+            myLogger.log(Logger.ERROR, String.format("Timeout error: no activity for %s seconds", TimeUnit.NANOSECONDS.toSeconds(myThresholdNanos)));
+            logged = true;
+          }
+          myCopyThread.interrupt();
+        } else {
+          try {
+            Thread.sleep(sleepInterval);
+          } catch (Exception e) {
+            //ignore
+          }
+        }
+      }
+    }
+  }
+
+
+  private class Copy extends Thread {
+    private final ChannelExec myChannel;
+    private final InputStream myInput;
+    private final AtomicLong myTimestamp = new AtomicLong(System.nanoTime());
+    private volatile Exception myError;
+    Copy(@NotNull ChannelExec channel, @NotNull InputStream input) {
+      myChannel = channel;
+      myInput = input;
+      setName("Copy");
+    }
+
+    @Override
+    public void run() {
+      byte[] buffer = new byte[BUF_SIZE];
+      int count;
+      try {
+        while (myChannel.isConnected() && !myChannel.isClosed() && (count = myInput.read(buffer)) != -1) {
+          System.out.write(buffer, 0, count);
+          myTimestamp.set(System.nanoTime());
+          if (System.out.checkError()) {
+            myLogger.log(Logger.ERROR, "Error while writing to stdout");
+            throw new IOException("Error while writing to stdout");
+          }
+        }
+      } catch (Exception e) {
+        myError = e;
+      }
+    }
+
+    long getTimestamp() {
+      return myTimestamp.get();
+    }
+
+    void rethrowError() throws Exception {
+      if (myError != null)
+        throw myError;
+    }
+  }
+
+
   private static class StdErrLogger implements Logger {
     private final SimpleDateFormat myDateFormat = new SimpleDateFormat("[HH:mm:ss.SSS]");
     @Override
index 42f8967cfe1af7497c328ad88ffe0356e98e040c..b47ed893068256f0c7ea6b7270c030306b8be084 100644 (file)
@@ -43,4 +43,10 @@ public class NoBuildContext implements Context {
   public boolean isProvideCredHelper() {
     return false;
   }
+
+  @Nullable
+  @Override
+  public AgentPluginConfig getConfig() {
+    return null;
+  }
 }
index ed1f8b1684381304e4e355e78a637db3c1223954..f76e9cc966b7cd67374c0eaaedd81d90b01376c7 100644 (file)
@@ -18,6 +18,7 @@ package jetbrains.buildServer.buildTriggers.vcs.git.agent.command.impl;
 
 import jetbrains.buildServer.buildTriggers.vcs.git.AuthSettings;
 import jetbrains.buildServer.buildTriggers.vcs.git.AuthenticationMethod;
+import jetbrains.buildServer.buildTriggers.vcs.git.agent.AgentPluginConfig;
 import jetbrains.buildServer.buildTriggers.vcs.git.agent.Context;
 import jetbrains.buildServer.buildTriggers.vcs.git.agent.GitCommandLine;
 import jetbrains.buildServer.log.Loggers;
@@ -100,6 +101,9 @@ public class SshHandler implements GitSSHService.Handler {
     if (ctx.getPreferredSshAuthMethods() != null)
       cmd.addEnvParam(GitSSHHandler.TEAMCITY_SSH_PREFERRED_AUTH_METHODS, ctx.getPreferredSshAuthMethods());
     cmd.addEnvParam(GitSSHHandler.TEAMCITY_DEBUG_SSH, String.valueOf(Loggers.VCS.isDebugEnabled()));
+    AgentPluginConfig config = ctx.getConfig();
+    if (config != null)
+      cmd.addEnvParam(GitSSHHandler.TEAMCITY_SSH_IDLE_TIMEOUT_SECONDS, String.valueOf(config.getIdleTimeoutSeconds()));
     String teamCityVersion = getTeamCityVersion();
     if (teamCityVersion != null) {
       cmd.addEnvParam(GitSSHHandler.TEAMCITY_VERSION, teamCityVersion);
index 9a4b7d411e19bdc7b2bc5fbe44aea2c9d09113f3..021284bdf5d89e8fc370126f02997ae12f427244 100644 (file)
@@ -55,6 +55,7 @@ public interface GitSSHHandler {
   String TEAMCITY_DEBUG_SSH = "TEAMCITY_DEBUG_SSH";
   String TEAMCITY_SSH_MAC_TYPE = "TEAMCITY_SSH_MAC_TYPE";
   String TEAMCITY_SSH_PREFERRED_AUTH_METHODS = "TEAMCITY_SSH_PREFERRED_AUTH_METHODS";
+  String TEAMCITY_SSH_IDLE_TIMEOUT_SECONDS = "TEAMCITY_SSH_IDLE_TIMEOUT_SECONDS";
   String TEAMCITY_VERSION = "TEAMCITY_VERSION";
 
   /**