/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly * * 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.slaves; import hudson.AbortException; import hudson.FilePath; import hudson.Functions; import hudson.Main; import hudson.RestrictedSince; import hudson.Util; import hudson.console.ConsoleLogFilter; import hudson.model.Computer; import hudson.model.Executor; import hudson.model.ExecutorListener; import hudson.model.Node; import hudson.model.Queue; import hudson.model.Slave; import hudson.model.TaskListener; import hudson.model.User; import hudson.remoting.Channel; import hudson.remoting.ChannelBuilder; import hudson.remoting.ChannelClosedException; import hudson.remoting.CommandTransport; import hudson.remoting.Launcher; import hudson.remoting.VirtualChannel; import hudson.security.ACL; import hudson.security.ACLContext; import hudson.slaves.OfflineCause.ChannelTermination; import hudson.util.Futures; import hudson.util.NullStream; import hudson.util.RingBufferLogHandler; import hudson.util.StreamTaskListener; import hudson.util.VersionNumber; import hudson.util.io.RewindableFileOutputStream; import hudson.util.io.RewindableRotatingFileOutputStream; import jenkins.agents.AgentComputerUtil; import jenkins.model.Jenkins; import jenkins.security.ChannelConfigurator; import jenkins.security.MasterToSlaveCallable; import jenkins.slaves.EncryptedSlaveAgentJnlpFile; import jenkins.slaves.JnlpAgentReceiver; import jenkins.slaves.RemotingVersionInfo; import jenkins.slaves.systemInfo.SlaveSystemInfo; import jenkins.util.SystemProperties; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.Beta; import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.WebMethod; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.interceptor.RequirePOST; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.CheckReturnValue; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.OverrideMustInvoke; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.nio.charset.Charset; import java.security.Security; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.Future; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; import java.util.stream.Stream; import static hudson.slaves.SlaveComputer.LogHolder.SLAVE_LOG_HANDLER; import org.jenkinsci.remoting.util.LoggingChannelListener; /** * {@link Computer} for {@link Slave}s. * * @author Kohsuke Kawaguchi */ public class SlaveComputer extends Computer { private volatile Channel channel; private volatile transient boolean acceptingTasks = true; private Charset defaultCharset; private Boolean isUnix; /** * Effective {@link ComputerLauncher} that hides the details of * how we launch a agent agent on this computer. * *
* This is normally the same as {@link Slave#getLauncher()} but
* can be different. See {@link #grabLauncher(Node)}.
*/
private ComputerLauncher launcher;
/**
* Perpetually writable log file.
*/
private final RewindableFileOutputStream log;
/**
* {@link StreamTaskListener} that wraps {@link #log}, hence perpetually writable.
*/
private final TaskListener taskListener;
/**
* Number of failed attempts to reconnect to this node
* (so that if we keep failing to reconnect, we can stop
* trying.)
*/
private transient int numRetryAttempt;
/**
* Tracks the status of the last launch operation, which is always asynchronous.
* This can be used to wait for the completion, or cancel the launch activity.
*/
private volatile Future> lastConnectActivity = null;
private Object constructed = new Object();
private transient volatile String absoluteRemoteFs;
public SlaveComputer(Slave slave) {
super(slave);
this.log = new RewindableRotatingFileOutputStream(getLogFile(), 10);
this.taskListener = new StreamTaskListener(decorate(this.log));
assert slave.getNumExecutors()!=0 : "Computer created with 0 executors";
}
/**
* Uses {@link ConsoleLogFilter} to decorate logger.
*/
private OutputStream decorate(OutputStream os) {
for (ConsoleLogFilter f : ConsoleLogFilter.all()) {
try {
os = f.decorateLogger(this,os);
} catch (IOException|InterruptedException e) {
LOGGER.log(Level.WARNING, "Failed to filter log with "+f, e);
}
}
return os;
}
@Override
@OverrideMustInvoke
public boolean isAcceptingTasks() {
// our boolean flag is an override on any additional programmatic reasons why this agent might not be
// accepting tasks.
return acceptingTasks && super.isAcceptingTasks();
}
/**
* @since 1.498
*/
public String getJnlpMac() {
return JnlpAgentReceiver.SLAVE_SECRET.mac(getName());
}
/**
* Allows suspension of tasks being accepted by the agent computer. While this could be called by a
* {@linkplain hudson.slaves.ComputerLauncher} or a {@linkplain hudson.slaves.RetentionStrategy}, such usage
* can result in fights between multiple actors calling setting differential values. A better approach
* is to override {@link hudson.slaves.RetentionStrategy#isAcceptingTasks(hudson.model.Computer)} if the
* {@link hudson.slaves.RetentionStrategy} needs to control availability.
*
* @param acceptingTasks {@code true} if the agent can accept tasks.
*/
public void setAcceptingTasks(boolean acceptingTasks) {
this.acceptingTasks = acceptingTasks;
}
@Override
public Boolean isUnix() {
return isUnix;
}
@CheckForNull
@Override
public Slave getNode() {
Node node = super.getNode();
if (node == null || node instanceof Slave) {
return (Slave)node;
} else {
logger.log(Level.WARNING, "found an unexpected kind of node {0} from {1} with nodeName={2}", new Object[] {node, this, nodeName});
return null;
}
}
/**
* Return the {@link TaskListener} for this SlaveComputer. Never null
* @since 2.9
*/
public TaskListener getListener() {
return taskListener;
}
@Override
public String getIcon() {
Future> l = lastConnectActivity;
if(l!=null && !l.isDone())
return "computer-flash.gif";
return super.getIcon();
}
/**
* @deprecated since 2008-05-20.
*/
@Deprecated @Override
public boolean isJnlpAgent() {
return launcher instanceof JNLPLauncher;
}
@Override
public boolean isLaunchSupported() {
return launcher.isLaunchSupported();
}
/**
* Return the {@link ComputerLauncher} for this SlaveComputer.
* @since 1.312
*/
public ComputerLauncher getLauncher() {
return launcher;
}
/**
* Return the {@link ComputerLauncher} for this SlaveComputer, strips off
* any {@link DelegatingComputerLauncher}s or {@link ComputerLauncherFilter}s.
* @since 2.83
*/
public ComputerLauncher getDelegatedLauncher() {
ComputerLauncher l = launcher;
while (true) {
if (l instanceof DelegatingComputerLauncher) {
l = ((DelegatingComputerLauncher) l).getLauncher();
} else if (l instanceof ComputerLauncherFilter) {
l = ((ComputerLauncherFilter) l).getCore();
} else {
break;
}
}
return l;
}
protected Future> _connect(boolean forceReconnect) {
if(channel!=null) return Futures.precomputed(null);
if(!forceReconnect && isConnecting())
return lastConnectActivity;
if(forceReconnect && isConnecting())
logger.fine("Forcing a reconnect on "+getName());
closeChannel();
return lastConnectActivity = Computer.threadPoolForRemoting.submit(() -> {
// do this on another thread so that the lengthy launch operation
// (which is typical) won't block UI thread.
try (ACLContext ctx = ACL.as(ACL.SYSTEM)) {// background activity should run like a super user
log.rewind();
try {
for (ComputerListener cl : ComputerListener.all())
cl.preLaunch(SlaveComputer.this, taskListener);
offlineCause = null;
launcher.launch(SlaveComputer.this, taskListener);
} catch (AbortException e) {
taskListener.error(e.getMessage());
throw e;
} catch (IOException e) {
Util.displayIOException(e,taskListener);
Functions.printStackTrace(e, taskListener.error(Messages.ComputerLauncher_unexpectedError()));
throw e;
} catch (InterruptedException e) {
Functions.printStackTrace(e, taskListener.error(Messages.ComputerLauncher_abortedLaunch()));
throw e;
} catch (Exception e) {
Functions.printStackTrace(e, taskListener.error(Messages.ComputerLauncher_unexpectedError()));
throw e;
}
} finally {
if (channel==null && offlineCause == null) {
offlineCause = new OfflineCause.LaunchFailed();
for (ComputerListener cl : ComputerListener.all())
cl.onLaunchFailure(SlaveComputer.this, taskListener);
}
}
if (channel==null)
throw new IOException("Agent failed to connect, even though the launcher didn't report it. See the log output for details.");
return null;
});
}
@Override
public void taskAccepted(Executor executor, Queue.Task task) {
super.taskAccepted(executor, task);
if (launcher instanceof ExecutorListener) {
((ExecutorListener)launcher).taskAccepted(executor, task);
}
//getNode() can return null at indeterminate times when nodes go offline
Slave node = getNode();
if (node != null && node.getRetentionStrategy() instanceof ExecutorListener) {
((ExecutorListener)node.getRetentionStrategy()).taskAccepted(executor, task);
}
}
@Override
public void taskCompleted(Executor executor, Queue.Task task, long durationMS) {
super.taskCompleted(executor, task, durationMS);
if (launcher instanceof ExecutorListener) {
((ExecutorListener)launcher).taskCompleted(executor, task, durationMS);
}
RetentionStrategy r = getRetentionStrategy();
if (r instanceof ExecutorListener) {
((ExecutorListener) r).taskCompleted(executor, task, durationMS);
}
}
@Override
public void taskCompletedWithProblems(Executor executor, Queue.Task task, long durationMS, Throwable problems) {
super.taskCompletedWithProblems(executor, task, durationMS, problems);
if (launcher instanceof ExecutorListener) {
((ExecutorListener)launcher).taskCompletedWithProblems(executor, task, durationMS, problems);
}
RetentionStrategy r = getRetentionStrategy();
if (r instanceof ExecutorListener) {
((ExecutorListener) r).taskCompletedWithProblems(executor, task, durationMS, problems);
}
}
@Override
public boolean isConnecting() {
Future> l = lastConnectActivity;
return isOffline() && l!=null && !l.isDone();
}
public OutputStream openLogFile() {
try {
log.rewind();
return log;
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to create log file "+getLogFile(),e);
return new NullStream();
}
}
private final Object channelLock = new Object();
/**
* Creates a {@link Channel} from the given stream and sets that to this agent.
*
* Same as {@link #setChannel(InputStream, OutputStream, OutputStream, Channel.Listener)}, but for
* {@link TaskListener}.
*/
public void setChannel(@NonNull InputStream in, @NonNull OutputStream out,
@NonNull TaskListener taskListener,
@CheckForNull Channel.Listener listener) throws IOException, InterruptedException {
setChannel(in,out,taskListener.getLogger(),listener);
}
/**
* Creates a {@link Channel} from the given stream and sets that to this agent.
*
* @param in
* Stream connected to the remote agent. It's the caller's responsibility to do
* buffering on this stream, if that's necessary.
* @param out
* Stream connected to the remote peer. It's the caller's responsibility to do
* buffering on this stream, if that's necessary.
* @param launchLog
* If non-null, receive the portion of data in {@code is} before
* the data goes into the "binary mode". This is useful
* when the established communication channel might include some data that might
* be useful for debugging/trouble-shooting.
* @param listener
* Gets a notification when the channel closes, to perform clean up. Can be null.
* By the time this method is called, the cause of the termination is reported to the user,
* so the implementation of the listener doesn't need to do that again.
*/
public void setChannel(@NonNull InputStream in, @NonNull OutputStream out,
@CheckForNull OutputStream launchLog,
@CheckForNull Channel.Listener listener) throws IOException, InterruptedException {
ChannelBuilder cb = new ChannelBuilder(nodeName,threadPoolForRemoting)
.withMode(Channel.Mode.NEGOTIATE)
.withHeaderStream(launchLog);
for (ChannelConfigurator cc : ChannelConfigurator.all()) {
cc.onChannelBuilding(cb,this);
}
Channel channel = cb.build(in,out);
setChannel(channel,launchLog,listener);
}
/**
* Creates a {@link Channel} from the given Channel Builder and Command Transport.
* This method can be used to allow {@link ComputerLauncher}s to create channels not based on I/O streams.
*
* @param cb
* Channel Builder.
* To print launch logs this channel builder should have a Header Stream defined
* (see {@link ChannelBuilder#getHeaderStream()}) in this argument or by one of {@link ChannelConfigurator}s.
* @param commandTransport
* Command Transport
* @param listener
* Gets a notification when the channel closes, to perform clean up. Can be {@code null}.
* By the time this method is called, the cause of the termination is reported to the user,
* so the implementation of the listener doesn't need to do that again.
* @since 2.127
*/
@Restricted(Beta.class)
public void setChannel(@NonNull ChannelBuilder cb,
@NonNull CommandTransport commandTransport,
@CheckForNull Channel.Listener listener) throws IOException, InterruptedException {
for (ChannelConfigurator cc : ChannelConfigurator.all()) {
cc.onChannelBuilding(cb,this);
}
OutputStream headerStream = cb.getHeaderStream();
if (headerStream == null) {
LOGGER.log(Level.WARNING, "No header stream defined when setting channel for computer {0}. " +
"Launch log won't be printed", this);
}
Channel channel = cb.build(commandTransport);
setChannel(channel, headerStream, listener);
}
/**
* Shows {@link Channel#classLoadingCount}.
* @return Requested value or {@code -1} if the agent is offline.
* @since 1.495
*/
@CheckReturnValue
public int getClassLoadingCount() throws IOException, InterruptedException {
if (channel == null) {
return -1;
}
return channel.call(new LoadingCount(false));
}
/**
* Shows {@link Channel#classLoadingPrefetchCacheCount}.
* @return Requested value or {@code -1} in case that capability is not supported or if the agent is offline.
* @since 1.519
*/
@CheckReturnValue
public int getClassLoadingPrefetchCacheCount() throws IOException, InterruptedException {
if (channel == null) {
return -1;
}
if (!channel.remoteCapability.supportsPrefetch()) {
return -1;
}
return channel.call(new LoadingPrefetchCacheCount());
}
/**
* Shows {@link Channel#resourceLoadingCount}.
* @return Requested value or {@code -1} if the agent is offline.
* @since 1.495
*/
@CheckReturnValue
public int getResourceLoadingCount() throws IOException, InterruptedException {
if (channel == null) {
return -1;
}
return channel.call(new LoadingCount(true));
}
/**
* Shows {@link Channel#classLoadingTime}.
* @return Requested value or {@code -1} if the agent is offline.
* @since 1.495
*/
@CheckReturnValue
public long getClassLoadingTime() throws IOException, InterruptedException {
if (channel == null) {
return -1;
}
return channel.call(new LoadingTime(false));
}
/**
* Shows {@link Channel#resourceLoadingTime}.
* @return Requested value or {@code -1} if the agent is offline.
* @since 1.495
*/
@CheckReturnValue
public long getResourceLoadingTime() throws IOException, InterruptedException {
if (channel == null) {
return -1;
}
return channel.call(new LoadingTime(true));
}
/**
* Returns the remote FS root absolute path or {@code null} if the agent is off-line. The absolute path may change
* between connections if the connection method does not provide a consistent working directory and the node's
* remote FS is specified as a relative path.
*
* @return the remote FS root absolute path or {@code null} if the agent is off-line.
* @since 1.606
*/
@CheckForNull
public String getAbsoluteRemoteFs() {
return channel == null ? null : absoluteRemoteFs;
}
/**
* Just for restFul api.
* Returns the remote FS root absolute path or {@code null} if the agent is off-line. The absolute path may change
* between connections if the connection method does not provide a consistent working directory and the node's
* remote FS is specified as a relative path.
* @see #getAbsoluteRemoteFs()
* @return the remote FS root absolute path or {@code null} if the agent is off-line or don't have connect permission.
* @since 2.125
*/
@Exported
@Restricted(DoNotUse.class)
@CheckForNull
public String getAbsoluteRemotePath() {
if(hasPermission(CONNECT)) {
return getAbsoluteRemoteFs();
} else {
return null;
}
}
static class LoadingCount extends MasterToSlaveCallable
* Subtypes that needs to decorate {@link ComputerLauncher} can do so by overriding this method.
* This is useful for {@link SlaveComputer}s for clouds for example, where one normally needs
* additional pre-launch step (such as waiting for the provisioned node to become available)
* before the user specified launch step (like SSH connection) kicks in.
*
* @see ComputerLauncherFilter
*/
protected ComputerLauncher grabLauncher(Node node) {
return ((Slave)node).getLauncher();
}
/**
* Get the agent version
*/
@CheckReturnValue
public String getSlaveVersion() throws IOException, InterruptedException {
if (channel == null) {
return "Unknown (agent is offline)";
}
return channel.call(new SlaveVersion());
}
/**
* Get the OS description.
*/
@CheckReturnValue
public String getOSDescription() throws IOException, InterruptedException {
if (channel == null) {
return "Unknown (agent is offline)";
}
return channel.call(new DetectOS()) ? "Unix" : "Windows";
}
/**
* Expose real full env vars map from agent for UI presentation
*/
@CheckReturnValue
public Map