Slave.java 19.5 KB
Newer Older
K
kohsuke 已提交
1 2 3 4
package hudson.model;

import hudson.FilePath;
import hudson.Launcher;
5
import hudson.Launcher.RemoteLauncher;
K
kohsuke 已提交
6
import hudson.Util;
7 8
import hudson.maven.agent.Main;
import hudson.maven.agent.PluginManagerInterceptor;
K
kohsuke 已提交
9
import hudson.model.Descriptor.FormException;
K
kohsuke 已提交
10 11
import hudson.remoting.Callable;
import hudson.remoting.Channel;
12
import hudson.remoting.Channel.Listener;
K
kohsuke 已提交
13
import hudson.remoting.VirtualChannel;
14
import hudson.remoting.Which;
15 16 17
import hudson.tasks.DynamicLabeler;
import hudson.tasks.LabelFinder;
import hudson.util.*;
18 19
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
20
import org.jvnet.winp.WinProcess;
K
kohsuke 已提交
21

22 23
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
24
import java.io.*;
25 26
import java.net.URL;
import java.net.URLConnection;
27
import java.util.*;
K
kohsuke 已提交
28
import java.util.logging.Level;
K
kohsuke 已提交
29
import java.util.logging.LogRecord;
30
import java.util.logging.Logger;
K
kohsuke 已提交
31

K
kohsuke 已提交
32 33 34 35 36
/**
 * Information about a Hudson slave node.
 *
 * @author Kohsuke Kawaguchi
 */
K
kohsuke 已提交
37
public final class Slave implements Node, Serializable {
K
kohsuke 已提交
38
    /**
K
kohsuke 已提交
39
     * Name of this slave node.
K
kohsuke 已提交
40
     */
K
kohsuke 已提交
41
    protected final String name;
K
kohsuke 已提交
42 43 44 45 46 47 48 49

    /**
     * Description of this node.
     */
    private final String description;

    /**
     * Path to the root of the workspace
K
kohsuke 已提交
50
     * from the view point of this node, such as "/hudson"
K
kohsuke 已提交
51
     */
K
kohsuke 已提交
52
    protected final String remoteFS;
K
kohsuke 已提交
53 54 55 56 57 58 59 60 61 62 63

    /**
     * Number of executors of this node.
     */
    private int numExecutors = 2;

    /**
     * Job allocation strategy.
     */
    private Mode mode;

K
kohsuke 已提交
64 65 66 67 68 69
    /**
     * Command line to launch the agent, like
     * "ssh myslave java -jar /path/to/hudson-remoting.jar"
     */
    private String agentCommand;

70 71 72 73 74 75 76 77 78 79
    /**
     * Whitespace-separated labels.
     */
    private String label="";

    /**
     * Lazily computed set of labels from {@link #label}.
     */
    private transient volatile Set<Label> labels;

80 81 82
    private transient volatile Set<Label> dynamicLabels;
    private transient volatile int dynamicLabelsInstanceHash;

K
kohsuke 已提交
83 84 85
    /**
     * @stapler-constructor
     */
86
    public Slave(String name, String description, String command, String remoteFS, int numExecutors, Mode mode,
87
                 String label) throws FormException {
K
kohsuke 已提交
88 89 90 91
        this.name = name;
        this.description = description;
        this.numExecutors = numExecutors;
        this.mode = mode;
K
kohsuke 已提交
92 93
        this.agentCommand = command;
        this.remoteFS = remoteFS;
94 95
        this.label = Util.fixNull(label).trim();
        getAssignedLabels();    // compute labels now
K
kohsuke 已提交
96

K
d'oh!  
kohsuke 已提交
97
        if (name.equals(""))
K
kohsuke 已提交
98
            throw new FormException("Invalid slave configuration. Name is empty", null);
K
kohsuke 已提交
99

100 101 102 103 104
        // this prevents the config from being saved when slaves are offline.
        // on a large deployment with a lot of slaves, some slaves are bound to be offline,
        // so this check is harmful.
        //if (!localFS.exists())
        //    throw new FormException("Invalid slave configuration for " + name + ". No such directory exists: " + localFS, null);
K
kohsuke 已提交
105 106
        if (remoteFS.equals(""))
            throw new FormException("Invalid slave configuration for " + name + ". No remote directory given", null);
107 108 109

        if (numExecutors<=0)
            throw new FormException("Invalid slave configuration for " + name + ". Invalid # of executors.", null);
K
kohsuke 已提交
110 111 112
    }

