KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > hudson > model > Slave


1 package hudson.model;
2
3 import hudson.CloseProofOutputStream;
4 import hudson.FilePath;
5 import hudson.Launcher;
6 import hudson.Launcher.LocalLauncher;
7 import hudson.Proc;
8 import hudson.Proc.RemoteProc;
9 import hudson.Util;
10 import hudson.maven.agent.Main;
11 import hudson.maven.agent.PluginManagerInterceptor;
12 import hudson.model.Descriptor.FormException;
13 import hudson.remoting.Callable;
14 import hudson.remoting.Channel;
15 import hudson.remoting.Channel.Listener;
16 import hudson.remoting.RemoteInputStream;
17 import hudson.remoting.RemoteOutputStream;
18 import hudson.remoting.VirtualChannel;
19 import hudson.remoting.Pipe;
20 import hudson.remoting.Which;
21 import hudson.util.NullStream;
22 import hudson.util.StreamCopyThread;
23 import hudson.util.StreamTaskListener;
24 import org.kohsuke.stapler.StaplerRequest;
25 import org.kohsuke.stapler.StaplerResponse;
26
27 import javax.servlet.ServletException JavaDoc;
28 import javax.servlet.http.HttpServletResponse JavaDoc;
29 import java.io.File JavaDoc;
30 import java.io.FileNotFoundException JavaDoc;
31 import java.io.FileOutputStream JavaDoc;
32 import java.io.IOException JavaDoc;
33 import java.io.InputStream JavaDoc;
34 import java.io.OutputStream JavaDoc;
35 import java.io.Serializable JavaDoc;
36 import java.io.PrintWriter JavaDoc;
37 import java.net.URL JavaDoc;
38 import java.net.URLConnection JavaDoc;
39 import java.util.logging.Level JavaDoc;
40 import java.util.logging.Logger JavaDoc;
41
42 /**
43  * Information about a Hudson slave node.
44  *
45  * @author Kohsuke Kawaguchi
46  */

