hudson.remoting.Engine.java Source code

Java tutorial

Introduction

Here is the source code for hudson.remoting.Engine.java

Source

/*
 * 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 org.apache.commons.codec.binary.Base64;
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 String(Base64.encodeBase64(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";
}