Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[JENKINS-26580] Refactor slave JNLP engine to make it easier to add m…
…ore protocols in the future. The engine will now call a factory that returns a list of protocols to try in order. Developers can implement new protocols in separate classes and add them to the factory to be used. Added tests for existing protocols. I noticed the Engine class doesn't have a corresponding Test class. It would be nice to add one, but I think it needs more refactoring before it would be realistic to add tests for it.
  • Loading branch information
akshayabd committed Jan 24, 2015
1 parent e62cef4 commit 90dd966
Show file tree
Hide file tree
Showing 10 changed files with 727 additions and 80 deletions.
18 changes: 18 additions & 0 deletions pom.xml
Expand Up @@ -123,6 +123,24 @@ THE SOFTWARE.
<version>1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.5</version>
<scope>test</scope>
</dependency>
<dependency>
<!-- for JRE requirement check annotation -->
<groupId>org.jvnet</groupId>
Expand Down
118 changes: 38 additions & 80 deletions src/main/java/hudson/remoting/Engine.java
Expand Up @@ -24,23 +24,22 @@
package hudson.remoting;

import hudson.remoting.Channel.Mode;
import hudson.remoting.engine.JnlpProtocol;
import hudson.remoting.engine.JnlpProtocolFactory;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
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.Collections;
import java.util.List;
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.INFO;
Expand Down Expand Up @@ -105,13 +104,6 @@ public void run() {

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;

private JarCache jarCache = new FileSystemJarCache(new File(System.getProperty("user.home"),".jenkins/cache/jars"),true);

public Engine(EngineListener listener, List<URL> hudsonUrls, String secretKey, String slaveName) {
Expand Down Expand Up @@ -163,6 +155,9 @@ public void removeListener(EngineListener el) {
@SuppressWarnings({"ThrowableInstanceNeverThrown"})
@Override
public void run() {
// Create the protocols that will be attempted to connect to the master.
List<JnlpProtocol> protocols = JnlpProtocolFactory.createProtocols(secretKey, slaveName);

try {
boolean first = true;
while(true) {
Expand All @@ -184,13 +179,13 @@ public void run() {

// find out the TCP port
HttpURLConnection con = (HttpURLConnection)salURL.openConnection();
if (con instanceof HttpURLConnection) {
if (con != null) {
if (credentials != null) {
// TODO /tcpSlaveAgentListener is unprotected so why do we need to pass any credentials?
String encoding = Base64.encode(credentials.getBytes("UTF-8"));
con.setRequestProperty("Authorization", "Basic " + encoding);
}

if (proxyCredentials != null) {
String encoding = Base64.encode(proxyCredentials.getBytes("UTF-8"));
con.setRequestProperty("Proxy-Authorization", "Basic " + encoding);
Expand Down Expand Up @@ -234,54 +229,41 @@ public void run() {
return;
}

Socket s = connect(port);

events.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;
Socket jnlpSocket = connect(port);
DataOutputStream outputStream = new DataOutputStream(jnlpSocket.getOutputStream());
BufferedInputStream inputStream = new BufferedInputStream(jnlpSocket.getInputStream());
String response = "";

// Try available protocols.
for (JnlpProtocol protocol : protocols) {
events.status("Trying protocol: " + protocol.getName());
response = protocol.performHandshake(outputStream, inputStream);

// On success do not try other protocols.
if (response.equals(GREETING_SUCCESS)) {
break;
}

// On failure log the response and form a new connection.
events.status("Server didn't understand the protocol: " + response);
jnlpSocket.close();
Thread.sleep(500);
jnlpSocket = connect(port);
outputStream = new DataOutputStream(jnlpSocket.getOutputStream());
inputStream = new BufferedInputStream(jnlpSocket.getInputStream());
}

// If no protocol worked.
if (!response.equals(GREETING_SUCCESS)) {
onConnectionRejected("None of the protocols were accepted");
continue;
}

final Channel channel = new ChannelBuilder("channel", executor)
.withJarCache(jarCache)
.withMode(Mode.BINARY)
.build(in, new BufferedOutputStream(s.getOutputStream()));
.build(inputStream, new BufferedOutputStream(jnlpSocket.getOutputStream()));

events.status("Connected");
channel.join();
Expand All @@ -307,30 +289,6 @@ private void onConnectionRejected(String greeting) throws InterruptedException {
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("UTF-8").trim(); // trim off possible '\r'
baos.write(ch);
}
}

/**
* Connects to TCP slave port, with a few retries.
*/
Expand Down Expand Up @@ -416,5 +374,5 @@ public static Engine current() {

private static final Logger LOGGER = Logger.getLogger(Engine.class.getName());

public static final String GREETING_SUCCESS = "Welcome";
public static final String GREETING_SUCCESS = JnlpProtocol.GREETING_SUCCESS;
}
76 changes: 76 additions & 0 deletions src/main/java/hudson/remoting/engine/EngineUtil.java
@@ -0,0 +1,76 @@
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, CloudBees, Inc.
*
* 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.engine;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
* Engine utility methods.
*
* @author Akshay Dayal
*/
public class EngineUtil {

/**
* Read until '\n' and returns it as a string.
*
* @param inputStream The input stream to read from.
* @return The line read.
* @throws IOException
*/
public static String readLine(InputStream inputStream) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while (true) {
int ch = inputStream.read();
if (ch<0 || ch=='\n') {
// Trim off possible '\r'
return byteArrayOutputStream.toString("UTF-8").trim();
}
byteArrayOutputStream.write(ch);
}
}

/**
* Read headers from a response.
*
* @param inputStream The input stream to read from.
* @return The set of headers stored in a {@link Properties}.
* @throws IOException
*/
public static Properties readResponseHeaders(BufferedInputStream inputStream) throws IOException {
Properties response = new Properties();
while (true) {
String line = EngineUtil.readLine(inputStream);
if (line.length()==0) {
return response;
}
int idx = line.indexOf(':');
response.put(line.substring(0,idx).trim(), line.substring(idx+1).trim());
}
}
}
68 changes: 68 additions & 0 deletions src/main/java/hudson/remoting/engine/JnlpProtocol.java
@@ -0,0 +1,68 @@
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, CloudBees, Inc.
*
* 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.engine;

import java.io.BufferedInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

/**
* Handshake protocol used by JNLP slave when initiating a connection to
* master.
*
* @author Akshay Dayal
*/
public abstract class JnlpProtocol {

final String secretKey;
final String slaveName;

JnlpProtocol(String secretKey, String slaveName) {
this.secretKey = secretKey;
this.slaveName = slaveName;
}

/**
* Get the name of the protocol.
*/
public abstract String getName();

/**
* Performs a handshake with the master.
*
* @param outputStream The stream to write into to initiate the handshake.
* @param inputStream The stream to read responses from the master.
* @return The greeting response from the master.
* @throws IOException
*/
public abstract String performHandshake(DataOutputStream outputStream,
BufferedInputStream inputStream) throws IOException;

// The expected response from the master on successful completion of the
// handshake.
public static final String GREETING_SUCCESS = "Welcome";

// Prefix when sending protocol name.
static final String PROTOCOL_PREFIX = "Protocol:";
}

0 comments on commit 90dd966

Please sign in to comment.