/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi * * 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.remoting; import hudson.remoting.Channel.Mode; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import org.kohsuke.args4j.Option; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.CmdLineException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.TrustManager; import javax.net.ssl.SSLContext; import javax.net.ssl.X509TrustManager; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.File; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLConnection; import java.net.ServerSocket; import java.net.Socket; import java.net.URLClassLoader; import java.net.InetSocketAddress; import java.net.HttpURLConnection; import java.net.Authenticator; import java.net.PasswordAuthentication; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.security.cert.X509Certificate; import java.security.cert.CertificateException; import java.security.NoSuchAlgorithmException; import java.security.KeyManagementException; import java.security.SecureRandom; import java.util.Properties; import org.apache.commons.codec.binary.Base64; /** * Entry point for running a {@link Channel}. This is the main method of the slave JVM. * * <p> * This class also defines several methods for * starting a channel on a fresh JVM. * * @author Kohsuke Kawaguchi */ public class Launcher { public Mode mode = Mode.BINARY; // no-op, but left for backward compatibility @Option(name="-ping") public boolean ping = true; @Option(name="-text",usage="encode communication with the master with base64. " + "Useful for running slave over 8-bit unsafe protocol like telnet") public void setTextMode(boolean b) { mode = b?Mode.TEXT:Mode.BINARY; System.out.println("Running in "+mode.name().toLowerCase(Locale.ENGLISH)+" mode"); } @Option(name="-jnlpUrl",usage="instead of talking to the master via stdin/stdout, " + "emulate a JNLP client by making a TCP connection to the master. " + "Connection parameters are obtained by parsing the JNLP file.") public URL slaveJnlpURL = null; @Option(name="-jnlpCredentials",metaVar="USER:PASSWORD",usage="HTTP BASIC AUTH header to pass in for making HTTP requests.") public String slaveJnlpCredentials = null; @Option(name="-cp",aliases="-classpath",metaVar="PATH", usage="add the given classpath elements to the system classloader.") public void addClasspath(String pathList) throws Exception { Method $addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); $addURL.setAccessible(true); for(String token : pathList.split(File.pathSeparator)) $addURL.invoke(ClassLoader.getSystemClassLoader(),new File(token).toURI().toURL()); // fix up the system.class.path to pretend that those jar files // are given through CLASSPATH or something. // some tools like JAX-WS RI and Hadoop relies on this. System.setProperty("java.class.path",System.getProperty("java.class.path")+File.pathSeparatorChar+pathList); } @Option(name="-tcp",usage="instead of talking to the master via stdin/stdout, " + "listens to a random local port, write that port number to the given file, " + "then wait for the master to connect to that port.") public File tcpPortFile=null; @Option(name="-auth",metaVar="user:pass",usage="If your Hudson is security-enabeld, specify a valid user name and password.") public String auth = null; public InetSocketAddress connectionTarget = null; @Option(name="-connectTo",usage="make a TCP connection to the given host and port, then start communication.",metaVar="HOST:PORT") public void setConnectTo(String target) { String[] tokens = target.split(":"); if(tokens.length!=2) { System.err.println("Illegal parameter: "+target); System.exit(1); } connectionTarget = new InetSocketAddress(tokens[0],Integer.valueOf(tokens[1])); } /** * Bypass HTTPS security check by using free-for-all trust manager. * * @param _ * This is ignored. */ @Option(name="-noCertificateCheck") public void setNoCertificateCheck(boolean _) throws NoSuchAlgorithmException, KeyManagementException { System.out.println("Skipping HTTPS certificate checks altoghether. Note that this is not secure at all."); SSLContext context = SSLContext.getInstance("TLS"); context.init(null, new TrustManager[]{new NoCheckTrustManager()}, new java.security.SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory()); // bypass host name check, too. HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { public boolean verify(String s, SSLSession sslSession) { return true; } }); } public static void main(String... args) throws Exception { Launcher launcher = new Launcher(); CmdLineParser parser = new CmdLineParser(launcher); try { parser.parseArgument(args); launcher.run(); } catch (CmdLineException e) { System.err.println(e.getMessage()); System.err.println("java -jar slave.jar [options...]"); parser.printUsage(System.err); System.err.println(); } } public void run() throws Exception { if(auth!=null) { final int idx = auth.indexOf(':'); if(idx<0) throw new CmdLineException(null, "No ':' in the -auth option"); Authenticator.setDefault(new Authenticator() { @Override public PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(auth.substring(0,idx), auth.substring(idx+1).toCharArray()); } }); } if(connectionTarget!=null) { runAsTcpClient(); System.exit(0); } else if(slaveJnlpURL!=null) { List<String> jnlpArgs = parseJnlpArguments(); try { hudson.remoting.jnlp.Main._main(jnlpArgs.toArray(new String[jnlpArgs.size()])); } catch (CmdLineException e) { System.err.println("JNLP file "+slaveJnlpURL+" has invalid arguments: "+jnlpArgs); System.err.println("Most likely a configuration error in the master"); System.err.println(e.getMessage()); System.exit(1); } } else if(tcpPortFile!=null) { runAsTcpServer(); System.exit(0); } else { runWithStdinStdout(); System.exit(0); } } /** * Parses the connection arguments from JNLP file given in the URL. */ public List<String> parseJnlpArguments() throws ParserConfigurationException, SAXException, IOException, InterruptedException { while (true) { try { URLConnection con = slaveJnlpURL.openConnection(); if (con instanceof HttpURLConnection && slaveJnlpCredentials != null) { HttpURLConnection http = (HttpURLConnection) con; String userPassword = slaveJnlpCredentials; String encoding = new String(Base64.encodeBase64(userPassword.getBytes())); http.setRequestProperty("Authorization", "Basic " + encoding); } con.connect(); if (con instanceof HttpURLConnection) { HttpURLConnection http = (HttpURLConnection) con; if(http.getResponseCode()>=400) // got the error code. report that (such as 401) throw new IOException("Failed to load "+slaveJnlpURL+": "+http.getResponseCode()+" "+http.getResponseMessage()); } Document dom; // check if this URL points to a .jnlp file String contentType = con.getHeaderField("Content-Type"); if(contentType==null || !contentType.startsWith("application/x-java-jnlp-file")) { // load DOM anyway, but if it fails to parse, that's probably because this is not an XML file to begin with. try { dom = loadDom(slaveJnlpURL, con); } catch (SAXException e) { throw new IOException(slaveJnlpURL+" doesn't look like a JNLP file; content type was "+contentType); } catch (IOException e) { throw new IOException(slaveJnlpURL+" doesn't look like a JNLP file; content type was "+contentType); } } else { dom = loadDom(slaveJnlpURL, con); } // exec into the JNLP launcher, to fetch the connection parameter through JNLP. NodeList argElements = dom.getElementsByTagName("argument"); List<String> jnlpArgs = new ArrayList<String>(); for( int i=0; i<argElements.getLength(); i++ ) jnlpArgs.add(argElements.item(i).getTextContent()); if (slaveJnlpCredentials != null) { jnlpArgs.add("-credentials"); jnlpArgs.add(slaveJnlpCredentials); } // force a headless mode jnlpArgs.add("-headless"); return jnlpArgs; } catch (SSLHandshakeException e) { if(e.getMessage().contains("PKIX path building failed")) { // invalid SSL certificate. One reason this happens is when the certificate is self-signed IOException x = new IOException("Failed to validate a server certificate. If you are using a self-signed certificate, you can use the -noCertificateCheck option to bypass this check."); x.initCause(e); throw x; } else throw e; } catch (IOException e) { System.err.println("Failing to obtain "+slaveJnlpURL); e.printStackTrace(System.err); System.err.println("Waiting 10 seconds before retry"); Thread.sleep(10*1000); // retry } } } private static Document loadDom(URL slaveJnlpURL, URLConnection con) throws ParserConfigurationException, SAXException, IOException { DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); return db.parse(con.getInputStream(),slaveJnlpURL.toExternalForm()); } /** * Listens on an ephemeral port, record that port number in a port file, * then accepts one TCP connection. */ private void runAsTcpServer() throws IOException, InterruptedException { // if no one connects for too long, assume something went wrong // and avoid hanging foreever ServerSocket ss = new ServerSocket(0,1); ss.setSoTimeout(30*1000); // write a port file to report the port number FileWriter w = new FileWriter(tcpPortFile); w.write(String.valueOf(ss.getLocalPort())); w.close(); // accept just one connection and that's it. // when we are done, remove the port file to avoid stale port file Socket s; try { s = ss.accept(); ss.close(); } finally { tcpPortFile.delete(); } runOnSocket(s); } private void runOnSocket(Socket s) throws IOException, InterruptedException { main(new BufferedInputStream(new SocketInputStream(s)), new BufferedOutputStream(new SocketOutputStream(s)), mode,ping); } /** * Connects to the given TCP port and then start running */ private void runAsTcpClient() throws IOException, InterruptedException { // if no one connects for too long, assume something went wrong // and avoid hanging foreever Socket s = new Socket(connectionTarget.getAddress(),connectionTarget.getPort()); runOnSocket(s); } private void runWithStdinStdout() throws IOException, InterruptedException { // use stdin/stdout for channel communication ttyCheck(); if (isWindows()) { /* To prevent the dead lock between GetFileType from _ioinit in C runtime and blocking read that ChannelReaderThread would do on stdin, load the crypto DLL first. This is a band-aid solution to the problem. Still searching for more fundamental fix. 02f1e750 7c90d99a ntdll!KiFastSystemCallRet 02f1e754 7c810f63 ntdll!NtQueryVolumeInformationFile+0xc 02f1e784 77c2c9f9 kernel32!GetFileType+0x7e 02f1e7e8 77c1f01d msvcrt!_ioinit+0x19f 02f1e88c 7c90118a msvcrt!__CRTDLL_INIT+0xac 02f1e8ac 7c91c4fa ntdll!LdrpCallInitRoutine+0x14 02f1e9b4 7c916371 ntdll!LdrpRunInitializeRoutines+0x344 02f1ec60 7c9164d3 ntdll!LdrpLoadDll+0x3e5 02f1ef08 7c801bbd ntdll!LdrLoadDll+0x230 02f1ef70 7c801d72 kernel32!LoadLibraryExW+0x18e 02f1ef84 7c801da8 kernel32!LoadLibraryExA+0x1f 02f1efa0 77de8830 kernel32!LoadLibraryA+0x94 02f1f05c 6d3eb1be ADVAPI32!CryptAcquireContextA+0x512 WARNING: Stack unwind information not available. Following frames may be wrong. 02f1f13c 6d99c844 java_6d3e0000!Java_sun_security_provider_NativeSeedGenerator_nativeGenerateSeed+0x6e see http://weblogs.java.net/blog/kohsuke/archive/2009/09/28/reading-stdin-may-cause-your-jvm-hang for more details */ new SecureRandom().nextBoolean(); } // this will prevent programs from accidentally writing to System.out // and messing up the stream. OutputStream os = System.out; System.setOut(System.err); main(System.in,os, mode,ping); } private static void ttyCheck() { try { Method m = System.class.getMethod("console"); Object console = m.invoke(null); if(console!=null) { // we seem to be running from interactive console. issue a warning. // but since this diagnosis could be wrong, go on and do what we normally do anyway. Don't exit. System.out.println( "WARNING: Are you running slave agent from an interactive console?\n" + "If so, you are probably using it incorrectly.\n" + "See http://wiki.eclipse.org/Hudson-ci/Launching_slave.jar_from_the_console"); } } catch (LinkageError e) { // we are probably running on JDK5 that doesn't have System.console() // we can't check } catch (InvocationTargetException e) { // this is impossible throw new AssertionError(e); } catch (NoSuchMethodException e) { // must be running on JDK5 } catch (IllegalAccessException e) { // this is impossible throw new AssertionError(e); } } public static void main(InputStream is, OutputStream os) throws IOException, InterruptedException { main(is,os,Mode.BINARY); } public static void main(InputStream is, OutputStream os, Mode mode) throws IOException, InterruptedException { main(is,os,mode,false); } public static void main(InputStream is, OutputStream os, Mode mode, boolean performPing) throws IOException, InterruptedException { ExecutorService executor = Executors.newCachedThreadPool(); Channel channel = new Channel("channel", executor, mode, is, os); System.err.println("channel started"); long timeout = 1000 * Long.parseLong( System.getProperty("hudson.remoting.Launcher.pingTimeoutSec", "240")), interval = 1000 * Long.parseLong( System.getProperty("hudson.remoting.Launcher.pingIntervalSec", "600")); if (performPing && timeout > 0 && interval > 0) { new PingThread(channel, timeout, interval) { @Override protected void onDead() { System.err.println("Ping failed. Terminating"); System.exit(-1); } }.start(); } channel.join(); System.err.println("channel stopped"); } /** * {@link X509TrustManager} that performs no check at all. */ private static class NoCheckTrustManager implements X509TrustManager { public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } public static boolean isWindows() { return File.pathSeparatorChar==';'; } private static String computeVersion() { Properties props = new Properties(); try { InputStream is = Launcher.class.getResourceAsStream("hudson-version.properties"); if(is!=null) props.load(is); } catch (IOException e) { e.printStackTrace(); } return props.getProperty("version", "?"); } /** * Version number of Hudson this slave.jar is from. */ public static final String VERSION = computeVersion(); }