/**
* *****************************************************************************
*
* Copyright (c) 2004-2013 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, Nikita Levyankov, Erik Ramfelt, Koichi
* Fujikawa, Seiji Sogabe, Stephen Connolly, Tom Huybrechts, Alan Harder, Duncan
* Mills
*
******************************************************************************
*/
package hudson.model;
import com.thoughtworks.xstream.XStream;
import hudson.BulkChange;
import hudson.DNSMultiCast;
import hudson.DescriptorExtensionList;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionListView;
import hudson.ExtensionPoint;
import hudson.FilePath;
import hudson.Functions;
import hudson.GlobalMessage;
import hudson.Launcher;
import hudson.Launcher.LocalLauncher;
import hudson.LocalPluginManager;
import hudson.Lookup;
import hudson.Plugin;
import hudson.PluginManager;
import hudson.PluginWrapper;
import hudson.ProxyConfiguration;
import hudson.StructuredForm;
import hudson.TcpSlaveAgentListener;
import hudson.UDPBroadcastThread;
import hudson.Util;
import static hudson.Util.fixEmpty;
import static hudson.Util.fixNull;
import hudson.XmlFile;
import hudson.cli.CLICommand;
import hudson.cli.CliEntryPoint;
import hudson.cli.CliManagerImpl;
import hudson.cli.declarative.CLIMethod;
import hudson.cli.declarative.CLIResolver;
import hudson.init.InitMilestone;
import static hudson.init.InitMilestone.*;
import hudson.init.InitReactorListener;
import hudson.init.InitStrategy;
import hudson.lifecycle.Lifecycle;
import hudson.lifecycle.RestartNotSupportedException;
import hudson.logging.LogRecorderManager;
import hudson.markup.MarkupFormatter;
import hudson.model.Descriptor.FormException;
import hudson.model.labels.LabelAtom;
import hudson.model.listeners.ItemListener;
import hudson.model.listeners.SCMListener;
import hudson.model.listeners.SaveableListener;
import hudson.remoting.Channel;
import hudson.remoting.LocalChannel;
import hudson.remoting.VirtualChannel;
import hudson.scm.RepositoryBrowser;
import hudson.scm.SCM;
import hudson.search.CollectionSearchIndex;
import hudson.search.SearchIndexBuilder;
import hudson.security.*;
import hudson.security.csrf.CrumbIssuer;
import hudson.slaves.Cloud;
import hudson.slaves.ComputerListener;
import hudson.slaves.DumbSlave;
import hudson.slaves.NodeDescriptor;
import hudson.slaves.NodeList;
import hudson.slaves.NodeProperty;
import hudson.slaves.NodePropertyDescriptor;
import hudson.slaves.NodeProvisioner;
import hudson.slaves.OfflineCause;
import hudson.slaves.RetentionStrategy;
import hudson.tasks.BuildWrapper;
import hudson.tasks.Builder;
import hudson.tasks.Mailer;
import hudson.tasks.Publisher;
import hudson.triggers.Trigger;
import hudson.triggers.TriggerDescriptor;
import hudson.util.AdministrativeError;
import hudson.util.CaseInsensitiveComparator;
import hudson.util.ClockDifference;
import hudson.util.CopyOnWriteList;
import hudson.util.CopyOnWriteMap;
import hudson.util.DaemonThreadFactory;
import hudson.util.DescribableList;
import hudson.util.FormValidation;
import hudson.util.Futures;
import hudson.util.HudsonIsLoading;
import hudson.util.HudsonIsRestarting;
import hudson.util.Iterators;
import hudson.util.Memoizer;
import hudson.util.MultipartFormDataParser;
import hudson.util.RemotingDiagnostics;
import hudson.util.RemotingDiagnostics.HeapDump;
import hudson.util.Service;
import hudson.util.StreamTaskListener;
import hudson.util.VersionNumber;
import hudson.util.XStream2;
import hudson.views.DefaultMyViewsTabBar;
import hudson.views.DefaultViewsTabBar;
import hudson.views.MyViewsTabBar;
import hudson.views.ViewsTabBar;
import hudson.widgets.Widget;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.BindException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.text.Collator;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.SecretKey;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import static javax.servlet.http.HttpServletResponse.*;
import net.sf.json.JSONObject;
import org.antlr.runtime.RecognitionException;
import org.apache.commons.jelly.JellyException;
import org.apache.commons.jelly.Script;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.FastDateFormat;
import org.apache.commons.logging.LogFactory;
import org.eclipse.hudson.WebAppController;
import org.eclipse.hudson.plugins.PluginCenter;
import org.eclipse.hudson.script.ScriptSupport;
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;
import org.jvnet.hudson.reactor.Executable;
import org.jvnet.hudson.reactor.Milestone;
import org.jvnet.hudson.reactor.Reactor;
import org.jvnet.hudson.reactor.ReactorException;
import org.jvnet.hudson.reactor.ReactorListener;
import org.jvnet.hudson.reactor.Task;
import org.jvnet.hudson.reactor.TaskBuilder;
import org.jvnet.hudson.reactor.TaskGraphBuilder;
import org.jvnet.hudson.reactor.TaskGraphBuilder.Handle;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.MetaClass;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerFallback;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.WebApp;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.framework.adjunct.AdjunctManager;
import org.kohsuke.stapler.jelly.JellyClassLoaderTearOff;
import org.kohsuke.stapler.jelly.JellyRequestDispatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.xml.sax.InputSource;
/**
* Root object of the system.
*
* @author Kohsuke Kawaguchi
* @author Nikita Levyankov
*/
@ExportedBean
public final class Hudson extends Node implements ItemGroup<TopLevelItem>, StaplerProxy, StaplerFallback, ViewGroup, AccessControlled, DescriptorByNameOwner {
private transient Logger logger = LoggerFactory.getLogger(Hudson.class);
private transient final Queue queue;
/**
* Stores various objects scoped to {@link Hudson}.
*/
public transient final Lookup lookup = new Lookup();
/**
* {@link Computer}s in this Hudson system. Read-only.
*/
private transient final Map<Node, Computer> computers = new CopyOnWriteMap.Hash<Node, Computer>();
/**
* We update this field to the current version of Hudson whenever we save
* {@code config.xml}. This can be used to detect when an upgrade happens
* from one version to next.
*
* <p>
* Since this field is introduced starting 1.301, "1.0" is used to represent
* every version up to 1.300. This value may also include non-standard
* versions like "1.301-SNAPSHOT" or "?", etc., so parsing needs to be done
* with a care.
*
* @since 1.301
*/
// this field needs to be at the very top so that other components can look at this value even during unmarshalling
private String version = "1.0";
/**
* Number of executors of the master node.
*/
private int numExecutors = 2;
/**
* Job allocation strategy.
*/
private Mode mode = Mode.NORMAL;
/**
* Message displayed in the top page.
*/
private String systemMessage;
/**
* Optional privacy message to be printed at the base of each page, this can
* be used for CopyRight, Confidentiality information and so forth
* Introduced Hudson 3.0.1
*/
private String privacyMessage;
/**
* Optional identifier for the instance to be printed on each page
* Introduced Hudson 3.0.1
*/
private String instanceTag;
private static transient final String HUDSON_WORKSPACES_PROPERTY_KEY = "HUDSON_WORKSPACES";
/**
* Workspace root dir which could be configured by setting HUDSON_WORKSPACES
* property.
*/
private volatile String configuredWorkspaceRoot;
/**
* Root directory of the system.
*/
public transient final File root;
/**
* Where are we in the initialization?
*/
private transient volatile InitMilestone initLevel = InitMilestone.STARTED;
/**
* All {@link Item}s keyed by their {@link Item#getName() name}s.
*/
/*package*/ transient final Map<String, TopLevelItem> items = new CopyOnWriteMap.Tree<String, TopLevelItem>(CaseInsensitiveComparator.INSTANCE);
/**
* A cache of top level items
*/
private transient final TopLevelItemsCache itemsCache = new TopLevelItemsCache();
/**
* The sole instance.
*/
private static Hudson theInstance;
private transient volatile boolean isQuietingDown;
private transient volatile boolean terminating;
private transient volatile boolean safeRestarting;
private List<JDK> jdks = new ArrayList<JDK>();
private transient volatile DependencyGraph dependencyGraph;
/**
* Currently active Views tab bar.
*/
private volatile ViewsTabBar viewsTabBar = new DefaultViewsTabBar();
/**
* Currently active My Views tab bar.
*/
private volatile MyViewsTabBar myViewsTabBar = new DefaultMyViewsTabBar();
/**
* Script support (if available)
*/
private volatile ScriptSupport scriptSupport;
private Boolean allowUnsecuredAction = false;
private Boolean allowCli = false;
/**
* All {@link ExtensionList} keyed by their
* {@link ExtensionList#extensionType}.
*/
private transient final Memoizer<Class, ExtensionList> extensionLists = new Memoizer<Class, ExtensionList>() {
public ExtensionList compute(Class key) {
return ExtensionList.create(Hudson.this, key);
}
};
/**
* All {@link DescriptorExtensionList} keyed by their
* {@link DescriptorExtensionList#describableType}.
*/
private transient final Memoizer<Class, DescriptorExtensionList> descriptorLists = new Memoizer<Class, DescriptorExtensionList>() {
public DescriptorExtensionList compute(Class key) {
return DescriptorExtensionList.createDescriptorList(Hudson.this, key);
}
};
/**
* Active {@link Cloud}s.
*/
public final CloudList clouds = new CloudList(this);
public static class CloudList extends DescribableList<Cloud, Descriptor<Cloud>> {
public CloudList(Hudson h) {
super(h);
}
// needed for XStream deserialization
public CloudList() {
}
public Cloud getByName(String name) {
for (Cloud c : this) {
if (c.name.equals(name)) {
return c;
}
}
return null;
}
@Override
protected void onModified() throws IOException {
super.onModified();
Hudson.getInstance().trimLabels();
}
}
/**
* Set of installed cluster nodes.
* <p>
* We use this field with copy-on-write semantics. This field has mutable
* list (to keep the serialization look clean), but it shall never be
* modified. Only new completely populated slave list can be set here.
* <p>
* The field name should be really {@code nodes}, but again the backward
* compatibility prevents us from renaming.
*/
private volatile NodeList slaves;
/**
* Quiet period.
*
* This is {@link Integer} so that we can initialize it to '5' for upgrading
* users.
*/
/*package*/ Integer quietPeriod;
/**
* Global default for {@link AbstractProject#getScmCheckoutRetryCount()}
*/
/*package*/ int scmCheckoutRetryCount;
/**
* {@link View}s.
*/
private final CopyOnWriteArrayList<View> views = new CopyOnWriteArrayList<View>();
/**
* Name of the primary view.
* <p>
* Start with null, so that we can upgrade pre-1.269 data well.
*
* @since 1.269
*/
private volatile String primaryView;
private transient final FingerprintMap fingerprintMap = new FingerprintMap();
/**
* Loaded plugins.
*/
public transient final PluginManager pluginManager;
private transient PluginCenter pluginCenter;
public transient volatile TcpSlaveAgentListener tcpSlaveAgentListener;
private transient UDPBroadcastThread udpBroadcastThread;
private transient DNSMultiCast dnsMultiCast;
/**
* List of registered {@link ItemListener}s.
*
* @deprecated as of 1.286
*/
private transient final CopyOnWriteList<ItemListener> itemListeners = ExtensionListView.createCopyOnWriteList(ItemListener.class);
/**
* List of registered {@link SCMListener}s.
*/
private transient final CopyOnWriteList<SCMListener> scmListeners = new CopyOnWriteList<SCMListener>();
/**
* List of registered {@link ComputerListener}s.
*
* @deprecated as of 1.286
*/
private transient final CopyOnWriteList<ComputerListener> computerListeners = ExtensionListView.createCopyOnWriteList(ComputerListener.class);
/**
* Whitespace-separated labels assigned to the master as a {@link Node}.
*/
private String label = "";
/**
* {@link hudson.security.csrf.CrumbIssuer}
*/
private volatile CrumbIssuer crumbIssuer;
/**
* All labels known to Hudson. This allows us to reuse the same label
* instances as much as possible, even though that's not a strict
* requirement.
*/
private transient final ConcurrentHashMap<String, Label> labels = new ConcurrentHashMap<String, Label>();
/**
* Load statistics of the entire system.
*/
@Exported
public transient final OverallLoadStatistics overallLoad = new OverallLoadStatistics();
/**
* {@link NodeProvisioner} that reacts to {@link OverallLoadStatistics}.
*/
public transient final NodeProvisioner overallNodeProvisioner = new NodeProvisioner(null, overallLoad);
public transient final ServletContext servletContext;
/**
* Transient action list. Useful for adding navigation items to the
* navigation bar on the left.
*/
private transient final List<Action> actions = new CopyOnWriteArrayList<Action>();
/**
* List of master node properties
*/
private DescribableList<NodeProperty<?>, NodePropertyDescriptor> nodeProperties = new DescribableList<NodeProperty<?>, NodePropertyDescriptor>(this);
/**
* List of global properties
*/
private DescribableList<NodeProperty<?>, NodePropertyDescriptor> globalNodeProperties = new DescribableList<NodeProperty<?>, NodePropertyDescriptor>(this);
/**
* {@link AdministrativeMonitor}s installed on this system.
*
* @see AdministrativeMonitor
*/
public transient final List<AdministrativeMonitor> administrativeMonitors = getExtensionList(AdministrativeMonitor.class);
/*package*/ final CopyOnWriteArraySet<String> disabledAdministrativeMonitors = new CopyOnWriteArraySet<String>();
/**
* Widgets on Hudson.
*/
private transient final List<Widget> widgets = getExtensionList(Widget.class);
/**
* Use Blue ball instead of the default green ball
*/
private Boolean useBlueBall = false;
/**
* {@link AdjunctManager}
*/
private transient final AdjunctManager adjuncts;
/**
* Code that handles {@link ItemGroup} work.
*/
private transient final ItemGroupMixIn itemGroupMixIn = new ItemGroupMixIn(this, this) {
@Override
protected void add(TopLevelItem item) {
assert item.getRootDir().exists();
final LazyTopLevelItem lzItem = Items.newLazyTopLevelItem(item);
items.put(item.getName(), lzItem);
}
@Override
protected File getRootDirFor(String name) {
return Hudson.this.getRootDirFor(name);
}
/**
* send the browser to the config page use View to trim
* view/{default-view} from URL if possible
*/
@Override
protected String redirectAfterCreateItem(StaplerRequest req, TopLevelItem result) throws IOException {
String redirect = result.getUrl() + "configure";
List<Ancestor> ancestors = req.getAncestors();
for (int i = ancestors.size() - 1; i >= 0; i--) {
Object o = ancestors.get(i).getObject();
if (o instanceof View) {
redirect = req.getContextPath() + '/' + ((View) o).getUrl() + redirect;
break;
}
}
return redirect;
}
};
public static class HudsonDateFormat extends FastDateFormat {
public HudsonDateFormat(String format) {
super(format, TimeZone.getDefault(), Locale.getDefault());
}
}
@CLIResolver
public static Hudson getInstance() {
return theInstance;
}
private transient final UpdateCenter updateCenter = new UpdateCenter();
/**
* True if the user opted out from the statistics tracking. We'll never send
* anything if this is true.
*/
private Boolean noUsageStatistics;
/**
* Number of times Hudson has been started in this JVM.
*/
private static int invocationCount = 0;
/**
* HTTP proxy configuration.
*/
public transient volatile ProxyConfiguration proxy;
/**
* Bound to "/log".
*/
private transient final LogRecorderManager log = new LogRecorderManager();
public Hudson(File root, ServletContext context) throws IOException, InterruptedException, ReactorException {
this(root, context, null, false);
}
public Hudson(File root, ServletContext context, PluginManager pluginManager) throws IOException, InterruptedException, ReactorException {
this(root, context, pluginManager, false);
}
/**
* @param pluginManager If non-null, use existing plugin manager. create a
* new one.
*/
public Hudson(File root, ServletContext context, PluginManager pluginManager, boolean restart) throws IOException, InterruptedException, ReactorException {
// As hudson is starting, grant this process full control
HudsonSecurityManager.grantFullControl();
try {
this.root = root;
this.servletContext = context;
computeVersion(context);
if (theInstance != null) {
if (!restart) {
throw new IllegalStateException("second instance");
}
}
theInstance = this;
invocationCount++;
// doing this early allows InitStrategy to set environment upfront
final InitStrategy is = InitStrategy.get(Thread.currentThread().getContextClassLoader());
// In case reinitializing for soft restart, cancel all events
if (Trigger.timer != null) {
Trigger.timer.cancel();
Trigger.timer.purge();
}
Trigger.timer = new Timer("Hudson cron thread " + invocationCount);
queue = new Queue(CONSISTENT_HASH ? LoadBalancer.CONSISTENT_HASH : LoadBalancer.DEFAULT);
try {
dependencyGraph = DependencyGraph.EMPTY;
} catch (InternalError e) {
if (e.getMessage().contains("window server")) {
throw new Error("Looks like the server runs without X. Please specify -Djava.awt.headless=true as JVM option", e);
}
throw e;
}
try {
proxy = new ProxyConfiguration(getRootDir());
} catch (IOException e) {
logger.error("Failed to load proxy configuration", e);
}
if (pluginManager == null) {
pluginManager = new LocalPluginManager(this);
}
this.pluginManager = pluginManager;
pluginCenter = new PluginCenter(root);
// JSON binding needs to be able to see all the classes from all the plugins
WebApp.get(servletContext).setClassLoader(pluginManager.uberClassLoader);
adjuncts = new AdjunctManager(servletContext, pluginManager.uberClassLoader, "adjuncts/" + VERSION_HASH);
// initialization consists of ...
executeReactor(is,
pluginManager.initTasks(is), // loading and preparing plugins
loadTasks(), // load jobs
InitMilestone.ordering() // forced ordering among key milestones
);
if (KILL_AFTER_LOAD) {
System.exit(0);
}
int slaveAgentPort = HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getSlaveAgentPort();
if (slaveAgentPort != -1) {
try {
tcpSlaveAgentListener = new TcpSlaveAgentListener(slaveAgentPort);
} catch (BindException e) {
new AdministrativeError(getClass().getName() + ".tcpBind",
"Failed to listen to incoming slave connection",
"Failed to listen to incoming slave connection. <a href='configure'>Change the port number</a> to solve the problem.", e);
}
} else {
tcpSlaveAgentListener = null;
}
try {
udpBroadcastThread = new UDPBroadcastThread(this);
udpBroadcastThread.start();
} catch (IOException e) {
logger.warn("Faild to broadcast over UDP", e);
}
dnsMultiCast = new DNSMultiCast(this);
updateComputerList();
// master is online now
{
Computer c = toComputer();
if (c != null) {
for (ComputerListener cl : ComputerListener.all()) {
cl.onOnline(c, StreamTaskListener.fromStdout());
}
}
}
for (ItemListener l : ItemListener.all()) {
l.onLoaded();
}
if (!ScriptSupport.getAvailableScriptSupports().isEmpty()) {
scriptSupport = ScriptSupport.getAvailableScriptSupports().get(0);
}
} finally {
HudsonSecurityManager.resetFullControl();
}
}
/**
* Executes a reactor.
*
* @param is If non-null, this can be consulted for ignoring some tasks.
* Only used during the initialization of Hudson.
*/
private void executeReactor(final InitStrategy is, TaskBuilder... builders) throws IOException, InterruptedException, ReactorException {
Reactor reactor = new Reactor(builders) {
/**
* Sets the thread name to the task for better diagnostics.
*/
@Override
protected void runTask(Task task) throws Exception {
if (is != null && is.skipInitTask(task)) {
return;
}
// full access in the initialization thread
HudsonSecurityManager.grantFullControl();
String taskName = task.getDisplayName();
Thread t = Thread.currentThread();
String name = t.getName();
if (taskName != null) {
t.setName(taskName);
}
try {
long start = System.currentTimeMillis();
super.runTask(task);
if (LOG_STARTUP_PERFORMANCE) {
logger.info(String.format("Took %dms for %s by %s",
System.currentTimeMillis() - start, taskName, name));
}
} finally {
t.setName(name);
HudsonSecurityManager.resetFullControl();
}
}
};
ExecutorService es;
if (PARALLEL_LOAD) {
es = new ThreadPoolExecutor(
TWICE_CPU_NUM, TWICE_CPU_NUM, 5L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new DaemonThreadFactory());
} else {
es = Executors.newSingleThreadExecutor(new DaemonThreadFactory());
}
try {
reactor.execute(es, buildReactorListener());
} finally {
es.shutdownNow(); // upon a successful return the executor queue should be empty. Upon an exception, we want to cancel all pending tasks
}
}
/**
* Aggregates all the listeners into one and returns it.
*
* <p>
* At this point plugins are not loaded yet, so we fall back to the
* META-INF/services look up to discover implementations. As such there's no
* way for plugins to participate into this process.
*/
private ReactorListener buildReactorListener() throws IOException {
List<ReactorListener> r = (List) Service.loadInstances(Thread.currentThread().getContextClassLoader(), InitReactorListener.class);
r.add(new ReactorListener() {
public void onTaskStarted(Task t) {
logger.debug("Started {}", t.getDisplayName());
}
public void onTaskCompleted(Task t) {
logger.debug("Completed {}", t.getDisplayName());
}
public void onTaskFailed(Task t, Throwable err, boolean fatal) {
logger.error("Failed " + t.getDisplayName(), err);
}
public void onAttained(Milestone milestone) {
String s = "Attained " + milestone.toString();
if (milestone instanceof InitMilestone) {
initLevel = (InitMilestone) milestone;
s = initLevel.toString();
}
logger.info(s);
}
});
return new ReactorListener.Aggregator(r);
}
public TcpSlaveAgentListener getTcpSlaveAgentListener() {
return tcpSlaveAgentListener;
}
/**
* Makes {@link AdjunctManager} URL-bound. The dummy parameter allows us to
* use different URLs for the same adjunct, for proper cache handling.
*/
public AdjunctManager getAdjuncts(String dummy) {
return adjuncts;
}
/**
* If you are calling this on Hudson something is wrong.
*
* @deprecated
*/
@Deprecated
@Override
public String getNodeName() {
return "";
}
public void setNodeName(String name) {
throw new UnsupportedOperationException(); // not allowed
}
public String getNodeDescription() {
return Messages.Hudson_NodeDescription();
}
@Exported
public String getDescription() {
return systemMessage;
}
public PluginManager getPluginManager() {
return pluginManager;
}
public PluginCenter getPluginCenter() {
return pluginCenter;
}
/**
* @return true if team management is enabled
* @since 3.1.0
*/
public boolean isTeamManagementEnabled() {
return getTeamManager().isTeamManagementEnabled();
}
/**
* TeamManager is returned whether or not Team based authorization is set.
*
* @return TeamManager
* @since 3.1.0
*/
public TeamManager getTeamManager() {
return getSecurityManager().getTeamManager();
}
/**
* Do not use this API, for internal purpose only
*
* @since 3.1.0
*/
public void replaceItem(String oldItemName, String newItemName) {
if (items.containsKey(oldItemName)) {
TopLevelItem item = items.get(oldItemName);
items.remove(oldItemName);
items.put(newItemName, item);
}
}
public HudsonSecurityManager getSecurityManager() {
return HudsonSecurityEntitiesHolder.getHudsonSecurityManager();
}
public UpdateCenter getUpdateCenter() {
return updateCenter;
}
public boolean isUsageStatisticsCollected() {
return noUsageStatistics == null || !noUsageStatistics;
}
public void setNoUsageStatistics(Boolean noUsageStatistics) throws IOException {
this.noUsageStatistics = noUsageStatistics;
save();
}
public View.People getPeople() {
return new View.People(this);
}
/**
* Does this {@link View} has any associated user information recorded?
*/
public boolean hasPeople() {
return View.People.isApplicable(items.values());
}
public Api getApi() {
return new Api(this);
}
/**
* Returns a secret key that survives across container start/stop.
* <p>
* This value is useful for implementing some of the security features.
*/
public String getSecretKey() {
return getSecurityManager().getSecretKey();
}
/**
* Gets {@linkplain #getSecretKey() the secret key} as a key for AES-128.
*
* @since 1.308
*/
public SecretKey getSecretKeyAsAES128() {
return getSecurityManager().getSecretKeyAsAES128();
}
/**
* Gets the SCM descriptor by name. Primarily used for making them
* web-visible.
*/
public Descriptor<SCM> getScm(String shortClassName) {
return findDescriptor(shortClassName, SCM.all());
}
/**
* Gets the repository browser descriptor by name. Primarily used for making
* them web-visible.
*/
public Descriptor<RepositoryBrowser<?>> getRepositoryBrowser(String shortClassName) {
return findDescriptor(shortClassName, RepositoryBrowser.all());
}
/**
* Gets the builder descriptor by name. Primarily used for making them
* web-visible.
*/
public Descriptor<Builder> getBuilder(String shortClassName) {
return findDescriptor(shortClassName, Builder.all());
}
/**
* Gets the build wrapper descriptor by name. Primarily used for making them
* web-visible.
*/
public Descriptor<BuildWrapper> getBuildWrapper(String shortClassName) {
return findDescriptor(shortClassName, BuildWrapper.all());
}
/**
* Gets the publisher descriptor by name. Primarily used for making them
* web-visible.
*/
public Descriptor<Publisher> getPublisher(String shortClassName) {
return findDescriptor(shortClassName, Publisher.all());
}
/**
* Gets the trigger descriptor by name. Primarily used for making them
* web-visible.
*/
public TriggerDescriptor getTrigger(String shortClassName) {
return (TriggerDescriptor) findDescriptor(shortClassName, Trigger.all());
}
/**
* Gets the retention strategy descriptor by name. Primarily used for making
* them web-visible.
*/
public Descriptor<RetentionStrategy<?>> getRetentionStrategy(String shortClassName) {
return findDescriptor(shortClassName, RetentionStrategy.all());
}
/**
* Gets the {@link JobPropertyDescriptor} by name. Primarily used for making
* them web-visible.
*/
public JobPropertyDescriptor getJobProperty(String shortClassName) {
// combining these two lines triggers javac bug. See issue #610.
Descriptor d = findDescriptor(shortClassName, JobPropertyDescriptor.all());
return (JobPropertyDescriptor) d;
}
/**
* Exposes {@link Descriptor} by its name to URL.
*
* After doing all the {@code getXXX(shortClassName)} methods, I finally
* realized that this just doesn't scale.
*
* @param id Either {@link Descriptor#getId()} (recommended) or the short
* name of a {@link Describable} subtype (for compatibility)
*/
public Descriptor getDescriptor(String id) {
// legacy descriptors that are reigstered manually doesn't show up in getExtensionList, so check them explicitly.
for (Descriptor d : Iterators.sequence(getExtensionList(Descriptor.class), DescriptorExtensionList.listLegacyInstances())) {
String name = d.getId();
if (name.equals(id)) {
return d;
}
if (name.substring(name.lastIndexOf('.') + 1).equals(id)) {
return d;
}
}
return null;
}
/**
* Alias for {@link #getDescriptor(String)}.
*/
public Descriptor getDescriptorByName(String id) {
return getDescriptor(id);
}
/**
* Gets the {@link Descriptor} that corresponds to the given
* {@link Describable} type.
* <p>
* If you have an instance of {@code type} and call
* {@link Describable#getDescriptor()}, you'll get the same instance that
* this method returns.
*/
public Descriptor getDescriptor(Class<? extends Describable> type) {
for (Descriptor d : getExtensionList(Descriptor.class)) {
if (d.clazz == type) {
return d;
}
}
return null;
}
/**
* Works just like {@link #getDescriptor(Class)} but don't take no for an
* answer.
*
* @throws AssertionError If the descriptor is missing.
* @since 1.326
*/
public Descriptor getDescriptorOrDie(Class<? extends Describable> type) {
Descriptor d = getDescriptor(type);
if (d == null) {
throw new AssertionError(type + " is missing its descriptor");
}
return d;
}
/**
* Gets the {@link Descriptor} instance in the current Hudson by its type.
*/
public <T extends Descriptor> T getDescriptorByType(Class<T> type) {
for (Descriptor d : getExtensionList(Descriptor.class)) {
if (d.getClass() == type) {
return type.cast(d);
}
}
return null;
}
/**
* Gets the {@link SecurityRealm} descriptors by name. Primarily used for
* making them web-visible.
*/
public Descriptor<SecurityRealm> getSecurityRealms(String shortClassName) {
return findDescriptor(shortClassName, SecurityRealm.all());
}
/**
* Finds a descriptor that has the specified name.
*/
private <T extends Describable<T>> Descriptor<T> findDescriptor(String shortClassName, Collection<? extends Descriptor<T>> descriptors) {
String name = '.' + shortClassName;
for (Descriptor<T> d : descriptors) {
if (d.clazz.getName().endsWith(name)) {
return d;
}
}
return null;
}
/**
* Gets all the installed {@link ItemListener}s.
*
* @deprecated as of 1.286. Use {@link ItemListener#all()}.
*/
public CopyOnWriteList<ItemListener> getJobListeners() {
return itemListeners;
}
/**
* Gets all the installed {@link SCMListener}s.
*/
public CopyOnWriteList<SCMListener> getSCMListeners() {
return scmListeners;
}
/**
* Gets all the installed {@link ComputerListener}s.
*
* @deprecated as of 1.286. Use {@link ComputerListener#all()}.
*/
public CopyOnWriteList<ComputerListener> getComputerListeners() {
return computerListeners;
}
/**
* Gets the plugin object from its short name.
*
* <p>
* This allows URL <tt>hudson/plugin/ID</tt> to be served by the views of
* the plugin class.
*/
public Plugin getPlugin(String shortName) {
PluginWrapper p = pluginManager.getPlugin(shortName);
if (p == null) {
return null;
}
return p.getPlugin();
}
/**
* Gets the plugin object from its class.
*
* <p>
* This allows easy storage of plugin information in the plugin singleton
* without every plugin reimplementing the singleton pattern.
*
* @param clazz The plugin class (beware class-loader fun, this will
* probably only work from within the hpi that defines the plugin class, it
* may or may not work in other cases)
*
* @return The plugin instance.
*/
@SuppressWarnings("unchecked")
public <P extends Plugin> P getPlugin(Class<P> clazz) {
PluginWrapper p = pluginManager.getPlugin(clazz);
if (p == null) {
return null;
}
return (P) p.getPlugin();
}
/**
* Gets the plugin objects from their super-class.
*
* @param clazz The plugin class (beware class-loader fun)
*
* @return The plugin instances.
*/
public <P extends Plugin> List<P> getPlugins(Class<P> clazz) {
List<P> result = new ArrayList<P>();
for (PluginWrapper w : pluginManager.getPlugins(clazz)) {
result.add((P) w.getPlugin());
}
return Collections.unmodifiableList(result);
}
/**
* Synonym to {@link #getNodeDescription()}.
*/
public String getSystemMessage() {
return systemMessage;
}
// Here for backward compatibility 2.x
public MarkupFormatter getMarkupFormatter() {
return getSecurityManager().getMarkupFormatter();
}
// Here for backward compatibility to 2.x
public void setMarkupFormatter(MarkupFormatter markupFormatter) {
getSecurityManager().setMarkupFormatter(markupFormatter);
}
/**
* Sets the system message.
*/
public void setSystemMessage(String message) throws IOException {
this.systemMessage = message;
save();
}
/**
* Get the Privacy Message
*/
public String getPrivacyMessage() {
return privacyMessage;
}
/**
* Sets the privacy message.
*/
public void setPrivacyMessage(String message) throws IOException {
this.privacyMessage = message;
save();
}
/**
* Get the Instance identifier
*/
public String getInstanceTag() {
return instanceTag;
}
/**
* Sets the Instance Identifier.
*/
public void setInstanceTag(String name) throws IOException {
this.instanceTag = name;
save();
}
public FederatedLoginService getFederatedLoginService(String name) {
for (FederatedLoginService fls : FederatedLoginService.all()) {
if (fls.getUrlName().equals(name)) {
return fls;
}
}
return null;
}
public List<FederatedLoginService> getFederatedLoginServices() {
return FederatedLoginService.all();
}
public Launcher createLauncher(TaskListener listener) {
return new LocalLauncher(listener).decorateFor(this);
}
private final transient Object updateComputerLock = new Object();
/**
* Updates {@link #computers} by using {@link #getSlaves()}.
*
* <p>
* This method tries to reuse existing {@link Computer} objects so that we
* won't upset {@link Executor}s running in it.
*/
private void updateComputerList() throws IOException {
// just so that we don't have two code updating computer list at the same time
synchronized (updateComputerLock) {
Map<String, Computer> byName = new HashMap<String, Computer>();
for (Computer c : computers.values()) {
if (c.getNode() == null) {
continue; // this computer is gone
}
byName.put(c.getNode().getNodeName(), c);
}
Set<Computer> old = new HashSet<Computer>(computers.values());
Set<Computer> used = new HashSet<Computer>();
updateComputer(this, byName, used);
for (Node s : getNodes()) {
updateComputer(s, byName, used);
}
// find out what computers are removed, and kill off all executors.
// when all executors exit, it will be removed from the computers map.
// so don't remove too quickly
old.removeAll(used);
for (Computer c : old) {
c.kill();
}
}
getQueue().scheduleMaintenance();
for (ComputerListener cl : ComputerListener.all()) {
cl.onConfigurationChange();
}
}
private void updateComputer(Node n, Map<String, Computer> byNameMap, Set<Computer> used) {
Computer c;
c = byNameMap.get(n.getNodeName());
if (c != null) {
c.setNode(n); // reuse
} else {
if (n.getNumExecutors() > 0) {
computers.put(n, c = n.createComputer());
if (!n.holdOffLaunchUntilSave && AUTOMATIC_SLAVE_LAUNCH) {
RetentionStrategy retentionStrategy = c.getRetentionStrategy();
if (retentionStrategy != null) {
// if there is a retention strategy, it is responsible for deciding to start the computer
retentionStrategy.start(c);
} else {
// we should never get here, but just in case, we'll fall back to the legacy behaviour
c.connect(true);
}
}
}
}
used.add(c);
}
/*package*/ void removeComputer(Computer computer) {
for (Entry<Node, Computer> e : computers.entrySet()) {
if (e.getValue() == computer) {
computers.remove(e.getKey());
return;
}
}
throw new IllegalStateException("Trying to remove unknown computer");
}
public String getFullName() {
return "";
}
public String getFullDisplayName() {
return "";
}
/**
* Returns the transient {@link Action}s associated with the top page.
*
* <p>
* Adding {@link Action} is primarily useful for plugins to contribute an
* item to the navigation bar of the top page. See existing {@link Action}
* implementation for it affects the GUI.
*
* <p>
* To register an {@link Action}, implement {@link RootAction} extension
* point, or write code like
* {@code Hudson.getInstance().getActions().add(...)}.
*
* @return Live list where the changes can be made. Can be empty but never
* null.
* @since 1.172
*/
public List<Action> getActions() {
return actions;
}
/**
* Gets just the immediate children of {@link Hudson}.
*
* @see #getAllItems(Class)
*/
@Exported(name = "jobs")
public List<TopLevelItem> getItems() {
List<TopLevelItem> viewableItems = new ArrayList<TopLevelItem>();
for (TopLevelItem item : items.values()) {
if (item.hasPermission(Item.READ)) {
TopLevelItem instance = LazyTopLevelItem.getIfInstanceOf(item, TopLevelItem.class);
if (instance != null) {
viewableItems.add(instance);
}
}
}
return viewableItems;
}
// Bug HUDSON/api/xml and HUDSON/api/json return names of jobs user not entitled to see
//@Exported(name = "securedJobs")
public List<TopLevelItem> getSecuredItems() {
List<TopLevelItem> viewableItems = new ArrayList<TopLevelItem>();
for (TopLevelItem item : items.values()) {
if (!item.hasPermission(Item.READ)) {
TopLevelItem instance = LazyTopLevelItem.getIfInstanceOf(item, TopLevelItem.class);
if (instance != null) {
viewableItems.add(instance);
}
}
}
return viewableItems;
}
/**
* Returns the read-only view of all the {@link TopLevelItem}s keyed by
* their names.
* <p>
* This method is efficient, as it doesn't involve any copying.
*
* @since 1.296
*/
public Map<String, TopLevelItem> getItemMap() {
// Is this circumventing permissions?
return Collections.unmodifiableMap(items);
}
/**
* Gets just the immediate children of {@link Hudson} but of the given type.
*/
public <T> List<T> getItems(Class<T> type) {
List<T> r = new ArrayList<T>();
for (TopLevelItem i : getItems()) {
T t = LazyTopLevelItem.getIfInstanceOf(i, type);
if (t != null) {
r.add(t);
}
}
return r;
}
/**
* Gets all the {@link Item}s recursively in the {@link ItemGroup} tree and
* filter them by the given type.
*/
public <T extends Item> List<T> getAllItems(Class<T> type) {
List<T> r = new ArrayList<T>();
Stack<ItemGroup> q = new Stack<ItemGroup>();
q.push(this);
while (!q.isEmpty()) {
ItemGroup<?> parent = q.pop();
for (Item i : parent.getItems()) {
T t = LazyTopLevelItem.getIfInstanceOf(i, type);
if (t != null) {
if (t.hasPermission(Item.READ)) {
r.add(t);
}
}
ItemGroup ig = LazyTopLevelItem.getIfInstanceOf(i, ItemGroup.class);
if (ig != null) {
q.push(ig);
}
}
}
return r;
}
/**
* Gets the list of all the projects.
*
* <p>
* Since {@link Project} can only show up under {@link Hudson}, no need to
* search recursively.
*/
public List<Project> getProjects() {
return getItems(Project.class);
// This line seems to circumvent the check for Permission.
// It also won't work with LazyTopLevelItem, so replacing it with
// line above.
//
// return Util.createSubList(items.values(), Project.class);
}
/**
* Gets the names of all the {@link Job}s.
*/
public Collection<String> getJobNames() {
List<String> names = new ArrayList<String>();
for (Job j : getAllItems(Job.class)) {
names.add(j.getFullName());
}
return names;
}
/**
* Gets the names of all the {@link TopLevelItem}s.
*/
public Collection<String> getTopLevelItemNames() {
List<String> names = new ArrayList<String>();
for (TopLevelItem j : items.values()) {
names.add(j.getName());
}
return names;
}
public synchronized View getView(String name) {
for (View v : views) {
if (v.getViewName().equals(name)) {
//return v;
if (isTeamManagementEnabled()) {
return v.hasPermission(View.READ) ? v : null;
} else {
return v;
}
}
}
if (name != null && !name.equals(primaryView)) {
// Fallback to subview of primary view if it is a ViewGroup
View pv = getPrimaryView();
if (pv instanceof ViewGroup) {
View view = ((ViewGroup) pv).getView(name);
if (isTeamManagementEnabled()) {
return view.hasPermission(View.READ) ? view : null;
} else {
return view;
}
}
}
return null;
}
/**
* Gets the read-only list of all {@link View}s.
*/
@Exported
@Override
public synchronized Collection<View> getViews() {
List<View> copy = new ArrayList<View>();
if (this.isTeamManagementEnabled()) {
for (View view : views) {
if (view.hasPermission(View.READ)) {
copy.add(view);
}
}
} else {
copy.addAll(views);
}
Collections.sort(copy, View.SORTER);
return copy;
}
public synchronized Collection<View> getAllViews() {
List<View> copy = new ArrayList<View>(views);
Collections.sort(copy, View.SORTER);
return copy;
}
public void addView(View v) throws IOException {
v.owner = this;
views.add(v);
save();
}
public boolean canDelete(View view) {
return !view.isDefault(); // Cannot delete primary view
}
public synchronized void deleteView(View view) throws IOException {
if (views.size() <= 1) {
throw new IllegalStateException("Cannot delete last view");
}
views.remove(view);
save();
}
public ViewsTabBar getViewsTabBar() {
return viewsTabBar;
}
public MyViewsTabBar getMyViewsTabBar() {
return myViewsTabBar;
}
public ScriptSupport getScriptSupport() {
return scriptSupport;
}
/**
* Returns true if the current running Hudson is upgraded from a version
* earlier than the specified version.
*
* <p>
* This method continues to return true until the system configuration is
* saved, at which point {@link #version} will be overwritten and Hudson
* forgets the upgrade history.
*
* <p>
* To handle SNAPSHOTS correctly, pass in "1.N.*" to test if it's upgrading
* from the version equal or younger than N. So say if you implement a
* feature in 1.301 and you want to check if the installation upgraded from
* pre-1.301, pass in "1.300.*"
*
* @since 1.301
*/
public boolean isUpgradedFromBefore(VersionNumber v) {
try {
return new VersionNumber(version).isOlderThan(v);
} catch (IllegalArgumentException e) {
// fail to parse this version number
return false;
}
}
/**
* Gets the read-only list of all {@link Computer}s.
*/
public Computer[] getComputers() {
Computer[] allComputers = getAllComputers();
List<Computer> copy = new ArrayList<Computer>();
if (this.isTeamManagementEnabled()) {
for (Computer computer : allComputers) {
if (computer.hasPermission(Computer.READ)) {
copy.add(computer);
}
}
return copy.toArray(new Computer[copy.size()]);
} else {
return allComputers;
}
}
public Computer[] getAllComputers() {
Computer[] r = computers.values().toArray(new Computer[computers.size()]);
Arrays.sort(r, new Comparator<Computer>() {
final Collator collator = Collator.getInstance();
public int compare(Computer lhs, Computer rhs) {
if (lhs.getNode() == Hudson.this) {
return -1;
}
if (rhs.getNode() == Hudson.this) {
return 1;
}
return collator.compare(lhs.getDisplayName(), rhs.getDisplayName());
}
});
return r;
}
/*package*/ Computer getComputer(Node n) {
return computers.get(n);
}
@CLIResolver
public Computer getComputer(@Argument(required = true, metaVar = "NAME", usage = "Node name") String name) {
return getComputer(name, false);
}
public Computer getComputer(String name, boolean system) {
if (name.equals("(master)")) {
name = "";
}
for (Computer c : computers.values()) {
if (c.getName().equals(name)) {
if (isTeamManagementEnabled() && !system) {
return c.hasPermission(Computer.READ) ? c : null;
} else {
return c;
}
}
}
return null;
}
/**
* @deprecated UI method. Not meant to be used programmatically.
*/
public ComputerSet getComputer() {
return new ComputerSet();
}
/**
* Gets the label that exists on this system by the name.
*
* @return null if name is null.
* @see Label#parseExpression(String) (String)
*/
public Label getLabel(String expr) {
if (expr == null) {
return null;
}
while (true) {
Label l = labels.get(expr);
if (l != null) {
return l;
}
// non-existent
try {
labels.putIfAbsent(expr, Label.parseExpression(expr));
} catch (RecognitionException e) {
// laxly accept it as a single label atom for backward compatibility
return getLabelAtom(expr);
}
}
}
/**
* Returns the label atom of the given name.
*/
public LabelAtom getLabelAtom(String name) {
if (name == null) {
return null;
}
while (true) {
Label l = labels.get(name);
if (l != null) {
return (LabelAtom) l;
}
// non-existent
LabelAtom la = new LabelAtom(name);
if (labels.putIfAbsent(name, la) == null) {
la.load();
}
}
}
/**
* Gets all the active labels in the current system.
*/
public Set<Label> getLabels() {
Set<Label> r = new TreeSet<Label>();
for (Label l : labels.values()) {
if (!l.isEmpty()) {
r.add(l);
}
}
return r;
}
public Set<Label> getBuildableLabels(AbstractProject job) {
if (isTeamManagementEnabled()) {
Set<Label> availableLabels = getLabels();
Set<Label> buildableLabels = new TreeSet<Label>();
for (Label availableLabel : availableLabels) {
boolean buildable = true;
for (Node node : availableLabel.getNodes()) {
String name = node.getNodeName();
if (node instanceof Hudson) {
name = "Master";
}
if (!getTeamManager().canNodeExecuteJob(name, job.getName())) {
buildable = false;
}
}
if (buildable) {
buildableLabels.add(availableLabel);
}
}
return buildableLabels;
} else {
return getLabels();
}
}
public Set<LabelAtom> getLabelAtoms() {
Set<LabelAtom> r = new TreeSet<LabelAtom>();
for (Label l : labels.values()) {
if (!l.isEmpty() && l instanceof LabelAtom) {
r.add((LabelAtom) l);
}
}
return r;
}
public Queue getQueue() {
return queue;
}
@Override
public String getDisplayName() {
return Messages.Hudson_DisplayName();
}
public List<JDK> getJDKs() {
if (jdks == null) {
jdks = new ArrayList<JDK>();
}
return jdks;
}
/**
* Gets the JDK installation of the given name, or returns null.
*/
public JDK getJDK(String name) {
if (name == null) {
// if only one JDK is configured, "default JDK" should mean that JDK.
List<JDK> jdks = getJDKs();
if (jdks.size() == 1) {
return jdks.get(0);
}
return null;
}
for (JDK j : getJDKs()) {
if (j.getName().equals(name)) {
return j;
}
}
return null;
}
/**
* Gets the slave node of the give name, hooked under this Hudson.
*
* @deprecated Use {@link #getNode(String)}. Since 1.252.
*/
public Slave getSlave(String name) {
Node n = getNode(name);
if (n instanceof Slave) {
return (Slave) n;
}
return null;
}
/**
* Gets the slave node of the give name, hooked under this Hudson.
*/
public Node getNode(String name) {
for (Node s : getNodes()) {
if (s.getNodeName().equals(name)) {
return s;
}
}
return null;
}
/**
* Gets a {@link Cloud} by {@link Cloud#name its name}, or null.
*/
public Cloud getCloud(String name) {
return clouds.getByName(name);
}
/**
* @deprecated Use {@link #getNodes()}. Since 1.252.
*/
public List<Slave> getSlaves() {
return (List) Collections.unmodifiableList(slaves);
}
/**
* Returns all {@link Node}s in the system, excluding {@link Hudson}
* instance itself which represents the master.
*/
public List<Node> getNodes() {
return Collections.unmodifiableList(slaves);
}
/**
* Updates the slave list.
*
* @deprecated Use {@link #setNodes(List)}. Since 1.252.
*/
public void setSlaves(List<Slave> slaves) throws IOException {
setNodes(slaves);
}
/**
* Adds one more {@link Node} to Hudson.
*/
public synchronized void addNode(Node n) throws IOException {
if (n == null) {
throw new IllegalArgumentException();
}
ArrayList<Node> nl = new ArrayList<Node>(this.slaves);
// defensive check
if (!nl.contains(n)) {
nl.add(n);
}
setNodes(nl);
}
/**
* Removes a {@link Node} from Hudson.
*/
public synchronized void removeNode(Node n) throws IOException {
Computer c = n.toComputer();
if (c != null) {
c.disconnect(OfflineCause.create(Messages._Hudson_NodeBeingRemoved()));
}
ArrayList<Node> nl = new ArrayList<Node>(this.slaves);
nl.remove(n);
setNodes(nl);
}
public void setNodes(List<? extends Node> nodes) throws IOException {
// make sure that all names are unique
Set<String> names = new HashSet<String>();
for (Node n : nodes) {
if (!names.add(n.getNodeName())) {
throw new IllegalArgumentException(n.getNodeName() + " is defined more than once");
}
}
this.slaves = new NodeList(nodes);
updateComputerList();
trimLabels();
save();
}
public DescribableList<NodeProperty<?>, NodePropertyDescriptor> getNodeProperties() {
return nodeProperties;
}
public DescribableList<NodeProperty<?>, NodePropertyDescriptor> getGlobalNodeProperties() {
return globalNodeProperties;
}
/**
* Resets all labels and remove invalid ones.
*/
private void trimLabels() {
for (Iterator<Label> itr = labels.values().iterator(); itr.hasNext();) {
Label l = itr.next();
l.reset();
if (l.isEmpty()) {
itr.remove();
}
}
}
/**
* Binds {@link AdministrativeMonitor}s to URL.
*/
public AdministrativeMonitor getAdministrativeMonitor(String id) {
for (AdministrativeMonitor m : administrativeMonitors) {
if (m.id.equals(id)) {
return m;
}
}
return null;
}
public NodeDescriptor getDescriptor() {
return DescriptorImpl.INSTANCE;
}
public static final class DescriptorImpl extends NodeDescriptor {
@Extension
public static final DescriptorImpl INSTANCE = new DescriptorImpl();
public String getDisplayName() {
throw new UnsupportedOperationException();
}
@Override
public boolean isInstantiable() {
return false;
}
public FormValidation doCheckNumExecutors(@QueryParameter String value) {
return FormValidation.validateNonNegativeInteger(value);
}
// to route /descriptor/FQCN/xxx to getDescriptor(FQCN).xxx
public Object getDynamic(String token) {
return Hudson.getInstance().getDescriptor(token);
}
}
/**
* Gets the system default quiet period.
*/
public int getQuietPeriod() {
return quietPeriod != null ? quietPeriod : 5;
}
/**
* Gets the global SCM check out retry count.
*/
public int getScmCheckoutRetryCount() {
return scmCheckoutRetryCount;
}
/**
* @deprecated Why are you calling a method that always returns ""? Perhaps
* you meant {@link #getRootUrl()}.
*/
public String getUrl() {
return "";
}
@Override
public String getSearchUrl() {
return "";
}
public void onViewRenamed(View view, String oldName, String newName) {
TeamManager teamManager = Hudson.getInstance().getTeamManager();
if (teamManager.isTeamManagementEnabled()) {
try {
Team team = teamManager.findViewOwnerTeam(oldName);
teamManager.renameView(team, oldName, newName);
} catch (IOException ex) {
logger.warn("Failed to rename view in team.", ex);
}
}
}
@Override
public SearchIndexBuilder makeSearchIndex() {
return super.makeSearchIndex().add("configure", "config", "configure").add("manage").add("log").add(getPrimaryView().makeSearchIndex()).add(new CollectionSearchIndex() {
// for computers
protected Computer get(String key) {
return getComputer(key);
}
protected Collection<Computer> all() {
return computers.values();
}
}).add(new CollectionSearchIndex() {
// for users
protected User get(String key) {
return User.get(key, false);
}
protected Collection<User> all() {
return User.getAll();
}
}).add(new CollectionSearchIndex() {
// for views
protected View get(String key) {
return getView(key);
}
protected Collection<View> all() {
return views;
}
});
}
/**
* Returns the primary {@link View} that renders the top-page of Hudson.
*/
@Exported
public View getPrimaryView() {
View v = getView(primaryView);
if (getTeamManager().isTeamManagementEnabled()) {
String teamPrimaryView = getTeamManager().getCurrentUserPrimaryView();
if (teamPrimaryView != null){
v = getView(teamPrimaryView);
}
}
// fallback
if (v == null) {
v = views.get(0);
}
return v;
}
public String getUrlChildPrefix() {
return "job";
}
/**
* Gets the absolute URL of Hudson, such as "http://localhost/hudson/".
*
* <p>
* This method first tries to use the manually configured value, then fall
* back to {@link StaplerRequest#getRootPath()}. It is done in this order so
* that it can work correctly even in the face of a reverse proxy.
*
* @return This method returns null if this parameter is not configured by
* the user. The caller must gracefully deal with this situation. The
* returned URL will always have the trailing '/'.
* @since 1.66
* @see Descriptor#getCheckUrl(String)
* @see #getRootUrlFromRequest()
*/
public String getRootUrl() {
// for compatibility. the actual data is stored in Mailer
String url = Mailer.descriptor().getUrl();
if (url != null) {
url = url.trim();
if (!url.endsWith("/")){
url = url + "/";
}
return url;
}
StaplerRequest req = Stapler.getCurrentRequest();
if (req != null) {
return getRootUrlFromRequest();
}
return null;
}
/**
* Gets the absolute URL of Hudson top page, such as
* "http://localhost/hudson/".
*
* <p>
* Unlike {@link #getRootUrl()}, which uses the manually configured value,
* this one uses the current request to reconstruct the URL. The benefit is
* that this is immune to the configuration mistake (users often fail to set
* the root URL correctly, especially when a migration is involved), but the
* downside is that unless you are processing a request, this method doesn't
* work.
*
* @since 1.263
*/
public String getRootUrlFromRequest() {
StaplerRequest req = Stapler.getCurrentRequest();
StringBuilder buf = new StringBuilder();
buf.append(req.getScheme() + "://");
buf.append(req.getServerName());
if (req.getServerPort() != 80) {
buf.append(':').append(req.getServerPort());
}
buf.append(req.getContextPath()).append('/');
return buf.toString();
}
public File getRootDir() {
return root;
}
/**
* {@inheritDoc}
* <p/>
* If {@link Hudson#HUDSON_WORKSPACES_PROPERTY_KEY} is set through JNDI or
* system properties or environment variables. workspaceRoot will be set
* based on property value.
*/
public FilePath getWorkspaceFor(TopLevelItem item) {
String workspaceRoot = getConfiguredWorkspaceRoot();
if (StringUtils.isNotBlank(workspaceRoot)) {
return new FilePath(new File(workspaceRoot + "/" + item.getName(), WORKSPACE_DIRNAME));
} else {
return new FilePath(new File(item.getRootDir(), WORKSPACE_DIRNAME));
}
}
/**
* Checks jndi,environment properties and system properties for
* {@link Hudson#HUDSON_WORKSPACES_PROPERTY_KEY} value
*
* @return configured workspace root or "" if it is not set
*/
private String getConfiguredWorkspaceRoot() {
if (null == configuredWorkspaceRoot) {
String resultValue = getConfiguredHudsonProperty(HUDSON_WORKSPACES_PROPERTY_KEY);
configuredWorkspaceRoot = (null != resultValue) ? resultValue : StringUtils.EMPTY;
}
return configuredWorkspaceRoot;
}
public FilePath getRootPath() {
return new FilePath(getRootDir());
}
@Override
public FilePath createPath(String absolutePath) {
return new FilePath((VirtualChannel) null, absolutePath);
}
public ClockDifference getClockDifference() {
return ClockDifference.ZERO;
}
/**
* For binding {@link LogRecorderManager} to "/log". Everything below here
* is admin-only, so do the check here.
*/
public LogRecorderManager getLog() {
checkPermission(ADMINISTER);
return log;
}
/**
* A convenience method to check if there's some security restrictions in
* place.
*/
@Exported
public boolean isUseSecurity() {
return getSecurityManager().isUseSecurity();
}
/**
* If true, all the POST requests to Hudson would have to have crumb in it
* to protect Hudson from CSRF vulnerabilities.
*/
@Exported
public boolean isUseCrumbs() {
return crumbIssuer != null;
}
/**
* Returns the constant that captures the three basic security modes in
* Hudson.
*/
public SecurityMode getSecurity() {
return getSecurityManager().getSecurity();
}
/**
* @return never null.
*/
public SecurityRealm getSecurityRealm() {
return getSecurityManager().getSecurityRealm();
}
public void setSecurityRealm(SecurityRealm securityRealm) {
getSecurityManager().setSecurityRealm(securityRealm);
}
public Lifecycle getLifecycle() {
return Lifecycle.get();
}
/**
* Returns {@link ExtensionList} that retains the discovered instances for
* the given extension type.
*
* @param extensionType The base type that represents the extension point.
* Normally {@link ExtensionPoint} subtype but that's not a hard
* requirement.
* @return Can be an empty list but never null.
*/
@SuppressWarnings({"unchecked"})
public <T> ExtensionList<T> getExtensionList(Class<T> extensionType) {
return extensionLists.get(extensionType);
}
/**
* Used to bind {@link ExtensionList}s to URLs.
*
* @since 1.349
*/
public ExtensionList getExtensionList(String extensionType) throws ClassNotFoundException {
return getExtensionList(pluginManager.uberClassLoader.loadClass(extensionType));
}
/**
* Returns {@link ExtensionList} that retains the discovered
* {@link Descriptor} instances for the given kind of {@link Describable}.
*
* @return Can be an empty list but never null.
*/
@SuppressWarnings({"unchecked"})
public <T extends Describable<T>, D extends Descriptor<T>> DescriptorExtensionList<T, D> getDescriptorList(Class<T> type) {
return descriptorLists.get(type);
}
/**
* Returns the root {@link ACL}.
*
* @see AuthorizationStrategy#getRootACL()
*/
@Override
public ACL getACL() {
return getSecurityManager().getACL();
}
/**
* @return never null.
*/
public AuthorizationStrategy getAuthorizationStrategy() {
return getSecurityManager().getAuthorizationStrategy();
}
public void setAuthorizationStrategy(AuthorizationStrategy authorizationStrategy) {
getSecurityManager().setAuthorizationStrategy(authorizationStrategy);
}
/**
* Returns true if Hudson is quieting down.
* <p>
* No further jobs will be executed unless it can be finished while other
* current pending builds are still in progress.
*/
public boolean isQuietingDown() {
return isQuietingDown;
}
/**
* Returns true if the container initiated the termination of the web
* application.
*/
public boolean isTerminating() {
return terminating;
}
/**
* Gets the initialization milestone that we've already reached.
*
* @return {@link InitMilestone#STARTED} even if the initialization hasn't
* been started, so that this method never returns null.
*/
public InitMilestone getInitLevel() {
return initLevel;
}
public void setNumExecutors(int n) throws IOException {
this.numExecutors = n;
save();
}
/**
* @deprecated Left only for the compatibility of URLs. Should not be
* invoked for any other purpose.
*/
public TopLevelItem getJob(String name) {
return getItem(name);
}
/**
* @deprecated Used only for mapping jobs to URL in a case-insensitive
* fashion.
*/
public TopLevelItem getJobCaseInsensitive(String name) {
String match = Functions.toEmailSafeString(name);
for (Entry<String, TopLevelItem> e : items.entrySet()) {
if (Functions.toEmailSafeString(e.getKey()).equalsIgnoreCase(match)) {
TopLevelItem item = e.getValue();
return item.hasPermission(Item.READ)
? LazyTopLevelItem.getIfInstanceOf(item, TopLevelItem.class)
: null;
}
}
return null;
}
/**
* {@inheritDoc}.
*
* Note that the look up is case-insensitive.
*/
@Override
public TopLevelItem getItem(String name) {
TopLevelItem item = items.get(name);
if (item == null || !item.hasPermission(Item.READ)) {
return null;
}
return LazyTopLevelItem.getIfInstanceOf(item, TopLevelItem.class);
}
public Item getItem(String relativeName, ItemGroup context) {
if (context == null) {
context = this;
}
if (relativeName == null) {
return null;
}
// absolute
if (relativeName.startsWith("/")) {
return getItemByFullName(relativeName);
}
Object/*Item|ItemGroup*/ ctx = context;
StringTokenizer tokens = new StringTokenizer(relativeName, "/");
while (tokens.hasMoreTokens()) {
String s = tokens.nextToken();
if (s.equals("..")) {
if (ctx instanceof Item) {
ctx = ((Item) ctx).getParent();
continue;
}
ctx = null; // can't go up further
break;
}
if (s.equals(".")) {
continue;
}
if (ctx instanceof ItemGroup) {
ItemGroup g = (ItemGroup) ctx;
Item i = g.getItem(s);
if (i == null || !i.hasPermission(Item.READ)) {
ctx = null; // can't go up further
break;
}
ctx = i;
}
}
if (ctx instanceof Item) {
return (Item) ctx;
}
// fall back to the classic interpretation
return getItemByFullName(relativeName);
}
public final Item getItem(String relativeName, Item context) {
return getItem(relativeName, context != null ? context.getParent() : null);
}
public final <T extends Item> T getItem(String relativeName, ItemGroup context, Class<T> type) {
Item r = getItem(relativeName, context);
if (type.isInstance(r)) {
return type.cast(r);
}
return null;
}
public final <T extends Item> T getItem(String relativeName, Item context, Class<T> type) {
return getItem(relativeName, context != null ? context.getParent() : null, type);
}
@Override
public File getRootDirFor(TopLevelItem child) {
return getRootDirFor(child.getName());
}
private File getRootDirFor(String jobName) {
return getTeamManager().getRootFolderForJob(jobName);
}
/**
* Gets the {@link Item} object by its full name. Full names are like path
* names, where each name of {@link Item} is combined by '/'.
*
* @return null if either such {@link Item} doesn't exist under the given
* full name, or it exists but it's no an instance of the given type.
*/
public <T extends Item> T getItemByFullName(String fullName, Class<T> type) {
StringTokenizer tokens = new StringTokenizer(fullName, "/");
ItemGroup parent = this;
if (!tokens.hasMoreTokens()) {
return null; // for example, empty full name.
}
while (true) {
Item item = parent.getItem(tokens.nextToken());
if (!tokens.hasMoreTokens()) {
if (type.isInstance(item)) {
return type.cast(item);
} else {
return null;
}
}
if (!(item instanceof ItemGroup)) {
return null; // this item can't have any children
}
parent = (ItemGroup) item;
}
}
public Item getItemByFullName(String fullName) {
return getItemByFullName(fullName, Item.class);
}
/**
* Gets the user of the given name.
*
* @return This method returns a non-null object for any user name, without
* validation.
*/
public User getUser(String name) {
return User.get(name, false);
}
/**
* Creates a new job.
*
* @throws IllegalArgumentException if the project of the given name already
* exists.
*/
public synchronized TopLevelItem createProject(TopLevelItemDescriptor type, String name) throws IOException {
return createProject(type, name, true);
}
/**
* Creates a new job.
*
* @param type Descriptor for job type
* @param name Name for job
* @param notify Whether to fire onCreated method for all ItemListeners
* @throws IllegalArgumentException if a project of the give name already
* exists.
*/
public synchronized TopLevelItem createProject(TopLevelItemDescriptor type, String name, boolean notify) throws IOException {
return itemGroupMixIn.createProject(type, name, notify);
}
/**
* Overwrites the existing item by new one.
*
* <p>
* This is a short cut for deleting an existing job and adding a new one.
*/
public synchronized void putItem(TopLevelItem item) throws IOException, InterruptedException {
TopLevelItem old = items.get(item.getName());
if (old == item) {
return; // noop
}
checkPermission(Item.CREATE);
if (old != null) {
old.delete();
}
items.put(item.getName(), item);
ItemListener.fireOnCreated(item);
}
/**
* Creates a new job.
*
* <p>
* This version infers the descriptor from the type of the top-level item.
*
* @throws IllegalArgumentException if the project of the given name already
* exists.
*/
public synchronized <T extends TopLevelItem> T createProject(Class<T> type, String name) throws IOException {
return type.cast(createProject((TopLevelItemDescriptor) getDescriptor(type), name));
}
/**
* Called by {@link Job#renameTo(String)} to update relevant data structure.
* assumed to be synchronized on Hudson by the caller.
*/
public void onRenamed(TopLevelItem job, String oldName, String newName) throws IOException {
items.remove(oldName);
items.put(newName, job);
for (View v : views) {
try {
v.onJobRenamed(job, oldName, newName);
} catch (Exception e) {
logger.warn("Exception from View", e);
}
}
save();
}
/**
* Called in response to
* {@link Job#doDoDelete(StaplerRequest, StaplerResponse)}
*/
public void onDeleted(TopLevelItem item) throws IOException {
for (ItemListener l : ItemListener.all()) {
try {
l.onDeleted(item);
} catch (Exception e) {
// Bug 452400 - Flaky ItemListener can leave job in half-deleted state
logger.warn("Exception from ItemListener", e);
}
}
items.remove(item.getName());
for (View v : views) {
try {
v.onJobRenamed(item, item.getName(), null);
} catch (Exception e) {
logger.warn("Exception from View", e);
}
}
save();
}
public FingerprintMap getFingerprintMap() {
return fingerprintMap;
}
// if no finger print matches, display "not found page".
public Object getFingerprint(String md5sum) throws IOException {
Fingerprint r = fingerprintMap.get(md5sum);
if (r == null) {
return new NoFingerprintMatch(md5sum);
} else {
return r;
}
}
/**
* Gets a {@link Fingerprint} object if it exists. Otherwise null.
*/
public Fingerprint _getFingerprint(String md5sum) throws IOException {
return fingerprintMap.get(md5sum);
}
/**
* The file we save our configuration.
*/
private XmlFile getConfigFile() {
return new XmlFile(XSTREAM, new File(root, "config.xml"));
}
public int getNumExecutors() {
return numExecutors;
}
public Mode getMode() {
return mode;
}
public String getLabelString() {
return fixNull(label).trim();
}
@Override
public LabelAtom getSelfLabel() {
return getLabelAtom("master");
}
public Computer createComputer() {
return new MasterComputer();
}
private synchronized TaskBuilder loadTasks() throws IOException {
File projectsDir = new File(root, "jobs");
if (!projectsDir.isDirectory() && !projectsDir.mkdirs()) {
if (projectsDir.exists()) {
throw new IOException(projectsDir + " is not a directory");
}
throw new IOException("Unable to create " + projectsDir + "\nPermission issue? Please create this directory manually.");
}
File[] jobsRootDirs = getTeamManager().getJobsRootFolders();
TaskGraphBuilder g = new TaskGraphBuilder();
Handle loadHudson = g.requires(EXTENSIONS_AUGMENTED).attains(JOB_LOADED).add("Loading global config", new Executable() {
public void run(Reactor session) throws Exception {
XmlFile cfg = getConfigFile();
if (cfg.exists()) {
// reset some data that may not exist in the disk file
// so that we can take a proper compensation action later.
primaryView = null;
views.clear();
// load from disk
cfg.unmarshal(Hudson.this);
}
// if we are loading old data that doesn't have this field
if (slaves == null) {
slaves = new NodeList();
}
clouds.setOwner(Hudson.this);
items.clear();
}
});
for (final File jobRootDir : jobsRootDirs) {
g.requires(loadHudson).attains(JOB_LOADED).notFatal().add("Loading job " + jobRootDir.getName(), new Executable() {
public void run(Reactor session) throws Exception {
TopLevelItem item = (TopLevelItem) Items.load(Hudson.this, jobRootDir);
items.put(item.getName(), item);
}
});
}
g.requires(JOB_LOADED).add("Finalizing set up", new Executable() {
public void run(Reactor session) throws Exception {
for (AbstractProject job : Hudson.getInstance().getAllItems(AbstractProject.class)) {
job.cleanCascading();
}
rebuildDependencyGraph();
// recompute label objects - populates the labels mapping.
{
for (Node slave : slaves) {
// Note that not all labels are visible until the slaves have connected.
slave.getAssignedLabels();
}
getAssignedLabels();
}
// initialize views by inserting the default view if necessary
// this is both for clean Hudson and for backward compatibility.
if (views.size() == 0 || primaryView == null) {
View v = new AllView(Messages.Hudson_ViewName());
v.owner = Hudson.this;
views.add(0, v);
primaryView = v.getViewName();
}
// Initialize the filter with the crumb issuer
setCrumbIssuer(crumbIssuer);
// auto register root actions
for (Action a : getExtensionList(RootAction.class)) {
if (!actions.contains(a)) {
actions.add(a);
}
}
// auto register unsecured root actions
for (Action a : getExtensionList(UnsecuredRootAction.class)) {
if (!actions.contains(a)) {
actions.add(a);
}
}
}
});
return g;
}
/**
* Save the settings to a file.
*/
public synchronized void save() throws IOException {
if (BulkChange.contains(this)) {
return;
}
getConfigFile().write(this);
SaveableListener.fireOnChange(this, getConfigFile());
}
/**
* Called to shut down the system.
*/
public void cleanUp() {
Set<Future<?>> pending = new HashSet<Future<?>>();
terminating = true;
for (Computer c : computers.values()) {
c.interrupt();
c.kill();
pending.add(c.disconnect(null));
}
if (udpBroadcastThread != null) {
udpBroadcastThread.shutdown();
}
if (dnsMultiCast != null) {
dnsMultiCast.close();
}
ExternalJob.reloadThread.interrupt();
Trigger.timer.cancel();
// TODO: how to wait for the completion of the last job?
Trigger.timer = null;
if (tcpSlaveAgentListener != null) {
tcpSlaveAgentListener.shutdown();
}
if (pluginManager != null) // be defensive. there could be some ugly timing related issues
{
pluginManager.stop();
}
// if we are aborting because we failed to create HUDSON_HOME,
// don't try to save. Issue #536
if (getRootDir().exists()) {
getQueue().save();
}
threadPoolForLoad.shutdown();
for (Future<?> f : pending) {
try {
f.get(10, TimeUnit.SECONDS); // if clean up operation didn't complete in time, we fail the test
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break; // someone wants us to die now. quick!
} catch (ExecutionException e) {
logger.warn("Failed to shut down properly", e);
} catch (TimeoutException e) {
logger.warn("Failed to shut down properly", e);
}
}
updateCenter.shutdown();
pluginCenter.shutdown();
LogFactory.releaseAll();
theInstance = null;
}
public Object getDynamic(String token) {
for (Action a : getActions()) {
if (a.getUrlName().equals(token) || a.getUrlName().equals('/' + token)) {
return a;
}
}
for (Action a : getManagementLinks()) {
if (a.getUrlName().equals(token)) {
return a;
}
}
return null;
}
/**
* Should Hudson use blue ball instead of default green ball for success
*
* @return boolean
*/
public boolean useBlueBall() {
return useBlueBall.booleanValue();
}
/**
* Should Unsecured action be allowed by Hudson
*
* @return boolean
*/
public boolean allowUnsecuredAction() {
return allowUnsecuredAction;
}
/**
* Should Hudson allow Command line interface
*
* @return boolean
*/
public boolean allowCli() {
return allowCli;
}
public void setSlaveAgentPort(int slaveAgentPort) throws IOException {
checkPermission(ADMINISTER);
// relaunch the agent
if (tcpSlaveAgentListener == null) {
if (slaveAgentPort != -1) {
tcpSlaveAgentListener = new TcpSlaveAgentListener(slaveAgentPort);
}
} else {
if (tcpSlaveAgentListener.configuredPort != slaveAgentPort) {
tcpSlaveAgentListener.shutdown();
tcpSlaveAgentListener = null;
if (slaveAgentPort != -1) {
tcpSlaveAgentListener = new TcpSlaveAgentListener(slaveAgentPort);
}
}
}
}
//
//
// actions
//
//
/**
* Accepts submission from the configuration page.
*/
public synchronized void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
BulkChange bc = new BulkChange(this);
try {
checkPermission(ADMINISTER);
JSONObject json = req.getSubmittedForm();
if (json.has("csrf")) {
JSONObject csrf = json.getJSONObject("csrf");
setCrumbIssuer(CrumbIssuer.all().newInstanceFromRadioList(csrf, "issuer"));
} else {
setCrumbIssuer(null);
}
if (json.has("viewsTabBar")) {
viewsTabBar = req.bindJSON(ViewsTabBar.class, json.getJSONObject("viewsTabBar"));
} else {
viewsTabBar = new DefaultViewsTabBar();
}
if (json.has("myViewsTabBar")) {
myViewsTabBar = req.bindJSON(MyViewsTabBar.class, json.getJSONObject("myViewsTabBar"));
} else {
myViewsTabBar = new DefaultMyViewsTabBar();
}
primaryView = json.has("primaryView") ? json.getString("primaryView") : getViews().iterator().next().getViewName();
useBlueBall = json.has("useBlueBall");
allowUnsecuredAction = json.has("allowUnsecuredAction");
allowCli = json.has("allowCli");
noUsageStatistics = json.has("usageStatisticsCollected") ? null : true;
numExecutors = json.getInt("numExecutors");
if (req.hasParameter("master.mode")) {
mode = Mode.valueOf(req.getParameter("master.mode"));
} else {
mode = Mode.NORMAL;
}
label = json.optString("labelString", "");
quietPeriod = json.getInt("quiet_period");
scmCheckoutRetryCount = json.getInt("retry_count");
instanceTag = json.optString("instance_tag", "");
systemMessage = Util.nullify(req.getParameter("system_message"));
privacyMessage = json.optString("privacy_message", "");
jdks.clear();
jdks.addAll(req.bindJSONToList(JDK.class, json.get("jdks")));
boolean result = true;
for (Descriptor<?> d : Functions.getSortedDescriptorsForGlobalConfig()) {
result &= configureDescriptor(req, json, d);
}
for (JSONObject o : StructuredForm.toList(json, "plugin")) {
pluginManager.getPlugin(o.getString("name")).getPlugin().configure(req, o);
}
clouds.rebuildHetero(req, json, Cloud.all(), "cloud");
JSONObject np = json.getJSONObject("globalNodeProperties");
if (np != null) {
globalNodeProperties.rebuild(req, np, NodeProperty.for_(this));
}
version = VERSION;
save();
updateComputerList();
if (result) {
rsp.sendRedirect(req.getContextPath() + '/'); // go to the top page
} else {
rsp.sendRedirect("configure"); // back to config
}
} finally {
bc.commit();
}
}
public CrumbIssuer getCrumbIssuer() {
return crumbIssuer;
}
public void setCrumbIssuer(CrumbIssuer issuer) {
crumbIssuer = issuer;
}
public synchronized void doTestPost(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
rsp.sendRedirect("foo");
}
private boolean configureDescriptor(StaplerRequest req, JSONObject json, Descriptor<?> d) throws FormException {
// collapse the structure to remain backward compatible with the JSON structure before 1.
String name = d.getJsonSafeClassName();
JSONObject js = json.has(name) ? json.getJSONObject(name) : new JSONObject(); // if it doesn't have the property, the method returns invalid null object.
json.putAll(js);
return d.configure(req, js);
}
/**
* Accepts submission from the configuration page.
*/
public synchronized void doConfigExecutorsSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
checkPermission(ADMINISTER);
BulkChange bc = new BulkChange(this);
try {
JSONObject json = req.getSubmittedForm();
setNumExecutors(Integer.parseInt(req.getParameter("numExecutors")));
if (req.hasParameter("master.mode")) {
mode = Mode.valueOf(req.getParameter("master.mode"));
} else {
mode = Mode.NORMAL;
}
setNodes(req.bindJSONToList(Slave.class, json.get("slaves")));
} finally {
bc.commit();
}
rsp.sendRedirect(req.getContextPath() + '/'); // go to the top page
}
/**
* Accepts the new description.
*/
public synchronized void doSubmitDescription(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
getPrimaryView().doSubmitDescription(req, rsp);
}
/**
* @deprecated as of 1.317 Use {@link #doQuietDown()} instead.
*/
public synchronized void doQuietDown(StaplerResponse rsp) throws IOException, ServletException {
doQuietDown().generateResponse(null, rsp, this);
}
public synchronized HttpRedirect doQuietDown() throws IOException {
try {
return doQuietDown(false, 0);
} catch (InterruptedException e) {
throw new AssertionError(); // impossible
}
}
public void doSlaveJnlp(StaplerRequest req, StaplerResponse rsp, @QueryParameter String name) throws ServletException, IOException {
Computer slave = null;
for (Computer c : computers.values()) {
if (c.getName().equals(name)) {
slave = c;
}
}
if (slave != null) {
req.getView(slave, "slave-agent.jnlp.jelly").forward(req, rsp);
}else{
req.getView(this, "jnlp-error.jelly").forward(req, rsp);
}
}
@CLIMethod(name = "quiet-down")
public HttpRedirect doQuietDown(
@Option(name = "-block", usage = "Block until the system really quiets down and no builds are running") @QueryParameter boolean block,
@Option(name = "-timeout", usage = "If non-zero, only block up to the specified number of milliseconds") @QueryParameter int timeout) throws InterruptedException, IOException {
synchronized (this) {
checkPermission(ADMINISTER);
isQuietingDown = true;
}
if (block) {
if (timeout > 0) {
timeout += System.currentTimeMillis();
}
while (isQuietingDown
&& (timeout <= 0 || System.currentTimeMillis() < timeout)
&& !RestartListener.isAllReady()) {
Thread.sleep(1000);
}
}
return new HttpRedirect(".");
}
@CLIMethod(name = "cancel-quiet-down")
public synchronized HttpRedirect doCancelQuietDown() {
checkPermission(ADMINISTER);
isQuietingDown = false;
getQueue().scheduleMaintenance();
return new HttpRedirect(".");
}
/**
* Backward compatibility. Redirect to the thread dump.
*/
public void doClassicThreadDump(StaplerResponse rsp) throws IOException, ServletException {
rsp.sendRedirect2("threadDump");
}
public synchronized Item doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
return itemGroupMixIn.createTopLevelItem(req, rsp);
}
/**
* Creates a new job from its configuration XML. The type of the job created
* will be determined by what's in this XML.
*
* @since 1.319
*/
public TopLevelItem createProjectFromXML(String name, InputStream xml) throws IOException {
return itemGroupMixIn.createProjectFromXML(name, xml);
}
/**
* Creates a new job from its configuration XML in a specific team. The type
* of the job created will be determined by what's in this XML.
*
* @since 3.1.0
*/
public TopLevelItem createProjectFromXML(String name, String teamName, InputStream xml) throws IOException {
return itemGroupMixIn.createProjectFromXML(name, teamName, xml);
}
/**
* Reload a project to update its definition.
*
* @since 2.x.x
*/
public TopLevelItem reloadProjectFromDisk(File jobDir) throws IOException {
TopLevelItem item = (TopLevelItem) Items.load(this, jobDir, false);
items.put(item.getName(), item);
rebuildDependencyGraph();
return item;
}
/**
* Copys a job.
*
* @param src A {@link TopLevelItem} to be copied.
* @param name Name of the newly created project.
* @return Newly created {@link TopLevelItem}.
*/
@SuppressWarnings({"unchecked"})
public <T extends TopLevelItem> T copy(T src, String name) throws IOException {
return itemGroupMixIn.copy(src, name);
}
/**
* Copys a job to a specific team.
*
* @param src A {@link TopLevelItem} to be copied.
* @param name Name of the newly created project.
* @return Newly created {@link TopLevelItem}.
*
* @since 3.1.0
*/
@SuppressWarnings({"unchecked"})
public <T extends TopLevelItem> T copy(T src, String name, String teamName) throws IOException {
return itemGroupMixIn.copy(src, name, teamName);
}
// a little more convenient overloading that assumes the caller gives us the right type
// (or else it will fail with ClassCastException)
public <T extends AbstractProject<?, ?>> T copy(T src, String name) throws IOException {
return (T) copy((TopLevelItem) src, name);
}
public synchronized void doCreateView(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
checkPermission(View.CREATE);
addView(View.create(req, rsp, this));
}
/**
* Check if the given name is suitable as a name for job, view, etc.
*
* @throws ParseException if the given name is not good
*/
public static void checkGoodName(String name) throws Failure {
if (name == null || name.length() == 0) {
throw new Failure(Messages.Hudson_NoName());
}
for (int i = 0; i < name.length(); i++) {
char ch = name.charAt(i);
if (Character.isISOControl(ch)) {
throw new Failure(Messages.Hudson_ControlCodeNotAllowed(toPrintableName(name)));
}
if ("?*/\\%!@#$^&|<>[]:;".indexOf(ch) != -1) {
throw new Failure(Messages.Hudson_UnsafeChar(ch));
}
}
// looks good
}
private static final Pattern TEAM_NAME_PATTERN = Pattern.compile("^[-_a-zA-Z0-9]+$");
public static String checkGoodTeamName(String name) {
name = name.trim();
if (!TEAM_NAME_PATTERN.matcher(name).find()) {
throw new Failure("Only alphanumeric characters, - or _ allowed in team name.");
}
return name;
}
/**
* Makes sure the given name is good as a job name.
*
* @return trimmed name if good
* @throws Failure
*/
public static String checkGoodJobName(String name) {
Hudson.checkGoodName(name);
name = name.trim();
if (Hudson.getInstance().isTeamManagementEnabled()) {
if (name.indexOf(TeamManager.TEAM_SEPARATOR) != -1) {
throw new Failure("The job name cannot contain " + TeamManager.TEAM_SEPARATOR + "when team management is enabled.");
}
if (name.length() > Hudson.JOB_NAME_LIMIT_TEAM) {
throw new Failure("Job name cannot exceed " + Hudson.JOB_NAME_LIMIT_TEAM + " characters when team management is enabled. ");
}
} else {
if (name.length() > Hudson.JOB_NAME_LIMIT_NO_TEAM) {
throw new Failure("Job name cannot exceed " + Hudson.JOB_NAME_LIMIT_NO_TEAM + " characters. ");
}
}
return name;
}
private static String toPrintableName(String name) {
StringBuilder printableName = new StringBuilder();
for (int i = 0; i < name.length(); i++) {
char ch = name.charAt(i);
if (Character.isISOControl(ch)) {
printableName.append("\\u").append((int) ch).append(';');
} else {
printableName.append(ch);
}
}
return printableName.toString();
}
/**
* Checks if the user was successfully authenticated.
*
* @see BasicAuthenticationFilter
*/
public void doSecured(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
if (req.getUserPrincipal() == null) {
// authentication must have failed
rsp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
// the user is now authenticated, so send him back to the target
String path = req.getContextPath() + req.getOriginalRestOfPath();
String q = req.getQueryString();
if (q != null) {
path += '?' + q;
}
rsp.sendRedirect2(path);
}
/**
* Called once the user logs in. Just forward to the top page.
*/
public void doLoginEntry(StaplerRequest req, StaplerResponse rsp) throws IOException {
if (req.getUserPrincipal() == null) {
rsp.sendRedirect2("noPrincipal");
return;
}
String from = req.getParameter("from");
if (from != null && from.startsWith("/") && !from.equals("/loginError")) {
rsp.sendRedirect2(from); // I'm bit uncomfortable letting users redircted to other sites, make sure the URL falls into this domain
return;
}
// Spring security 3.x will handle the URL redirect
// String url = AbstractAuthenticationProcessingFilter.obtainFullSavedRequestUrl(req);
// if (url != null) {
// // if the login redirect is initiated by Spring Security
// // this should send the user back to where s/he was from.
// rsp.sendRedirect2(url);
// return;
// }
rsp.sendRedirect2(".");
}
/**
* Logs out the user.
*/
public void doLogout(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
getSecurityManager().doLogout(req, rsp);
}
/**
* Serves jar files for JNLP slave agents.
*/
public Slave.JnlpJar getJnlpJars(String fileName) {
return new Slave.JnlpJar(fileName);
}
public Slave.JnlpJar doJnlpJars(StaplerRequest req) {
return new Slave.JnlpJar(req.getRestOfPath());
}
/**
* RSS feed for log entries.
*
* @deprecated As on 1.267, moved to "/log/rss..."
*/
public void doLogRss(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
String qs = req.getQueryString();
rsp.sendRedirect2("./log/rss" + (qs == null ? "" : '?' + qs));
}
/**
* Reloads the configuration.
*/
@CLIMethod(name = "reload-configuration")
public synchronized HttpResponse doReload() throws IOException {
checkPermission(ADMINISTER);
// engage "loading ..." UI and then run the actual task in a separate thread
WebAppController.get().install(new HudsonIsLoading());
new Thread("Hudson config reload thread") {
@Override
public void run() {
try {
HudsonSecurityManager.grantFullControl();
reload();
} catch (IOException e) {
logger.error("Failed to reload Hudson config", e);
} catch (ReactorException e) {
logger.error("Failed to reload Hudson config", e);
} catch (InterruptedException e) {
logger.error("Failed to reload Hudson config", e);
}
}
}.start();
return HttpResponses.redirectViaContextPath("/");
}
/**
* Reloads the configuration synchronously.
*/
public void reload() throws IOException, InterruptedException, ReactorException {
executeReactor(null, loadTasks());
User.reload();
initLevel = InitMilestone.COMPLETED;
WebAppController.get().install(this);
}
/**
* Do a finger-print check.
*/
public void doDoFingerprintCheck(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
// Parse the request
MultipartFormDataParser p = new MultipartFormDataParser(req);
if (Hudson.getInstance().isUseCrumbs() && !Hudson.getInstance().getCrumbIssuer().validateCrumb(req, p)) {
rsp.sendError(HttpServletResponse.SC_FORBIDDEN, "No crumb found");
}
try {
rsp.sendRedirect2(req.getContextPath() + "/fingerprint/"
+ Util.getDigestOf(p.getFileItem("name").getInputStream()) + '/');
} finally {
p.cleanUp();
}
}
/**
* For debugging. Expose URL to perform GC.
*/
public void doGc(StaplerResponse rsp) throws IOException {
checkPermission(Hudson.ADMINISTER);
System.gc();
rsp.setStatus(HttpServletResponse.SC_OK);
rsp.setContentType("text/plain");
rsp.getWriter().println("GCed");
}
/**
* Obtains the heap dump.
*/
public HeapDump getHeapDump() throws IOException {
return new HeapDump(this, MasterComputer.localChannel);
}
/**
* Simulates OutOfMemoryError. Useful to make sure OutOfMemoryHeapDump
* setting.
*/
public void doSimulateOutOfMemory() throws IOException {
checkPermission(ADMINISTER);
System.out.println("Creating artificial OutOfMemoryError situation");
List<Object> args = new ArrayList<Object>();
while (true) {
args.add(new byte[1024 * 1024]);
}
}
private transient final Map<UUID, FullDuplexHttpChannel> duplexChannels = new HashMap<UUID, FullDuplexHttpChannel>();
/**
* Handles HTTP requests for duplex channels for CLI.
*/
public void doCli(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException {
if (!"POST".equals(req.getMethod())) {
// for GET request, serve _cli.jelly, assuming this is a browser
checkPermission(READ);
req.getView(this, "_cli.jelly").forward(req, rsp);
return;
}
// do not require any permission to establish a CLI connection
// the actual authentication for the connecting Channel is done by CLICommand
UUID uuid = UUID.fromString(req.getHeader("Session"));
rsp.setHeader("Hudson-Duplex", ""); // set the header so that the client would know
FullDuplexHttpChannel server;
if (req.getHeader("Side").equals("download")) {
duplexChannels.put(uuid, server = new FullDuplexHttpChannel(uuid, !hasPermission(ADMINISTER)) {
protected void main(Channel channel) throws IOException, InterruptedException {
// capture the identity given by the transport, since this can be useful for SecurityRealm.createCliAuthenticator()
channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION, getAuthentication());
channel.setProperty(CliEntryPoint.class.getName(), new CliManagerImpl());
}
});
try {
server.download(req, rsp);
} finally {
duplexChannels.remove(uuid);
}
} else {
duplexChannels.get(uuid).upload(req, rsp);
}
}
/**
* Binds /userContent/... to $HUDSON_HOME/userContent.
*/
public DirectoryBrowserSupport doUserContent() {
return new DirectoryBrowserSupport(this, getRootPath().child("userContent"), "User content", "folder.png", true);
}
/**
* Perform a restart of Hudson, if we can.
*
* This first replaces "app" to {@link HudsonIsRestarting}
*/
@CLIMethod(name = "restart")
public void doRestart(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, RestartNotSupportedException {
checkPermission(ADMINISTER);
if (req != null && req.getMethod().equals("GET")) {
req.getView(this, "_restart.jelly").forward(req, rsp);
return;
}
restart();
// null for CLI
if (rsp != null) {
rsp.sendRedirect2(".");
}
}
/**
* Queues up a restart of Hudson for when there are no builds running, if we
* can.
*
* This first replaces "app" to {@link HudsonIsRestarting}
*
* @since 1.332
*/
@CLIMethod(name = "safe-restart")
public void doSafeRestart(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, RestartNotSupportedException {
checkPermission(ADMINISTER);
if (req != null && req.getMethod().equals("GET")) {
req.getView(this, "_safeRestart.jelly").forward(req, rsp);
return;
}
safeRestart();
// null for CLI
if (rsp != null) {
rsp.sendRedirect2(".");
}
}
/**
* Performs a restart.
*/
public void restart() throws RestartNotSupportedException {
final Lifecycle lifecycle = Lifecycle.get();
lifecycle.verifyRestartable(); // verify that Hudson is restartable
WebAppController.get().install(new HudsonIsRestarting());
new Thread("restart thread") {
final String exitUser = getAuthentication().getName();
@Override
public void run() {
try {
HudsonSecurityManager.grantFullControl();
// give some time for the browser to load the "reloading" page
Thread.sleep(5000);
logger.error(String.format("Restarting VM as requested by %s", exitUser));
for (RestartListener listener : RestartListener.all()) {
listener.onRestart();
}
lifecycle.restart();
} catch (InterruptedException e) {
logger.warn("Failed to restart Hudson", e);
} catch (IOException e) {
logger.warn("Failed to restart Hudson", e);
}
}
}.start();
}
/**
* Allow lifecycle to determine if it was invoked safely.
*
* @return true if safeRestart is in progress
*
* @since 3.1.0
*/
public boolean isSafeRestarting() {
return safeRestarting;
}
private void verifySafeRestartable(Lifecycle lifecycle) throws RestartNotSupportedException {
// In case the lifecycle requires safe restart
safeRestarting = true;
try {
lifecycle.verifyRestartable(); // verify that Hudson is restartable
} catch (RestartNotSupportedException e) {
safeRestarting = false;
throw e;
}
}
/**
* Queues up a restart to be performed once there are no builds currently
* running.
*
* @since 1.332
*/
public void safeRestart() throws RestartNotSupportedException {
final Lifecycle lifecycle = Lifecycle.get();
verifySafeRestartable(lifecycle);
// Quiet down so that we won't launch new builds.
isQuietingDown = true;
new Thread("safe-restart thread") {
final String exitUser = getAuthentication().getName();
@Override
public void run() {
try {
HudsonSecurityManager.grantFullControl();
// Wait 'til we have no active executors.
doQuietDown(true, 0);
// Make sure isQuietingDown is still true.
if (isQuietingDown) {
WebAppController.get().install(new HudsonIsRestarting());
// give some time for the browser to load the "reloading" page
logger.info("Restart in 10 seconds");
Thread.sleep(10000);
logger.error(String.format("Restarting VM as requested by %s", exitUser));
for (RestartListener listener : RestartListener.all()) {
listener.onRestart();
}
lifecycle.restart();
} else {
logger.info("Safe-restart mode cancelled");
}
} catch (InterruptedException e) {
logger.warn("Failed to restart Hudson", e);
} catch (IOException e) {
logger.warn("Failed to restart Hudson", e);
} finally {
Hudson.this.safeRestarting = false;
}
}
}.start();
}
/**
* Shutdown the system.
*
* @since 1.161
*/
public void doExit(StaplerRequest req, StaplerResponse rsp) throws IOException {
checkPermission(ADMINISTER);
logger.error(String.format("Shutting down VM as requested by %s from %s",
getAuthentication().getName(), req.getRemoteAddr()));
rsp.setStatus(HttpServletResponse.SC_OK);
rsp.setContentType("text/plain");
PrintWriter w = rsp.getWriter();
w.println("Shutting down");
w.close();
System.exit(0);
}
/**
* Shutdown the system safely.
*
* @since 1.332
*/
public void doSafeExit(StaplerRequest req, StaplerResponse rsp) throws IOException {
checkPermission(ADMINISTER);
rsp.setStatus(HttpServletResponse.SC_OK);
rsp.setContentType("text/plain");
PrintWriter w = rsp.getWriter();
w.println("Shutting down as soon as all jobs are complete");
w.close();
isQuietingDown = true;
final String exitUser = getAuthentication().getName();
final String exitAddr = req.getRemoteAddr().toString();
new Thread("safe-exit thread") {
@Override
public void run() {
try {
HudsonSecurityManager.grantFullControl();
logger.error(String.format("Shutting down VM as requested by %s from %s",
exitUser, exitAddr));
// Wait 'til we have no active executors.
while (isQuietingDown
&& (overallLoad.computeTotalExecutors() > overallLoad.computeIdleExecutors())) {
Thread.sleep(5000);
}
// Make sure isQuietingDown is still true.
if (isQuietingDown) {
cleanUp();
System.exit(0);
}
} catch (InterruptedException e) {
logger.warn("Failed to shutdown Hudson", e);
}
}
}.start();
}
/**
* @deprecated As of release 3.0.0, replaced by
* {@link HudsonSecurityManager#getAuthentication()}
*/
@Deprecated
public static Authentication getAuthentication() {
return HudsonSecurityManager.getAuthentication();
}
/**
* For system diagnostics. Run arbitrary Dynamic Language script.
*/
public void doScript(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
doScript(req, rsp, req.getView(this, "_script.jelly"));
}
/**
* Run arbitrary Dynamic Language script and return result as plain text.
*/
public void doScriptText(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
doScript(req, rsp, req.getView(this, "_scriptText.jelly"));
}
private void doScript(StaplerRequest req, StaplerResponse rsp, RequestDispatcher view) throws IOException, ServletException {
// ability to run arbitrary script is dangerous
// So allow only Admin to run script
checkPermission(ADMINISTER);
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, MasterComputer.localChannel, scriptSupport));
} catch (InterruptedException e) {
throw new ServletException(e);
}
}
view.forward(req, rsp);
}
/**
* Evaluates the Jelly script submitted by the client.
*
* This is useful for system administration as well as unit testing.
*/
public void doEval(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
checkPermission(ADMINISTER);
requirePOST();
try {
MetaClass mc = WebApp.getCurrent().getMetaClass(getClass());
Script script = mc.classLoader.loadTearOff(JellyClassLoaderTearOff.class).createContext().compileScript(new InputSource(req.getReader()));
new JellyRequestDispatcher(this, script).forward(req, rsp);
} catch (JellyException e) {
throw new ServletException(e);
}
}
/**
* Sign up for the user account.
*/
public void doSignup(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
req.getView(getSecurityRealm(), "signup.jelly").forward(req, rsp);
}
/**
* Changes the icon size by changing the cookie
*/
public void doIconSize(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
String qs = req.getQueryString();
if (qs == null || !ICON_SIZE.matcher(qs).matches()) {
throw new ServletException();
}
Cookie cookie = new Cookie("iconSize", qs);
cookie.setMaxAge(/* ~4 mo. */9999999); // #762
rsp.addCookie(cookie);
String ref = req.getHeader("Referer");
if (ref == null) {
ref = ".";
}
rsp.sendRedirect2(ref);
}
public void doFingerprintCleanup(StaplerResponse rsp) throws IOException {
FingerprintCleanupThread.invoke();
rsp.setStatus(HttpServletResponse.SC_OK);
rsp.setContentType("text/plain");
rsp.getWriter().println("Invoked");
}
public void doWorkspaceCleanup(StaplerResponse rsp) throws IOException {
WorkspaceCleanupThread.invoke();
rsp.setStatus(HttpServletResponse.SC_OK);
rsp.setContentType("text/plain");
rsp.getWriter().println("Invoked");
}
/**
* If the user chose the default JDK, make sure we got 'java' in PATH.
*/
public FormValidation doDefaultJDKCheck(StaplerRequest request, @QueryParameter String value) {
if (!value.equals("(Default)")) // assume the user configured named ones properly in system config ---
// or else system config should have reported form field validation errors.
{
return FormValidation.ok();
}
// default JDK selected. Does such java really exist?
if (JDK.isDefaultJDKValid(Hudson.this)) {
return FormValidation.ok();
} else {
return FormValidation.errorWithMarkup(Messages.Hudson_NoJavaInPath(request.getContextPath()));
}
}
public static final int TEAM_NAME_LIMIT = 64;
public static final int JOB_NAME_LIMIT_TEAM = 128;
public static final int JOB_NAME_LIMIT_NO_TEAM = TEAM_NAME_LIMIT + JOB_NAME_LIMIT_TEAM + 1;
/**
* Makes sure that the given name is good as a job name.
*/
public FormValidation doCheckJobName(@QueryParameter String value) {
// this method can be used to check if a file exists anywhere in the file system,
// so it should be protected.
checkPermission(Item.CREATE);
if (fixEmpty(value) == null) {
return FormValidation.ok();
}
try {
String name = checkGoodJobName(value);
if (getItem(name) != null) {
throw new Failure(Messages.Hudson_JobAlreadyExists(name));
}
return FormValidation.ok();
} catch (Failure e) {
return FormValidation.error(e.getMessage());
}
}
/**
* Makes sure that the given name is good as a team name.
*/
public FormValidation doCheckTeamName(@QueryParameter String value) {
// this method can be used to check if a file exists anywhere in the file system,
// so it should be protected.
checkPermission(Item.CREATE);
if (fixEmpty(value) == null) {
return FormValidation.ok();
}
try {
if (isTeamManagementEnabled()) {
try {
getTeamManager().findTeam(value);
} catch (TeamManager.TeamNotFoundException ex) {
throw new Failure("No such team " + value);
}
if (!getTeamManager().isCurrentUserHasAccessToTeam(value)) {
throw new Failure("Current user cannot access team " + value);
}
}
return FormValidation.ok();
} catch (Failure e) {
return FormValidation.error(e.getMessage());
}
}
/**
* Checks if a top-level view with the given name exists.
*/
public FormValidation doViewExistsCheck(@QueryParameter String value) {
checkPermission(View.CREATE);
String view = fixEmpty(value);
if (view == null) {
return FormValidation.ok();
}
if (getView(view) == null) {
return FormValidation.ok();
} else {
return FormValidation.error(Messages.Hudson_ViewAlreadyExists(view));
}
}
/**
* @deprecated as of 1.294 Define your own check method, instead of relying
* on this generic one.
*/
public void doFieldCheck(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
doFieldCheck(
fixEmpty(req.getParameter("value")),
fixEmpty(req.getParameter("type")),
fixEmpty(req.getParameter("errorText")),
fixEmpty(req.getParameter("warningText"))).generateResponse(req, rsp, this);
}
/**
* Checks if the value for a field is set; if not an error or warning text
* is displayed. If the parameter "value" is not set then the parameter
* "errorText" is displayed as an error text. If the parameter "errorText"
* is not set, then the parameter "warningText" is displayed as a warning
* text.
* <p>
* If the text is set and the parameter "type" is set, it will validate that
* the value is of the correct type. Supported types are "number,
* "number-positive" and "number-negative".
*
* @deprecated as of 1.324 Either use client-side validation (e.g.
* class="required number") or define your own check method, instead of
* relying on this generic one.
*/
public FormValidation doFieldCheck(@QueryParameter(fixEmpty = true) String value,
@QueryParameter(fixEmpty = true) String type,
@QueryParameter(fixEmpty = true) String errorText,
@QueryParameter(fixEmpty = true) String warningText) {
if (value == null) {
if (errorText != null) {
return FormValidation.error(errorText);
}
if (warningText != null) {
return FormValidation.warning(warningText);
}
return FormValidation.error("No error or warning text was set for fieldCheck().");
}
if (type != null) {
try {
if (type.equalsIgnoreCase("number")) {
NumberFormat.getInstance().parse(value);
} else if (type.equalsIgnoreCase("number-positive")) {
if (NumberFormat.getInstance().parse(value).floatValue() <= 0) {
return FormValidation.error(Messages.Hudson_NotAPositiveNumber());
}
} else if (type.equalsIgnoreCase("number-negative")) {
if (NumberFormat.getInstance().parse(value).floatValue() >= 0) {
return FormValidation.error(Messages.Hudson_NotANegativeNumber());
}
}
} catch (ParseException e) {
return FormValidation.error(Messages.Hudson_NotANumber());
}
}
return FormValidation.ok();
}
/**
* Serves static resources placed along with Jelly view files.
* <p>
* This method can serve a lot of files, so care needs to be taken to make
* this method secure. It's not clear to me what's the best strategy here,
* though the current implementation is based on file extensions.
*/
public void doResources(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
String path = req.getRestOfPath();
// cut off the "..." portion of /resources/.../path/to/file
// as this is only used to make path unique (which in turn
// allows us to set a long expiration date
path = path.substring(path.indexOf('/', 1) + 1);
int idx = path.lastIndexOf('.');
String extension = path.substring(idx + 1);
if (ALLOWED_RESOURCE_EXTENSIONS.contains(extension)) {
URL url = pluginManager.uberClassLoader.getResource(path);
if (url != null) {
long expires = MetaClass.NO_CACHE ? 0 : 365L * 24 * 60 * 60 * 1000; /*1 year*/
rsp.serveFile(req, url, expires);
return;
}
}
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
}
/**
* Extension list that {@link #doResources(StaplerRequest, StaplerResponse)}
* can serve. This set is mutable to allow plugins to add additional
* extensions.
*/
public static final Set<String> ALLOWED_RESOURCE_EXTENSIONS = new HashSet<String>(Arrays.asList(
"js|css|jpeg|jpg|png|gif|html|htm".split("\\|")));
/**
* Checks if container uses UTF-8 to decode URLs. See
* http://wiki.hudson-ci.org/display/HUDSON/Tomcat#Tomcat-i18n
*/
public FormValidation doCheckURIEncoding(StaplerRequest request) throws IOException {
// expected is non-ASCII String
final String expected = "\u57f7\u4e8b";
final String value = fixEmpty(request.getParameter("value"));
if (!expected.equals(value)) {
return FormValidation.warningWithMarkup(Messages.Hudson_NotUsesUTF8ToDecodeURL());
}
return FormValidation.ok();
}
/**
* Does not check when system default encoding is "ISO-8859-1".
*/
public static boolean isCheckURIEncodingEnabled() {
return !"ISO-8859-1".equalsIgnoreCase(System.getProperty("file.encoding"));
}
/**
* @deprecated Use {@link Functions#isWindows()}.
*/
public static boolean isWindows() {
return File.pathSeparatorChar == ';';
}
public static boolean isDarwin() {
// according to http://developer.apple.com/technotes/tn2002/tn2110.html
return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).startsWith("mac");
}
/**
* Rebuilds the dependency map.
*/
public void rebuildDependencyGraph() {
dependencyGraph = new DependencyGraph();
}
public DependencyGraph getDependencyGraph() {
return dependencyGraph;
}
// for Jelly
public List<ManagementLink> getManagementLinks() {
return ManagementLink.all();
}
/**
* Exposes the current user to <tt>/me</tt> URL.
*/
public User getMe() {
User u = User.current();
if (u == null) {
throw new AccessDeniedException("/me is not available when not logged in");
}
return u;
}
/**
* Gets the {@link Widget}s registered on this object.
*
* <p>
* Plugins who wish to contribute boxes on the side panel can add widgets by
* {@code getWidgets().add(new MyWidget())} from {@link Plugin#start()}.
*/
public List<Widget> getWidgets() {
return widgets;
}
public Object getTarget() {
try {
checkPermission(READ);
} catch (AccessDeniedException e) {
String rest = Stapler.getCurrentRequest().getRestOfPath();
if (rest.startsWith("/login")
|| rest.startsWith("/logout")
|| rest.startsWith("/accessDenied")
|| rest.startsWith("/signup")
|| rest.startsWith("/jnlpJars/")
|| rest.startsWith("/slaveAgent/")
|| rest.startsWith("/tcpSlaveAgentListener")
|| rest.startsWith("/cli")
|| rest.startsWith("/whoAmI")
|| rest.startsWith("/federatedLoginService/")
|| rest.startsWith("/securityRealm")) {
return this; // URLs that are always visible without READ permission
}
// For those "unsecured" actions outside of Hudson security envelop
if (allowUnsecuredAction) {
for (Action action : getActions()) {
if (action instanceof UnsecuredRootAction) {
if (rest.startsWith("/" + action.getUrlName() + "/")) {
return this;
}
}
}
}
throw e;
}
return this;
}
/**
* @since 2.1.0
*/
public Iterator<GlobalMessage> getGlobalMessages() {
return Iterators.readOnly(getExtensionList(GlobalMessage.class).iterator());
}
/**
* Fallback to the primary view.
*/
public View getStaplerFallback() {
return getPrimaryView();
}
public static final class MasterComputer extends Computer {
private MasterComputer() {
super(Hudson.getInstance());
}
/**
* Returns "" to match with {@link Hudson#getNodeName()}.
*/
@Override
public String getName() {
return "";
}
@Override
public boolean isConnecting() {
return false;
}
@Override
public String getDisplayName() {
return Messages.Hudson_Computer_DisplayName();
}
@Override
public String getCaption() {
return Messages.Hudson_Computer_Caption();
}
@Override
public String getUrl() {
return "computer/(master)/";
}
public RetentionStrategy getRetentionStrategy() {
return RetentionStrategy.NOOP;
}
/**
* Report an error.
*/
@Override
public HttpResponse doDoDelete() throws IOException {
throw HttpResponses.status(SC_BAD_REQUEST);
}
@Override
public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
// the master node isn't in the Hudson.getNodes(), so this method makes no sense.
throw new UnsupportedOperationException();
}
@Override
public boolean hasPermission(Permission permission) {
// no one should be allowed to delete the master.
// this hides the "delete" link from the /computer/(master) page.
if (permission == Computer.DELETE) {
return false;
}
// Configuration of master node requires ADMINISTER permission
return super.hasPermission(permission == Computer.CONFIGURE ? Hudson.ADMINISTER : permission);
}
@Override
public VirtualChannel getChannel() {
return localChannel;
}
@Override
public Charset getDefaultCharset() {
return Charset.defaultCharset();
}
public List<LogRecord> getLogRecords() throws IOException, InterruptedException {
return logRecords;
}
public void doLaunchSlaveAgent(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
// this computer never returns null from channel, so
// this method shall never be invoked.
rsp.sendError(SC_NOT_FOUND);
}
/**
* Redirect the master configuration to /configure.
*/
public void doConfigure(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
rsp.sendRedirect2(req.getContextPath() + "/configure");
}
protected Future<?> _connect(boolean forceReconnect) {
return Futures.precomputed(null);
}
/**
* {@link LocalChannel} instance that can be used to execute programs
* locally.
*/
public static final LocalChannel localChannel = new LocalChannel(threadPoolForRemoting);
}
/**
* Shortcut for {@code Hudson.getInstance().lookup.get(type)}
*/
public static <T> T lookup(Class<T> type) {
return Hudson.getInstance().lookup.get(type);
}
/**
* @deprecated since 2007-12-18. Use {@link #checkPermission(Permission)}
*/
public static boolean adminCheck() throws IOException {
return adminCheck(Stapler.getCurrentRequest(), Stapler.getCurrentResponse());
}
/**
* @deprecated since 2007-12-18. Use {@link #checkPermission(Permission)}
*/
public static boolean adminCheck(StaplerRequest req, StaplerResponse rsp) throws IOException {
if (isAdmin(req)) {
return true;
}
rsp.sendError(StaplerResponse.SC_FORBIDDEN);
return false;
}
/**
* Checks if the current user (for which we are processing the current
* request) has the admin access.
*
* @deprecated since 2007-12-18. This method is deprecated when Hudson moved
* from simple Unix root-like model of "admin gets to do everything, and
* others don't have any privilege" to more complex {@link ACL} and
* {@link Permission} based scheme.
*
* <p>
* For a quick migration, use
* {@code Hudson.getInstance().getACL().hasPermission(Hudson.ADMINISTER)} To
* check if the user has the 'administer' role in Hudson.
*
* <p>
* But ideally, your plugin should first identify a suitable
* {@link Permission} (or create one, if appropriate), then identify a
* suitable {@link AccessControlled} object to check its permission against.
*/
public static boolean isAdmin() {
return HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getACL().hasPermission(ADMINISTER);
}
/**
* @deprecated since 2007-12-18. Define a custom {@link Permission} and
* check against ACL. See {@link #isAdmin()} for more instructions.
*/
public static boolean isAdmin(StaplerRequest req) {
return isAdmin();
}
/**
* Live view of recent {@link LogRecord}s produced by Hudson.
*/
public static List<LogRecord> logRecords = Collections.emptyList(); // initialized to dummy value to avoid NPE
/**
* Thread-safe reusable {@link XStream}.
*/
public static final XStream XSTREAM = new XStream2();
private static final int TWICE_CPU_NUM = Runtime.getRuntime().availableProcessors() * 2;
/**
* Thread pool used to load configuration in parallel, to improve the start
* up time.
* <p>
* The idea here is to overlap the CPU and I/O, so we want more threads than
* CPU numbers.
*/
/*package*/ transient final ExecutorService threadPoolForLoad = new ThreadPoolExecutor(
TWICE_CPU_NUM, TWICE_CPU_NUM,
5L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new DaemonThreadFactory());
private static void computeVersion(ServletContext context) {
// set the version
Properties props = new Properties();
try {
InputStream is = Hudson.class.getResourceAsStream("hudson-version.properties");
if (is != null) {
props.load(is);
}
} catch (IOException e) {
e.printStackTrace(); // if the version properties is missing, that's OK.
}
String ver = props.getProperty("version");
if (ver == null) {
ver = "?";
}
VERSION = ver;
context.setAttribute("version", ver);
VERSION_HASH = Util.getDigestOf(ver).substring(0, 8);
if (ver.equals("?") || Boolean.getBoolean("hudson.script.noCache")) {
RESOURCE_PATH = "";
} else {
RESOURCE_PATH = "/static/" + VERSION_HASH;
}
VIEW_RESOURCE_PATH = "/resources/" + VERSION_HASH;
}
/**
* Version number of this Hudson.
*/
public static String VERSION = "?";
/**
* Parses {@link #VERSION} into {@link VersionNumber}, or null if it's not
* parseable as a version number (such as when Hudson is run with "mvn
* hudson-dev:run")
*/
public static VersionNumber getVersion() {
try {
return new VersionNumber(VERSION);
} catch (NumberFormatException e) {
try {
// for non-released version of Hudson, this looks like "1.345 (private-foobar), so try to approximate.
int idx = VERSION.indexOf(' ');
if (idx > 0) {
return new VersionNumber(VERSION.substring(0, idx));
}
} catch (NumberFormatException _) {
// fall through
}
// totally unparseable
return null;
} catch (IllegalArgumentException e) {
// totally unparseable
return null;
}
}
TopLevelItemsCache itemsCache() {
return itemsCache;
}
/**
* Hash of {@link #VERSION}.
*/
public static String VERSION_HASH;
/**
* Prefix to static resources like images and javascripts in the war file.
* Either "" or strings like "/static/VERSION", which avoids Hudson to pick
* up stale cache when the user upgrades to a different version.
* <p>
* Value computed in {@link WebAppMain}.
*/
public static String RESOURCE_PATH = "";
/**
* Prefix to resources alongside view scripts. Strings like
* "/resources/VERSION", which avoids Hudson to pick up stale cache when the
* user upgrades to a different version.
* <p>
* Value computed in {@link WebAppMain}.
*/
public static String VIEW_RESOURCE_PATH = "/resources/TBD";
public static boolean PARALLEL_LOAD = !"false".equals(System.getProperty(Hudson.class.getName() + ".parallelLoad"));
public static boolean KILL_AFTER_LOAD = Boolean.getBoolean(Hudson.class.getName() + ".killAfterLoad");
public static boolean LOG_STARTUP_PERFORMANCE = Boolean.getBoolean(Hudson.class.getName() + ".logStartupPerformance");
private static final boolean CONSISTENT_HASH = true; // Boolean.getBoolean(Hudson.class.getName()+".consistentHash");
/**
* Enabled by default as of 1.337. Will keep it for a while just in case we
* have some serious problems.
*/
public static boolean FLYWEIGHT_SUPPORT = !"false".equals(System.getProperty(Hudson.class.getName() + ".flyweightSupport"));
/**
* Tentative switch to activate the concurrent build behavior. When we merge
* this back to the trunk, this allows us to keep this feature hidden for a
* while until we iron out the kinks.
*
* @see AbstractProject#isConcurrentBuild()
*/
public static boolean CONCURRENT_BUILD = true;
/**
* Switch to enable people to use a shorter workspace name.
*/
private static final String WORKSPACE_DIRNAME = System.getProperty(Hudson.class.getName() + ".workspaceDirName", "workspace");
/**
* Automatically try to launch a slave when Hudson is initialized or a new
* slave is created.
*/
public static boolean AUTOMATIC_SLAVE_LAUNCH = true;
private static final Pattern ICON_SIZE = Pattern.compile("\\d+x\\d+");
public static final PermissionGroup PERMISSIONS = Permission.HUDSON_PERMISSIONS;
public static final Permission ADMINISTER = Permission.HUDSON_ADMINISTER;
public static final Permission READ = new Permission(PERMISSIONS, "Read", Messages._Hudson_ReadPermission_Description(), Permission.READ);
public static final Authentication ANONYMOUS = HudsonSecurityManager.ANONYMOUS;
static {
XSTREAM.alias("hudson", Hudson.class);
XSTREAM.alias("slave", DumbSlave.class);
XSTREAM.alias("jdk", JDK.class);
// for backward compatibility with <1.75, recognize the tag name "view" as well.
XSTREAM.alias("view", ListView.class);
XSTREAM.alias("listView", ListView.class);
// this seems to be necessary to force registration of converter early enough
Mode.class.getEnumConstants();
// double check that initialization order didn't do any harm
assert PERMISSIONS != null;
assert ADMINISTER != null;
}
}