/remoting/src/main/java/hudson/remoting/Engine.java
Java | 369 lines | 252 code | 41 blank | 76 comment | 35 complexity | 6ce9b5fb421ceb10f1dd123d29e52961 MD5 | raw file
Possible License(s): MIT, BSD-3-Clause
- /*
- * The MIT License
- *
- * Copyright (c) 2004-2009, 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 java.io.BufferedInputStream;
- import java.io.BufferedOutputStream;
- import java.io.DataOutputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.ByteArrayOutputStream;
- import java.net.HttpURLConnection;
- import java.net.Socket;
- import java.net.URL;
- import java.util.Properties;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.ThreadFactory;
- import java.util.List;
- import java.util.Collections;
- import java.util.logging.Logger;
- import static java.util.logging.Level.SEVERE;
- /**
- * Slave agent engine that proactively connects to Hudson master.
- *
- * @author Kohsuke Kawaguchi
- */
- public class Engine extends Thread {
- /**
- * Thread pool that sets {@link #CURRENT}.
- */
- private final ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactory() {
- private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
- public Thread newThread(final Runnable r) {
- return defaultFactory.newThread(new Runnable() {
- public void run() {
- CURRENT.set(Engine.this);
- r.run();
- }
- });
- }
- });
- public final EngineListener listener;
- /**
- * To make Hudson more graceful against user error,
- * JNLP agent can try to connect to multiple possible Hudson URLs.
- * This field specifies those candidate URLs, such as
- * "http://foo.bar/hudson/".
- */
- private List<URL> candidateUrls;
- /**
- * URL that points to Hudson's tcp slage agent listener, like <tt>http://myhost/hudson/</tt>
- *
- * <p>
- * This value is determined from {@link #candidateUrls} after a successful connection.
- * Note that this URL <b>DOES NOT</b> have "tcpSlaveAgentListener" in it.
- */
- private URL hudsonUrl;
- private final String secretKey;
- public final String slaveName;
- private String credentials;
- /**
- * See Main#tunnel in the jnlp-agent module for the details.
- */
- private String tunnel;
- private boolean noReconnect;
- /**
- * This cookie identifiesof the current connection, allowing us to force the server to drop
- * the client if we initiate a reconnection from our end (even when the server still thinks
- * the connection is alive.)
- */
- private String cookie;
- public Engine(EngineListener listener, List<URL> hudsonUrls, String secretKey, String slaveName) {
- this.listener = listener;
- this.candidateUrls = hudsonUrls;
- this.secretKey = secretKey;
- this.slaveName = slaveName;
- if(candidateUrls.isEmpty())
- throw new IllegalArgumentException("No URLs given");
- }
- public URL getHudsonUrl() {
- return hudsonUrl;
- }
- public void setTunnel(String tunnel) {
- this.tunnel = tunnel;
- }
- public void setCredentials(String creds) {
- this.credentials = creds;
- }
- public void setNoReconnect(boolean noReconnect) {
- this.noReconnect = noReconnect;
- }
- @SuppressWarnings({"ThrowableInstanceNeverThrown"})
- @Override
- public void run() {
- try {
- boolean first = true;
- while(true) {
- if(first) {
- first = false;
- } else {
- if(noReconnect)
- return; // exit
- }
- listener.status("Locating server among " + candidateUrls);
- Throwable firstError=null;
- String port=null;
- for (URL url : candidateUrls) {
- String s = url.toExternalForm();
- if(!s.endsWith("/")) s+='/';
- URL salURL = new URL(s+"tcpSlaveAgentListener/");
- // find out the TCP port
- HttpURLConnection con = (HttpURLConnection)salURL.openConnection();
- if (con instanceof HttpURLConnection && credentials != null) {
- String encoding = new sun.misc.BASE64Encoder().encode(credentials.getBytes());
- con.setRequestProperty("Authorization", "Basic " + encoding);
- }
- try {
- try {
- con.setConnectTimeout(30000);
- con.setReadTimeout(60000);
- con.connect();
- } catch (IOException x) {
- if (firstError == null) {
- firstError = new IOException("Failed to connect to " + salURL + ": " + x.getMessage()).initCause(x);
- }
- continue;
- }
- port = con.getHeaderField("X-Hudson-JNLP-Port");
- if(con.getResponseCode()!=200) {
- if(firstError==null)
- firstError = new Exception(salURL+" is invalid: "+con.getResponseCode()+" "+con.getResponseMessage());
- continue;
- }
- if(port ==null) {
- if(firstError==null)
- firstError = new Exception(url+" is not Hudson");
- continue;
- }
- } finally {
- con.disconnect();
- }
- // this URL works. From now on, only try this URL
- hudsonUrl = url;
- firstError = null;
- candidateUrls = Collections.singletonList(hudsonUrl);
- break;
- }
- if(firstError!=null) {
- listener.error(firstError);
- return;
- }
- Socket s = connect(port);
- listener.status("Handshaking");
- DataOutputStream dos = new DataOutputStream(s.getOutputStream());
- BufferedInputStream in = new BufferedInputStream(s.getInputStream());
- dos.writeUTF("Protocol:JNLP2-connect");
- Properties props = new Properties();
- props.put("Secret-Key", secretKey);
- props.put("Node-Name", slaveName);
- if (cookie!=null)
- props.put("Cookie", cookie);
- ByteArrayOutputStream o = new ByteArrayOutputStream();
- props.store(o, null);
- dos.writeUTF(o.toString("UTF-8"));
- String greeting = readLine(in);
- if (greeting.startsWith("Unknown protocol")) {
- LOGGER.info("The server didn't understand the v2 handshake. Falling back to v1 handshake");
- s.close();
- s = connect(port);
- in = new BufferedInputStream(s.getInputStream());
- dos = new DataOutputStream(s.getOutputStream());
- dos.writeUTF("Protocol:JNLP-connect");
- dos.writeUTF(secretKey);
- dos.writeUTF(slaveName);
- greeting = readLine(in); // why, oh why didn't I use DataOutputStream when writing to the network?
- if (!greeting.equals(GREETING_SUCCESS)) {
- onConnectionRejected(greeting);
- continue;
- }
- } else {
- if (greeting.equals(GREETING_SUCCESS)) {
- Properties responses = readResponseHeaders(in);
- cookie = responses.getProperty("Cookie");
- } else {
- onConnectionRejected(greeting);
- continue;
- }
- }
- final Socket socket = s;
- final Channel channel = new Channel("channel", executor,
- in,
- new BufferedOutputStream(s.getOutputStream()));
- PingThread t = new PingThread(channel) {
- protected void onDead() {
- try {
- if (!channel.isInClosed()) {
- LOGGER.info("Ping failed. Terminating the socket.");
- socket.close();
- }
- } catch (IOException e) {
- LOGGER.log(SEVERE, "Failed to terminate the socket", e);
- }
- }
- };
- t.start();
- listener.status("Connected");
- channel.join();
- listener.status("Terminated");
- t.interrupt(); // make sure the ping thread is terminated
- listener.onDisconnect();
- if(noReconnect)
- return; // exit
- // try to connect back to the server every 10 secs.
- waitForServerToBack();
- }
- } catch (Throwable e) {
- listener.error(e);
- }
- }
- private void onConnectionRejected(String greeting) throws InterruptedException {
- listener.error(new Exception("The server rejected the connection: "+greeting));
- Thread.sleep(10*1000);
- }
- private Properties readResponseHeaders(BufferedInputStream in) throws IOException {
- Properties response = new Properties();
- while (true) {
- String line = readLine(in);
- if (line.length()==0)
- return response;
- int idx = line.indexOf(':');
- response.put(line.substring(0,idx).trim(), line.substring(idx+1).trim());
- }
- }
- /**
- * Read until '\n' and returns it as a string.
- */
- private static String readLine(InputStream in) throws IOException {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- while (true) {
- int ch = in.read();
- if (ch<0 || ch=='\n')
- return baos.toString().trim(); // trim off possible '\r'
- baos.write(ch);
- }
- }
- /**
- * Connects to TCP slave port, with a few retries.
- */
- private Socket connect(String port) throws IOException, InterruptedException {
- String host = this.hudsonUrl.getHost();
- if(tunnel!=null) {
- String[] tokens = tunnel.split(":",3);
- if(tokens.length!=2)
- throw new IOException("Illegal tunneling parameter: "+tunnel);
- if(tokens[0].length()>0) host = tokens[0];
- if(tokens[1].length()>0) port = tokens[1];
- }
- String msg = "Connecting to " + host + ':' + port;
- listener.status(msg);
- int retry = 1;
- while(true) {
- try {
- Socket s = new Socket(host, Integer.parseInt(port));
- s.setTcpNoDelay(true); // we'll do buffering by ourselves
- // set read time out to avoid infinite hang. the time out should be long enough so as not
- // to interfere with normal operation. the main purpose of this is that when the other peer dies
- // abruptly, we shouldn't hang forever, and at some point we should notice that the connection
- // is gone.
- s.setSoTimeout(30*60*1000); // 30 mins. See PingThread for the ping interval
- return s;
- } catch (IOException e) {
- if(retry++>10)
- throw (IOException)new IOException("Failed to connect to "+host+':'+port).initCause(e);
- Thread.sleep(1000*10);
- listener.status(msg+" (retrying:"+retry+")",e);
- }
- }
- }
- /**
- * Waits for the server to come back.
- */
- private void waitForServerToBack() throws InterruptedException {
- while(true) {
- Thread.sleep(1000*10);
- try {
- // Hudson top page might be read-protected. see http://www.nabble.com/more-lenient-retry-logic-in-Engine.waitForServerToBack-td24703172.html
- HttpURLConnection con = (HttpURLConnection)new URL(hudsonUrl,"tcpSlaveAgentListener/").openConnection();
- con.connect();
- if(con.getResponseCode()==200)
- return;
- } catch (IOException e) {
- // retry
- }
- }
- }
- /**
- * When invoked from within remoted {@link Callable} (that is,
- * from the thread that carries out the remote requests),
- * this method returns the {@link Engine} in which the remote operations
- * run.
- */
- public static Engine current() {
- return CURRENT.get();
- }
- private static final ThreadLocal<Engine> CURRENT = new ThreadLocal<Engine>();
- private static final Logger LOGGER = Logger.getLogger(Engine.class.getName());
- public static final String GREETING_SUCCESS = "Welcome";
- }