    public String getCommand() {
K
kohsuke 已提交
113
        return agentCommand;
K
kohsuke 已提交
114 115 116 117 118 119
    }

    public String getRemoteFS() {
        return remoteFS;
    }

K
kohsuke 已提交
120 121
    public String getNodeName() {
        return name;
K
kohsuke 已提交
122 123 124 125 126 127 128 129 130 131 132 133 134 135
    }

    public String getNodeDescription() {
        return description;
    }

    public int getNumExecutors() {
        return numExecutors;
    }

    public Mode getMode() {
        return mode;
    }

136 137 138
    public String getLabelString() {
        return Util.fixNull(label).trim();
    }
139

140
    public Set<Label> getAssignedLabels() {
141 142
        // todo refactor to make dynamic labels a bit less hacky
        if(labels==null || isChangedDynamicLabels()) {
143 144 145 146 147 148 149 150
            Set<Label> r = new HashSet<Label>();
            String ls = getLabelString();
            if(ls.length()>0) {
                for( String l : ls.split(" +")) {
                    r.add(Hudson.getInstance().getLabel(l));
                }
            }
            r.add(getSelfLabel());
151
            r.addAll(getDynamicLabels());
152 153 154 155 156
            this.labels = Collections.unmodifiableSet(r);
        }
        return labels;
    }

157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
    /**
     * Check if we should rebuild the list of dynamic labels.
     * @todo make less hacky
     * @return
     */
    private boolean isChangedDynamicLabels() {
        Computer comp = getComputer();
        if (comp == null) {
            return dynamicLabelsInstanceHash != 0;
        } else {
            if (dynamicLabelsInstanceHash == comp.hashCode()) {
                return false;
            }
            dynamicLabels = null; // force a re-calc
            return true;
        }
    }

    /**
     * Returns the possibly empty set of labels that it has been determined as supported by this node.
     *
     * @todo make less hacky
     * @see hudson.tasks.LabelFinder
180 181 182
     *
     * @return
     *      never null.
183 184
     */
    public Set<Label> getDynamicLabels() {
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
        // another thread may preempt and set dynamicLabels field to null,
        // so a care needs to be taken to avoid race conditions under all circumstances.
        Set<Label> labels = dynamicLabels;
        if (labels != null)     return labels;

        synchronized (this) {
            labels = dynamicLabels;
            if (labels != null)     return labels;

            dynamicLabels = labels = new HashSet<Label>();
            Computer computer = getComputer();
            VirtualChannel channel;
            if (computer != null && (channel = computer.getChannel()) != null) {
                dynamicLabelsInstanceHash = computer.hashCode();
                for (DynamicLabeler labeler : LabelFinder.LABELERS) {
                    for (String label : labeler.findLabels(channel)) {
                        labels.add(Hudson.getInstance().getLabel(label));
202 203
                    }
                }
204 205
            } else {
                dynamicLabelsInstanceHash = 0;
206
            }
207 208

            return labels;
209 210
        }
    }
211 212 213 214 215

