Java tutorial
/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, Koichi Fujikawa, Red Hat, Inc., Seiji Sogabe, Stephen Connolly, Tom Huybrechts, Yahoo! Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.model; import com.thoughtworks.xstream.XStream; import hudson.BulkChange; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; import hudson.Launcher.LocalLauncher; import hudson.Plugin; import hudson.PluginManager; import hudson.PluginWrapper; import hudson.ProxyConfiguration; import hudson.StructuredForm; import hudson.TcpSlaveAgentListener; import hudson.Util; import static hudson.Util.fixEmpty; import static hudson.Util.fixNull; import hudson.WebAppMain; import hudson.XmlFile; import hudson.UDPBroadcastThread; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.DescriptorExtensionList; import hudson.ExtensionListView; import hudson.Extension; import hudson.tools.ToolInstallation; import hudson.tools.ToolDescriptor; import hudson.cli.CliEntryPoint; import hudson.cli.CliManagerImpl; import hudson.logging.LogRecorderManager; import hudson.lifecycle.Lifecycle; import hudson.model.Descriptor.FormException; import hudson.model.listeners.ItemListener; import hudson.model.listeners.JobListener; import hudson.model.listeners.JobListener.JobListenerAdapter; import hudson.model.listeners.SCMListener; import hudson.remoting.LocalChannel; import hudson.remoting.VirtualChannel; import hudson.remoting.Channel; import hudson.scm.CVSSCM; import hudson.scm.RepositoryBrowser; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.search.CollectionSearchIndex; import hudson.search.SearchIndexBuilder; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.AuthorizationStrategy; import hudson.security.BasicAuthenticationFilter; import hudson.security.HudsonFilter; import hudson.security.LegacyAuthorizationStrategy; import hudson.security.LegacySecurityRealm; import hudson.security.Permission; import hudson.security.PermissionGroup; import hudson.security.SecurityMode; import hudson.security.SecurityRealm; import hudson.security.csrf.CrumbIssuer; import hudson.slaves.ComputerListener; import hudson.slaves.NodeProperty; import hudson.slaves.NodePropertyDescriptor; import hudson.slaves.RetentionStrategy; import hudson.slaves.NodeList; import hudson.slaves.Cloud; import hudson.slaves.DumbSlave; import hudson.slaves.NodeDescriptor; import hudson.slaves.NodeProvisioner; import hudson.tasks.BuildWrapper; import hudson.tasks.Builder; import hudson.tasks.DynamicLabeler; import hudson.tasks.LabelFinder; import hudson.tasks.Mailer; import hudson.tasks.Publisher; import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; import hudson.util.CaseInsensitiveComparator; import hudson.util.ClockDifference; import hudson.util.CopyOnWriteList; import hudson.util.CopyOnWriteMap; import hudson.util.DaemonThreadFactory; import hudson.util.HudsonIsLoading; import hudson.util.MultipartFormDataParser; import hudson.util.RemotingDiagnostics; import hudson.util.TextFile; import hudson.util.XStream2; import hudson.util.HudsonIsRestarting; import hudson.util.DescribableList; import hudson.util.Futures; import hudson.util.Memoizer; import hudson.util.Iterators; import hudson.util.FormValidation; import hudson.util.VersionNumber; import hudson.util.StreamTaskListener; import hudson.util.AdministrativeError; import hudson.widgets.Widget; import net.sf.json.JSONObject; import org.acegisecurity.*; import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.acegisecurity.ui.AbstractProcessingFilter; import org.apache.commons.logging.LogFactory; import org.apache.commons.jelly.Script; import org.apache.commons.jelly.JellyException; import org.apache.commons.io.FileUtils; import org.kohsuke.stapler.MetaClass; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerProxy; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.StaplerFallback; import org.kohsuke.stapler.WebApp; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.jelly.JellyClassLoaderTearOff; import org.kohsuke.stapler.jelly.JellyRequestDispatcher; import org.kohsuke.stapler.framework.adjunct.AdjunctManager; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.xml.sax.InputSource; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.io.InputStream; import java.net.URL; import java.net.BindException; import java.security.SecureRandom; import java.text.NumberFormat; import java.text.ParseException; import java.text.Collator; 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.Map; import java.util.Map.Entry; import java.util.Set; import java.util.Stack; import java.util.StringTokenizer; import java.util.Timer; import java.util.TreeSet; import java.util.Properties; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeoutException; import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.regex.Pattern; import java.nio.charset.Charset; import javax.servlet.RequestDispatcher; import javax.servlet.ServletInputStream; import javax.crypto.SecretKey; import groovy.lang.GroovyShell; /** * Root object of the system. * * @author Kohsuke Kawaguchi */ @ExportedBean public final class Hudson extends Node implements ItemGroup<TopLevelItem>, StaplerProxy, StaplerFallback, ViewGroup, AccessControlled, DescriptorByNameOwner { private transient final Queue queue; /** * {@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; /** * False to enable anyone to do anything. * Left as a field so that we can still read old data that uses this flag. * * @see #authorizationStrategy * @see #securityRealm */ private Boolean useSecurity; /** * Controls how the * <a href="http://en.wikipedia.org/wiki/Authorization">authorization</a> * is handled in Hudson. * <p> * This ultimately controls who has access to what. * * Never null. */ private volatile AuthorizationStrategy authorizationStrategy = AuthorizationStrategy.UNSECURED; /** * Controls a part of the * <a href="http://en.wikipedia.org/wiki/Authentication">authentication</a> * handling in Hudson. * <p> * Intuitively, this corresponds to the user database. * * See {@link HudsonFilter} for the concrete authentication protocol. * * Never null. Always use {@link #setSecurityRealm(SecurityRealm)} to * update this field. * * @see #getSecurity() * @see #setSecurityRealm(SecurityRealm) */ private volatile SecurityRealm securityRealm = SecurityRealm.NO_AUTHENTICATION; /** * Message displayed in the top page. */ private String systemMessage; /** * Root directory of the system. */ public transient final File root; /** * 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); /** * The sole instance. */ private static Hudson theInstance; private transient volatile boolean isQuietingDown; private transient volatile boolean terminating; private List<JDK> jdks = new ArrayList<JDK>(); private transient volatile DependencyGraph dependencyGraph; /** * 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.create(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); } public CloudList() {// needed for XStream deserialization } 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; public transient volatile TcpSlaveAgentListener tcpSlaveAgentListener; private transient UDPBroadcastThread udpBroadcastThread; /** * 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); /** * TCP slave agent port. * 0 for random, -1 to disable. */ private int slaveAgentPort = 0; /** * 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>(); private transient volatile Set<Label> labelSet; private transient volatile Set<Label> dynamicLabels = null; /** * Load statistics of the entire system. */ 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); /** * {@link AdjunctManager} */ private transient final AdjunctManager adjuncts; public static Hudson getInstance() { return theInstance; } /** * Secrete key generated once and used for a long time, beyond * container start/stop. Persisted outside <tt>config.xml</tt> to avoid * accidental exposure. */ private transient final String secretKey; private transient final UpdateCenter updateCenter = new UpdateCenter(this); /** * True if the user opted out from the statistics tracking. We'll never send anything if this is true. */ private Boolean noUsageStatistics; /** * 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 { // As hudson is starting, grant this process full controll SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM); try { this.root = root; this.servletContext = context; computeVersion(context); if (theInstance != null) throw new IllegalStateException("second instance"); theInstance = this; log.load(); Trigger.timer = new Timer("Hudson cron thread"); 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; } // get or create the secret TextFile secretFile = new TextFile(new File(Hudson.getInstance().getRootDir(), "secret.key")); if (secretFile.exists()) { secretKey = secretFile.readTrim(); } else { SecureRandom sr = new SecureRandom(); byte[] random = new byte[32]; sr.nextBytes(random); secretKey = Util.toHexString(random); secretFile.write(secretKey); } try { proxy = ProxyConfiguration.load(); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Failed to load proxy configuration", e); } // load plugins. pluginManager = new PluginManager(context); pluginManager.initialize(); // if we are loading old data that doesn't have this field if (slaves == null) slaves = new NodeList(); adjuncts = new AdjunctManager(servletContext, pluginManager.uberClassLoader, "adjuncts/" + VERSION_HASH); load(); // try { // // fill up the cache // load(); // // Controller c = new Controller(); // c.startCPUProfiling(ProfilingModes.CPU_TRACING,""); // "java.*"); // load(); // c.stopCPUProfiling(); // c.captureSnapshot(ProfilingModes.SNAPSHOT_WITHOUT_HEAP); // } catch (Exception e) { // throw new Error(e); // } 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; udpBroadcastThread = new UDPBroadcastThread(this); udpBroadcastThread.start(); updateComputerList(); {// master is online now Computer c = toComputer(); if (c != null) for (ComputerListener cl : ComputerListener.all()) cl.onOnline(c, new StreamTaskListener(System.out)); } getQueue().load(); for (ItemListener l : ItemListener.all()) l.onLoaded(); // run the initialization script, if it exists. File initScript = new File(getRootDir(), "init.groovy"); if (initScript.exists()) { LOGGER.info("Executing " + initScript); GroovyShell shell = new GroovyShell(pluginManager.uberClassLoader); try { shell.evaluate(initScript); } catch (Throwable t) { t.printStackTrace(); } } File userContentDir = new File(getRootDir(), "userContent"); if (!userContentDir.exists()) { userContentDir.mkdirs(); FileUtils.writeStringToFile(new File(userContentDir, "readme.txt"), Messages.Hudson_USER_CONTENT_README()); } Trigger.init(); // start running trigger } finally { SecurityContextHolder.clearContext(); } } 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; } @Exported public int getSlaveAgentPort() { return slaveAgentPort; } /** * If you are calling this on Hudson something is wrong. * * @deprecated */ @Deprecated public String getNodeName() { return ""; } public void setNodeName(String name) { throw new UnsupportedOperationException(); // not allowed } public String getNodeDescription() { return "the master Hudson node"; } @Exported public String getDescription() { return systemMessage; } public PluginManager getPluginManager() { return pluginManager; } 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 final 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 secretKey; } /** * Gets {@linkplain #getSecretKey() the secret key} as a key for AES-128. * @since 1.308 */ public SecretKey getSecretKeyAsAES128() { return Util.toAes128Key(secretKey); } /** * 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 className * Either fully qualified class name (recommended) or the short name. */ public Descriptor getDescriptor(String className) { // 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.clazz.getName(); if (name.equals(className)) return d; if (name.substring(name.lastIndexOf('.') + 1).equals(className)) return d; } return null; } /** * Alias for {@link #getDescriptor(String)}. */ public Descriptor getDescriptorByName(String className) { return getDescriptor(className); } /** * 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; } /** * 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; } /** * Adds a new {@link JobListener}. * * @deprecated * Use {@code getJobListeners().add(l)} instead. */ public void addListener(JobListener l) { itemListeners.add(new JobListenerAdapter(l)); } /** * Deletes an existing {@link JobListener}. * * @deprecated * Use {@code getJobListeners().remove(l)} instead. */ public boolean removeListener(JobListener l) { return itemListeners.remove(new JobListenerAdapter(l)); } /** * 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; } /** * Sets the system message. */ public void setSystemMessage(String message) throws IOException { this.systemMessage = message; save(); } 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 { synchronized (updateComputerLock) {// just so that we don't have two code updating computer list at the same time 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(); } 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) { 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)) viewableItems.add(item); } 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() { 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()) if (type.isInstance(i)) r.add(type.cast(i)); 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()) { if (type.isInstance(i)) { if (i.hasPermission(Item.READ)) r.add(type.cast(i)); } if (i instanceof ItemGroup) q.push((ItemGroup) i); } } 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 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; } return null; } /** * Gets the read-only list of all {@link View}s. */ @Exported public synchronized Collection<View> getViews() { List<View> copy = new ArrayList<View>(views); Collections.sort(copy, View.SORTER); return copy; } public void addView(View v) throws IOException { views.add(v); save(); } public synchronized void deleteView(View view) throws IOException { if (views.size() <= 1) throw new IllegalStateException(); views.remove(view); save(); } /** * 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[] 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); } public Computer getComputer(String name) { if (name.equals("(master)")) name = ""; for (Computer c : computers.values()) { if (c.getNode().getNodeName().equals(name)) return c; } return null; } /** * @deprecated * UI method. Not meant to be used programatically. */ public ComputerSet getComputer() { return new ComputerSet(); } /** * Gets the label that exists on this system by the name. * * @return null if no such label exists. * @see Label#parse(String) */ public Label getLabel(String name) { if (name == null) return null; while (true) { Label l = labels.get(name); if (l != null) return l; // non-existent labels.putIfAbsent(name, new Label(name)); } } /** * 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 Queue getQueue() { return queue; } 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) { for (Cloud nf : clouds) if (nf.name.equals(name)) return nf; return null; } /** * @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); if (!nl.contains(n)) // defensive check nl.add(n); setNodes(nl); } /** * Removes a {@link Node} from Hudson. */ public synchronized void removeNode(Node n) throws IOException { n.toComputer().disconnect(); 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 ""; } public String getSearchUrl() { return ""; } public void onViewRenamed(View view, String oldName, String newName) { // implementation of Hudson is immune to view name change. } @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 (v == null) // fallback 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) 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("http://"); 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; } public FilePath getWorkspaceFor(TopLevelItem item) { return new FilePath(new File(item.getRootDir(), "workspace")); } public FilePath getRootPath() { return new FilePath(getRootDir()); } 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 securityRealm != SecurityRealm.NO_AUTHENTICATION || authorizationStrategy != AuthorizationStrategy.UNSECURED; } /** * 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() { // fix the variable so that this code works under concurrent modification to securityRealm. SecurityRealm realm = securityRealm; if (realm == SecurityRealm.NO_AUTHENTICATION) return SecurityMode.UNSECURED; if (realm instanceof LegacySecurityRealm) return SecurityMode.LEGACY; return SecurityMode.SECURED; } /** * @return * never null. */ public SecurityRealm getSecurityRealm() { return securityRealm; } public void setSecurityRealm(SecurityRealm securityRealm) { if (securityRealm == null) securityRealm = SecurityRealm.NO_AUTHENTICATION; this.securityRealm = securityRealm; // reset the filters and proxies for the new SecurityRealm try { HudsonFilter filter = HudsonFilter.get(servletContext); if (filter == null) { // Fix for #3069: This filter is not necessarily initialized before the servlets. // when HudsonFilter does come back, it'll initialize itself. LOGGER.fine("HudsonFilter has not yet been initialized: Can't perform security setup for now"); } else { LOGGER.fine("HudsonFilter has been previously initialized: Setting security up"); filter.reset(securityRealm); LOGGER.fine("Security is now fully set up"); } } catch (ServletException e) { // for binary compatibility, this method cannot throw a checked exception throw new AcegiSecurityException("Failed to configure filter", e) { }; } } public void setAuthorizationStrategy(AuthorizationStrategy a) { if (a == null) a = AuthorizationStrategy.UNSECURED; authorizationStrategy = a; } 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); } /** * 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() */ public ACL getACL() { return authorizationStrategy.getRootACL(); } /** * @return * never null. */ public AuthorizationStrategy getAuthorizationStrategy() { return 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; } 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) { for (Entry<String, TopLevelItem> e : items.entrySet()) { if (Functions.toEmailSafeString(e.getKey()).equalsIgnoreCase(Functions.toEmailSafeString(name))) return e.getValue(); } return null; } /** * {@inheritDoc}. * * Note that the look up is case-insensitive. */ public TopLevelItem getItem(String name) { TopLevelItem item = items.get(name); if (item == null || !item.hasPermission(Item.READ)) return null; return item; } public File getRootDirFor(TopLevelItem child) { return getRootDirFor(child.getName()); } private File getRootDirFor(String name) { return new File(new File(getRootDir(), "jobs"), name); } /** * 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; 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); } /** * 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 { if (items.containsKey(name)) throw new IllegalArgumentException(); TopLevelItem item; try { item = type.newInstance(name); } catch (Exception e) { throw new IllegalArgumentException(e); } item.save(); items.put(name, item); return 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 in response to {@link Job#doDoDelete(StaplerRequest, StaplerResponse)} */ /*package*/ void deleteJob(TopLevelItem item) throws IOException { for (ItemListener l : ItemListener.all()) l.onDeleted(item); items.remove(item.getName()); for (View v : views) v.onJobRenamed(item, item.getName(), null); save(); } /** * Called by {@link Job#renameTo(String)} to update relevant data structure. * assumed to be synchronized on Hudson by the caller. */ /*package*/ void onRenamed(TopLevelItem job, String oldName, String newName) throws IOException { items.remove(oldName); items.put(newName, job); for (View v : views) v.onJobRenamed(job, oldName, newName); 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(); } public Set<Label> getAssignedLabels() { Set<Label> lset = labelSet; // labelSet may be set by another thread while we are in this method, so capture it. if (lset == null) { Set<Label> r = Label.parse(getLabelString()); r.addAll(getDynamicLabels()); r.add(getSelfLabel()); this.labelSet = lset = Collections.unmodifiableSet(r); } return lset; } /** * Returns the possibly empty set of labels that it has been determined as supported by this node. * * @see hudson.tasks.LabelFinder */ public Set<Label> getDynamicLabels() { if (dynamicLabels == null) { // in the worst cast, two threads end up doing the same computation twice, // but that won't break the semantics. // OTOH, not locking prevents dead-lock. See #1390 Set<Label> r = new HashSet<Label>(); Computer comp = getComputer(""); if (comp != null) { VirtualChannel channel = comp.getChannel(); if (channel != null) { for (DynamicLabeler labeler : LabelFinder.LABELERS) { for (String label : labeler.findLabels(channel)) { r.add(getLabel(label)); } } } } dynamicLabels = r; } return dynamicLabels; } public Label getSelfLabel() { return getLabel("master"); } public Computer createComputer() { return new MasterComputer(); } private synchronized void load() throws IOException { long startTime = System.currentTimeMillis(); XmlFile cfg = getConfigFile(); if (cfg.exists()) { // reset some data that may not exit in the disk file // so that we can take a proper compensation action later. primaryView = null; views.clear(); cfg.unmarshal(this); } clouds.setOwner(this); 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[] subdirs = projectsDir.listFiles(new FileFilter() { public boolean accept(File child) { return child.isDirectory() && Items.getConfigFile(child).exists(); } }); items.clear(); if (PARALLEL_LOAD) { // load jobs in parallel for better performance LOGGER.info("Loading in " + TWICE_CPU_NUM + " parallel threads"); List<Future<TopLevelItem>> loaders = new ArrayList<Future<TopLevelItem>>(); for (final File subdir : subdirs) { loaders.add(threadPoolForLoad.submit(new Callable<TopLevelItem>() { public TopLevelItem call() throws Exception { Thread t = Thread.currentThread(); String name = t.getName(); t.setName("Loading " + subdir); try { long start = System.currentTimeMillis(); TopLevelItem item = (TopLevelItem) Items.load(Hudson.this, subdir); if (LOG_STARTUP_PERFORMANCE) LOGGER.info("Loaded " + item.getName() + " in " + (System.currentTimeMillis() - start) + "ms by " + name); return item; } finally { t.setName(name); } } })); } for (Future<TopLevelItem> loader : loaders) { try { TopLevelItem item = loader.get(); items.put(item.getName(), item); } catch (ExecutionException e) { LOGGER.log(Level.WARNING, "Failed to load a project", e.getCause()); } catch (InterruptedException e) { e.printStackTrace(); // this is probably not the right thing to do } } } else { for (File subdir : subdirs) { try { long start = System.currentTimeMillis(); TopLevelItem item = (TopLevelItem) Items.load(this, subdir); if (LOG_STARTUP_PERFORMANCE) LOGGER.info( "Loaded " + item.getName() + " in " + (System.currentTimeMillis() - start) + "ms"); items.put(item.getName(), item); } catch (Error e) { LOGGER.log(Level.WARNING, "Failed to load " + subdir, e); } catch (RuntimeException e) { LOGGER.log(Level.WARNING, "Failed to load " + subdir, e); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to load " + subdir, e); } } } rebuildDependencyGraph(); {// recompute label objects for (Node slave : slaves) 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 = this; views.add(0, v); primaryView = v.getViewName(); } // read in old data that doesn't have the security field set if (authorizationStrategy == null) { if (useSecurity == null || !useSecurity) authorizationStrategy = AuthorizationStrategy.UNSECURED; else authorizationStrategy = new LegacyAuthorizationStrategy(); } if (securityRealm == null) { if (useSecurity == null || !useSecurity) setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); else setSecurityRealm(new LegacySecurityRealm()); } else { // force the set to proxy setSecurityRealm(securityRealm); } if (useSecurity != null && !useSecurity) { // forced reset to the unsecure mode. // this works as an escape hatch for people who locked themselves out. authorizationStrategy = AuthorizationStrategy.UNSECURED; setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); } // Initialize the filter with the crumb issuer setCrumbIssuer(crumbIssuer); // auto register root actions actions.addAll(getExtensionList(RootAction.class)); LOGGER.info(String.format("Took %s ms to load", System.currentTimeMillis() - startTime)); if (KILL_AFTER_LOAD) System.exit(0); } /** * Save the settings to a file. */ public synchronized void save() throws IOException { if (BulkChange.contains(this)) return; getConfigFile().write(this); } /** * 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()); } if (udpBroadcastThread != null) udpBroadcastThread.shutdown(); 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 (getRootDir().exists()) // if we are aborting because we failed to create HUDSON_HOME, // don't try to save. Issue #536 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.log(Level.WARNING, "Failed to shut down properly", e); } catch (TimeoutException e) { LOGGER.log(Level.WARNING, "Failed to shut down properly", e); } 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; } // // // 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); req.setCharacterEncoding("UTF-8"); JSONObject json = req.getSubmittedForm(); // keep using 'useSecurity' field as the main configuration setting // until we get the new security implementation working // useSecurity = null; if (json.has("use_security")) { useSecurity = true; JSONObject security = json.getJSONObject("use_security"); setSecurityRealm(SecurityRealm.all().newInstanceFromRadioList(security, "realm")); setAuthorizationStrategy( AuthorizationStrategy.all().newInstanceFromRadioList(security, "authorization")); } else { useSecurity = null; setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); authorizationStrategy = AuthorizationStrategy.UNSECURED; } if (json.has("csrf")) { JSONObject csrf = json.getJSONObject("csrf"); setCrumbIssuer(CrumbIssuer.all().newInstanceFromRadioList(csrf, "issuer")); } else { setCrumbIssuer(null); } primaryView = json.has("primaryView") ? json.getString("primaryView") : getViews().iterator().next().getViewName(); noUsageStatistics = json.has("usageStatisticsCollected") ? null : true; { String v = req.getParameter("slaveAgentPortType"); if (!isUseSecurity() || v == null || v.equals("random")) slaveAgentPort = 0; else if (v.equals("disable")) slaveAgentPort = -1; else { try { slaveAgentPort = Integer.parseInt(req.getParameter("slaveAgentPort")); } catch (NumberFormatException e) { throw new FormException(Messages.Hudson_BadPortNumber(req.getParameter("slaveAgentPort")), "slaveAgentPort"); } } // 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); } } } numExecutors = Integer.parseInt(req.getParameter("_.numExecutors")); if (req.hasParameter("master.mode")) mode = Mode.valueOf(req.getParameter("master.mode")); else mode = Mode.NORMAL; label = fixNull(req.getParameter("_.labelString")); labelSet = null; quietPeriod = Integer.parseInt(req.getParameter("quiet_period")); scmCheckoutRetryCount = Integer.parseInt(req.getParameter("retry_count")); systemMessage = Util.nullify(req.getParameter("system_message")); jdks.clear(); jdks.addAll(req.bindJSONToList(JDK.class, json.get("jdks"))); boolean result = true; for (Descriptor<Builder> d : Builder.all()) result &= configureDescriptor(req, json, d); for (Descriptor<Publisher> d : Publisher.all()) result &= configureDescriptor(req, json, d); for (Descriptor<BuildWrapper> d : BuildWrapper.all()) result &= configureDescriptor(req, json, d); for (SCMDescriptor scmd : SCM.all()) result &= configureDescriptor(req, json, scmd); for (TriggerDescriptor d : Trigger.all()) result &= configureDescriptor(req, json, d); for (JobPropertyDescriptor d : JobPropertyDescriptor.all()) result &= configureDescriptor(req, json, d); for (PageDecorator d : PageDecorator.all()) result &= configureDescriptor(req, json, d); for (Descriptor<CrumbIssuer> d : CrumbIssuer.all()) result &= configureDescriptor(req, json, d); for (ToolDescriptor d : ToolInstallation.all()) 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 { JSONObject form = req.getSubmittedForm(); 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, ServletException { checkPermission(ADMINISTER); isQuietingDown = true; return new HttpRedirect("."); } public synchronized HttpRedirect doCancelQuietDown() throws IOException, ServletException { 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 { checkPermission(Job.CREATE); TopLevelItem result; String requestContentType = req.getContentType(); if (requestContentType == null) { rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, "No Content-Type header set"); return null; } boolean isXmlSubmission = requestContentType.startsWith("application/xml") || requestContentType.startsWith("text/xml"); if (!isXmlSubmission) { // containers often implement RFCs incorrectly in that it doesn't interpret query parameter // decoding with UTF-8. This will ensure we get it right. // but doing this for config.xml submission could potentiall overwrite valid // "text/xml;charset=xxx" req.setCharacterEncoding("UTF-8"); } String name = req.getParameter("name"); if (name == null) { rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Query parameter 'name' is required"); return null; } name = name.trim(); String mode = req.getParameter("mode"); try { checkGoodName(name); } catch (ParseException e) { rsp.setStatus(SC_BAD_REQUEST); sendError(e, req, rsp); return null; } if (getItem(name) != null) { rsp.setStatus(SC_BAD_REQUEST); sendError(Messages.Hudson_JobAlreadyExists(name), req, rsp); return null; } if (mode != null && mode.equals("copy")) { String from = req.getParameter("from"); TopLevelItem src = getItem(from); if (src == null) { rsp.setStatus(SC_BAD_REQUEST); if (Util.fixEmpty(from) == null) sendError("Specify which job to copy", req, rsp); else sendError("No such job: " + from, req, rsp); return null; } result = copy(src, name); } else { if (isXmlSubmission) { result = createProjectFromXML(name, req.getInputStream()); rsp.setStatus(HttpServletResponse.SC_OK); return result; } else { // create empty job and redirect to the project config screen if (mode == null) { rsp.sendError(SC_BAD_REQUEST); return null; } result = createProject(Items.getDescriptor(mode), name); } for (ItemListener l : ItemListener.all()) l.onCreated(result); } // send the browser to the config page rsp.sendRedirect2(result.getUrl() + "configure"); return result; } /** * 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 { // place it as config.xml File configXml = Items.getConfigFile(getRootDirFor(name)).getFile(); configXml.getParentFile().mkdirs(); try { FileOutputStream fos = new FileOutputStream(configXml); try { Util.copyStream(xml, fos); } finally { fos.close(); } // load it TopLevelItem result = (TopLevelItem) Items.load(this, configXml.getParentFile()); items.put(name, result); return result; } catch (IOException e) { // if anything fails, delete the config file to avoid further confusion Util.deleteRecursive(configXml.getParentFile()); throw e; } } /** * 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 { T result = (T) createProject(src.getDescriptor(), name); // copy config Util.copyFile(Items.getConfigFile(src).getFile(), Items.getConfigFile(result).getFile()); // reload from the new config result = (T) Items.load(this, result.getRootDir()); result.onCopiedFrom(src); items.put(name, result); for (ItemListener l : ItemListener.all()) l.onCreated(result); return result; } // 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 { try { checkPermission(View.CREATE); addView(View.create(req, rsp, this)); } catch (ParseException e) { sendError(e, req, rsp); } } /** * 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 ParseException { if (name == null || name.length() == 0) throw new ParseException(Messages.Hudson_NoName(), 0); for (int i = 0; i < name.length(); i++) { char ch = name.charAt(i); if (Character.isISOControl(ch)) { throw new ParseException(Messages.Hudson_ControlCodeNotAllowed(toPrintableName(name)), i); } if ("?*/\\%!@#$^&|<>[]:;".indexOf(ch) != -1) throw new ParseException(Messages.Hudson_UnsafeChar(ch), i); } // looks good } 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.getRestOfPath(); 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"); 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; } String url = AbstractProcessingFilter.obtainFullRequestUrl(req); if (url != null) { // if the login redirect is initiated by Acegi // 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 { securityRealm.doLogout(req, rsp); } /** * Serves jar files for JNLP slave agents. */ public Slave.JnlpJar getJnlpJars(String fileName) { return new Slave.JnlpJar(fileName); } /** * 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. */ public synchronized void doReload(StaplerRequest req, StaplerResponse rsp) throws IOException { checkPermission(ADMINISTER); // engage "loading ..." UI and then run the actual task in a separate thread final ServletContext context = req.getServletContext(); context.setAttribute("app", new HudsonIsLoading()); rsp.sendRedirect2(req.getContextPath() + "/"); new Thread("Hudson config reload thread") { public void run() { try { load(); User.reload(); context.setAttribute("app", Hudson.this); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Failed to reload Hudson config", e); } } }.start(); } /** * 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(); } } /** * Serves static resources without the "Last-Modified" header to work around * a bug in Firefox. * * <p> * See https://bugzilla.mozilla.org/show_bug.cgi?id=89419 */ public void doNocacheImages(StaplerRequest req, StaplerResponse rsp) throws IOException { String path = req.getRestOfPath(); if (path.length() == 0) path = "/"; if (path.indexOf("..") != -1 || path.length() < 1) { // don't serve anything other than files in the artifacts dir rsp.sendError(SC_BAD_REQUEST); return; } File f = new File(req.getServletContext().getRealPath("/images"), path.substring(1)); if (!f.exists()) { rsp.sendError(SC_NOT_FOUND); return; } if (f.isDirectory()) { // listing not allowed rsp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } FileInputStream in = new FileInputStream(f); // serve the file String contentType = req.getServletContext().getMimeType(f.getPath()); rsp.setContentType(contentType); rsp.setContentLength((int) f.length()); Util.copyStream(in, rsp.getOutputStream()); in.close(); } /** * For debugging. Expose URL to perform GC. */ public void doGc(StaplerResponse rsp) throws IOException { System.gc(); rsp.setStatus(HttpServletResponse.SC_OK); rsp.setContentType("text/plain"); rsp.getWriter().println("GCed"); } 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 { checkPermission(READ); if (!"POST".equals(Stapler.getCurrentRequest().getMethod())) { // for GET request, serve _cli.jelly, assuming this is a browser req.getView(this, "_cli.jelly").forward(req, rsp); return; } UUID uuid = UUID.fromString(req.getHeader("Session")); rsp.setHeader("Hudson-Duplex", ""); // set the header so that the client would know final Authentication auth = getAuthentication(); 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 { channel.setProperty(CliEntryPoint.class.getName(), new CliManagerImpl(auth)); } }); 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.gif", true); } /** * Perform a restart of Hudson, if we can. * * This first replaces "app" to {@link HudsonIsRestarting} */ public void doRestart(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { checkPermission(ADMINISTER); if (Stapler.getCurrentRequest().getMethod().equals("GET")) { req.getView(this, "_restart.jelly").forward(req, rsp); return; } restart(); rsp.sendRedirect2("."); } /** * Performs a restart. */ public void restart() throws ServletException, IOException { final Lifecycle lifecycle = Lifecycle.get(); if (!lifecycle.canRestart()) throw new Failure("Restart is not supported in this running mode."); servletContext.setAttribute("app", new HudsonIsRestarting()); new Thread("restart thread") { @Override public void run() { try { // give some time for the browser to load the "reloading" page Thread.sleep(5000); lifecycle.restart(); } catch (InterruptedException e) { LOGGER.log(Level.WARNING, "Failed to restart Hudson", e); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to restart Hudson", e); } } }.start(); } /** * Shutdown the system. * @since 1.161 */ public void doExit(StaplerRequest req, StaplerResponse rsp) throws IOException { checkPermission(ADMINISTER); LOGGER.severe(String.format("Shutting down VM as requested by %s from %s", getAuthentication(), req.getRemoteAddr())); rsp.setStatus(HttpServletResponse.SC_OK); rsp.setContentType("text/plain"); PrintWriter w = rsp.getWriter(); w.println("Shutting down"); w.close(); System.exit(0); } /** * Gets the {@link Authentication} object that represents the user * associated with the current request. */ public static Authentication getAuthentication() { Authentication a = SecurityContextHolder.getContext().getAuthentication(); // on Tomcat while serving the login page, this is null despite the fact // that we have filters. Looking at the stack trace, Tomcat doesn't seem to // run the request through filters when this is the login request. // see http://www.nabble.com/Matrix-authorization-problem-tp14602081p14886312.html if (a == null) a = new AnonymousAuthenticationToken("anonymous", "anonymous", new GrantedAuthority[] { new GrantedAuthorityImpl("anonymous") }); return a; } /** * For system diagnostics. * Run arbitrary Groovy script. */ public void doScript(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { doScript(req, rsp, req.getView(this, "_script.jelly")); } /** * Run arbitrary Groovy script and return result as plain text. */ public void doScriptText(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { doScript(req, rsp, req.getView(this, "_scriptText.jelly")); } private void doScript(StaplerRequest req, StaplerResponse rsp, RequestDispatcher view) throws IOException, ServletException { // ability to run arbitrary script is dangerous checkPermission(ADMINISTER); String text = req.getParameter("script"); if (text != null) { try { req.setAttribute("output", RemotingDiagnostics.executeGroovy(text, MasterComputer.localChannel)); } 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())); } /** * Checks if the top-level item with the given name exists. */ public FormValidation doItemExistsCheck(@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); String job = fixEmpty(value); if (job == null) return FormValidation.ok(); if (getItem(job) == null) return FormValidation.ok(); else return FormValidation.error(Messages.Hudson_JobAlreadyExists(job)); } /** * 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". */ 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(1); path = path.substring(path.indexOf('/') + 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://hudson.gotdns.com/wiki/display/HUDSON/Tomcat#Tomcat-i18n */ public FormValidation doCheckURIEncoding(StaplerRequest request, @QueryParameter String value) throws IOException { request.setCharacterEncoding("UTF-8"); // expected is non-ASCII String final String expected = "\u57f7\u4e8b"; value = fixEmpty(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")); } 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").startsWith("mac"); } /** * Returns all {@code CVSROOT} strings used in the current Hudson installation. * * <p> * Ideally this shouldn't be defined in here * but EL doesn't provide a convenient way of invoking a static function, * so I'm putting it here for now. */ public Set<String> getAllCvsRoots() { Set<String> r = new TreeSet<String>(); for (AbstractProject p : getAllItems(AbstractProject.class)) { SCM scm = p.getScm(); if (scm instanceof CVSSCM) { CVSSCM cvsscm = (CVSSCM) scm; r.add(cvsscm.getCvsRoot()); } } return r; } /** * 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("/tcpSlaveAgentListener") || rest.startsWith("/securityRealm")) return this; // URLs that are always visible without READ permission throw e; } return this; } /** * 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()}. */ 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(); } public String getUrl() { return "computer/(master)/"; } public RetentionStrategy getRetentionStrategy() { return RetentionStrategy.NOOP; } /** * Report an error. */ @Override public void doDoDelete(StaplerResponse rsp) throws IOException { rsp.sendError(SC_BAD_REQUEST); } 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; return super.hasPermission(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); } /** * @deprecated * Use {@link #checkPermission(Permission)} */ public static boolean adminCheck() throws IOException { return adminCheck(Stapler.getCurrentRequest(), Stapler.getCurrentResponse()); } /** * @deprecated * 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 * 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 Hudson.getInstance().getACL().hasPermission(ADMINISTER); } /** * @deprecated * 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 = "?"; /** * 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"); public static boolean FLYWEIGHT_SUPPORT = Boolean.getBoolean(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; private static final Logger LOGGER = Logger.getLogger(Hudson.class.getName()); 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); 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(); // doule check that initialization order didn't do any harm assert PERMISSIONS != null; assert ADMINISTER != null; } }