47 public final class Slave implements Node, Serializable JavaDoc {
48     /**
49      * PluginName of this slave node.
50      */

51     protected final String JavaDoc name;
52
53     /**
54      * Description of this node.
55      */

56     private final String JavaDoc description;
57
58     /**
59      * Path to the root of the workspace
60      * from the view point of this node, such as "/hudson"
61      */

62     protected final String JavaDoc remoteFS;
63
64     /**
65      * Number of executors of this node.
66      */

67     private int numExecutors = 2;
68
69     /**
70      * Job allocation strategy.
71      */

72     private Mode mode;
73
74     /**
75      * Command line to launch the agent, like
76      * "ssh myslave java -jar /path/to/hudson-remoting.jar"
77      */

78     private String JavaDoc agentCommand;
79
80     /**
81      * @stapler-constructor
82      */

83     public Slave(String JavaDoc name, String JavaDoc description, String JavaDoc command, String JavaDoc remoteFS, int numExecutors, Mode mode) throws FormException {
84         this.name = name;
85         this.description = description;
86         this.numExecutors = numExecutors;
87         this.mode = mode;
88         this.agentCommand = command;
89         this.remoteFS = remoteFS;
90
91         if (name.equals(""))
92             throw new FormException("Invalid slave configuration. PluginName is empty", null);
93
94         // this prevents the config from being saved when slaves are offline.
95
// on a large deployment with a lot of slaves, some slaves are bound to be offline,
96
// so this check is harmful.
97
//if (!localFS.exists())
98
// throw new FormException("Invalid slave configuration for " + name + ". No such directory exists: " + localFS, null);
99
if (remoteFS.equals(""))
100             throw new FormException("Invalid slave configuration for " + name + ". No remote directory given", null);
101     }
102
103     public String JavaDoc getCommand() {
104         return agentCommand;
105     }
106
107     public String JavaDoc getRemoteFS() {
108         return remoteFS;
109     }
110
111     public String JavaDoc getNodeName() {
112         return name;
113     }
114
115     public String JavaDoc getNodeDescription() {
116         return description;
117     }
118
119     /**
120      * Gets the root directory of the Hudson workspace on this slave.
121      */

122     public FilePath getFilePath() {
123         return new FilePath(getComputer().getChannel(),remoteFS);
124     }
125
126     public int getNumExecutors() {
127         return numExecutors;
128     }
129
130     public Mode getMode() {
131         return mode;
132     }
133
134     /**
135      * Estimates the clock difference with this slave.
136      *
137      * @return
138      * difference in milli-seconds.
139      * a positive value indicates that the master is ahead of the slave,
140      * and negative value indicates otherwise.
141      */

142     public long getClockDifference() throws IOException JavaDoc {
143         VirtualChannel channel = getComputer().getChannel();
144         if(channel==null) return 0; // can't check
145

146         try {
147             long startTime = System.currentTimeMillis();
148             long slaveTime = channel.call(new Callable<Long JavaDoc,RuntimeException JavaDoc>() {
149                 public Long JavaDoc call() {
150                     return System.currentTimeMillis();
151                 }
152             });
153             long endTime = System.currentTimeMillis();
154
155             return (startTime+endTime)/2 - slaveTime;
156         } catch (InterruptedException JavaDoc e) {
157             return 0; // couldn't check
158
}
159     }
160
161
162     /**
163      * Gets the clock difference in HTML string.
164      */

165     public String JavaDoc getClockDifferenceString() {
166         try {
167             long diff = getClockDifference();
168             if(-1000<diff && diff <1000)
169                 return "In sync"; // clock is in sync
170

171             long abs = Math.abs(diff);
172
173             String JavaDoc s = Util.getTimeSpanString(abs);
174             if(diff<0)
175                 s += " ahead";
176             else
177                 s += " behind";
178
179             if(abs>100*60) // more than a minute difference
180
s = "<span class='error'>"+s+"</span>";
181
182             return s;
183         } catch (IOException JavaDoc e) {
184             return "<span class='error'>Unable to check</span>";
185         }
186     }
187
188     public Computer createComputer() {
189         return new ComputerImpl(this);
190     }
191
192     public FilePath getWorkspaceFor(TopLevelItem item) {
193         return getWorkspaceRoot().child(item.getName());
194     }
195
196     /**
197      * Root directory on this slave where all the job workspaces are laid out.
198      */

199     public FilePath getWorkspaceRoot() {
200         return getFilePath().child("workspace");
201     }
202
203     public static final class ComputerImpl extends Computer {
204         private volatile Channel channel;
205
206         /**
207          * This is where the log from the remote agent goes.
208          */

209         private File JavaDoc getLogFile() {
210             return new File JavaDoc(Hudson.getInstance().getRootDir(),"slave-"+nodeName+".log");
211         }
212
213         private ComputerImpl(Slave slave) {
214             super(slave);
215         }
216
217         public Slave getNode() {
218             return (Slave)super.getNode();
219         }
220
221         @Override JavaDoc
222         public boolean isJnlpAgent() {
223             return getNode().getCommand().length()==0;
224         }
225
226         /**
227          * Launches a remote agent.
228          */

229         private void launch(final Slave slave) {
230             closeChannel();
231
232             OutputStream os;
233             try {
234                 os = new FileOutputStream JavaDoc(getLogFile());
235             } catch (FileNotFoundException JavaDoc e) {
236                 logger.log(Level.SEVERE, "Failed to create log file "+getLogFile(),e);
237                 os = new NullStream();
238             }
239             final OutputStream launchLog = os;
240
241             if(slave.agentCommand.length()>0) {
242                 // launch the slave agent asynchronously
243
threadPoolForRemoting.execute(new Runnable JavaDoc() {
244                     // TODO: do this only for nodes that are so configured.
245
// TODO: support passive connection via JNLP
246
public void run() {
247                         final StreamTaskListener listener = new StreamTaskListener(launchLog);
248                         try {
249                             listener.getLogger().println("Launching slave agent");
250                             listener.getLogger().println("$ "+slave.agentCommand);
251                             final Process JavaDoc proc = Runtime.getRuntime().exec(slave.agentCommand);
252
253                             // capture error information from stderr. this will terminate itself
254
// when the process is killed.
255
new StreamCopyThread("stderr copier for remote agent on "+slave.getNodeName(),
256                                 proc.getErrorStream(), launchLog).start();
257
258                             setChannel(proc.getInputStream(),proc.getOutputStream(),launchLog,new Listener() {
259                                 public void onClosed(Channel channel, IOException JavaDoc cause) {
260                                     if(cause!=null)
261                                         cause.printStackTrace(listener.error("slave agent was terminated"));
262                                     proc.destroy();
263                                 }
264                             });
265
266                             logger.info("slave agent launched for "+slave.getNodeName());
267
268                         } catch (InterruptedException JavaDoc e) {
269                             e.printStackTrace(listener.error("aborted"));
270                         } catch (IOException JavaDoc e) {
271                             Util.displayIOException(e,listener);
272
273                             String JavaDoc msg = Util.getWin32ErrorMessage(e);
274                             if(msg==null) msg="";
275                             else msg=" : "+msg;
276                             msg = "Unable to launch the slave agent for " + slave.getNodeName() + msg;
277                             logger.log(Level.SEVERE,msg,e);
278                             e.printStackTrace(listener.error(msg));
279                         }
280                     }
281                 });
282             }
283         }
284
285         private final Object JavaDoc channelLock = new Object JavaDoc();
286
287         /**
288          * Creates a {@link Channel} from the given stream and sets that to this slave.
289          */

290         public void setChannel(InputStream in, OutputStream out, OutputStream launchLog, Listener listener) throws IOException JavaDoc, InterruptedException JavaDoc {
291             synchronized(channelLock) {
292                 if(this.channel!=null)
293                     throw new IllegalStateException JavaDoc("Already connected");
294
295                 Channel channel = new Channel(nodeName,threadPoolForRemoting,
296                     in,out, launchLog);
297                 channel.addListener(new Listener() {
298                     public void onClosed(Channel c,IOException JavaDoc cause) {
299                         ComputerImpl.this.channel = null;
300                     }
301                 });
302                 channel.addListener(listener);
303
304                 {// send jars that we need for our operations
305
// TODO: maybe I should generalize this kind of "post initialization" processing
306
PrintWriter JavaDoc log = new PrintWriter JavaDoc(launchLog,true);
307                     FilePath dst = new FilePath(channel,getNode().getRemoteFS());
308                     new FilePath(Which.jarFile(Main.class)).copyTo(dst.child("maven-agent.jar"));
309                     log.println("Copied maven-agent.jar");
310                     new FilePath(Which.jarFile(PluginManagerInterceptor.class)).copyTo(dst.child("maven-interceptor.jar"));
311                     log.println("Copied maven-interceptor.jar");
312                 }
313
314                 // prevent others from seeing a channel that's not properly initialized yet
315
this.channel = channel;
316             }
317             Hudson.getInstance().getQueue().scheduleMaintenance();
318         }
319
320         @Override JavaDoc
321         public VirtualChannel getChannel() {
322             return channel;
323         }
324
325         public void doLaunchSlaveAgent(StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc, ServletException JavaDoc {
326             if(channel!=null) {
327                 rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
328                 return;
329             }
330
331             launch(getNode());
332
333             // TODO: would be nice to redirect the user to "launching..." wait page,
334
// then spend a few seconds there and poll for the completion periodically.
335
rsp.sendRedirect("log");
336         }
337
338         /**
339          * Gets the string representation of the slave log.
340          */

341         public String JavaDoc getLog() throws IOException JavaDoc {
342             return Util.loadFile(getLogFile());
343         }
344
345         /**
346          * Handles incremental log.
347          */

348         public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc {
349             new LargeText(getLogFile(),false).doProgressText(req,rsp);
350         }
351
352         /**
353          * Serves jar files for JNLP slave agents.
354          */

355         public JnlpJar getJnlpJars(String JavaDoc fileName) {
356             return new JnlpJar(fileName);
357         }
358
359         @Override JavaDoc
360         protected void kill() {
361             super.kill();
362             closeChannel();
363         }
364
365         private void closeChannel() {
366             Channel c = channel;
367             channel = null;
368             if(c!=null)
369                 try {
370                     c.close();
371                 } catch (IOException JavaDoc e) {
372                     logger.log(Level.SEVERE, "Failed to terminate channel to "+getDisplayName(),e);
373                 }
374         }
375
376         @Override JavaDoc
377         protected void setNode(Node node) {
378             super.setNode(node);
379             if(channel==null)
380                 // maybe the configuration was changed to relaunch the slave, so try it now.
381
launch((Slave)node);
382         }
383
384         private static final Logger JavaDoc logger = Logger.getLogger(ComputerImpl.class.getName());
385     }
386
387     /**
388      * Web-bound object used to serve jar files for JNLP.
389      */

390     public static final class JnlpJar {
391         private final String JavaDoc fileName;
392
393         public JnlpJar(String JavaDoc fileName) {
394             this.fileName = fileName;
395         }
396
397         public void doIndex( StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc, ServletException JavaDoc {
398             URL JavaDoc res = req.getServletContext().getResource("/WEB-INF/" + fileName);
399             if(res==null) {
400                 // during the development this path doesn't have the files.
401
res = new URL JavaDoc(new File JavaDoc(".").getAbsoluteFile().toURL(),"target/generated-resources/WEB-INF/"+fileName);
402             }
403
404             URLConnection JavaDoc con = res.openConnection();
405             InputStream in = con.getInputStream();
406             rsp.serveFile(req, in, con.getLastModified(), con.getContentLength(), "*.jar" );
407             in.close();
408         }
409
410     }
411
412     public Launcher createLauncher(TaskListener listener) {
413         return new Launcher(listener, getComputer().getChannel()) {
414             public Proc launch(final String JavaDoc[] cmd, final String JavaDoc[] env, InputStream _in, OutputStream _out, FilePath _workDir) throws IOException JavaDoc {
415                 printCommandLine(cmd,_workDir);
416
417                 final OutputStream out = new RemoteOutputStream(new CloseProofOutputStream(_out));
418                 final InputStream in = _in==null ? null : new RemoteInputStream(_in);
419                 final String JavaDoc workDir = _workDir==null ? null : _workDir.getRemote();
420
421                 return new RemoteProc(getChannel().callAsync(new RemoteLaunchCallable(cmd, env, in, out, workDir)));
422             }
423
424             public Channel launchChannel(String JavaDoc[] cmd, OutputStream err, FilePath _workDir) throws IOException JavaDoc, InterruptedException JavaDoc {
425                 printCommandLine(cmd, _workDir);
426
427                 Pipe out = Pipe.createRemoteToLocal();
428                 final String JavaDoc workDir = _workDir==null ? null : _workDir.getRemote();
429
430                 OutputStream os = getChannel().call(new RemoteChannelLaunchCallable(cmd, out, err, workDir));
431
432                 return new Channel("remotely launched channel on "+getNodeName(),
433                     Computer.threadPoolForRemoting, out.getIn(), os);
434             }
435
436             @Override JavaDoc
437             public boolean isUnix() {
438                 // Windows can handle '/' as a path separator but Unix can't,
439
// so err on Unix side
440
return remoteFS.indexOf("\\")==-1;
441             }
442         };
443     }
444
445     /**
446      * Gets th ecorresponding computer object.
447      */

448     public Computer getComputer() {
449         return Hudson.getInstance().getComputer(getNodeName());
450     }
451
452     public boolean equals(Object JavaDoc o) {
453         if (this == o) return true;
454         if (o == null || getClass() != o.getClass()) return false;
455
456         final Slave that = (Slave) o;
457
458         return name.equals(that.name);
459     }
460
461     public int hashCode() {
462         return name.hashCode();
463     }
464
465     /**
466      * Invoked by XStream when this object is read into memory.
467      */

468     private Object JavaDoc readResolve() {
469         // convert the old format to the new one
470
if(command!=null && agentCommand==null) {
471             if(command.length()>0) command += ' ';
472             agentCommand = command+"java -jar ~/bin/slave.jar";
473         }
474         return this;
475     }
476
477 //
478
// backwrad compatibility
479
//
480
/**
481      * In Hudson < 1.69 this was used to store the local file path
482      * to the remote workspace. No longer in use.
483      *
484      * @deprecated
485      * ... but still in use during the transition.
486      */

487     private File JavaDoc localFS;
488
489     /**
490      * In Hudson < 1.69 this was used to store the command
491      * to connect to the remote machine, like "ssh myslave".
492      *
493      * @deprecated
494      */

495     private transient String JavaDoc command;
496
497     private static class RemoteLaunchCallable implements Callable<Integer JavaDoc,IOException JavaDoc> {
498         private final String JavaDoc[] cmd;
499         private final String JavaDoc[] env;
500         private final InputStream in;
501         private final OutputStream out;
502         private final String JavaDoc workDir;
503
504         public RemoteLaunchCallable(String JavaDoc[] cmd, String JavaDoc[] env, InputStream in, OutputStream out, String JavaDoc workDir) {
505             this.cmd = cmd;
506             this.env = env;
507             this.in = in;
508             this.out = out;
509             this.workDir = workDir;
510         }
511
512         public Integer JavaDoc call() throws IOException JavaDoc {
513             Proc p = new LocalLauncher(TaskListener.NULL).launch(cmd, env, in, out,
514                 workDir ==null ? null : new FilePath(new File JavaDoc(workDir)));
515             return p.join();
516         }
517
518         private static final long serialVersionUID = 1L;
519     }
520
521     private static class RemoteChannelLaunchCallable implements Callable<OutputStream,IOException JavaDoc> {
522         private final String JavaDoc[] cmd;
523         private final Pipe out;
524         private final String JavaDoc workDir;
525         private final OutputStream err;
526
527         public RemoteChannelLaunchCallable(String JavaDoc[] cmd, Pipe out, OutputStream err, String JavaDoc workDir) {
528             this.cmd = cmd;
529             this.out = out;
530             this.err = new RemoteOutputStream(err);
531             this.workDir = workDir;
532         }
533
534         public OutputStream call() throws IOException JavaDoc {
535             Process JavaDoc p = Runtime.getRuntime().exec(cmd, null, workDir == null ? null : new File JavaDoc(workDir));
536
537             new StreamCopyThread("stdin copier for remote agent on "+cmd,
538                 p.getInputStream(), out.getOut()).start();
539             new StreamCopyThread("stderr copier for remote agent on "+cmd,
540                 p.getErrorStream(), err).start();
541
542             // TODO: don't we need to join?
543

544             return new RemoteOutputStream(p.getOutputStream());
545         }
546
547         private static final long serialVersionUID = 1L;
548     }
549 }
550
Popular Tags