    public Label getSelfLabel() {
        return Hudson.getInstance().getLabel(name);
    }

216
    public ClockDifference getClockDifference() throws IOException, InterruptedException {
K
kohsuke 已提交
217
        VirtualChannel channel = getComputer().getChannel();
K
kohsuke 已提交
218 219
        if(channel==null)
            throw new IOException(getNodeName()+" is offline");
K
kohsuke 已提交
220

K
kohsuke 已提交
221 222 223 224 225 226 227
        long startTime = System.currentTimeMillis();
        long slaveTime = channel.call(new Callable<Long,RuntimeException>() {
            public Long call() {
                return System.currentTimeMillis();
            }
        });
        long endTime = System.currentTimeMillis();
K
kohsuke 已提交
228

229
        return new ClockDifference((startTime+endTime)/2 - slaveTime);
K
kohsuke 已提交
230 231
    }

K
kohsuke 已提交
232 233 234 235
    public Computer createComputer() {
        return new ComputerImpl(this);
    }

236
    public FilePath getWorkspaceFor(TopLevelItem item) {
K
kohsuke 已提交
237 238 239
        FilePath r = getWorkspaceRoot();
        if(r==null)     return null;    // offline
        return r.child(item.getName());
240 241
    }

K
kohsuke 已提交
242 243 244 245 246 247
    public FilePath getRootPath() {
        VirtualChannel ch = getComputer().getChannel();
        if(ch==null)    return null;    // offline
        return new FilePath(ch,remoteFS);
    }

K
kohsuke 已提交
248 249
    /**
     * Root directory on this slave where all the job workspaces are laid out.
K
kohsuke 已提交
250 251
     * @return
     *      null if not connected.
K
kohsuke 已提交
252 253
     */
    public FilePath getWorkspaceRoot() {
K
kohsuke 已提交
254 255 256
        FilePath r = getRootPath();
        if(r==null) return null;
        return r.child("workspace");
K
kohsuke 已提交
257
    }
K
kohsuke 已提交
258

K
kohsuke 已提交
259 260
    public static final class ComputerImpl extends Computer {
        private volatile Channel channel;
261
        private Boolean isUnix;
K
kohsuke 已提交
262

K
kohsuke 已提交
263 264 265 266 267 268
        /**
         * This is where the log from the remote agent goes.
         */
        private File getLogFile() {
            return new File(Hudson.getInstance().getRootDir(),"slave-"+nodeName+".log");
        }
K
kohsuke 已提交
269

K
kohsuke 已提交
270 271 272 273
        private ComputerImpl(Slave slave) {
            super(slave);
        }

274 275 276 277 278 279 280 281 282
        public Slave getNode() {
            return (Slave)super.getNode();
        }

