All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hudson.remoting.Engine Maven / Gradle / Ivy

/*
 * 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 candidateUrls;

    /**
     * URL that points to Hudson's tcp slage agent listener, like http://myhost/hudson/
     *
     * 

* This value is determined from {@link #candidateUrls} after a successful connection. * Note that this URL DOES NOT 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 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 CURRENT = new ThreadLocal(); private static final Logger LOGGER = Logger.getLogger(Engine.class.getName()); public static final String GREETING_SUCCESS = "Welcome"; }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy