Java tutorial
/* * The MIT License * * Copyright (c) 2004-, all the contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.plugins.sshslaves; import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator; import com.cloudbees.jenkins.plugins.sshcredentials.SSHUser; import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; import com.cloudbees.plugins.credentials.Credentials; import com.cloudbees.plugins.credentials.CredentialsMatcher; import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.CredentialsStore; import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.domains.HostnamePortRequirement; import com.cloudbees.plugins.credentials.domains.SchemeRequirement; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import com.trilead.ssh2.ChannelCondition; import com.trilead.ssh2.Connection; import com.trilead.ssh2.SCPClient; import com.trilead.ssh2.SFTPv3Client; import com.trilead.ssh2.SFTPv3FileAttributes; import com.trilead.ssh2.Session; import com.trilead.ssh2.transport.TransportManager; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.AbortException; import hudson.EnvVars; import hudson.Extension; import hudson.Util; import hudson.model.Descriptor; import hudson.model.Hudson; import hudson.model.ItemGroup; import hudson.model.JDK; import hudson.model.Node; import hudson.model.Slave; import hudson.model.TaskListener; import hudson.security.ACL; import hudson.slaves.ComputerLauncher; import hudson.slaves.EnvironmentVariablesNodeProperty; import hudson.slaves.NodeProperty; import hudson.slaves.NodePropertyDescriptor; import hudson.slaves.SlaveComputer; import hudson.tools.JDKInstaller; import hudson.tools.JDKInstaller.CPU; import hudson.tools.JDKInstaller.Platform; import hudson.tools.ToolLocationNodeProperty; import hudson.tools.ToolLocationNodeProperty.ToolLocation; import hudson.util.DescribableList; import hudson.util.IOException2; import hudson.util.ListBoxModel; import hudson.util.NullStream; import hudson.util.Secret; import jenkins.model.Jenkins; import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.TeeOutputStream; import org.apache.commons.lang.StringUtils; import org.kohsuke.putty.PuTTYKey; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.StringWriter; import java.lang.InterruptedException; import java.lang.reflect.Field; import java.net.URL; import java.text.MessageFormat; import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; import java.util.regex.Pattern; import static com.cloudbees.plugins.credentials.CredentialsMatchers.*; import com.cloudbees.plugins.credentials.common.StandardUsernameListBoxModel; import edu.umd.cs.findbugs.annotations.CheckForNull; import static hudson.Util.*; import hudson.model.Computer; import hudson.security.AccessControlled; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import static java.util.logging.Level.*; /** * A computer launcher that tries to start a linux slave by opening an SSH connection and trying to find java. */ public class SSHLauncher extends ComputerLauncher { /** * The scheme requirement. */ public static final SchemeRequirement SSH_SCHEME = new SchemeRequirement("ssh"); public static final String JDKVERSION = "jdk-7u80"; public static final String DEFAULT_JDK = JDKVERSION + "-oth-JPR"; /** * @deprecated * Subtype of {@link JDKInstaller} causes JENKINS-10641. */ public static class DefaultJDKInstaller extends JDKInstaller { public DefaultJDKInstaller() { super(DEFAULT_JDK, true); } public Object readResolve() { return new JDKInstaller(DEFAULT_JDK, true); } } /** * Field host */ private final String host; /** * Field port */ private final int port; /** * The id of the credentials to use. */ private String credentialsId; /** * Transient stash of the credentials to use, mostly just for providing floating user object. */ private transient StandardUsernameCredentials credentials; /** * Field username * @deprecated */ @Deprecated private transient String username; /** * Field password * @deprecated */ @Deprecated private transient Secret password; /** * File path of the private key. * @deprecated */ @Deprecated private transient String privatekey; /** * Field jvmOptions. */ private final String jvmOptions; /** * Field javaPath. */ public final String javaPath; /** * to install JDK, keep this field null. This avoids baking the default value into the persisted form. * @see #getJDKInstaller() */ private JDKInstaller jdk = null; /** * SSH connection to the slave. */ private transient Connection connection; /** * The session inside {@link #connection} that controls the slave process. */ private transient Session session; /** * Field prefixStartSlaveCmd. */ public final String prefixStartSlaveCmd; /** * Field suffixStartSlaveCmd. */ public final String suffixStartSlaveCmd; /** * Field launchTimeoutSeconds. */ public final Integer launchTimeoutSeconds; /** * Field maxNumRetries. */ public final Integer maxNumRetries; /** * Field retryWaitTime. */ public final Integer retryWaitTime; /** * Constructor SSHLauncher creates a new SSHLauncher instance. * * @param host The host to connect to. * @param port The port to connect on. * @param credentialsId The credentials id to connect as. * @param jvmOptions Options passed to the java vm. * @param javaPath Path to the host jdk installation. If <code>null</code> the jdk will be auto detected or installed by the JDKInstaller. * @param prefixStartSlaveCmd This will prefix the start slave command. For instance if you want to execute the command with a different shell. * @param suffixStartSlaveCmd This will suffix the start slave command. * @param launchTimeoutSeconds Launch timeout in seconds * @param maxNumRetries The number of times to retry connection if the SSH connection is refused during initial connect * @param retryWaitTime The number of seconds to wait between retries */ @DataBoundConstructor public SSHLauncher(String host, int port, String credentialsId, String jvmOptions, String javaPath, String prefixStartSlaveCmd, String suffixStartSlaveCmd, Integer launchTimeoutSeconds, Integer maxNumRetries, Integer retryWaitTime) { this(host, port, lookupSystemCredentials(credentialsId), jvmOptions, javaPath, null, prefixStartSlaveCmd, suffixStartSlaveCmd, launchTimeoutSeconds, maxNumRetries, retryWaitTime); } /** @deprecated Use {@link #SSHLauncher(String, int, String, String, String, String, String, Integer, Integer, Integer)} instead. */ @Deprecated public SSHLauncher(String host, int port, String credentialsId, String jvmOptions, String javaPath, String prefixStartSlaveCmd, String suffixStartSlaveCmd, Integer launchTimeoutSeconds) { this(host, port, lookupSystemCredentials(credentialsId), jvmOptions, javaPath, null, prefixStartSlaveCmd, suffixStartSlaveCmd, launchTimeoutSeconds, null, null); } /** * @deprecated use {@link SSHLauncher#(String,int,String,String,String,String,String,Integer)} */ @Deprecated public SSHLauncher(String host, int port, String credentialsId, String jvmOptions, String javaPath, String prefixStartSlaveCmd, String suffixStartSlaveCmd) { this(host, port, lookupSystemCredentials(credentialsId), jvmOptions, javaPath, null, prefixStartSlaveCmd, suffixStartSlaveCmd, null, null, null); } public static StandardUsernameCredentials lookupSystemCredentials(String credentialsId) { return CredentialsMatchers .firstOrNull( CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, Jenkins.getInstance(), ACL.SYSTEM, SSH_SCHEME), CredentialsMatchers.withId(credentialsId)); } public static StandardUsernameCredentials lookupSystemCredentials(String credentialsId, String host, int port) { return CredentialsMatchers.firstOrNull( CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, Jenkins.getInstance(), ACL.SYSTEM, SSH_SCHEME, new HostnamePortRequirement(host, port)), CredentialsMatchers.withId(credentialsId)); } /** * Constructor SSHLauncher creates a new SSHLauncher instance. * * @param host The host to connect to. * @param port The port to connect on. * @param credentials The credentials to connect as. * @param jvmOptions Options passed to the java vm. * @param javaPath Path to the host jdk installation. If <code>null</code> the jdk will be auto detected or installed by the JDKInstaller. * @param prefixStartSlaveCmd This will prefix the start slave command. For instance if you want to execute the command with a different shell. * @param suffixStartSlaveCmd This will suffix the start slave command. * @param launchTimeoutSeconds Launch timeout in seconds * @param maxNumRetries The number of times to retry connection if the SSH connection is refused during initial connect * @param retryWaitTime The number of seconds to wait between retries */ public SSHLauncher(String host, int port, StandardUsernameCredentials credentials, String jvmOptions, String javaPath, String prefixStartSlaveCmd, String suffixStartSlaveCmd, Integer launchTimeoutSeconds, Integer maxNumRetries, Integer retryWaitTime) { this(host, port, credentials, jvmOptions, javaPath, null, prefixStartSlaveCmd, suffixStartSlaveCmd, launchTimeoutSeconds, maxNumRetries, retryWaitTime); } /** @deprecated Use {@link #SSHLauncher(String, int, StandardUsernameCredentials, String, String, String, String, Integer, Integer, Integer)} instead. */ @Deprecated public SSHLauncher(String host, int port, StandardUsernameCredentials credentials, String jvmOptions, String javaPath, String prefixStartSlaveCmd, String suffixStartSlaveCmd, Integer launchTimeoutSeconds) { this(host, port, credentials, jvmOptions, javaPath, null, prefixStartSlaveCmd, suffixStartSlaveCmd, launchTimeoutSeconds, null, null); } /** @deprecated Use {@link #SSHLauncher(String, int, StandardUsernameCredentials, String, String, String, String, Integer, Integer, Integer)} instead. */ @Deprecated public SSHLauncher(String host, int port, StandardUsernameCredentials credentials, String jvmOptions, String javaPath, String prefixStartSlaveCmd, String suffixStartSlaveCmd) { this(host, port, credentials, jvmOptions, javaPath, prefixStartSlaveCmd, suffixStartSlaveCmd, null, null, null); } /** @deprecated Use {@link #SSHLauncher(String, int, StandardUsernameCredentials, String, String, String, String)} instead. */ @Deprecated public SSHLauncher(String host, int port, SSHUser credentials, String jvmOptions, String javaPath, String prefixStartSlaveCmd, String suffixStartSlaveCmd) { this(host, port, (StandardUsernameCredentials) credentials, jvmOptions, javaPath, prefixStartSlaveCmd, suffixStartSlaveCmd, null, null, null); } /** * Constructor SSHLauncher creates a new SSHLauncher instance. * * @param host The host to connect to. * @param port The port to connect on. * @param username The username to connect as. * @param password The password to connect with. * @param privatekey The ssh privatekey to connect with. * @param jvmOptions Options passed to the java vm. * @param javaPath Path to the host jdk installation. If <code>null</code> the jdk will be auto detected or installed by the JDKInstaller. * @param prefixStartSlaveCmd This will prefix the start slave command. For instance if you want to execute the command with a different shell. * @param suffixStartSlaveCmd This will suffix the start slave command. * @deprecated use the {@link StandardUsernameCredentials} based version */ @Deprecated public SSHLauncher(String host, int port, String username, String password, String privatekey, String jvmOptions, String javaPath, String prefixStartSlaveCmd, String suffixStartSlaveCmd) { this(host, port, username, password, privatekey, jvmOptions, javaPath, null, prefixStartSlaveCmd, suffixStartSlaveCmd); } /** * Constructor SSHLauncher creates a new SSHLauncher instance. * * @param host The host to connect to. * @param port The port to connect on. * @param username The username to connect as. * @param password The password to connect with. * @param privatekey The ssh privatekey to connect with. * @param jvmOptions Options passed to the java vm. * @param javaPath Path to the host jdk installation. If <code>null</code> the jdk will be auto detected or installed by the JDKInstaller. * @param jdkInstaller The jdk installer that will be used if no java vm is found on the specified host. If <code>null</code> the {@link DefaultJDKInstaller} will be used. * @param prefixStartSlaveCmd This will prefix the start slave command. For instance if you want to execute the command with a different shell. * @param suffixStartSlaveCmd This will suffix the start slave command. * @deprecated use the {@link StandardUsernameCredentials} based version */ @Deprecated public SSHLauncher(String host, int port, String username, String password, String privatekey, String jvmOptions, String javaPath, JDKInstaller jdkInstaller, String prefixStartSlaveCmd, String suffixStartSlaveCmd) { this.host = host; this.jvmOptions = fixEmpty(jvmOptions); this.port = port == 0 ? 22 : port; this.username = fixEmpty(username); this.password = Secret.fromString(fixEmpty(password)); this.privatekey = fixEmpty(privatekey); this.credentials = null; this.credentialsId = null; this.javaPath = fixEmpty(javaPath); if (jdkInstaller != null) { this.jdk = jdkInstaller; } this.prefixStartSlaveCmd = fixEmpty(prefixStartSlaveCmd); this.suffixStartSlaveCmd = fixEmpty(suffixStartSlaveCmd); this.launchTimeoutSeconds = null; this.maxNumRetries = null; this.retryWaitTime = null; } /** * Constructor SSHLauncher creates a new SSHLauncher instance. * * @param host The host to connect to. * @param port The port to connect on. * @param credentials The credentials to connect as. * @param jvmOptions Options passed to the java vm. * @param javaPath Path to the host jdk installation. If <code>null</code> the jdk will be auto detected or installed by the JDKInstaller. * @param jdkInstaller The jdk installer that will be used if no java vm is found on the specified host. If <code>null</code> the {@link DefaultJDKInstaller} will be used. * @param prefixStartSlaveCmd This will prefix the start slave command. For instance if you want to execute the command with a different shell. * @param suffixStartSlaveCmd This will suffix the start slave command. * @deprecated */ @Deprecated public SSHLauncher(String host, int port, StandardUsernameCredentials credentials, String jvmOptions, String javaPath, JDKInstaller jdkInstaller, String prefixStartSlaveCmd, String suffixStartSlaveCmd) { this(host, port, credentials, jvmOptions, javaPath, jdkInstaller, prefixStartSlaveCmd, suffixStartSlaveCmd, null, null, null); } /** * Constructor SSHLauncher creates a new SSHLauncher instance. * * @param host The host to connect to. * @param port The port to connect on. * @param credentials The credentials to connect as. * @param jvmOptions Options passed to the java vm. * @param javaPath Path to the host jdk installation. If <code>null</code> the jdk will be auto detected or installed by the JDKInstaller. * @param jdkInstaller The jdk installer that will be used if no java vm is found on the specified host. If <code>null</code> the {@link DefaultJDKInstaller} will be used. * @param prefixStartSlaveCmd This will prefix the start slave command. For instance if you want to execute the command with a different shell. * @param suffixStartSlaveCmd This will suffix the start slave command. * @param launchTimeoutSeconds Launch timeout in seconds * @param maxNumRetries The number of times to retry connection if the SSH connection is refused during initial connect * @param retryWaitTime The number of seconds to wait between retries */ public SSHLauncher(String host, int port, StandardUsernameCredentials credentials, String jvmOptions, String javaPath, JDKInstaller jdkInstaller, String prefixStartSlaveCmd, String suffixStartSlaveCmd, Integer launchTimeoutSeconds, Integer maxNumRetries, Integer retryWaitTime) { this.host = host; this.jvmOptions = fixEmpty(jvmOptions); this.port = port == 0 ? 22 : port; this.username = null; this.password = null; this.privatekey = null; this.credentials = credentials; this.credentialsId = credentials == null ? null : credentials.getId(); this.javaPath = fixEmpty(javaPath); if (jdkInstaller != null) { this.jdk = jdkInstaller; } this.prefixStartSlaveCmd = fixEmpty(prefixStartSlaveCmd); this.suffixStartSlaveCmd = fixEmpty(suffixStartSlaveCmd); this.launchTimeoutSeconds = launchTimeoutSeconds == null || launchTimeoutSeconds <= 0 ? null : launchTimeoutSeconds; this.maxNumRetries = maxNumRetries != null && maxNumRetries > 0 ? maxNumRetries : 0; this.retryWaitTime = retryWaitTime != null && retryWaitTime > 0 ? retryWaitTime : 0; } /** @deprecated Use {@link #SSHLauncher(String, int, StandardUsernameCredentials, String, String, JDKInstaller, String, String)} instead. */ @Deprecated public SSHLauncher(String host, int port, SSHUser credentials, String jvmOptions, String javaPath, JDKInstaller jdkInstaller, String prefixStartSlaveCmd, String suffixStartSlaveCmd) { this(host, port, (StandardUsernameCredentials) credentials, jvmOptions, javaPath, jdkInstaller, prefixStartSlaveCmd, suffixStartSlaveCmd); } public SSHLauncher(String host, int port, String username, String password, String privatekey, String jvmOptions) { this(host, port, username, password, privatekey, jvmOptions, null, null, null); } public String getCredentialsId() { return credentialsId; } public StandardUsernameCredentials getCredentials() { String credentialsId = this.credentialsId == null ? (this.credentials == null ? null : this.credentials.getId()) : this.credentialsId; try { // only ever want from the system // lookup every time so that we always have the latest StandardUsernameCredentials credentials = credentialsId != null ? SSHLauncher.lookupSystemCredentials(credentialsId) : null; if (credentials != null) { this.credentials = credentials; return credentials; } } catch (Throwable t) { // ignore } if (credentials == null) { if (credentialsId == null && (username != null || password != null || privatekey != null)) { credentials = upgrade(username, password, privatekey, host); this.credentialsId = credentials.getId(); } } return this.credentials; } /** * Take the legacy local credential configuration and create an equivalent global {@link StandardUsernameCredentials}. */ @NonNull static synchronized StandardUsernameCredentials upgrade(String username, Secret password, String privatekey, String description) { username = StringUtils.isEmpty(username) ? System.getProperty("user.name") : username; StandardUsernameCredentials u = retrieveExistingCredentials(username, password, privatekey); if (u != null) return u; // no matching, so make our own. if (StringUtils.isEmpty(privatekey) && (password == null || StringUtils.isEmpty(password.getPlainText()))) { // no private key nor password set, must be user's own SSH key u = new BasicSSHUserPrivateKey(CredentialsScope.SYSTEM, null, username, new BasicSSHUserPrivateKey.UsersPrivateKeySource(), null, description); } else if (StringUtils.isNotEmpty(privatekey)) { u = new BasicSSHUserPrivateKey(CredentialsScope.SYSTEM, null, username, new BasicSSHUserPrivateKey.FileOnMasterPrivateKeySource(privatekey), password == null ? null : password.getEncryptedValue(), MessageFormat.format("{0} - key file: {1}", description, privatekey)); } else { u = new UsernamePasswordCredentialsImpl(CredentialsScope.SYSTEM, null, description, username, password == null ? null : password.getEncryptedValue()); } final SecurityContext securityContext = ACL.impersonate(ACL.SYSTEM); try { CredentialsStore s = CredentialsProvider.lookupStores(Jenkins.getInstance()).iterator().next(); try { s.addCredentials(Domain.global(), u); return u; } catch (IOException e) { // ignore } } finally { SecurityContextHolder.setContext(securityContext); } return u; } private static StandardUsernameCredentials retrieveExistingCredentials(String username, final Secret password, String privatekey) { final String privatekeyContent = getPrivateKeyContent(password, privatekey); return CredentialsMatchers .firstOrNull( CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, Hudson.getInstance(), ACL.SYSTEM, SSH_SCHEME), allOf(withUsername(username), new CredentialsMatcher() { public boolean matches(@NonNull Credentials item) { if (item instanceof StandardUsernamePasswordCredentials && password != null && StandardUsernamePasswordCredentials.class.cast(item).getPassword() .equals(password)) { return true; } if (privatekeyContent != null && item instanceof SSHUserPrivateKey) { for (String key : SSHUserPrivateKey.class.cast(item).getPrivateKeys()) { if (pemKeyEquals(key, privatekeyContent)) { return true; } } } return false; } })); } /** * Returns {@code true} if they two keys are the same. There are two levels of comparison: the first is a simple * string comparison with all whitespace removed. If that fails then the Base64 decoded bytes of the first * PEM entity will be compared (to allow for comments in the key outside the PEM boundaries) * * @param key1 the first key * @param key2 the second key * @return {@code true} if they two keys are the same. */ private static boolean pemKeyEquals(String key1, String key2) { key1 = StringUtils.trim(key1); key2 = StringUtils.trim(key2); return StringUtils.equals(key1.replaceAll("\\s+", ""), key2.replace("\\s+", "")) || Arrays.equals(quickNDirtyExtract(key1), quickNDirtyExtract(key2)); } /** * Extract the bytes of the first PEM encoded key in a string. This is a quick and dirty method just to * establish if two keys are equal, we do not do any serious decoding of the key and this method could give "issues" * but should be very unlikely to result in a false positive match. * * @param key the key to extract. * @return the base64 decoded bytes from the key after discarding the key type and any header information. */ private static byte[] quickNDirtyExtract(String key) { StringBuilder builder = new StringBuilder(key.length()); boolean begin = false; boolean header = false; for (String line : StringUtils.split(key, "\n")) { line = line.trim(); if (line.startsWith("---") && line.endsWith("---")) { if (begin && line.contains("---END")) { break; } if (!begin && line.contains("---BEGIN")) { header = true; begin = true; continue; } } if (StringUtils.isBlank(line)) { header = false; continue; } if (!header) { builder.append(line); } } return Base64.decodeBase64(builder.toString()); } private static String getPrivateKeyContent(Secret password, String privatekey) { privatekey = Util.fixEmpty(privatekey); if (privatekey != null) { try { File key = new File(privatekey); if (key.exists()) { if (PuTTYKey.isPuTTYKeyFile(key)) { return Util.fixEmptyAndTrim(new PuTTYKey(key, password.getPlainText()).toOpenSSH()); } else { return Util.fixEmptyAndTrim(FileUtils.readFileToString(key)); } } } catch (Throwable t) { LOGGER.warning("invalid private key file " + privatekey); } } return null; } /** * {@inheritDoc} */ @Override public boolean isLaunchSupported() { return true; } /** * Gets the JVM Options used to launch the slave JVM. * @return */ public String getJvmOptions() { return jvmOptions == null ? "" : jvmOptions; } /** * Gets the optionnal java command to use to launch the slave JVM. * @return */ public String getJavaPath() { return javaPath == null ? "" : javaPath; } /** * Gets the formatted current time stamp. * * @return the formatted current time stamp. */ protected String getTimestamp() { return String.format("[%1$tD %1$tT]", new Date()); } /** * Returns the remote root workspace (without trailing slash). * * @param computer The slave computer to get the root workspace of. * * @return the remote root workspace (without trailing slash). */ @CheckForNull private static String getWorkingDirectory(SlaveComputer computer) { return getWorkingDirectory(computer.getNode()); } @CheckForNull private static String getWorkingDirectory(@CheckForNull Slave slave) { if (slave == null) { return null; } String workingDirectory = slave.getRemoteFS(); while (workingDirectory.endsWith("/")) { workingDirectory = workingDirectory.substring(0, workingDirectory.length() - 1); } return workingDirectory; } /** * {@inheritDoc} */ @Override public synchronized void launch(final SlaveComputer computer, final TaskListener listener) throws InterruptedException { connection = new Connection(host, port); ExecutorService executorService = Executors.newSingleThreadExecutor(); Set<Callable<Boolean>> callables = new HashSet<Callable<Boolean>>(); callables.add(new Callable<Boolean>() { public Boolean call() throws InterruptedException { Boolean rval = Boolean.FALSE; try { openConnection(listener); verifyNoHeaderJunk(listener); reportEnvironment(listener); String java = resolveJava(computer, listener); final String workingDirectory = getWorkingDirectory(computer); if (workingDirectory == null) { listener.error("Cannot get the working directory for " + computer); return Boolean.FALSE; } copySlaveJar(listener, workingDirectory); startSlave(computer, listener, java, workingDirectory); PluginImpl.register(connection); rval = Boolean.TRUE; } catch (RuntimeException e) { e.printStackTrace(listener.error(Messages.SSHLauncher_UnexpectedError())); } catch (Error e) { e.printStackTrace(listener.error(Messages.SSHLauncher_UnexpectedError())); } catch (IOException e) { e.printStackTrace(listener.getLogger()); } finally { return rval; } } }); final Node node = computer.getNode(); final String nodeName = node != null ? node.getNodeName() : "unknown"; try { long time = System.currentTimeMillis(); List<Future<Boolean>> results; if (this.getLaunchTimeoutMillis() > 0) { results = executorService.invokeAll(callables, this.getLaunchTimeoutMillis(), TimeUnit.MILLISECONDS); } else { results = executorService.invokeAll(callables); } long duration = System.currentTimeMillis() - time; Boolean res; try { res = results.get(0).get(); } catch (ExecutionException e) { res = Boolean.FALSE; } if (!res) { System.out.println( Messages.SSHLauncher_LaunchFailedDuration(getTimestamp(), nodeName, host, duration)); listener.getLogger().println(getTimestamp() + " Launch failed - cleaning up connection"); cleanupConnection(listener); } else { System.out.println( Messages.SSHLauncher_LaunchCompletedDuration(getTimestamp(), nodeName, host, duration)); } executorService.shutdown(); } catch (InterruptedException e) { System.out.println(Messages.SSHLauncher_LaunchFailed(getTimestamp(), nodeName, host)); } } /** * Called to terminate the SSH connection. Used liberally when we back out from an error. */ private void cleanupConnection(TaskListener listener) { // we might be called multiple times from multiple finally/catch block, if (connection != null) { connection.close(); connection = null; listener.getLogger().println(Messages.SSHLauncher_ConnectionClosed(getTimestamp())); } } /** * return javaPath if specified in the configuration. * Finds local Java, and if none exist, install one. */ protected String resolveJava(SlaveComputer computer, TaskListener listener) throws InterruptedException, IOException2 { if (StringUtils.isNotBlank(javaPath)) { return expandExpression(computer, javaPath); } final String workingDirectory = getWorkingDirectory(computer); if (workingDirectory == null) { throw new IOException2("Cannot retrieve a working directory of " + computer, null); } List<String> tried = new ArrayList<String>(); for (JavaProvider provider : JavaProvider.all()) { for (String javaCommand : provider.getJavas(computer, listener, connection)) { LOGGER.fine("Trying Java at " + javaCommand); try { tried.add(javaCommand); return checkJavaVersion(listener, javaCommand); } catch (IOException e) { LOGGER.log(FINE, "Failed to check the Java version", e); // try the next one } } } // attempt auto JDK installation try { return attemptToInstallJDK(listener, workingDirectory); } catch (IOException e) { throw new IOException2("Could not find any known supported java version in " + tried + ", and we also failed to install JDK as a fallback", e); } } private String expandExpression(SlaveComputer computer, String expression) { return getEnvVars(computer).expand(expression); } private EnvVars getEnvVars(SlaveComputer computer) { final EnvVars global = getEnvVars(Hudson.getInstance()); final Node node = computer.getNode(); final EnvVars local = node != null ? getEnvVars(node) : null; if (global != null) { if (local != null) { final EnvVars merged = new EnvVars(global); merged.overrideAll(local); return merged; } else { return global; } } else if (local != null) { return local; } else { return new EnvVars(); } } private EnvVars getEnvVars(Hudson h) { return getEnvVars(h.getGlobalNodeProperties()); } private EnvVars getEnvVars(Node n) { return getEnvVars(n.getNodeProperties()); } private EnvVars getEnvVars(DescribableList<NodeProperty<?>, NodePropertyDescriptor> dl) { final EnvironmentVariablesNodeProperty evnp = dl.get(EnvironmentVariablesNodeProperty.class); if (evnp == null) { return null; } return evnp.getEnvVars(); } /** * Makes sure that SSH connection won't produce any unwanted text, which will interfere with sftp execution. */ private void verifyNoHeaderJunk(TaskListener listener) throws IOException, InterruptedException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); connection.exec("true", baos); final String s; //TODO: Seems we need to retrieve the encoding from the connection destination try { s = baos.toString(Charset.defaultCharset().name()); } catch (UnsupportedEncodingException ex) { // Should not happen throw new IOException("Default encoding is unsupported", ex); } if (s.length() != 0) { listener.getLogger().println(Messages.SSHLauncher_SSHHeeaderJunkDetected()); listener.getLogger().println(s); throw new AbortException(); } } private JDKInstaller getJDKInstaller() { return jdk != null ? jdk : new JDKInstaller(SSHLauncher.DEFAULT_JDK, true); } /** * Attempts to install JDK, and return the path to Java. */ private String attemptToInstallJDK(TaskListener listener, String workingDirectory) throws IOException, InterruptedException { ByteArrayOutputStream unameOutput = new ByteArrayOutputStream(); if (connection.exec("uname -a", new TeeOutputStream(unameOutput, listener.getLogger())) != 0) throw new IOException("Failed to run 'uname' to obtain the environment"); // guess the platform from uname output. I don't use the specific options because I'm not sure // if various platforms have the consistent options // // === some of the output collected ==== // Linux bear 2.6.28-15-generic #49-Ubuntu SMP Tue Aug 18 19:25:34 UTC 2009 x86_64 GNU/Linux // Linux wssqe20 2.6.24-24-386 #1 Tue Aug 18 16:24:26 UTC 2009 i686 GNU/Linux // SunOS hudson 5.11 snv_79a i86pc i386 i86pc // SunOS legolas 5.9 Generic_112233-12 sun4u sparc SUNW,Sun-Fire-280R // CYGWIN_NT-5.1 franz 1.7.0(0.185/5/3) 2008-07-22 19:09 i686 Cygwin // Windows_NT WINXPIE7 5 01 586 // (this one is from MKS) //TODO: Seems we need to retrieve the encoding from the connection destination final String uname; try { uname = unameOutput.toString(Charset.defaultCharset().name()); } catch (UnsupportedEncodingException ex) { // Should not happen throw new IOException("Default encoding is unsupported", ex); } Platform p = null; CPU cpu = null; if (uname.contains("GNU/Linux")) p = Platform.LINUX; if (uname.contains("SunOS")) p = Platform.SOLARIS; if (uname.contains("CYGWIN")) p = Platform.WINDOWS; if (uname.contains("Windows_NT")) p = Platform.WINDOWS; if (uname.contains("sparc")) cpu = CPU.Sparc; if (uname.contains("x86_64")) cpu = CPU.amd64; if (Pattern.compile("\\bi?[3-6]86\\b").matcher(uname).find()) cpu = CPU.i386; // look for ix86 as a word if (p == null || cpu == null) throw new IOException(Messages.SSHLauncher_FailedToDetectEnvironment(uname)); String javaDir = workingDirectory + "/jdk"; // this is where we install Java to String bundleFile = workingDirectory + "/" + p.bundleFileName; // this is where we download the bundle to SFTPClient sftp = new SFTPClient(connection); // wipe out and recreate the Java directory connection.exec("rm -rf " + javaDir, listener.getLogger()); sftp.mkdirs(javaDir, 0755); URL bundle = getJDKInstaller().locate(listener, p, cpu); listener.getLogger().println("Installing " + JDKVERSION); Util.copyStreamAndClose(bundle.openStream(), new BufferedOutputStream(sftp.writeToFile(bundleFile), 32 * 1024)); sftp.chmod(bundleFile, 0755); getJDKInstaller().install(new RemoteLauncher(listener, connection), p, new SFTPFileSystem(sftp), listener, javaDir, bundleFile); return javaDir + "/bin/java"; } /** * Starts the slave process. * * @param computer The computer. * @param listener The listener. * @param java The full path name of the java executable to use. * @param workingDirectory The working directory from which to start the java process. * * @throws IOException If something goes wrong. */ private void startSlave(SlaveComputer computer, final TaskListener listener, String java, String workingDirectory) throws IOException { session = connection.openSession(); expandChannelBufferSize(session, listener); String cmd = "cd \"" + workingDirectory + "\" && " + java + " " + getJvmOptions() + " -jar slave.jar"; //This will wrap the cmd with prefix commands and suffix commands if they are set. cmd = getPrefixStartSlaveCmd() + cmd + getSuffixStartSlaveCmd(); listener.getLogger().println(Messages.SSHLauncher_StartingSlaveProcess(getTimestamp(), cmd)); session.execCommand(cmd); session.pipeStderr(new DelegateNoCloseOutputStream(listener.getLogger())); try { computer.setChannel(session.getStdout(), session.getStdin(), listener.getLogger(), null); } catch (InterruptedException e) { session.close(); throw new IOException2(Messages.SSHLauncher_AbortedDuringConnectionOpen(), e); } catch (IOException e) { try { // often times error this early means the JVM has died, so let's see if we can capture all stderr // and exit code throw new IOException2(getSessionOutcomeMessage(session, false), e); } catch (InterruptedException x) { throw (IOException) new IOException().initCause(e); } } } private void expandChannelBufferSize(Session session, TaskListener listener) { // see hudson.remoting.Channel.PIPE_WINDOW_SIZE for the discussion of why 1MB is in the right ball park // but this particular session is where all the master/slave communication will happen, so // it's worth using a bigger buffer to really better utilize bandwidth even when the latency is even larger // (and since we are draining this pipe very rapidly, it's unlikely that we'll actually accumulate this much data) int sz = 4; session.setWindowSize(sz * 1024 * 1024); listener.getLogger().println("Expanded the channel window size to " + sz + "MB"); } /** * Method copies the slave jar to the remote system. * * @param listener The listener. * @param workingDirectory The directory into whihc the slave jar will be copied. * * @throws IOException If something goes wrong. */ private void copySlaveJar(TaskListener listener, String workingDirectory) throws IOException, InterruptedException { String fileName = workingDirectory + "/slave.jar"; listener.getLogger().println(Messages.SSHLauncher_StartingSFTPClient(getTimestamp())); SFTPClient sftpClient = null; try { sftpClient = new SFTPClient(connection); try { SFTPv3FileAttributes fileAttributes = sftpClient._stat(workingDirectory); if (fileAttributes == null) { listener.getLogger() .println(Messages.SSHLauncher_RemoteFSDoesNotExist(getTimestamp(), workingDirectory)); sftpClient.mkdirs(workingDirectory, 0700); } else if (fileAttributes.isRegularFile()) { throw new IOException(Messages.SSHLauncher_RemoteFSIsAFile(workingDirectory)); } try { // try to delete the file in case the slave we are copying is shorter than the slave // that is already there sftpClient.rm(fileName); } catch (IOException e) { // the file did not exist... so no need to delete it! } listener.getLogger().println(Messages.SSHLauncher_CopyingSlaveJar(getTimestamp())); try { byte[] slaveJar = new Slave.JnlpJar("slave.jar").readFully(); OutputStream os = sftpClient.writeToFile(fileName); try { os.write(slaveJar); } finally { os.close(); } listener.getLogger() .println(Messages.SSHLauncher_CopiedXXXBytes(getTimestamp(), slaveJar.length)); } catch (Error error) { throw error; } catch (Throwable e) { throw new IOException2(Messages.SSHLauncher_ErrorCopyingSlaveJarTo(fileName), e); } } catch (Error error) { throw error; } catch (Throwable e) { throw new IOException2(Messages.SSHLauncher_ErrorCopyingSlaveJarInto(workingDirectory), e); } } catch (IOException e) { if (sftpClient == null) { // lets try to recover if the slave doesn't have an SFTP service copySlaveJarUsingSCP(listener, workingDirectory); } else { throw e; } } finally { if (sftpClient != null) { sftpClient.close(); } } } /** * Method copies the slave jar to the remote system using scp. * * @param listener The listener. * @param workingDirectory The directory into which the slave jar will be copied. * * @throws IOException If something goes wrong. * @throws InterruptedException If something goes wrong. */ private void copySlaveJarUsingSCP(TaskListener listener, String workingDirectory) throws IOException, InterruptedException { listener.getLogger().println(Messages.SSHLauncher_StartingSCPClient(getTimestamp())); SCPClient scp = new SCPClient(connection); try { // check if the working directory exists if (connection.exec("test -d " + workingDirectory, listener.getLogger()) != 0) { listener.getLogger() .println(Messages.SSHLauncher_RemoteFSDoesNotExist(getTimestamp(), workingDirectory)); // working directory doesn't exist, lets make it. if (connection.exec("mkdir -p " + workingDirectory, listener.getLogger()) != 0) { listener.getLogger().println("Failed to create " + workingDirectory); } } // delete the slave jar as we do with SFTP connection.exec("rm " + workingDirectory + "/slave.jar", new NullStream()); // SCP it to the slave. hudson.Util.ByteArrayOutputStream2 doesn't work for this. It pads the byte array. InputStream is = Hudson.getInstance().servletContext.getResourceAsStream("/WEB-INF/slave.jar"); listener.getLogger().println(Messages.SSHLauncher_CopyingSlaveJar(getTimestamp())); scp.put(IOUtils.toByteArray(is), "slave.jar", workingDirectory, "0644"); } catch (IOException e) { throw new IOException2(Messages.SSHLauncher_ErrorCopyingSlaveJarInto(workingDirectory), e); } } protected void reportEnvironment(TaskListener listener) throws IOException, InterruptedException { listener.getLogger().println(Messages._SSHLauncher_RemoteUserEnvironment(getTimestamp())); connection.exec("set", listener.getLogger()); } @NonNull private String checkJavaVersion(TaskListener listener, String javaCommand) throws IOException, InterruptedException { listener.getLogger().println(Messages.SSHLauncher_CheckingDefaultJava(getTimestamp(), javaCommand)); StringWriter output = new StringWriter(); // record output from Java ByteArrayOutputStream out = new ByteArrayOutputStream(); connection.exec(javaCommand + " " + getJvmOptions() + " -version", out); //TODO: Seems we need to retrieve the encoding from the connection destination BufferedReader r = new BufferedReader( new InputStreamReader(new ByteArrayInputStream(out.toByteArray()), Charset.defaultCharset())); final String result = checkJavaVersion(listener.getLogger(), javaCommand, r, output); if (null == result) { listener.getLogger().println(Messages.SSHLauncher_UknownJavaVersion(javaCommand)); listener.getLogger().println(output); throw new IOException(Messages.SSHLauncher_UknownJavaVersion(javaCommand)); } else { return result; } } // XXX switch to standard method in 1.479+ /** * Given the output of "java -version" in <code>r</code>, determine if this * version of Java is supported. This method has default visiblity for testing. * * @param logger * where to log the output * @param javaCommand * the command executed, used for logging * @param r * the output of "java -version" * @param output * copy the data from <code>r</code> into this output buffer */ @CheckForNull protected String checkJavaVersion(final PrintStream logger, String javaCommand, final BufferedReader r, final StringWriter output) throws IOException { String line; while (null != (line = r.readLine())) { output.write(line); output.write("\n"); line = line.toLowerCase(Locale.ENGLISH); if (line.startsWith("java version \"") || line.startsWith("openjdk version \"")) { final String versionStr = line.substring(line.indexOf('\"') + 1, line.lastIndexOf('\"')); logger.println(Messages.SSHLauncher_JavaVersionResult(getTimestamp(), javaCommand, versionStr)); // parse as a number and we should be OK as all we care about is up through the first dot. try { final Number version = NumberFormat.getNumberInstance(Locale.US).parse(versionStr); if (version.doubleValue() < 1.5) { throw new IOException(Messages.SSHLauncher_NoJavaFound(line)); } } catch (final ParseException e) { throw new IOException(Messages.SSHLauncher_NoJavaFound(line)); } return javaCommand; } } return null; } protected void openConnection(TaskListener listener) throws IOException, InterruptedException { listener.getLogger().println(Messages.SSHLauncher_OpeningSSHConnection(getTimestamp(), host + ":" + port)); connection.setTCPNoDelay(true); int maxNumRetries = this.maxNumRetries == null || this.maxNumRetries < 0 ? 0 : this.maxNumRetries; for (int i = 0; i <= maxNumRetries; i++) { try { connection.connect(); break; } catch (IOException ioexception) { listener.getLogger().println(ioexception.getCause().getMessage()); String ioExceptionMessageCause = ""; if (ioexception.getCause() != null) { ioExceptionMessageCause = ioexception.getCause().getMessage(); } if (!ioExceptionMessageCause.equals("Connection refused")) { break; } if (maxNumRetries - i > 0) { listener.getLogger() .println("SSH Connection failed with IOException: \"" + ioExceptionMessageCause + "\", retrying in " + retryWaitTime + " seconds. There are " + (maxNumRetries - i) + " more retries left."); } else { listener.getLogger().println( "SSH Connection failed with IOException: \"" + ioExceptionMessageCause + "\"."); throw ioexception; } } Thread.sleep(TimeUnit.SECONDS.toMillis(retryWaitTime)); } StandardUsernameCredentials credentials = getCredentials(); if (credentials == null) { throw new AbortException("Cannot find SSH User credentials with id: " + credentialsId); } if (SSHAuthenticator.newInstance(connection, credentials).authenticate(listener) && connection.isAuthenticationComplete()) { listener.getLogger().println(Messages.SSHLauncher_AuthenticationSuccessful(getTimestamp())); } else { listener.getLogger().println(Messages.SSHLauncher_AuthenticationFailed(getTimestamp())); throw new AbortException(Messages.SSHLauncher_AuthenticationFailedException()); } } /** * {@inheritDoc} */ @Override public synchronized void afterDisconnect(SlaveComputer slaveComputer, final TaskListener listener) { if (connection != null) { boolean connectionLost = reportTransportLoss(connection, listener); if (session != null) { // give the process 3 seconds to write out its dying message before we cut the loss // and give up on this process. if the slave process had JVM crash, OOME, or any other // critical problem, this will allow us to capture that. // exit code is also an useful info to figure out why the process has died. try { listener.getLogger().println(getSessionOutcomeMessage(session, connectionLost)); session.getStdout().close(); session.close(); } catch (Throwable t) { t.printStackTrace(listener.error(Messages.SSHLauncher_ErrorWhileClosingConnection())); } session = null; } Slave n = slaveComputer.getNode(); if (n != null && !connectionLost) { String workingDirectory = getWorkingDirectory(n); final String fileName = workingDirectory + "/slave.jar"; Future<?> tidyUp = Computer.threadPoolForRemoting.submit(new Runnable() { public void run() { // this would fail if the connection is already lost, so we want to check that. // TODO: Connection class should expose whether it is still connected or not. SFTPv3Client sftpClient = null; try { sftpClient = new SFTPv3Client(connection); sftpClient.rm(fileName); } catch (Exception e) { if (sftpClient == null) {// system without SFTP try { connection.exec("rm " + fileName, listener.getLogger()); } catch (Error error) { throw error; } catch (Throwable x) { x.printStackTrace( listener.error(Messages.SSHLauncher_ErrorDeletingFile(getTimestamp()))); // We ignore other Exception types } } else { e.printStackTrace( listener.error(Messages.SSHLauncher_ErrorDeletingFile(getTimestamp()))); } } finally { if (sftpClient != null) { sftpClient.close(); } } } }); try { // the delete is best effort only and if it takes longer than 60 seconds - or the launch // timeout (if specified) - then we should just give up and leave the file there. tidyUp.get(launchTimeoutSeconds == null ? 60 : launchTimeoutSeconds, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(listener.error(Messages.SSHLauncher_ErrorDeletingFile(getTimestamp()))); // we should either re-apply our interrupt flag or propagate... we don't want to propagate, so... Thread.currentThread().interrupt(); } catch (ExecutionException e) { e.printStackTrace(listener.error(Messages.SSHLauncher_ErrorDeletingFile(getTimestamp()))); } catch (TimeoutException e) { e.printStackTrace(listener.error(Messages.SSHLauncher_ErrorDeletingFile(getTimestamp()))); } finally { if (!tidyUp.isDone()) { tidyUp.cancel(true); } } } PluginImpl.unregister(connection); cleanupConnection(listener); } } /** * If the SSH connection as a whole is lost, report that information. */ private boolean reportTransportLoss(Connection c, TaskListener listener) { // TODO: switch to Connection.getReasonClosedCause() post build217-jenkins-8 // in the mean time, rely on reflection to get to the object TransportManager tm = null; try { Field f = Connection.class.getDeclaredField("tm"); f.setAccessible(true); tm = (TransportManager) f.get(c); } catch (NoSuchFieldException e) { e.printStackTrace(listener.error("Failed to get to TransportManager")); } catch (IllegalAccessException e) { e.printStackTrace(listener.error("Failed to get to TransportManager")); } if (tm == null) { listener.error("Couldn't get to TransportManager."); return false; } Throwable cause = tm.getReasonClosedCause(); if (cause != null) { cause.printStackTrace(listener.error("Socket connection to SSH server was lost")); } return cause != null; } /** * Find the exit code or exit status, which are differentiated in SSH protocol. */ private String getSessionOutcomeMessage(Session session, boolean isConnectionLost) throws InterruptedException { session.waitForCondition(ChannelCondition.EXIT_STATUS | ChannelCondition.EXIT_SIGNAL, 3000); Integer exitCode = session.getExitStatus(); if (exitCode != null) return "Slave JVM has terminated. Exit code=" + exitCode; String sig = session.getExitSignal(); if (sig != null) return "Slave JVM has terminated. Exit signal=" + sig; if (isConnectionLost) return "Slave JVM has not reported exit code before the socket was lost"; return "Slave JVM has not reported exit code. Is it still running?"; } /** * Getter for property 'host'. * * @return Value for property 'host'. */ public String getHost() { return host; } /** * Getter for property 'port'. * * @return Value for property 'port'. */ public int getPort() { return port; } /** * Getter for property 'username'. * * @return Value for property 'username'. * @deprecated */ @Deprecated public String getUsername() { return username; } /** * Getter for property 'password'. * * @return Value for property 'password'. * @deprecated */ @Deprecated public String getPassword() { return password != null ? Secret.toString(password) : null; } /** * Getter for property 'privatekey'. * * @return Value for property 'privatekey'. * @deprecated */ @Deprecated public String getPrivatekey() { return privatekey; } public Connection getConnection() { return connection; } public String getPrefixStartSlaveCmd() { return prefixStartSlaveCmd == null ? "" : prefixStartSlaveCmd; } public String getSuffixStartSlaveCmd() { return suffixStartSlaveCmd == null ? "" : suffixStartSlaveCmd; } /** * Getter for property 'launchTimeoutSeconds' * * @return launchTimeoutSeconds */ public Integer getLaunchTimeoutSeconds() { return launchTimeoutSeconds; } private long getLaunchTimeoutMillis() { return launchTimeoutSeconds == null ? 0L : TimeUnit.SECONDS.toMillis(launchTimeoutSeconds); } /** * Getter for property 'maxNumRetries' * * @return maxNumRetries */ public Integer getMaxNumRetries() { return maxNumRetries == null || maxNumRetries < 0 ? Integer.valueOf(0) : maxNumRetries; } /** * Getter for property 'retryWaitTime' * * @return retryWaitTime */ public Integer getRetryWaitTime() { return retryWaitTime; } @Extension public static class DescriptorImpl extends Descriptor<ComputerLauncher> { // TODO move the authentication storage to descriptor... see SubversionSCM.java /** * {@inheritDoc} */ public String getDisplayName() { return Messages.SSHLauncher_DescriptorDisplayName(); } public Class getSshConnectorClass() { return SSHConnector.class; } /** * Delegates the help link to the {@link SSHConnector}. */ @Override public String getHelpFile(String fieldName) { String n = super.getHelpFile(fieldName); if (n == null) n = Hudson.getInstance().getDescriptor(SSHConnector.class).getHelpFile(fieldName); return n; } public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context, @QueryParameter String host, @QueryParameter String port) { if (!(context instanceof AccessControlled ? (AccessControlled) context : Jenkins.getInstance()) .hasPermission(Computer.CONFIGURE)) { return new ListBoxModel(); } int portValue = Integer.parseInt(port); return new StandardUsernameListBoxModel().withMatching(SSHAuthenticator.matcher(Connection.class), CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, context, ACL.SYSTEM, SSHLauncher.SSH_SCHEME, new HostnamePortRequirement(host, portValue))); } } @Extension public static class DefaultJavaProvider extends JavaProvider { @Override public List<String> getJavas(SlaveComputer computer, TaskListener listener, Connection connection) { List<String> javas = new ArrayList<String>( Arrays.asList("java", "/usr/bin/java", "/usr/java/default/bin/java", "/usr/java/latest/bin/java", "/usr/local/bin/java", "/usr/local/java/bin/java")); // this is where we attempt to auto-install String workingDirectory = getWorkingDirectory(computer); if (workingDirectory != null) { javas.add(workingDirectory + "/jdk/bin/java"); } final Node node = computer.getNode(); DescribableList<NodeProperty<?>, NodePropertyDescriptor> list = node != null ? node.getNodeProperties() : null; if (list != null) { Descriptor jdk = Hudson.getInstance().getDescriptorByType(JDK.DescriptorImpl.class); for (NodeProperty prop : list) { if (prop instanceof EnvironmentVariablesNodeProperty) { EnvVars env = ((EnvironmentVariablesNodeProperty) prop).getEnvVars(); if (env != null && env.containsKey("JAVA_HOME")) javas.add(env.get("JAVA_HOME") + "/bin/java"); } else if (prop instanceof ToolLocationNodeProperty) { for (ToolLocation tool : ((ToolLocationNodeProperty) prop).getLocations()) if (tool.getType() == jdk) javas.add(tool.getHome() + "/bin/java"); } } } return javas; } } private static final Logger LOGGER = Logger.getLogger(SSHLauncher.class.getName()); private static class DelegateNoCloseOutputStream extends OutputStream { private OutputStream out; public DelegateNoCloseOutputStream(OutputStream out) { this.out = out; } @Override public void write(int b) throws IOException { if (out != null) out.write(b); } @Override public void close() throws IOException { out = null; } @Override public void flush() throws IOException { if (out != null) out.flush(); } @Override public void write(byte[] b) throws IOException { if (out != null) out.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { if (out != null) out.write(b, off, len); } } // static { // com.trilead.ssh2.log.Logger.enabled = true; // com.trilead.ssh2.log.Logger.logger = new DebugLogger() { // public void log(int level, String className, String message) { // System.out.println(className+"\n"+message); // } // }; // } }