/******************************************************************************* * * Copyright (c) 2004-2012 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi, Winston Prakash, Seiji Sogabe, Stephen Connolly, Thomas J. Black, Tom Huybrechts * *******************************************************************************/ package hudson.model; import hudson.EnvVars; import hudson.Util; import hudson.cli.declarative.CLIMethod; import hudson.console.AnnotatedLargeText; import hudson.model.Descriptor.FormException; import hudson.model.queue.WorkUnit; import hudson.node_monitors.NodeMonitor; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; import hudson.remoting.Callable; import hudson.security.*; import hudson.slaves.ComputerLauncher; import hudson.slaves.RetentionStrategy; import hudson.slaves.WorkspaceList; import hudson.slaves.OfflineCause; import hudson.slaves.OfflineCause.ByCLI; import hudson.tasks.BuildWrapper; import hudson.tasks.Publisher; import hudson.util.BuildHistoryList; import hudson.util.DaemonThreadFactory; import hudson.util.ExceptionCatchingThreadFactory; import hudson.util.RemotingDiagnostics; import hudson.util.RemotingDiagnostics.HeapDump; import hudson.util.RunList; import hudson.util.Futures; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.args4j.Option; import javax.servlet.ServletException; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Enumeration; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ExecutionException; import java.util.logging.LogRecord; import java.util.logging.Level; import java.util.logging.Logger; import java.nio.charset.Charset; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.Inet4Address; import java.util.Collection; import org.eclipse.hudson.security.HudsonSecurityEntitiesHolder; import org.eclipse.hudson.security.HudsonSecurityManager; import org.eclipse.hudson.security.team.Team; import org.eclipse.hudson.security.team.TeamManager; /** * Represents the running state of a remote computer that holds * {@link Executor}s. * * <p> {@link Executor}s on one {@link Computer} are transparently * interchangeable (that is the definition of {@link Computer}.) * * <p> This object is related to {@link Node} but they have some significant * difference. {@link Computer} primarily works as a holder of * {@link Executor}s, so if a {@link Node} is configured (probably temporarily) * with 0 executors, you won't have a {@link Computer} object for it. * * Also, even if you remove a {@link Node}, it takes time for the corresponding * {@link Computer} to be removed, if some builds are already in progress on * that node. Or when the node configuration is changed, unaffected * {@link Computer} object remains intact, while all the {@link Node} objects * will go away. * * <p> This object also serves UI (since {@link Node} is an interface and can't * have related side pages.) * * @author Kohsuke Kawaguchi */ @ExportedBean public /*transient*/ abstract class Computer extends Actionable implements AccessControlled, ExecutorListener { /** * Parameter in request for deleting the slave. */ private static final String DELETE_MODE_KEY = "deleteMode"; /** * Key for delete mode when the jobs are stopping before the slave will * delete */ private static final String DELETE_MODE_STOP_KEY = "0"; private final CopyOnWriteArrayList<Executor> executors = new CopyOnWriteArrayList<Executor>(); // TODO: private final CopyOnWriteArrayList<OneOffExecutor> oneOffExecutors = new CopyOnWriteArrayList<OneOffExecutor>(); private int numExecutors; /** * Contains info about reason behind computer being offline. */ protected volatile OfflineCause offlineCause; private long connectTime = 0; /** * True if Hudson shouldn't start new builds on this node. */ private boolean temporarilyOffline; /** * {@link Node} object may be created and deleted independently from this * object. */ protected String nodeName; /** * @see #getHostName() */ private volatile String cachedHostName; private volatile boolean hostNameCached; private final WorkspaceList workspaceList = new WorkspaceList(); public Computer(Node node) { assert node.getNumExecutors() != 0 : "Computer created with 0 executors"; setNode(node); } /** * This is where the log from the remote agent goes. */ protected File getLogFile() { return new File(Hudson.getInstance().getRootDir(), "slave-" + nodeName + ".log"); } /** * Gets the object that coordinates the workspace allocation on this * computer. */ public WorkspaceList getWorkspaceList() { return workspaceList; } /** * Gets the string representation of the slave log. */ public String getLog() throws IOException { return Util.loadFile(getLogFile()); } /** * Used to URL-bind {@link AnnotatedLargeText}. */ public AnnotatedLargeText<Computer> getLogText() { return new AnnotatedLargeText<Computer>(getLogFile(), Charset.defaultCharset(), false, this); } public ACL getACL() { return HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getAuthorizationStrategy().getACL(this); } public void checkPermission(Permission permission) { getACL().checkPermission(permission); } public boolean hasPermission(Permission permission) { return getACL().hasPermission(permission); } /** * If the computer was offline (either temporarily or not), this method will * return the cause. * * @return null if the system was put offline without given a cause. */ @Exported public OfflineCause getOfflineCause() { return offlineCause; } /** * Gets the channel that can be used to run a program on this computer. * * @return never null when {@link #isOffline()}==false. */ public abstract VirtualChannel getChannel(); /** * Gets the default charset of this computer. * * @return never null when {@link #isOffline()}==false. */ public abstract Charset getDefaultCharset(); /** * Gets the logs recorded by this slave. */ public abstract List<LogRecord> getLogRecords() throws IOException, InterruptedException; /** * If {@link #getChannel()}==null, attempts to relaunch the slave agent. */ public abstract void doLaunchSlaveAgent(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException; /** * @deprecated since 2009-01-06. Use {@link #connect(boolean)} */ public final void launch() { connect(true); } /** * Do the same as * {@link #doLaunchSlaveAgent(StaplerRequest, StaplerResponse)} but outside * the context of serving a request. * * <p> If already connected or if this computer doesn't support proactive * launching, no-op. This method may return immediately while the launch * operation happens asynchronously. * * @see #disconnect() * * @param forceReconnect If true and a connect activity is already in * progress, it will be cancelled and the new one will be started. If false, * and a connect activity is already in progress, this method will do * nothing and just return the pending connection operation. * @return A {@link Future} representing pending completion of the task. The * 'completion' includes both a successful completion and a non-successful * completion (such distinction typically doesn't make much sense because as * soon as {@link Computer} is connected it can be disconnected by some * other threads.) */ public final Future<?> connect(boolean forceReconnect) { connectTime = System.currentTimeMillis(); return _connect(forceReconnect); } /** * Allows implementing-classes to provide an implementation for the connect * method. * * <p> If already connected or if this computer doesn't support proactive * launching, no-op. This method may return immediately while the launch * operation happens asynchronously. * * @see #disconnect() * * @param forceReconnect If true and a connect activity is already in * progress, it will be cancelled and the new one will be started. If false, * and a connect activity is already in progress, this method will do * nothing and just return the pending connection operation. * @return A {@link Future} representing pending completion of the task. The * 'completion' includes both a successful completion and a non-successful * completion (such distinction typically doesn't make much sense because as * soon as {@link Computer} is connected it can be disconnected by some * other threads.) */ protected abstract Future<?> _connect(boolean forceReconnect); /** * CLI command to reconnect this node. */ @CLIMethod(name = "connect-node") public void cliConnect(@Option(name = "-f", usage = "Cancel any currently pending connect operation and retry from scratch") boolean force) throws ExecutionException, InterruptedException { checkPermission(Hudson.ADMINISTER); connect(force).get(); } /** * Gets the time (since epoch) when this computer connected. * * @return The time in ms since epoch when this computer last connected. */ public final long getConnectTime() { return connectTime; } /** * Disconnect this computer. * * If this is the master, no-op. This method may return immediately while * the launch operation happens asynchronously. * * @param cause Object that identifies the reason the node was disconnected. * * @return {@link Future} to track the asynchronous disconnect operation. * @see #connect(boolean) * @since 1.320 */ public Future<?> disconnect(OfflineCause cause) { offlineCause = cause; if (Util.isOverridden(Computer.class, getClass(), "disconnect")) { return disconnect(); // legacy subtypes that extend disconnect(). } connectTime = 0; return Futures.precomputed(null); } /** * Equivalent to {@code disconnect(null)} * * @deprecated as of 1.320. Use {@link #disconnect(OfflineCause)} and * specify the cause. */ public Future<?> disconnect() { if (Util.isOverridden(Computer.class, getClass(), "disconnect", OfflineCause.class)) // if the subtype already derives disconnect(OfflineCause), delegate to it { return disconnect(null); } connectTime = 0; return Futures.precomputed(null); } /** * CLI command to disconnects this node. */ @CLIMethod(name = "disconnect-node") public void cliDisconnect(@Option(name = "-m", usage = "Record the note about why you are disconnecting this node") String cause) throws ExecutionException, InterruptedException { checkPermission(Hudson.ADMINISTER); disconnect(new ByCLI(cause)).get(); } /** * CLI command to mark the node offline. */ @CLIMethod(name = "offline-node") public void cliOffline(@Option(name = "-m", usage = "Record the note about why you are disconnecting this node") String cause) throws ExecutionException, InterruptedException { checkPermission(CONFIGURE); setTemporarilyOffline(true, new ByCLI(cause)); } @CLIMethod(name = "online-node") public void cliOnline() throws ExecutionException, InterruptedException { checkPermission(CONFIGURE); setTemporarilyOffline(false, null); } /** * Number of {@link Executor}s that are configured for this computer. * * <p> When this value is decreased, it is temporarily possible for * {@link #executors} to have a larger number than this. */ // ugly name to let EL access this @Exported public int getNumExecutors() { return numExecutors; } /** * Returns {@link Node#getNodeName() the name of the node}. */ public String getName() { return nodeName; } /** * Returns the {@link Node} that this computer represents. * * @return null if the configuration has changed and the node is removed, * yet the corresponding {@link Computer} is not yet gone. */ public Node getNode() { if (nodeName == null) { return Hudson.getInstance(); } return Hudson.getInstance().getNode(nodeName); } @Exported public LoadStatistics getLoadStatistics() { return getNode().getSelfLabel().loadStatistics; } public BuildTimelineWidget getTimeline() { Collection<Job> allJobs = Hudson.getInstance().getAllItems(Job.class); return new BuildTimelineWidget(BuildHistoryList.newBuildHistoryList(allJobs)); } /** * {@inheritDoc} */ public void taskAccepted(Executor executor, Queue.Task task) { // dummy implementation } /** * {@inheritDoc} */ public void taskCompleted(Executor executor, Queue.Task task, long durationMS) { // dummy implementation } /** * {@inheritDoc} */ public void taskCompletedWithProblems(Executor executor, Queue.Task task, long durationMS, Throwable problems) { // dummy implementation } @Exported public boolean isOffline() { return temporarilyOffline || getChannel() == null; } public final boolean isOnline() { return !isOffline(); } /** * This method is called to determine whether manual launching of the slave * is allowed at this point in time. * * @return {@code true} if manual launching of the slave is allowed at this * point in time. */ @Exported public boolean isManualLaunchAllowed() { return getRetentionStrategy().isManualLaunchAllowed(this); } /** * Is a {@link #connect(boolean)} operation in progress? */ public abstract boolean isConnecting(); /** * Returns true if this computer is supposed to be launched via JNLP. * * @deprecated since 2008-05-18. See {@linkplain #isLaunchSupported()} and * {@linkplain ComputerLauncher} */ @Exported @Deprecated public boolean isJnlpAgent() { return false; } /** * Returns true if this computer can be launched by Hudson proactively and * automatically. * * <p> For example, JNLP slaves return {@code false} from this, because the * launch process needs to be initiated from the slave side. */ @Exported public boolean isLaunchSupported() { return true; } /** * Returns true if this node is marked temporarily offline by the user. * * <p> In contrast, {@link #isOffline()} represents the actual * online/offline state. For example, this method may return false while * {@link #isOffline()} returns true if the slave agent failed to launch. * * @deprecated You should almost always want {@link #isOffline()}. This * method is marked as deprecated to warn people when they accidentally call * this method. */ @Exported public boolean isTemporarilyOffline() { return temporarilyOffline; } /** * @deprecated as of 1.320. Use * {@link #setTemporarilyOffline(boolean, OfflineCause)} */ public void setTemporarilyOffline(boolean temporarilyOffline) { setTemporarilyOffline(temporarilyOffline, null); } /** * Marks the computer as temporarily offline. This retains the underlying * {@link Channel} connection, but prevent builds from executing. * * @param cause If the first argument is true, specify the reason why the * node is being put offline. */ public void setTemporarilyOffline(boolean temporarilyOffline, OfflineCause cause) { offlineCause = temporarilyOffline ? cause : null; this.temporarilyOffline = temporarilyOffline; Node node = getNode(); if (null != node) { node.setOfflineCause(offlineCause); } Hudson.getInstance().getQueue().scheduleMaintenance(); } @Exported public String getIcon() { if (isOffline()) { return "computer-x.png"; } else { return "computer.png"; } } public String getIconAltText() { if (isOffline()) { return "[offline]"; } else { return "[online]"; } } @Exported public String getDisplayName() { return nodeName; } public String getCaption() { return Messages.Computer_Caption(nodeName); } public String getUrl() { return "computer/" + getDisplayName() + "/"; } /** * Returns projects that are tied on this node. */ public List<AbstractProject> getTiedJobs() { return getNode().getSelfLabel().getTiedJobs(); } public RunList getBuilds() { return new RunList(Hudson.getInstance().getAllItems(Job.class)).node(getNode()); } /** * Returns jobs that running on current computer. * * @return List<AbstractProject>. */ @Exported public List<AbstractProject> getRunningJobs() { List<AbstractProject> jobs = new ArrayList<AbstractProject>(); Queue queue = Hudson.getInstance().getQueue(); if (getTiedJobs() != null) { for (AbstractProject project : getTiedJobs()) { if (project.isBuilding() || queue.contains(project)) { jobs.add(project); } } } return jobs; } /** * Called to notify {@link Computer} that its corresponding {@link Node} * configuration is updated. */ protected void setNode(Node node) { assert node != null; if (node instanceof Slave) { this.nodeName = node.getNodeName(); } else { this.nodeName = null; } setNumExecutors(node.getNumExecutors()); if (temporarilyOffline) { node.setOfflineCause(offlineCause); } } /** * Called by {@link Hudson#updateComputerList()} to notify {@link Computer} * that it will be discarded. */ protected void kill() { setNumExecutors(0); } private synchronized void setNumExecutors(int n) { if (numExecutors == n) { return; // no-op } int diff = n - numExecutors; this.numExecutors = n; if (diff < 0) { // send signal to all idle executors to potentially kill them off for (Executor e : executors) { if (e.isIdle()) { e.interrupt(); } } } else { // if the number is increased, add new ones while (executors.size() < numExecutors) { Executor e = new Executor(this, executors.size()); e.start(); executors.add(e); } } } /** * @since 2.1.0 */ public int getIdleCount() { return countIdle(); } /** * Returns the number of idle {@link Executor}s that can start working * immediately. */ public int countIdle() { int n = 0; for (Executor e : executors) { if (e.isIdle()) { n++; } } return n; } /** * Returns the number of {@link Executor}s that are doing some work right * now. */ public final int countBusy() { return countExecutors() - countIdle(); } /** * @since 2.1.0 */ public int getBusyCount() { return countBusy(); } /** * Returns the current size of the executor pool for this computer. This * number may temporarily differ from {@link #getNumExecutors()} if there * are busy tasks when the configured size is decreased. OneOffExecutors are * not included in this count. */ public final int countExecutors() { return executors.size(); } /** * @since 2.1.0 */ public int getExecutorsCount() { return countExecutors(); } /** * @since 2.1.0 */ public int getOneOffExecutorsCount() { return oneOffExecutors.size(); } /** * Gets the read-only snapshot view of all {@link Executor}s. */ @Exported public List<Executor> getExecutors() { return new ArrayList<Executor>(executors); } /** * Gets the read-only snapshot view of all {@link OneOffExecutor}s. */ @Exported public List<OneOffExecutor> getOneOffExecutors() { return new ArrayList<OneOffExecutor>(oneOffExecutors); } /** * Returns true if all the executors of this computer are idle. */ @Exported public final boolean isIdle() { if (!oneOffExecutors.isEmpty()) { return false; } for (Executor e : executors) { if (!e.isIdle()) { return false; } } return true; } public final boolean isPartiallyIdle() { for (Executor e : executors) { if (e.isIdle()) { return true; } } return false; } /** * Returns the time when this computer last became idle. * * <p> If this computer is already idle, the return value will point to the * time in the past since when this computer has been idle. * * <p> If this computer is busy, the return value will point to the time in * the future where this computer will be expected to become free. */ public final long getIdleStartMilliseconds() { long firstIdle = Long.MIN_VALUE; for (Executor e : oneOffExecutors) { firstIdle = Math.max(firstIdle, e.getIdleStartMilliseconds()); } for (Executor e : executors) { firstIdle = Math.max(firstIdle, e.getIdleStartMilliseconds()); } return firstIdle; } /** * Returns the time when this computer first became in demand. */ public final long getDemandStartMilliseconds() { long firstDemand = Long.MAX_VALUE; for (Queue.BuildableItem item : Hudson.getInstance().getQueue().getBuildableItems(this)) { firstDemand = Math.min(item.buildableStartMilliseconds, firstDemand); } return firstDemand; } /** * Called by {@link Executor} to kill excessive executors from this * computer. */ /*package*/ synchronized void removeExecutor(Executor e) { executors.remove(e); if (!isAlive()) { Hudson.getInstance().removeComputer(this); } } /** * Returns true if any of the executors are functioning. * * Note that if an executor dies, we'll leave it in {@link #executors} until * the administrator yanks it out, so that we can see why it died. */ private boolean isAlive() { for (Executor e : executors) { if (e.isAlive()) { return true; } } return false; } /** * Interrupt all {@link Executor}s. */ public void interrupt() { for (Executor e : executors) { e.interrupt(); } } public String getSearchUrl() { return "computer/" + nodeName; } /** * {@link RetentionStrategy} associated with this computer. * * @return never null. This method return * {@code RetentionStrategy<? super T>} where {@code T=this.getClass()}. */ public abstract RetentionStrategy getRetentionStrategy(); /** * Expose monitoring data for the remote API. */ @Exported(inline = true) public Map<String/*monitor name*/, Object> getMonitorData() { Map<String, Object> r = new HashMap<String, Object>(); for (NodeMonitor monitor : NodeMonitor.getAll()) { r.put(monitor.getClass().getName(), monitor.data(this)); } return r; } /** * Gets the system properties of the JVM on this computer. If this is the * master, it returns the system property of the master computer. */ public Map<Object, Object> getSystemProperties() throws IOException, InterruptedException { return RemotingDiagnostics.getSystemProperties(getChannel()); } /** * @deprecated as of 1.292 Use {@link #getEnvironment()} instead. */ public Map<String, String> getEnvVars() throws IOException, InterruptedException { return getEnvironment(); } /** * Gets the environment variables of the JVM on this computer. If this is * the master, it returns the system property of the master computer. */ public EnvVars getEnvironment() throws IOException, InterruptedException { return EnvVars.getRemote(getChannel()); } /** * Gets the thread dump of the slave JVM. * * @return key is the thread name, and the value is the pre-formatted dump. */ public Map<String, String> getThreadDump() throws IOException, InterruptedException { return RemotingDiagnostics.getThreadDump(getChannel()); } /** * Obtains the heap dump. */ public HeapDump getHeapDump() throws IOException { return new HeapDump(this, getChannel()); } /** * This method tries to compute the name of the host that's reachable by all * the other nodes. * * <p> Since it's possible that the slave is not reachable from the master * (it may be behind a firewall, connecting to master via JNLP), this method * may return null. * * It's surprisingly tricky for a machine to know a name that other systems * can get to, especially between things like DNS search suffix, the hosts * file, and YP. * * <p> So the technique here is to compute possible interfaces and names on * the slave, then try to ping them from the master, and pick the one that * worked. * * <p> The computation may take some time, so it employs caching to make the * successive lookups faster. * * @since 1.300 * @return null if the host name cannot be computed (for example because * this computer is offline, because the slave is behind the firewall, etc.) */ public String getHostName() throws IOException, InterruptedException { if (hostNameCached) // in the worst case we end up having multiple threads computing the host name simultaneously, but that's not harmful, just wasteful. { return cachedHostName; } VirtualChannel channel = getChannel(); if (channel == null) { return null; // can't compute right now } for (String address : channel.call(new ListPossibleNames())) { try { InetAddress ia = InetAddress.getByName(address); if (!(ia instanceof Inet4Address)) { LOGGER.fine(address + " is not an IPv4 address"); continue; } if (!ComputerPinger.checkIsReachable(ia, 3)) { LOGGER.fine(address + " didn't respond to ping"); continue; } cachedHostName = ia.getCanonicalHostName(); hostNameCached = true; return cachedHostName; } catch (IOException e) { // if a given name fails to parse on this host, we get this error LOGGER.log(Level.FINE, "Failed to parse " + address, e); } } // allow the administrator to manually specify the host name as a fallback. HUDSON-5373 cachedHostName = channel.call(new GetFallbackName()); hostNameCached = true; return null; } /** * Starts executing a fly-weight task. */ /*package*/ final void startFlyWeightTask(WorkUnit p) { OneOffExecutor e = new OneOffExecutor(this, p); e.start(); oneOffExecutors.add(e); } /*package*/ final void remove(OneOffExecutor e) { oneOffExecutors.remove(e); } private static class ListPossibleNames implements Callable<List<String>, IOException> { public List<String> call() throws IOException { List<String> names = new ArrayList<String>(); Enumeration<NetworkInterface> nis = NetworkInterface.getNetworkInterfaces(); while (nis.hasMoreElements()) { NetworkInterface ni = nis.nextElement(); LOGGER.fine("Listing up IP addresses for " + ni.getDisplayName()); Enumeration<InetAddress> e = ni.getInetAddresses(); while (e.hasMoreElements()) { InetAddress ia = e.nextElement(); if (ia.isLoopbackAddress()) { LOGGER.fine(ia + " is a loopback address"); continue; } if (!(ia instanceof Inet4Address)) { LOGGER.fine(ia + " is not an IPv4 address"); continue; } LOGGER.fine(ia + " is a viable candidate"); names.add(ia.getHostAddress()); } } return names; } private static final long serialVersionUID = 1L; } private static class GetFallbackName implements Callable<String, IOException> { public String call() throws IOException { return System.getProperty("host.name"); } private static final long serialVersionUID = 1L; } public static final ExecutorService threadPoolForRemoting = Executors.newCachedThreadPool(new ExceptionCatchingThreadFactory(new DaemonThreadFactory())); // // // UI // // public void doRssAll(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { rss(req, rsp, " all builds", getBuilds()); } public void doRssFailed(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { rss(req, rsp, " failed builds", getBuilds().failureOnly()); } private void rss(StaplerRequest req, StaplerResponse rsp, String suffix, RunList runs) throws IOException, ServletException { RSS.forwardToRss(getDisplayName() + suffix, getUrl(), runs.newBuilds(), Run.FEED_ADAPTER, req, rsp); } public HttpResponse doToggleOffline(@QueryParameter String offlineMessage) throws IOException, ServletException { checkPermission(CONFIGURE); if (!temporarilyOffline) { offlineMessage = Util.fixEmptyAndTrim(offlineMessage); setTemporarilyOffline(!temporarilyOffline, OfflineCause.create(hudson.slaves.Messages._SlaveComputer_DisconnectedBy( HudsonSecurityManager.getAuthentication().getName(), offlineMessage != null ? " : " + offlineMessage : ""))); } else { setTemporarilyOffline(!temporarilyOffline, null); } return HttpResponses.redirectToDot(); } public Api getApi() { return new Api(this); } /** * Dumps the contents of the export table. */ public void doDumpExportTable(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException { // this is a debug probe and may expose sensitive information checkPermission(Hudson.ADMINISTER); rsp.setContentType("text/plain"); PrintWriter w = new PrintWriter(rsp.getCompressedWriter(req)); VirtualChannel vc = getChannel(); if (vc instanceof Channel) { w.println("Master to slave"); ((Channel) vc).dumpExportTable(w); w.flush(); // flush here once so that even if the dump from the slave fails, the client gets some useful info w.println("\n\n\nSlave to master"); w.print(vc.call(new DumpExportTableTask())); } else { w.println(Messages.Computer_BadChannel()); } w.close(); } private static final class DumpExportTableTask implements Callable<String, IOException> { public String call() throws IOException { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); Channel.current().dumpExportTable(pw); pw.close(); return sw.toString(); } } /** * For system diagnostics. Run arbitrary script. */ public void doScript(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { _doScript(req, rsp, "_script.jelly"); } /** * Run the arbitrary script and return result as plain text. */ public void doScriptText(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { _doScript(req, rsp, "_scriptText.jelly"); } protected void _doScript(StaplerRequest req, StaplerResponse rsp, String view) throws IOException, ServletException { // ability to run arbitrary script is dangerous, // so tie it to the admin access checkPermission(Hudson.ADMINISTER); if (Hudson.getInstance().getScriptSupport() != null) { String text = req.getParameter("script"); if (text != null) { if (!"POST".equals(req.getMethod())) { throw HttpResponses.error(HttpURLConnection.HTTP_BAD_METHOD, "requires POST"); } try { if (getChannel() == null){ rsp.getWriter().println("Failed to run the script. Is node online?"); return; } req.setAttribute("output", RemotingDiagnostics.executeScript(text, getChannel(), Hudson.getInstance().getScriptSupport())); } catch (InterruptedException e) { throw new ServletException(e); } } } req.getView(this, view).forward(req, rsp); } /** * Accepts the update to the node configuration. */ public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException { checkPermission(CONFIGURE); final Hudson app = Hudson.getInstance(); Node result = getNode().getDescriptor().newInstance(req, req.getSubmittedForm()); // replace the old Node object by the new one synchronized (app) { List<Node> nodes = new ArrayList<Node>(app.getNodes()); Node prevNode = getNode(); int i = nodes.indexOf(prevNode); if (i < 0) { sendError("This slave appears to be removed while you were editing the configuration", req, rsp); return; } String oldName = prevNode.getNodeName(); String newName = result.getNodeName(); if (!newName.equals(oldName)) { // Check if the Appointed node of a job is same as node that is renamed, if so modify it // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=401683 for (Job job : Hudson.getInstance().getAllItems(Job.class)) { if (job instanceof AbstractProject) { AbstractProject project = (AbstractProject) job; AppointedNode appointedNode = project.getAppointedNode(); if ((appointedNode != null) && oldName.equals(appointedNode.getNodeName())) { appointedNode.setNodeName(newName); job.save(); } } } TeamManager teamManager = Hudson.getInstance().getTeamManager(); if (teamManager.isTeamManagementEnabled()) { try { Team team = teamManager.findNodeOwnerTeam(oldName); teamManager.renameNode(team, oldName, newName); } catch (IOException ex) { LOGGER.log(Level.WARNING, "Failed to rename node in team.", ex); } } } nodes.set(i, result); app.setNodes(nodes); } // take the user back to the slave top page. rsp.sendRedirect2("../" + result.getNodeName() + '/'); } /** * Really deletes the slave. */ @CLIMethod(name = "delete-node") public HttpResponse doDoDelete() throws IOException { checkPermission(DELETE); Hudson.getInstance().removeNode(getNode()); removeFromTeam(getName()); return new HttpRedirect(".."); } /** * Delete the slave. If request have parameter 'deleteMode' than deleting * performs in according with this parameter. * * @param req StaplerRequest * @return HttpResponse * @throws IOException if any. * @throws ServletException if any. */ public HttpResponse doDeleteWithParam(StaplerRequest req) throws IOException, ServletException { checkPermission(DELETE); String deleteMode = req.getParameter(DELETE_MODE_KEY); if (deleteMode != null) { if (DELETE_MODE_STOP_KEY.equals(deleteMode)) { if (getRunningJobs() != null) { for (AbstractProject job : getRunningJobs()) { Hudson.getInstance().getQueue().cancel(job); } } for (Executor executor : executors) { executor.interrupt(); } } } Hudson.getInstance().removeNode(getNode()); removeFromTeam(getName()); return new HttpRedirect(".."); } private void removeFromTeam(String name) throws IOException { TeamManager teamManager = Hudson.getInstance().getTeamManager(); if (teamManager.isTeamManagementEnabled()) { teamManager.removeNode(name); } } /** * Handles incremental log. */ public void doProgressiveLog(StaplerRequest req, StaplerResponse rsp) throws IOException { getLogText().doProgressText(req, rsp); } /** * Gets the current {@link Computer} that the build is running. This method * only works when called during a build, such as by * {@link Publisher}, {@link BuildWrapper}, etc. */ public static Computer currentComputer() { Executor e = Executor.currentExecutor(); // If no executor then must be on master node return e != null ? e.getOwner() : Hudson.getInstance().toComputer(); } /** * Returns {@code true} if the computer is accepting tasks. Needed to allow * slaves programmatic suspension of task scheduling that does not overlap * with being offline. * * @return {@code true} if the computer is accepting tasks */ public boolean isAcceptingTasks() { return true; } public static final PermissionGroup PERMISSIONS = new PermissionGroup(Computer.class, Messages._Computer_Permissions_Title()); /** * Permission to configure slaves. */ public static final Permission READ = new Permission(PERMISSIONS, "Read", Messages._Computer_ReadPermission_Description(), Permission.READ); public static final Permission CREATE = new Permission(PERMISSIONS, "Create", Messages._Computer_CreatePermission_Description(), Permission.CREATE); public static final Permission CONFIGURE = new Permission(PERMISSIONS, "Configure", Messages._Computer_ConfigurePermission_Description(), Permission.CONFIGURE); public static final Permission DELETE = new Permission(PERMISSIONS, "Delete", Messages._Computer_DeletePermission_Description(), Permission.DELETE); private static final Logger LOGGER = Logger.getLogger(Computer.class.getName()); }