Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Adding extension point so that the master can install code that restarts JNLP slaves.
  • Loading branch information
kohsuke committed Apr 1, 2014
1 parent 4a23ad9 commit 7815477
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 0 deletions.
@@ -0,0 +1,85 @@
package jenkins.slaves.restarter;

import hudson.Extension;
import hudson.model.Computer;
import hudson.model.TaskListener;
import hudson.remoting.Callable;
import hudson.remoting.Engine;
import hudson.remoting.EngineListener;
import hudson.remoting.EngineListenerAdapter;
import hudson.remoting.VirtualChannel;
import hudson.slaves.ComputerListener;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;

import static java.util.logging.Level.*;

/**
* Actual slave restart logic.
*
* <p>
* Use {@link ComputerListener} to install {@link EngineListener}, which in turn gets executed when
* the slave gets disconnected.
*
* @author Kohsuke Kawaguchi
*/
@Extension
public class JnlpSlaveRestarterInstaller extends ComputerListener {
@Override
public void onOnline(Computer c, TaskListener listener) throws IOException, InterruptedException {
final List<SlaveRestarter> restarters = new ArrayList<SlaveRestarter>(SlaveRestarter.all());

VirtualChannel ch = c.getChannel();
if (ch==null) return; // defensive check

List<SlaveRestarter> effective = ch.call(new Callable<List<SlaveRestarter>, IOException>() {
public List<SlaveRestarter> call() throws IOException {
Engine e = Engine.current();
if (e == null) return null; // not running under Engine

try {
Engine.class.getMethod("addListener", EngineListener.class);
} catch (NoSuchMethodException _) {
return null; // running with older version of remoting that doesn't support adding listener
}

// filter out ones that doesn't apply
for (Iterator<SlaveRestarter> itr = restarters.iterator(); itr.hasNext(); ) {
SlaveRestarter r = itr.next();
if (!r.canWork())
itr.remove();
}

e.addListener(new EngineListenerAdapter() {
@Override
public void onDisconnect() {
try {
for (SlaveRestarter r : restarters) {
try {
r.restart();
} catch (Exception x) {
LOGGER.log(SEVERE, "Failed to restart slave with "+r, x);
}
}
} finally {
// if we move on to the reconnection without restart,
// don't let the current implementations kick in when the slave loses connection again
restarters.clear();
}
}
});

return restarters;
}
});

// TODO: report this to GUI
LOGGER.fine("Effective SlaveRestarter on "+c.getDisplayName()+": "+effective);
}

private static final Logger LOGGER = Logger.getLogger(JnlpSlaveRestarterInstaller.class.getName());
}
49 changes: 49 additions & 0 deletions core/src/main/java/jenkins/slaves/restarter/SlaveRestarter.java
@@ -0,0 +1,49 @@
package jenkins.slaves.restarter;

import hudson.ExtensionList;
import hudson.ExtensionPoint;
import jenkins.model.Jenkins;

import java.io.Serializable;
import java.util.logging.Logger;

/**
* Extension point to control how to restart JNLP slave when it loses the connection with the master.
*
* <p>
* Objects are instantiated on the master, then transfered to a slave via serialization.
*
* @author Kohsuke Kawaguchi
*/

This comment has been minimized.

Copy link
@jglick

jglick Apr 2, 2014

Member

@since

public abstract class SlaveRestarter implements ExtensionPoint, Serializable {
/**
* Called on the slave to see if this restarter can work on this slave.
*/
public abstract boolean canWork();

/**
* If {@link #canWork()} method returns true, this method is called later when
* the connection is lost to restart the slave.
*
* <p>
* Note that by the time this method is called, classloader is no longer capable of
* loading any additional classes. Therefore {@link #canWork()} method must have
* exercised enough of the actual restart process so that this call can proceed
* without trying to load additional classes nor resources.
*
* <p>
* This method is not expected to return, and the JVM should terminate before this call returns.
* If the method returns normally, the JNLP slave will move on to the reconnection without restart.
* If an exception is thrown, it is reported as an error and then the JNLP slave will move on to the
* reconnection without restart.
*/
public abstract void restart() throws Exception;

public static ExtensionList<SlaveRestarter> all() {
return Jenkins.getInstance().getExtensionList(SlaveRestarter.class);
}

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

private static final long serialVersionUID = 1L;
}
@@ -0,0 +1,70 @@
package jenkins.slaves.restarter;