        @Override
        public boolean isJnlpAgent() {
            return getNode().getCommand().length()==0;
        }

283 284 285 286 287 288 289
        /**
         * Gets the formatted current time stamp.
         */
        private static String getTimestamp() {
            return String.format("[%1$tD %1$tT]",new Date());
        }

K
kohsuke 已提交
290 291 292 293 294 295
        /**
         * Launches a remote agent.
         */
        private void launch(final Slave slave) {
            closeChannel();

K
kohsuke 已提交
296
            final OutputStream launchLog = openLogFile();
K
kohsuke 已提交
297

298 299 300 301 302 303 304 305
            if(slave.agentCommand.length()>0) {
                // launch the slave agent asynchronously
                threadPoolForRemoting.execute(new Runnable() {
                    // TODO: do this only for nodes that are so configured.
                    // TODO: support passive connection via JNLP
                    public void run() {
                        final StreamTaskListener listener = new StreamTaskListener(launchLog);
                        try {
306
                            listener.getLogger().printf("%s Launching slave agent\n",getTimestamp());
307 308 309 310 311 312 313 314 315 316 317
                            listener.getLogger().println("$ "+slave.agentCommand);
                            final Process proc = Runtime.getRuntime().exec(slave.agentCommand);

                            // capture error information from stderr. this will terminate itself
                            // when the process is killed.
                            new StreamCopyThread("stderr copier for remote agent on "+slave.getNodeName(),
                                proc.getErrorStream(), launchLog).start();

                            setChannel(proc.getInputStream(),proc.getOutputStream(),launchLog,new Listener() {
                                public void onClosed(Channel channel, IOException cause) {
                                    if(cause!=null)
K
kohsuke 已提交
318
                                        cause.printStackTrace(listener.error("%s slave agent was terminated\n",getTimestamp()));
319 320 321 322
                                    if(Hudson.isWindows())
                                        new WinProcess(proc).killRecursively();
                                    else
                                        proc.destroy();
323 324 325 326 327
                                }
                            });

                            logger.info("slave agent launched for "+slave.getNodeName());

328 329
                        } catch (InterruptedException e) {
                            e.printStackTrace(listener.error("aborted"));
330 331 332 333 334 335 336 337 338 339
                        } catch (IOException e) {
                            Util.displayIOException(e,listener);

                            String msg = Util.getWin32ErrorMessage(e);
                            if(msg==null)   msg="";
                            else            msg=" : "+msg;
                            msg = "Unable to launch the slave agent for " + slave.getNodeName() + msg;
                            logger.log(Level.SEVERE,msg,e);
                            e.printStackTrace(listener.error(msg));
                        }
K
kohsuke 已提交
340
                    }
341 342
                });
            }
K
kohsuke 已提交
343
        }
K
kohsuke 已提交
344

K
kohsuke 已提交
345 346 347 348 349 350 351 352 353 354 355
        public OutputStream openLogFile() {
            OutputStream os;
            try {
                os = new FileOutputStream(getLogFile());
            } catch (FileNotFoundException e) {
                logger.log(Level.SEVERE, "Failed to create log file "+getLogFile(),e);
                os = new NullStream();
            }
            return os;
        }

356 357
        private final Object channelLock = new Object();

K
kohsuke 已提交
358 359 360
        /**
         * Creates a {@link Channel} from the given stream and sets that to this slave.
         */
361
        public void setChannel(InputStream in, OutputStream out, OutputStream launchLog, Listener listener) throws IOException, InterruptedException {
362
            synchronized(channelLock) {
K
kohsuke 已提交
363 364 365
                if(this.channel!=null)
                    throw new IllegalStateException("Already connected");

366
                Channel channel = new Channel(nodeName,threadPoolForRemoting, Channel.Mode.NEGOTIATE, 
K
kohsuke 已提交
367 368 369 370 371 372 373
                    in,out, launchLog);
                channel.addListener(new Listener() {
                    public void onClosed(Channel c,IOException cause) {
                        ComputerImpl.this.channel = null;
                    }
                });
                channel.addListener(listener);
374

375 376
                PrintWriter log = new PrintWriter(launchLog,true);

377 378 379 380
                {// send jars that we need for our operations
                    // TODO: maybe I should generalize this kind of "post initialization" processing
                    FilePath dst = new FilePath(channel,getNode().getRemoteFS());
                    new FilePath(Which.jarFile(Main.class)).copyTo(dst.child("maven-agent.jar"));
381
                    log.println("Copied maven-agent.jar");
382
                    new FilePath(Which.jarFile(PluginManagerInterceptor.class)).copyTo(dst.child("maven-interceptor.jar"));
383
                    log.println("Copied maven-interceptor.jar");
384 385
                }

K
kohsuke 已提交
386
                isUnix = channel.call(new DetectOS());
387 388
                log.println(isUnix?"This is a Unix slave":"This is a Windows slave");

K
kohsuke 已提交
389
                // install log handler
K
kohsuke 已提交
390
                channel.call(new LogInstaller());
K
kohsuke 已提交
391 392


393 394
                // prevent others from seeing a channel that's not properly initialized yet
                this.channel = channel;
K
kohsuke 已提交
395 396 397 398
            }
            Hudson.getInstance().getQueue().scheduleMaintenance();
        }

K
kohsuke 已提交
399 400 401 402 403
        @Override
        public VirtualChannel getChannel() {
            return channel;
        }

K
kohsuke 已提交
404 405 406 407 408 409 410 411 412 413 414
        public List<LogRecord> getLogRecords() throws IOException, InterruptedException {
            if(channel==null)
                return Collections.emptyList();
            else
                return channel.call(new Callable<List<LogRecord>,RuntimeException>() {
                    public List<LogRecord> call() {
                        return new ArrayList<LogRecord>(SLAVE_LOG_HANDLER.getView());
                    }
                });
        }

K
kohsuke 已提交
415
        public void doDoDisconnect(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
416
            Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
K
kohsuke 已提交
417 418 419 420
            closeChannel();
            rsp.sendRedirect(".");
        }

K
kohsuke 已提交
421 422 423 424
        public void doLaunchSlaveAgent(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
            if(channel!=null) {
                rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
                return;
K
kohsuke 已提交
425 426
            }

427
            launch();
K
kohsuke 已提交
428 429 430 431 432 433

            // TODO: would be nice to redirect the user to "launching..." wait page,
            // then spend a few seconds there and poll for the completion periodically.
            rsp.sendRedirect("log");
        }

434 435 436 437 438
        public void launch() {
            if(channel==null)
                launch(getNode());
        }

K
kohsuke 已提交
439 440 441 442 443 444 445 446 447 448 449 450 451 452
        /**
         * Gets the string representation of the slave log.
         */
        public String getLog() throws IOException {
            return Util.loadFile(getLogFile());
        }

        /**
         * Handles incremental log.
         */
        public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException {
            new LargeText(getLogFile(),false).doProgressText(req,rsp);
        }

453 454 455
        /**
         * Serves jar files for JNLP slave agents.
         */
K
kohsuke 已提交
456 457
        public JnlpJar getJnlpJars(String fileName) {
            return new JnlpJar(fileName);
458 459
        }

K
kohsuke 已提交
460 461 462 463 464 465 466 467 468
        @Override
        protected void kill() {
            super.kill();
            closeChannel();
        }

