/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Red Hat, Inc., Seiji Sogabe, Stephen Connolly, Thomas J. Black, Tom Huybrechts, * CloudBees, Inc., Christopher Simons * * 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.model; import edu.umd.cs.findbugs.annotations.OverrideMustInvoke; import edu.umd.cs.findbugs.annotations.When; import hudson.EnvVars; import hudson.Extension; import hudson.Launcher.ProcStarter; import jenkins.util.SystemProperties; import hudson.Util; import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; import hudson.console.AnnotatedLargeText; import hudson.init.Initializer; import hudson.model.Descriptor.FormException; import hudson.model.Queue.FlyweightTask; import hudson.model.labels.LabelAtom; import hudson.model.queue.WorkUnit; import hudson.node_monitors.NodeMonitor; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.Permission; import hudson.security.PermissionGroup; import hudson.security.PermissionScope; import hudson.slaves.AbstractCloudSlave; import hudson.slaves.ComputerLauncher; import hudson.slaves.ComputerListener; import hudson.slaves.NodeProperty; import hudson.slaves.RetentionStrategy; import hudson.slaves.WorkspaceList; import hudson.slaves.OfflineCause; import hudson.slaves.OfflineCause.ByCLI; import hudson.util.DaemonThreadFactory; import hudson.util.EditDistance; import hudson.util.ExceptionCatchingThreadFactory; import hudson.util.RemotingDiagnostics; import hudson.util.RemotingDiagnostics.HeapDump; import hudson.util.RunList; import hudson.util.Futures; import hudson.util.NamingThreadFactory; import jenkins.model.Jenkins; import jenkins.util.ContextResettingExecutorService; import jenkins.security.MasterToSlaveCallable; import org.jenkins.ui.icon.Icon; import org.jenkins.ui.icon.IconSet; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.stapler.Stapler; 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.WebMethod; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.args4j.Option; import org.kohsuke.stapler.interceptor.RequirePOST; import javax.annotation.concurrent.GuardedBy; import javax.servlet.ServletException; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.util.*; 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.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; import static javax.servlet.http.HttpServletResponse.*; /** * 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 differences. * {@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 (except for the master node, * which always gets its {@link Computer} in case we have no static executors and * we need to run a {@link FlyweightTask} - see JENKINS-7291 for more discussion.) * * 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 (unlike {@link Node}), and can be used along with * {@link TransientComputerActionFactory} to add {@link Action}s to {@link Computer}s. * * @author Kohsuke Kawaguchi */ @ExportedBean public /*transient*/ abstract class Computer extends Actionable implements AccessControlled, ExecutorListener { 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 Jenkins 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; /** * @see #getEnvironment() */ private volatile EnvVars cachedEnvironment; private final WorkspaceList workspaceList = new WorkspaceList(); protected transient List<Action> transientActions; protected final Object statusChangeLock = new Object(); /** * Keeps track of stack traces to track the tremination requests for this computer. * * @since 1.607 * @see Executor#resetWorkUnit(String) */ private transient final List<TerminationRequest> terminatedBy = Collections.synchronizedList(new ArrayList <TerminationRequest>()); /** * This method captures the information of a request to terminate a computer instance. Method is public as * it needs to be called from {@link AbstractCloudSlave} and {@link jenkins.model.Nodes}. In general you should * not need to call this method directly, however if implementing a custom node type or a different path * for removing nodes, it may make sense to call this method in order to capture the originating request. * * @since 1.607 */ public void recordTermination() { StaplerRequest request = Stapler.getCurrentRequest(); if (request != null) { terminatedBy.add(new TerminationRequest( String.format("Termination requested at %s by %s [id=%d] from HTTP request for %s", new Date(), Thread.currentThread(), Thread.currentThread().getId(), request.getRequestURL() ) )); } else { terminatedBy.add(new TerminationRequest( String.format("Termination requested at %s by %s [id=%d]", new Date(), Thread.currentThread(), Thread.currentThread().getId() ) )); } } /** * Returns the list of captured termination requests for this Computer. This method is used by {@link Executor} * to provide details on why a Computer was removed in-between work being scheduled against the {@link Executor} * and the {@link Executor} starting to execute the task. * * @return the (possibly empty) list of termination requests. * @see Executor#resetWorkUnit(String) * @since 1.607 */ public List<TerminationRequest> getTerminatedBy() { return new ArrayList<TerminationRequest>(terminatedBy); } public Computer(Node node) { setNode(node); } /** * Returns list of all boxes {@link ComputerPanelBox}s. */ public List<ComputerPanelBox> getComputerPanelBoxs(){ return ComputerPanelBox.all(this); } /** * Returns the transient {@link Action}s associated with the computer. */ @SuppressWarnings("deprecation") public List<Action> getActions() { List<Action> result = new ArrayList<Action>(); result.addAll(super.getActions()); synchronized (this) { if (transientActions == null) { transientActions = TransientComputerActionFactory.createAllFor(this); } result.addAll(transientActions); } return Collections.unmodifiableList(result); } @SuppressWarnings("deprecation") @Override public void addAction(Action a) { if(a==null) throw new IllegalArgumentException(); super.getActions().add(a); } /** * This is where the log from the remote agent goes. * The method also creates a log directory if required. * @see #getLogDir(), #relocateOldLogs() */ public @Nonnull File getLogFile() { return new File(getLogDir(),"slave.log"); } /** * Directory where rotated agent logs are stored. * * The method also creates a log directory if required. * * @since 1.613 */ protected @Nonnull File getLogDir() { File dir = new File(Jenkins.getInstance().getRootDir(),"logs/slaves/"+nodeName); if (!dir.exists() && !dir.mkdirs()) { LOGGER.severe("Failed to create agent log directory " + dir.getAbsolutePath()); } return dir; } /** * Gets the object that coordinates the workspace allocation on this computer. */ public WorkspaceList getWorkspaceList() { return workspaceList; } /** * Gets the string representation of the agent 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 Jenkins.getInstance().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; } /** * If the computer was offline (either temporarily or not), * this method will return the cause as a string (without user info). * * @return * empty string if the system was put offline without given a cause. */ @Exported public String getOfflineCauseReason() { if (offlineCause == null) { return ""; } // fetch the localized string for "Disconnected By" String gsub_base = hudson.slaves.Messages.SlaveComputer_DisconnectedBy("",""); // regex to remove commented reason base string String gsub1 = "^" + gsub_base + "[\\w\\W]* \\: "; // regex to remove non-commented reason base string String gsub2 = "^" + gsub_base + "[\\w\\W]*"; String newString = offlineCause.toString().replaceAll(gsub1, ""); return newString.replaceAll(gsub2, ""); } /** * Gets the channel that can be used to run a program on this computer. * * @return * never null when {@link #isOffline()}==false. */ public abstract @Nullable 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 agent. */ public abstract List<LogRecord> getLogRecords() throws IOException, InterruptedException; /** * If {@link #getChannel()}==null, attempts to relaunch the agent. */ public abstract void doLaunchSlaveAgent( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException; /** * @deprecated since 2009-01-06. Use {@link #connect(boolean)} */ @Deprecated 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); /** * @deprecated Implementation of CLI command "connect-node" moved to {@link hudson.cli.ConnectNodeCommand}. * * @param force * If true cancel any currently pending connect operation and retry from scratch */ @Deprecated public void cliConnect(boolean force) throws ExecutionException, InterruptedException { checkPermission(CONNECT); 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) { recordTermination(); 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. */ @Deprecated public Future<?> disconnect() { recordTermination(); 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); } /** * @deprecated Implementation of CLI command "disconnect-node" moved to {@link hudson.cli.DisconnectNodeCommand}. * * @param cause * Record the note about why you are disconnecting this node */ @Deprecated public void cliDisconnect(String cause) throws ExecutionException, InterruptedException { checkPermission(DISCONNECT); disconnect(new ByCLI(cause)).get(); } /** * @deprecated Implementation of CLI command "offline-node" moved to {@link hudson.cli.OfflineNodeCommand}. * * @param cause * Record the note about why you are disconnecting this node */ @Deprecated public void cliOffline(String cause) throws ExecutionException, InterruptedException { checkPermission(DISCONNECT); setTemporarilyOffline(true, new ByCLI(cause)); } /** * @deprecated Implementation of CLI command "online-node" moved to {@link hudson.cli.OnlineNodeCommand}. */ @Deprecated public void cliOnline() throws ExecutionException, InterruptedException { checkPermission(CONNECT); 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 @Nonnull String getName() { return nodeName != null ? nodeName : ""; } /** * True if this computer is a Unix machine (as opposed to Windows machine). * * @since 1.624 * @return * null if the computer is disconnected and therefore we don't know whether it is Unix or not. */ public abstract @CheckForNull Boolean isUnix(); /** * 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. */ @CheckForNull public Node getNode() { Jenkins j = Jenkins.getInstanceOrNull(); // TODO confirm safe to assume non-null and use getInstance() if (j == null) { return null; } if (nodeName == null) { return j; } return j.getNode(nodeName); } @Exported public LoadStatistics getLoadStatistics() { return LabelAtom.get(nodeName != null ? nodeName : Jenkins.getInstance().getSelfLabel().toString()).loadStatistics; } public BuildTimelineWidget getTimeline() { return new BuildTimelineWidget(getBuilds()); } /** * {@inheritDoc} */ @Override public void taskAccepted(Executor executor, Queue.Task task) { // dummy implementation } /** * {@inheritDoc} */ @Override public void taskCompleted(Executor executor, Queue.Task task, long durationMS) { // dummy implementation } /** * {@inheritDoc} */ @Override 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 agent is allowed at this point in time. * @return {@code true} if manual launching of the agent 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 agents return {@code false} from this, because the launch process * needs to be initiated from the agent 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 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 @Deprecated public boolean isTemporarilyOffline() { return temporarilyOffline; } /** * @deprecated as of 1.320. * Use {@link #setTemporarilyOffline(boolean, OfflineCause)} */ @Deprecated 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 (node != null) { node.setTemporaryOfflineCause(offlineCause); } synchronized (statusChangeLock) { statusChangeLock.notifyAll(); } for (ComputerListener cl : ComputerListener.all()) { if (temporarilyOffline) cl.onTemporarilyOffline(this,cause); else cl.onTemporarilyOnline(this); } } /** * Returns the icon for this computer. * * It is both the recommended and default implementation to serve different icons based on {@link #isOffline} * * @see #getIconClassName() */ @Exported public String getIcon() { if(isOffline()) return "computer-x.png"; else return "computer.png"; } /** * Returns the class name that will be used to lookup the icon. * * This class name will be added as a class tag to the html img tags where the icon should * show up followed by a size specifier given by {@link Icon#toNormalizedIconSizeClass(String)} * The conversion of class tag to src tag is registered through {@link IconSet#addIcon(Icon)} * * It is both the recommended and default implementation to serve different icons based on {@link #isOffline} * * @see #getIcon() */ @Exported public String getIconClassName() { if(isOffline()) return "icon-computer-x"; else return "icon-computer"; } public String getIconAltText() { if(isOffline()) return "[offline]"; else return "[online]"; } @Exported @Override public @Nonnull String getDisplayName() { return nodeName; } public String getCaption() { return Messages.Computer_Caption(nodeName); } public String getUrl() { return "computer/" + Util.rawEncode(getName()) + "/"; } /** * Returns projects that are tied on this node. */ public List<AbstractProject> getTiedJobs() { Node node = getNode(); return (node != null) ? node.getSelfLabel().getTiedJobs() : Collections.EMPTY_LIST; } public RunList getBuilds() { return new RunList(Jenkins.getInstance().getAllItems(Job.class)).node(getNode()); } /** * 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 (this.temporarilyOffline) { // When we get a new node, push our current temp offline // status to it (as the status is not carried across // configuration changes that recreate the node). // Since this is also called the very first time this // Computer is created, avoid pushing an empty status // as that could overwrite any status that the Node // brought along from its persisted config data. node.setTemporaryOfflineCause(this.offlineCause); } } /** * Called by {@link Jenkins#updateComputerList()} to notify {@link Computer} that it will be discarded. * * <p> * Note that at this point {@link #getNode()} returns null. * * @see #onRemoved() */ protected void kill() { // On most code paths, this should already be zero, and thus this next call becomes a no-op... and more // importantly it will not acquire a lock on the Queue... not that the lock is bad, more that the lock // may delay unnecessarily setNumExecutors(0); } /** * Called by {@link Jenkins#updateComputerList()} to notify {@link Computer} that it will be discarded. * * <p> * Note that at this point {@link #getNode()} returns null. * * <p> * Note that the Queue lock is already held when this method is called. * * @see #onRemoved() */ @Restricted(NoExternalUse.class) @GuardedBy("hudson.model.Queue.lock") /*package*/ void inflictMortalWound() { setNumExecutors(0); } /** * Called by {@link Jenkins} when this computer is removed. * * <p> * This happens when list of nodes are updated (for example by {@link Jenkins#setNodes(List)} and * the computer becomes redundant. Such {@link Computer}s get {@linkplain #kill() killed}, then * after all its executors are finished, this method is called. * * <p> * Note that at this point {@link #getNode()} returns null. * * @see #kill() * @since 1.510 */ protected void onRemoved(){ } private synchronized void setNumExecutors(int n) { this.numExecutors = n; final int diff = executors.size()-n; if (diff>0) { // we have too many executors // send signal to all idle executors to potentially kill them off // need the Queue maintenance lock held to prevent concurrent job assignment on the idle executors Queue.withLock(new Runnable() { @Override public void run() { for( Executor e : executors ) if(e.isIdle()) e.interrupt(); } }); } if (diff<0) { // if the number is increased, add new ones addNewExecutorIfNecessary(); } } private void addNewExecutorIfNecessary() { Set<Integer> availableNumbers = new HashSet<Integer>(); for (int i = 0; i < numExecutors; i++) availableNumbers.add(i); for (Executor executor : executors) availableNumbers.remove(executor.getNumber()); for (Integer number : availableNumbers) { /* There may be busy executors with higher index, so only fill up until numExecutors is reached. Extra executors will call removeExecutor(...) and that will create any necessary executors from #0 again. */ if (executors.size() < numExecutors) { Executor e = new Executor(this, number); executors.add(e); } } } /** * 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(); } /** * 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(); } /** * 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); } /** * Used to render the list of executors. * @return a snapshot of the executor display information * @since 1.607 */ @Restricted(NoExternalUse.class) public List<DisplayExecutor> getDisplayExecutors() { // The size may change while we are populating, but let's start with a reasonable guess to minimize resizing List<DisplayExecutor> result = new ArrayList<DisplayExecutor>(executors.size()+oneOffExecutors.size()); int index = 0; for (Executor e: executors) { if (e.isDisplayCell()) { result.add(new DisplayExecutor(Integer.toString(index + 1), String.format("executors/%d", index), e)); } index++; } index = 0; for (OneOffExecutor e: oneOffExecutors) { if (e.isDisplayCell()) { result.add(new DisplayExecutor("", String.format("oneOffExecutors/%d", index), e)); } index++; } return result; } /** * 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; } /** * Returns true if this computer has some idle executors that can take more workload. */ 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 : Jenkins.getInstance().getQueue().getBuildableItems(this)) { firstDemand = Math.min(item.buildableStartMilliseconds, firstDemand); } return firstDemand; } /** * Called by {@link Executor} to kill excessive executors from this computer. */ protected void removeExecutor(final Executor e) { final Runnable task = new Runnable() { @Override public void run() { synchronized (Computer.this) { executors.remove(e); addNewExecutorIfNecessary(); if (!isAlive()) { AbstractCIBase ciBase = Jenkins.getInstanceOrNull(); if (ciBase != null) { // TODO confirm safe to assume non-null and use getInstance() ciBase.removeComputer(Computer.this); } } } } }; if (!Queue.tryWithLock(task)) { // JENKINS-28840 if we couldn't get the lock push the operation to a separate thread to avoid deadlocks threadPoolForRemoting.submit(Queue.wrapWithLock(task)); } } /** * Returns true if any of the executors are {@linkplain Executor#isActive active}. * * @since 1.509 */ protected boolean isAlive() { for (Executor e : executors) if (e.isActive()) return true; return false; } /** * Interrupt all {@link Executor}s. * Called from {@link Jenkins#cleanUp}. */ public void interrupt() { Queue.withLock(new Runnable() { @Override public void run() { for (Executor e : executors) { e.interruptForShutdown(); } } }); } public String getSearchUrl() { return getUrl(); } /** * {@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. */ @Deprecated public Map<String,String> getEnvVars() throws IOException, InterruptedException { return getEnvironment(); } /** * Returns cached environment variables (copy to prevent modification) for 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 { EnvVars cachedEnvironment = this.cachedEnvironment; if (cachedEnvironment != null) { return new EnvVars(cachedEnvironment); } cachedEnvironment = EnvVars.getRemote(getChannel()); this.cachedEnvironment = cachedEnvironment; return new EnvVars(cachedEnvironment); } /** * Creates an environment variable override to be used for launching processes on this node. * * @see ProcStarter#envs(Map) * @since 1.489 */ public @Nonnull EnvVars buildEnvironment(@Nonnull TaskListener listener) throws IOException, InterruptedException { EnvVars env = new EnvVars(); Node node = getNode(); if (node==null) return env; // bail out for (NodeProperty nodeProperty: Jenkins.getInstance().getGlobalNodeProperties()) { nodeProperty.buildEnvVars(env,listener); } for (NodeProperty nodeProperty: node.getNodeProperties()) { nodeProperty.buildEnvVars(env,listener); } // TODO: hmm, they don't really belong String rootUrl = Jenkins.getInstance().getRootUrl(); if(rootUrl!=null) { env.put("HUDSON_URL", rootUrl); // Legacy. env.put("JENKINS_URL", rootUrl); } return env; } /** * Gets the thread dump of the agent 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 agent 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 agent, * 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 agent 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.log(Level.FINE, "{0} is not an IPv4 address", address); continue; } if(!ComputerPinger.checkIsReachable(ia, 3)) { LOGGER.log(Level.FINE, "{0} didn't respond to ping", address); 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 LogRecord lr = new LogRecord(Level.FINE, "Failed to parse {0}"); lr.setThrown(e); lr.setParameters(new Object[]{address}); LOGGER.log(lr); } } // allow the administrator to manually specify the host name as a fallback. HUDSON-5373 cachedHostName = channel.call(new GetFallbackName()); hostNameCached = true; return cachedHostName; } /** * Starts executing a fly-weight task. */ /*package*/ final void startFlyWeightTask(WorkUnit p) { OneOffExecutor e = new OneOffExecutor(this); e.start(p); oneOffExecutors.add(e); } /*package*/ final void remove(OneOffExecutor e) { oneOffExecutors.remove(e); } private static class ListPossibleNames extends MasterToSlaveCallable<List<String>,IOException> { /** * In the normal case we would use {@link Computer} as the logger's name, however to * do that we would have to send the {@link Computer} class over to the remote classloader * and then it would need to be loaded, which pulls in {@link Jenkins} and loads that * and then that fails to load as you are not supposed to do that. Another option * would be to export the logger over remoting, with increased complexity as a result. * Instead we just use a loger based on this class name and prevent any references to * other classes from being transferred over remoting. */ private static final Logger LOGGER = Logger.getLogger(ListPossibleNames.class.getName()); 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.log(Level.FINE, "Listing up IP addresses for {0}", ni.getDisplayName()); Enumeration<InetAddress> e = ni.getInetAddresses(); while (e.hasMoreElements()) { InetAddress ia = e.nextElement(); if(ia.isLoopbackAddress()) { LOGGER.log(Level.FINE, "{0} is a loopback address", ia); continue; } if(!(ia instanceof Inet4Address)) { LOGGER.log(Level.FINE, "{0} is not an IPv4 address", ia); continue; } LOGGER.log(Level.FINE, "{0} is a viable candidate", ia); names.add(ia.getHostAddress()); } } return names; } private static final long serialVersionUID = 1L; } private static class GetFallbackName extends MasterToSlaveCallable<String,IOException> { public String call() throws IOException { return SystemProperties.getString("host.name"); } private static final long serialVersionUID = 1L; } public static final ExecutorService threadPoolForRemoting = new ContextResettingExecutorService( Executors.newCachedThreadPool( new ExceptionCatchingThreadFactory( new NamingThreadFactory(new DaemonThreadFactory(), "Computer.threadPoolForRemoting")))); // // // 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); } @RequirePOST public HttpResponse doToggleOffline(@QueryParameter String offlineMessage) throws IOException, ServletException { if(!temporarilyOffline) { checkPermission(DISCONNECT); offlineMessage = Util.fixEmptyAndTrim(offlineMessage); setTemporarilyOffline(!temporarilyOffline, new OfflineCause.UserCause(User.current(), offlineMessage)); } else { checkPermission(CONNECT); setTemporarilyOffline(!temporarilyOffline,null); } return HttpResponses.redirectToDot(); } @RequirePOST public HttpResponse doChangeOfflineCause(@QueryParameter String offlineMessage) throws IOException, ServletException { checkPermission(DISCONNECT); offlineMessage = Util.fixEmptyAndTrim(offlineMessage); setTemporarilyOffline(true, new OfflineCause.UserCause(User.current(), offlineMessage)); 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(Jenkins.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 agent 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 extends MasterToSlaveCallable<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 Groovy script. */ public void doScript(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { _doScript(req, rsp, "_script.jelly"); } /** * Run arbitrary Groovy 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 { Jenkins._doScript(req, rsp, req.getView(this, view), getChannel(), getACL()); } /** * Accepts the update to the node configuration. */ @RequirePOST public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException { checkPermission(CONFIGURE); String proposedName = Util.fixEmptyAndTrim(req.getSubmittedForm().getString("name")); Jenkins.checkGoodName(proposedName); Node node = getNode(); if (node == null) { throw new ServletException("No such node " + nodeName); } if ((!proposedName.equals(nodeName)) && Jenkins.getActiveInstance().getNode(proposedName) != null) { throw new FormException(Messages.ComputerSet_SlaveAlreadyExists(proposedName), "name"); } Node result = node.reconfigure(req, req.getSubmittedForm()); Jenkins.getInstance().getNodesObject().replaceNode(this.getNode(), result); // take the user back to the agent top page. rsp.sendRedirect2("../" + result.getNodeName() + '/'); } /** * Accepts <tt>config.xml</tt> submission, as well as serve it. */ @WebMethod(name = "config.xml") public void doConfigDotXml(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { if (req.getMethod().equals("GET")) { // read checkPermission(EXTENDED_READ); rsp.setContentType("application/xml"); Node node = getNode(); if (node == null) { throw HttpResponses.notFound(); } Jenkins.XSTREAM2.toXMLUTF8(node, rsp.getOutputStream()); return; } if (req.getMethod().equals("POST")) { // submission updateByXml(req.getInputStream()); return; } // huh? rsp.sendError(SC_BAD_REQUEST); } /** * Updates Job by its XML definition. * * @since 1.526 */ public void updateByXml(final InputStream source) throws IOException, ServletException { checkPermission(CONFIGURE); Node result = (Node)Jenkins.XSTREAM2.fromXML(source); Jenkins.getInstance().getNodesObject().replaceNode(this.getNode(), result); } /** * Really deletes the agent. */ @RequirePOST public HttpResponse doDoDelete() throws IOException { checkPermission(DELETE); Node node = getNode(); if (node != null) { Jenkins.getInstance().removeNode(node); } else { AbstractCIBase app = Jenkins.getInstance(); app.removeComputer(this); } return new HttpRedirect(".."); } /** * Blocks until the node becomes online/offline. */ public void waitUntilOnline() throws InterruptedException { synchronized (statusChangeLock) { while (!isOnline()) statusChangeLock.wait(1000); } } public void waitUntilOffline() throws InterruptedException { synchronized (statusChangeLock) { while (!isOffline()) statusChangeLock.wait(1000); } } /** * 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 hudson.tasks.Publisher}, {@link hudson.tasks.BuildWrapper}, etc. * @return the {@link Computer} associated with {@link Executor#currentExecutor}, or (consistently as of 1.591) null if not on an executor thread */ public static @Nullable Computer currentComputer() { Executor e = Executor.currentExecutor(); return e != null ? e.getOwner() : null; } /** * Returns {@code true} if the computer is accepting tasks. Needed to allow agents programmatic suspension of task * scheduling that does not overlap with being offline. * * @return {@code true} if the computer is accepting tasks * @see hudson.slaves.RetentionStrategy#isAcceptingTasks(Computer) * @see hudson.model.Node#isAcceptingTasks() */ @OverrideMustInvoke(When.ANYTIME) public boolean isAcceptingTasks() { final Node node = getNode(); return getRetentionStrategy().isAcceptingTasks(this) && (node == null || node.isAcceptingTasks()); } /** * Used for CLI binding. */ @CLIResolver public static Computer resolveForCLI( @Argument(required=true,metaVar="NAME",usage="Agent name, or empty string for master") String name) throws CmdLineException { Jenkins h = Jenkins.getInstance(); Computer item = h.getComputer(name); if (item==null) { List<String> names = ComputerSet.getComputerNames(); String adv = EditDistance.findNearest(name, names); throw new IllegalArgumentException(adv == null ? hudson.model.Messages.Computer_NoSuchSlaveExistsWithoutAdvice(name) : hudson.model.Messages.Computer_NoSuchSlaveExists(name, adv)); } return item; } /** * Relocate log files in the old location to the new location. * * Files were used to be $JENKINS_ROOT/slave-NAME.log (and .1, .2, ...) * but now they are at $JENKINS_ROOT/logs/slaves/NAME/slave.log (and .1, .2, ...) * * @see #getLogFile() */ @Initializer public static void relocateOldLogs() { relocateOldLogs(Jenkins.getInstance().getRootDir()); } /*package*/ static void relocateOldLogs(File dir) { final Pattern logfile = Pattern.compile("slave-(.*)\\.log(\\.[0-9]+)?"); File[] logfiles = dir.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return logfile.matcher(name).matches(); } }); if (logfiles==null) return; for (File f : logfiles) { Matcher m = logfile.matcher(f.getName()); if (m.matches()) { File newLocation = new File(dir, "logs/slaves/" + m.group(1) + "/slave.log" + Util.fixNull(m.group(2))); newLocation.getParentFile().mkdirs(); boolean relocationSuccessful=f.renameTo(newLocation); if (relocationSuccessful) { // The operation will fail if mkdir fails LOGGER.log(Level.INFO, "Relocated log file {0} to {1}",new Object[] {f.getPath(),newLocation.getPath()}); } else { LOGGER.log(Level.WARNING, "Cannot relocate log file {0} to {1}",new Object[] {f.getPath(),newLocation.getPath()}); } } else { assert false; } } } /** * A value class to provide a consistent snapshot view of the state of an executor to avoid race conditions * during rendering of the executors list. * * @since 1.607 */ @Restricted(NoExternalUse.class) public static class DisplayExecutor implements ModelObject { @Nonnull private final String displayName; @Nonnull private final String url; @Nonnull private final Executor executor; public DisplayExecutor(@Nonnull String displayName, @Nonnull String url, @Nonnull Executor executor) { this.displayName = displayName; this.url = url; this.executor = executor; } @Override @Nonnull public String getDisplayName() { return displayName; } @Nonnull public String getUrl() { return url; } @Nonnull public Executor getExecutor() { return executor; } @Override public String toString() { final StringBuilder sb = new StringBuilder("DisplayExecutor{"); sb.append("displayName='").append(displayName).append('\''); sb.append(", url='").append(url).append('\''); sb.append(", executor=").append(executor); sb.append('}'); return sb.toString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } DisplayExecutor that = (DisplayExecutor) o; if (!executor.equals(that.executor)) { return false; } return true; } @Extension(ordinal = Double.MAX_VALUE) @Restricted(DoNotUse.class) public static class InternalComputerListener extends ComputerListener { @Override public void onOnline(Computer c, TaskListener listener) throws IOException, InterruptedException { c.cachedEnvironment = null; } } @Override public int hashCode() { return executor.hashCode(); } } /** * Used to trace requests to terminate a computer. * * @since 1.607 */ public static class TerminationRequest extends RuntimeException { private final long when; public TerminationRequest(String message) { super(message); this.when = System.currentTimeMillis(); } /** * Returns the when the termination request was created. * * @return the difference, measured in milliseconds, between * the time of the termination request and midnight, January 1, 1970 UTC. */ public long getWhen() { return when; } } public static final PermissionGroup PERMISSIONS = new PermissionGroup(Computer.class,Messages._Computer_Permissions_Title()); public static final Permission CONFIGURE = new Permission(PERMISSIONS,"Configure", Messages._Computer_ConfigurePermission_Description(), Permission.CONFIGURE, PermissionScope.COMPUTER); /** * @since 1.532 */ public static final Permission EXTENDED_READ = new Permission(PERMISSIONS,"ExtendedRead", Messages._Computer_ExtendedReadPermission_Description(), CONFIGURE, SystemProperties.getBoolean("hudson.security.ExtendedReadPermission"), new PermissionScope[]{PermissionScope.COMPUTER}); public static final Permission DELETE = new Permission(PERMISSIONS,"Delete", Messages._Computer_DeletePermission_Description(), Permission.DELETE, PermissionScope.COMPUTER); public static final Permission CREATE = new Permission(PERMISSIONS,"Create", Messages._Computer_CreatePermission_Description(), Permission.CREATE, PermissionScope.JENKINS); public static final Permission DISCONNECT = new Permission(PERMISSIONS,"Disconnect", Messages._Computer_DisconnectPermission_Description(), Jenkins.ADMINISTER, PermissionScope.COMPUTER); public static final Permission CONNECT = new Permission(PERMISSIONS,"Connect", Messages._Computer_ConnectPermission_Description(), DISCONNECT, PermissionScope.COMPUTER); public static final Permission BUILD = new Permission(PERMISSIONS, "Build", Messages._Computer_BuildPermission_Description(), Permission.WRITE, PermissionScope.COMPUTER); private static final Logger LOGGER = Logger.getLogger(Computer.class.getName()); }