import com.sun.akuma.Daemon;
import com.sun.akuma.JavaVMArguments;
import com.sun.jna.Native;
import com.sun.jna.StringArray;
import hudson.Extension;

import java.io.File;
import java.io.IOException;
import java.util.logging.Logger;

import static hudson.util.jna.GNUCLibrary.*;
import static java.util.logging.Level.*;

/**
* On Unix, restart via exec-ing to itself.
*/
@Extension
public class UnixSlaveRestarter extends SlaveRestarter {
private transient JavaVMArguments args;

@Override
public boolean canWork() {
try {
if (File.pathSeparatorChar!=':')
return false; // quick test to reject non-Unix without loading all the rest of the classes

args = JavaVMArguments.current();

// go through the whole motion to make sure all the relevant classes are loaded now
LIBC.getdtablesize();
int v = LIBC.fcntl(99999, F_GETFD);
LIBC.fcntl(99999, F_SETFD, v);

Daemon.getCurrentExecutable();
LIBC.execv("positively/no/such/executable", new StringArray(new String[]{"a","b","c"}));

return true;
} catch (UnsupportedOperationException e) {
LOGGER.log(FINE, getClass()+" unsuitable", e);
return false;
} catch (LinkageError e) {
LOGGER.log(FINE, getClass()+" unsuitable", e);
return false;
} catch (IOException e) {
LOGGER.log(FINE, getClass()+" unsuitable", e);
return false;
}
}

public void restart() throws Exception {
// close all files upon exec, except stdin, stdout, and stderr
int sz = LIBC.getdtablesize();
for (int i = 3; i < sz; i++) {
int flags = LIBC.fcntl(i, F_GETFD);
if (flags < 0) continue;
LIBC.fcntl(i, F_SETFD, flags | FD_CLOEXEC);
}

// exec to self
String exe = Daemon.getCurrentExecutable();
LIBC.execv(exe, new StringArray(args.toArray(new String[args.size()])));
throw new IOException("Failed to exec '" + exe + "' " + LIBC.strerror(Native.getLastError()));
}

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

private static final long serialVersionUID = 1L;
}
@@ -0,0 +1,52 @@
package jenkins.slaves.restarter;

import hudson.Extension;

import java.io.IOException;
import java.util.logging.Logger;

import static java.util.logging.Level.*;
import static org.apache.commons.io.IOUtils.*;

/**
* With winsw, restart via winsw
*/
@Extension
public class WinswSlaveRestarter extends SlaveRestarter {
private transient String exe;

@Override
public boolean canWork() {
try {
exe = System.getenv("WINSW_EXECUTABLE");
if (exe==null)
return false; // not under winsw

return exec("status") ==0;
} catch (InterruptedException e) {
LOGGER.log(FINE, getClass()+" unsuitable", e);
return false;
} catch (IOException e) {
LOGGER.log(FINE, getClass()+" unsuitable", e);
return false;
}
}

private int exec(String cmd) throws InterruptedException, IOException {
ProcessBuilder pb = new ProcessBuilder(exe, cmd);
pb.redirectErrorStream(true);
Process p = pb.start();
p.getOutputStream().close();
copy(p.getInputStream(), System.out);
return p.waitFor();
}

public void restart() throws Exception {
int r = exec("restart");
throw new IOException("Restart failure. '"+exe+" restart' completed with "+r+" but I'm still alive");
}

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

private static final long serialVersionUID = 1L;
}

0 comments on commit 7815477

Please sign in to comment.