        private void closeChannel() {
            Channel c = channel;
            channel = null;
469
            isUnix=null;
K
kohsuke 已提交
470 471 472 473 474
            if(c!=null)
                try {
                    c.close();
                } catch (IOException e) {
                    logger.log(Level.SEVERE, "Failed to terminate channel to "+getDisplayName(),e);
K
kohsuke 已提交
475
                }
K
kohsuke 已提交
476 477 478 479 480 481 482 483 484 485 486
        }

        @Override
        protected void setNode(Node node) {
            super.setNode(node);
            if(channel==null)
                // maybe the configuration was changed to relaunch the slave, so try it now.
                launch((Slave)node);
        }

        private static final Logger logger = Logger.getLogger(ComputerImpl.class.getName());
K
kohsuke 已提交
487 488 489 490 491 492

        private static final class DetectOS implements Callable<Boolean,IOException> {
            public Boolean call() throws IOException {
                return File.pathSeparatorChar==':';
            }
        }
K
kohsuke 已提交
493 494
    }

495 496 497 498
    /**
     * Web-bound object used to serve jar files for JNLP.
     */
    public static final class JnlpJar {
K
kohsuke 已提交
499
        private final String fileName;
500

K
kohsuke 已提交
501 502
        public JnlpJar(String fileName) {
            this.fileName = fileName;
503 504 505
        }

        public void doIndex( StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
K
kohsuke 已提交
506 507 508 509
            URL res = req.getServletContext().getResource("/WEB-INF/" + fileName);
            if(res==null) {
                // during the development this path doesn't have the files.
                res = new URL(new File(".").getAbsoluteFile().toURL(),"target/generated-resources/WEB-INF/"+fileName);
510 511
            }

K
kohsuke 已提交
512
            URLConnection con = res.openConnection();
513 514 515 516 517 518 519
            InputStream in = con.getInputStream();
            rsp.serveFile(req, in, con.getLastModified(), con.getContentLength(), "*.jar" );
            in.close();
        }

    }

K
kohsuke 已提交
520
    public Launcher createLauncher(TaskListener listener) {
521 522
        ComputerImpl c = getComputer();
        return new RemoteLauncher(listener, c.getChannel(), c.isUnix);
K
kohsuke 已提交
523 524
    }

K
kohsuke 已提交
525 526 527
    /**
     * Gets th ecorresponding computer object.
     */
528 529
    public ComputerImpl getComputer() {
        return (ComputerImpl)Hudson.getInstance().getComputer(getNodeName());
K
kohsuke 已提交
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544
    }

    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        final Slave that = (Slave) o;

        return name.equals(that.name);
    }

    public int hashCode() {
        return name.hashCode();
    }

K
kohsuke 已提交
545 546 547 548 549 550 551 552 553 554 555 556
    /**
     * Invoked by XStream when this object is read into memory.
     */
    private Object readResolve() {
        // convert the old format to the new one
        if(command!=null && agentCommand==null) {
            if(command.length()>0)  command += ' ';
            agentCommand = command+"java -jar ~/bin/slave.jar";
        }
        return this;
    }

K
kohsuke 已提交
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572
    /**
     * This field is used on each slave node to record log records on the slave.
     */
    private static final RingBufferLogHandler SLAVE_LOG_HANDLER = new RingBufferLogHandler();

    private static class LogInstaller implements Callable<Void,RuntimeException> {
        public Void call() {
            // avoid double installation of the handler
            Logger logger = Logger.getLogger("hudson");
            logger.removeHandler(SLAVE_LOG_HANDLER);
            logger.addHandler(SLAVE_LOG_HANDLER);
            return null;
        }
        private static final long serialVersionUID = 1L;
    }

K
kohsuke 已提交
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591
//
// backwrad compatibility
//
    /**
     * In Hudson < 1.69 this was used to store the local file path
     * to the remote workspace. No longer in use.
     *
     * @deprecated
     *      ... but still in use during the transition.
     */
    private File localFS;

    /**
     * In Hudson < 1.69 this was used to store the command
     * to connect to the remote machine, like "ssh myslave".
     *
     * @deprecated
     */
    private transient String command;
K
kohsuke 已提交
592
}