/* * The MIT License * * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Erik Ramfelt, Koichi Fujikawa, Red Hat, Inc., Seiji Sogabe, * Stephen Connolly, Tom Huybrechts, Yahoo! Inc., Alan Harder, CloudBees, Inc., * 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 jenkins.model; import antlr.ANTLRException; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.inject.Injector; import com.thoughtworks.xstream.XStream; import hudson.BulkChange; import hudson.DNSMultiCast; import hudson.DescriptorExtensionList; import hudson.Extension; import hudson.ExtensionComponent; import hudson.ExtensionFinder; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; import hudson.Launcher.LocalLauncher; import hudson.Lookup; import hudson.Main; import hudson.Plugin; import hudson.PluginManager; import hudson.PluginWrapper; import hudson.ProxyConfiguration; import jenkins.AgentProtocol; import jenkins.util.SystemProperties; import hudson.TcpSlaveAgentListener; import hudson.UDPBroadcastThread; import hudson.Util; import hudson.WebAppMain; import hudson.XmlFile; import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; import hudson.init.InitMilestone; import hudson.init.InitStrategy; import hudson.init.TermMilestone; import hudson.init.TerminatorFinder; import hudson.lifecycle.Lifecycle; import hudson.lifecycle.RestartNotSupportedException; import hudson.logging.LogRecorderManager; import hudson.markup.EscapedMarkupFormatter; import hudson.markup.MarkupFormatter; import hudson.model.AbstractCIBase; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.AdministrativeMonitor; import hudson.model.AllView; import hudson.model.Api; import hudson.model.Computer; import hudson.model.ComputerSet; import hudson.model.DependencyGraph; import hudson.model.Describable; import hudson.model.Descriptor; import hudson.model.Descriptor.FormException; import hudson.model.DescriptorByNameOwner; import hudson.model.DirectoryBrowserSupport; import hudson.model.Failure; import hudson.model.Fingerprint; import hudson.model.FingerprintCleanupThread; import hudson.model.FingerprintMap; import hudson.model.Hudson; import hudson.model.Item; import hudson.model.ItemGroup; import hudson.model.ItemGroupMixIn; import hudson.model.Items; import hudson.model.JDK; import hudson.model.Job; import hudson.model.JobPropertyDescriptor; import hudson.model.Label; import hudson.model.ListView; import hudson.model.LoadBalancer; import hudson.model.LoadStatistics; import hudson.model.ManagementLink; import hudson.model.Messages; import hudson.model.ModifiableViewGroup; import hudson.model.NoFingerprintMatch; import hudson.model.Node; import hudson.model.OverallLoadStatistics; import hudson.model.PaneStatusProperties; import hudson.model.Project; import hudson.model.Queue; import hudson.model.Queue.FlyweightTask; import hudson.model.RestartListener; import hudson.model.RootAction; import hudson.model.Slave; import hudson.model.TaskListener; import hudson.model.TopLevelItem; import hudson.model.TopLevelItemDescriptor; import hudson.model.UnprotectedRootAction; import hudson.model.UpdateCenter; import hudson.model.User; import hudson.model.View; import hudson.model.ViewGroupMixIn; import hudson.model.WorkspaceCleanupThread; import hudson.model.labels.LabelAtom; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SCMListener; import hudson.model.listeners.SaveableListener; import hudson.remoting.Callable; import hudson.remoting.LocalChannel; import hudson.remoting.VirtualChannel; import hudson.scm.RepositoryBrowser; import hudson.scm.SCM; import hudson.search.CollectionSearchIndex; import hudson.search.SearchIndexBuilder; import hudson.search.SearchItem; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.AuthorizationStrategy; import hudson.security.BasicAuthenticationFilter; import hudson.security.FederatedLoginService; import hudson.security.FullControlOnceLoggedInAuthorizationStrategy; import hudson.security.HudsonFilter; import hudson.security.LegacyAuthorizationStrategy; import hudson.security.LegacySecurityRealm; import hudson.security.Permission; import hudson.security.PermissionGroup; import hudson.security.PermissionScope; import hudson.security.SecurityMode; import hudson.security.SecurityRealm; import hudson.security.csrf.CrumbIssuer; import hudson.slaves.Cloud; import hudson.slaves.ComputerListener; import hudson.slaves.DumbSlave; import hudson.slaves.NodeDescriptor; import hudson.slaves.NodeList; import hudson.slaves.NodeProperty; import hudson.slaves.NodePropertyDescriptor; import hudson.slaves.NodeProvisioner; import hudson.slaves.OfflineCause; import hudson.slaves.RetentionStrategy; import hudson.tasks.BuildWrapper; import hudson.tasks.Builder; import hudson.tasks.Publisher; import hudson.triggers.SafeTimerTask; import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; import hudson.util.AdministrativeError; import hudson.util.CaseInsensitiveComparator; import hudson.util.ClockDifference; import hudson.util.CopyOnWriteList; import hudson.util.CopyOnWriteMap; import hudson.util.DaemonThreadFactory; import hudson.util.DescribableList; import hudson.util.FormApply; import hudson.util.FormValidation; import hudson.util.Futures; import hudson.util.HudsonIsLoading; import hudson.util.HudsonIsRestarting; import hudson.util.IOUtils; import hudson.util.Iterators; import hudson.util.JenkinsReloadFailed; import hudson.util.Memoizer; import hudson.util.MultipartFormDataParser; import hudson.util.NamingThreadFactory; import hudson.util.PluginServletFilter; import hudson.util.RemotingDiagnostics; import hudson.util.RemotingDiagnostics.HeapDump; import hudson.util.TextFile; import hudson.util.TimeUnit2; import hudson.util.VersionNumber; import hudson.util.XStream2; import hudson.views.DefaultMyViewsTabBar; import hudson.views.DefaultViewsTabBar; import hudson.views.MyViewsTabBar; import hudson.views.ViewsTabBar; import hudson.widgets.Widget; import java.util.Objects; import java.util.TimerTask; import java.util.concurrent.CountDownLatch; import jenkins.ExtensionComponentSet; import jenkins.ExtensionRefreshException; import jenkins.InitReactorRunner; import jenkins.install.InstallState; import jenkins.install.InstallUtil; import jenkins.install.SetupWizard; import jenkins.model.ProjectNamingStrategy.DefaultProjectNamingStrategy; import jenkins.security.ConfidentialKey; import jenkins.security.ConfidentialStore; import jenkins.security.SecurityListener; import jenkins.security.MasterToSlaveCallable; import jenkins.slaves.WorkspaceLocator; import jenkins.util.JenkinsJVM; import jenkins.util.Timer; import jenkins.util.io.FileBoolean; import jenkins.util.xml.XMLUtils; import net.jcip.annotations.GuardedBy; import net.sf.json.JSONObject; import org.acegisecurity.AccessDeniedException; import org.acegisecurity.AcegiSecurityException; import org.acegisecurity.Authentication; import org.acegisecurity.GrantedAuthority; import org.acegisecurity.GrantedAuthorityImpl; import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.acegisecurity.ui.AbstractProcessingFilter; import org.apache.commons.jelly.JellyException; import org.apache.commons.jelly.Script; import org.apache.commons.logging.LogFactory; import org.jvnet.hudson.reactor.Executable; import org.jvnet.hudson.reactor.Milestone; import org.jvnet.hudson.reactor.Reactor; import org.jvnet.hudson.reactor.ReactorException; import org.jvnet.hudson.reactor.ReactorListener; import org.jvnet.hudson.reactor.Task; import org.jvnet.hudson.reactor.TaskBuilder; import org.jvnet.hudson.reactor.TaskGraphBuilder; import org.jvnet.hudson.reactor.TaskGraphBuilder.Handle; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.args4j.Argument; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.MetaClass; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerFallback; import org.kohsuke.stapler.StaplerProxy; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.WebApp; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.framework.adjunct.AdjunctManager; import org.kohsuke.stapler.interceptor.RequirePOST; import org.kohsuke.stapler.jelly.JellyClassLoaderTearOff; import org.kohsuke.stapler.jelly.JellyRequestDispatcher; import org.xml.sax.InputSource; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.crypto.SecretKey; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.net.BindException; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.Charset; import java.security.SecureRandom; 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.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeSet; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import static hudson.Util.*; import static hudson.init.InitMilestone.*; import hudson.init.Initializer; import hudson.util.LogTaskListener; import static java.util.logging.Level.*; import static javax.servlet.http.HttpServletResponse.*; import org.kohsuke.stapler.WebMethod; /** * Root object of the system. * * @author Kohsuke Kawaguchi */ @ExportedBean public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLevelItemGroup, StaplerProxy, StaplerFallback, ModifiableViewGroup, AccessControlled, DescriptorByNameOwner, ModelObjectWithContextMenu, ModelObjectWithChildren { private transient final Queue queue; /** * Stores various objects scoped to {@link Jenkins}. */ public transient final Lookup lookup = new Lookup(); /** * We update this field to the current version of Jenkins 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"; /** * The Jenkins instance startup type i.e. NEW, UPGRADE etc */ private transient InstallState installState = InstallState.UNKNOWN; /** * If we're in the process of an initial setup, * this will be set */ private transient SetupWizard setupWizard; /** * 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 Jenkins. * <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 Jenkins. * <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; /** * Disables the remember me on this computer option in the standard login screen. * * @since 1.534 */ private volatile boolean disableRememberMe; /** * The project naming strategy defines/restricts the names which can be given to a project/job. e.g. does the name have to follow a naming convention? */ private ProjectNamingStrategy projectNamingStrategy = DefaultProjectNamingStrategy.DEFAULT_NAMING_STRATEGY; /** * Root directory for the workspaces. * This value will be variable-expanded as per {@link #expandVariablesForDirectory}. * @see #getWorkspaceFor(TopLevelItem) */ private String workspaceDir = "${ITEM_ROOTDIR}/"+WORKSPACE_DIRNAME; /** * Root directory for the builds. * This value will be variable-expanded as per {@link #expandVariablesForDirectory}. * @see #getBuildDirFor(Job) */ private String buildsDir = "${ITEM_ROOTDIR}/builds"; /** * Message displayed in the top page. */ private String systemMessage; private MarkupFormatter markupFormatter; /** * Root directory of the system. */ public transient final File root; /** * Where are we in the initialization? */ private transient volatile InitMilestone initLevel = InitMilestone.STARTED; /** * All {@link Item}s keyed by their {@link Item#getName() name}s. */ /*package*/ transient final Map<String,TopLevelItem> items = new CopyOnWriteMap.Tree<String,TopLevelItem>(CaseInsensitiveComparator.INSTANCE); /** * The sole instance. */ private static Jenkins theInstance; private transient volatile boolean isQuietingDown; private transient volatile boolean terminating; @GuardedBy("Jenkins.class") private transient boolean cleanUpStarted; private volatile List<JDK> jdks = new ArrayList<JDK>(); private transient volatile DependencyGraph dependencyGraph; private final transient AtomicBoolean dependencyGraphDirty = new AtomicBoolean(); /** * Currently active Views tab bar. */ private volatile ViewsTabBar viewsTabBar = new DefaultViewsTabBar(); /** * Currently active My Views tab bar. */ private volatile MyViewsTabBar myViewsTabBar = new DefaultMyViewsTabBar(); /** * 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(Jenkins.this,key); } }; /** * All {@link DescriptorExtensionList} keyed by their {@link DescriptorExtensionList#describableType}. */ private transient final Memoizer<Class,DescriptorExtensionList> descriptorLists = new Memoizer<Class,DescriptorExtensionList>() { public DescriptorExtensionList compute(Class key) { return DescriptorExtensionList.createDescriptorList(Jenkins.this,key); } }; /** * {@link Computer}s in this Jenkins system. Read-only. */ protected transient final Map<Node,Computer> computers = new CopyOnWriteMap.Hash<Node,Computer>(); /** * Active {@link Cloud}s. */ public final Hudson.CloudList clouds = new Hudson.CloudList(this); public static class CloudList extends DescribableList<Cloud,Descriptor<Cloud>> { public CloudList(Jenkins h) { super(h); } public CloudList() {// needed for XStream deserialization } public Cloud getByName(String name) { for (Cloud c : this) if (c.name.equals(name)) return c; return null; } @Override protected void onModified() throws IOException { super.onModified(); Jenkins.getInstance().trimLabels(); } } /** * Legacy store of the set of installed cluster nodes. * @deprecated in favour of {@link Nodes} */ @Deprecated protected transient volatile NodeList slaves; /** * The holder of the set of installed cluster nodes. * * @since 1.607 */ private transient final Nodes nodes = new Nodes(this); /** * 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 ViewGroupMixIn viewGroupMixIn = new ViewGroupMixIn(this) { protected List<View> views() { return views; } protected String primaryView() { return primaryView; } protected void primaryView(String name) { primaryView=name; } }; private transient final FingerprintMap fingerprintMap = new FingerprintMap(); /** * Loaded plugins. */ public transient final PluginManager pluginManager; public transient volatile TcpSlaveAgentListener tcpSlaveAgentListener; private transient final Object tcpSlaveAgentListenerLock = new Object(); private transient UDPBroadcastThread udpBroadcastThread; private transient DNSMultiCast dnsMultiCast; /** * List of registered {@link SCMListener}s. */ private transient final CopyOnWriteList<SCMListener> scmListeners = new CopyOnWriteList<SCMListener>(); /** * TCP agent port. * 0 for random, -1 to disable. */ private int slaveAgentPort = SystemProperties.getInteger(Jenkins.class.getName()+".slaveAgentPort",0); /** * The TCP agent protocols that are explicitly disabled (we store the disabled ones so that newer protocols * are enabled by default). Will be {@code null} instead of empty to simplify XML format. * * @since 2.16 */ @CheckForNull private List<String> disabledAgentProtocols; /** * The TCP agent protocols that are {@link AgentProtocol#isOptIn()} and explicitly enabled. * Will be {@code null} instead of empty to simplify XML format. * * @since 2.16 */ @CheckForNull private List<String> enabledAgentProtocols; /** * The TCP agent protocols that are enabled. Built from {@link #disabledAgentProtocols} and * {@link #enabledAgentProtocols}. * * @since 2.16 * @see #setAgentProtocols(Set) * @see #getAgentProtocols() */ private transient Set<String> agentProtocols; /** * 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 Jenkins. This allows us to reuse the same label instances * as much as possible, even though that's not a strict requirement. */ private transient final ConcurrentHashMap<String,Label> labels = new ConcurrentHashMap<String,Label>(); /** * Load statistics of the entire system. * * This includes every executor and every job in the system. */ @Exported public transient final OverallLoadStatistics overallLoad = new OverallLoadStatistics(); /** * Load statistics of the free roaming jobs and agents. * * This includes all executors on {@link hudson.model.Node.Mode#NORMAL} nodes and jobs that do not have any assigned nodes. * * @since 1.467 */ @Exported public transient final LoadStatistics unlabeledLoad = new UnlabeledLoadStatistics(); /** * {@link NodeProvisioner} that reacts to {@link #unlabeledLoad}. * @since 1.467 */ public transient final NodeProvisioner unlabeledNodeProvisioner = new NodeProvisioner(null,unlabeledLoad); /** * @deprecated as of 1.467 * Use {@link #unlabeledNodeProvisioner}. * This was broken because it was tracking all the executors in the system, but it was only tracking * free-roaming jobs in the queue. So {@link Cloud} fails to launch nodes when you have some exclusive * agents and free-roaming jobs in the queue. */ @Restricted(NoExternalUse.class) @Deprecated public transient final NodeProvisioner overallNodeProvisioner = unlabeledNodeProvisioner; 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); /** * Widgets on Jenkins. */ private transient final List<Widget> widgets = getExtensionList(Widget.class); /** * {@link AdjunctManager} */ private transient final AdjunctManager adjuncts; /** * Code that handles {@link ItemGroup} work. */ private transient final ItemGroupMixIn itemGroupMixIn = new ItemGroupMixIn(this,this) { @Override protected void add(TopLevelItem item) { items.put(item.getName(),item); } @Override protected File getRootDirFor(String name) { return Jenkins.this.getRootDirFor(name); } }; /** * Hook for a test harness to intercept Jenkins.getInstance() * * Do not use in the production code as the signature may change. */ public interface JenkinsHolder { @CheckForNull Jenkins getInstance(); } static JenkinsHolder HOLDER = new JenkinsHolder() { public @CheckForNull Jenkins getInstance() { return theInstance; } }; /** * Gets the {@link Jenkins} singleton. * {@link #getInstanceOrNull()} provides the unchecked versions of the method. * @return {@link Jenkins} instance * @throws IllegalStateException {@link Jenkins} has not been started, or was already shut down * @since 1.590 * @deprecated use {@link #getInstance()} */ @Deprecated @Nonnull public static Jenkins getActiveInstance() throws IllegalStateException { Jenkins instance = HOLDER.getInstance(); if (instance == null) { throw new IllegalStateException("Jenkins has not been started, or was already shut down"); } return instance; } /** * Gets the {@link Jenkins} singleton. * {@link #getActiveInstance()} provides the checked versions of the method. * @return The instance. Null if the {@link Jenkins} instance has not been started, * or was already shut down * @since 1.653 */ @CheckForNull public static Jenkins getInstanceOrNull() { return HOLDER.getInstance(); } /** * Gets the {@link Jenkins} singleton. In certain rare cases you may have code that is intended to run before * Jenkins starts or while Jenkins is being shut-down. For those rare cases use {@link #getInstanceOrNull()}. * In other cases you may have code that might end up running on a remote JVM and not on the Jenkins master, * for those cases you really should rewrite your code so that when the {@link Callable} is sent over the remoting * channel it uses a {@code writeReplace} method or similar to ensure that the {@link Jenkins} class is not being * loaded into the remote class loader * @return The instance. * @throws IllegalStateException {@link Jenkins} has not been started, or was already shut down */ @CLIResolver @Nonnull public static Jenkins getInstance() { Jenkins instance = HOLDER.getInstance(); if (instance == null) { if(SystemProperties.getBoolean(Jenkins.class.getName()+".enableExceptionOnNullInstance")) { // TODO: remove that second block around 2.20 (that is: ~20 versions to battle test it) // See https://github.com/jenkinsci/jenkins/pull/2297#issuecomment-216710150 throw new IllegalStateException("Jenkins has not been started, or was already shut down"); } } return instance; } /** * Secret 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 = UpdateCenter.createUpdateCenter(null); /** * 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(); private transient final boolean oldJenkinsJVM; protected Jenkins(File root, ServletContext context) throws IOException, InterruptedException, ReactorException { this(root, context, null); } /** * @param pluginManager * If non-null, use existing plugin manager. create a new one. */ @edu.umd.cs.findbugs.annotations.SuppressWarnings({ "SC_START_IN_CTOR", // bug in FindBugs. It flags UDPBroadcastThread.start() call but that's for another class "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD" // Trigger.timer }) protected Jenkins(File root, ServletContext context, PluginManager pluginManager) throws IOException, InterruptedException, ReactorException { oldJenkinsJVM = JenkinsJVM.isJenkinsJVM(); // capture to restore in cleanUp() JenkinsJVMAccess._setJenkinsJVM(true); // set it for unit tests as they will not have gone through WebAppMain long start = System.currentTimeMillis(); // As Jenkins is starting, grant this process full control ACL.impersonate(ACL.SYSTEM); try { this.root = root; this.servletContext = context; computeVersion(context); if(theInstance!=null) throw new IllegalStateException("second instance"); theInstance = this; if (!new File(root,"jobs").exists()) { // if this is a fresh install, use more modern default layout that's consistent with agents workspaceDir = "${JENKINS_HOME}/workspace/${ITEM_FULLNAME}"; } // doing this early allows InitStrategy to set environment upfront final InitStrategy is = InitStrategy.get(Thread.currentThread().getContextClassLoader()); Trigger.timer = new java.util.Timer("Jenkins cron thread"); queue = new Queue(LoadBalancer.CONSISTENT_HASH); 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(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); // this marker indicates that the secret.key is generated by the version of Jenkins post SECURITY-49. // this indicates that there's no need to rewrite secrets on disk new FileBoolean(new File(root,"secret.key.not-so-secret")).on(); } try { proxy = ProxyConfiguration.load(); } catch (IOException e) { LOGGER.log(SEVERE, "Failed to load proxy configuration", e); } if (pluginManager==null) pluginManager = PluginManager.createDefault(this); this.pluginManager = pluginManager; // JSON binding needs to be able to see all the classes from all the plugins WebApp.get(servletContext).setClassLoader(pluginManager.uberClassLoader); adjuncts = new AdjunctManager(servletContext, pluginManager.uberClassLoader,"adjuncts/"+SESSION_HASH, TimeUnit2.DAYS.toMillis(365)); // initialization consists of ... executeReactor( is, pluginManager.initTasks(is), // loading and preparing plugins loadTasks(), // load jobs InitMilestone.ordering() // forced ordering among key milestones ); if(KILL_AFTER_LOAD) System.exit(0); setupWizard = new SetupWizard(); InstallUtil.proceedToNextStateFrom(InstallState.UNKNOWN); launchTcpSlaveAgentListener(); if (UDPBroadcastThread.PORT != -1) { try { udpBroadcastThread = new UDPBroadcastThread(this); udpBroadcastThread.start(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to broadcast over UDP (use -Dhudson.udp=-1 to disable)", e); } } dnsMultiCast = new DNSMultiCast(this); Timer.get().scheduleAtFixedRate(new SafeTimerTask() { @Override protected void doRun() throws Exception { trimLabels(); } }, TimeUnit2.MINUTES.toMillis(5), TimeUnit2.MINUTES.toMillis(5), TimeUnit.MILLISECONDS); updateComputerList(); {// master is online now Computer c = toComputer(); if(c!=null) for (ComputerListener cl : ComputerListener.all()) cl.onOnline(c, new LogTaskListener(LOGGER, INFO)); } for (ItemListener l : ItemListener.all()) { long itemListenerStart = System.currentTimeMillis(); try { l.onLoaded(); } catch (RuntimeException x) { LOGGER.log(Level.WARNING, null, x); } if (LOG_STARTUP_PERFORMANCE) LOGGER.info(String.format("Took %dms for item listener %s startup", System.currentTimeMillis()-itemListenerStart,l.getClass().getName())); } if (LOG_STARTUP_PERFORMANCE) LOGGER.info(String.format("Took %dms for complete Jenkins startup", System.currentTimeMillis()-start)); } finally { SecurityContextHolder.clearContext(); } } /** * Maintains backwards compatibility. Invoked by XStream when this object is de-serialized. */ @SuppressWarnings({"unused"}) private Object readResolve() { if (jdks == null) { jdks = new ArrayList<>(); } return this; } /** * Get the Jenkins {@link jenkins.install.InstallState install state}. * @return The Jenkins {@link jenkins.install.InstallState install state}. */ @Nonnull @Restricted(NoExternalUse.class) public InstallState getInstallState() { if (installState == null || installState.name() == null) { return InstallState.UNKNOWN; } return installState; } /** * Update the current install state. This will invoke state.initializeState() * when the state has been transitioned. */ @Restricted(NoExternalUse.class) public void setInstallState(@Nonnull InstallState newState) { InstallState prior = installState; installState = newState; if (!prior.equals(newState)) { newState.initializeState(); } } /** * Executes a reactor. * * @param is * If non-null, this can be consulted for ignoring some tasks. Only used during the initialization of Jenkins. */ private void executeReactor(final InitStrategy is, TaskBuilder... builders) throws IOException, InterruptedException, ReactorException { Reactor reactor = new Reactor(builders) { /** * Sets the thread name to the task for better diagnostics. */ @Override protected void runTask(Task task) throws Exception { if (is!=null && is.skipInitTask(task)) return; ACL.impersonate(ACL.SYSTEM); // full access in the initialization thread String taskName = InitReactorRunner.getDisplayName(task); Thread t = Thread.currentThread(); String name = t.getName(); if (taskName !=null) t.setName(taskName); try { long start = System.currentTimeMillis(); super.runTask(task); if(LOG_STARTUP_PERFORMANCE) LOGGER.info(String.format("Took %dms for %s by %s", System.currentTimeMillis()-start, taskName, name)); } catch (Exception | Error x) { if (containsLinkageError(x)) { LOGGER.log(Level.WARNING, taskName + " failed perhaps due to plugin dependency issues", x); } else { throw x; } } finally { t.setName(name); SecurityContextHolder.clearContext(); } } private boolean containsLinkageError(Throwable x) { if (x instanceof LinkageError) { return true; } Throwable x2 = x.getCause(); return x2 != null && containsLinkageError(x2); } }; new InitReactorRunner() { @Override protected void onInitMilestoneAttained(InitMilestone milestone) { initLevel = milestone; if (milestone==PLUGINS_PREPARED) { // set up Guice to enable injection as early as possible // before this milestone, ExtensionList.ensureLoaded() won't actually try to locate instances ExtensionList.lookup(ExtensionFinder.class).getComponents(); } } }.run(reactor); } 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; } /** * @param port * 0 to indicate random available TCP port. -1 to disable this service. */ public void setSlaveAgentPort(int port) throws IOException { this.slaveAgentPort = port; launchTcpSlaveAgentListener(); } /** * Returns the enabled agent protocols. * * @return the enabled agent protocols. * @since 2.16 */ public Set<String> getAgentProtocols() { if (agentProtocols == null) { // idempotent, so don't care if we do this concurrently, should all get same result Set<String> result = new TreeSet<>(); Set<String> disabled = new TreeSet<>(); for (String p : Util.fixNull(disabledAgentProtocols)) { disabled.add(p.trim()); } Set<String> enabled = new TreeSet<>(); for (String p : Util.fixNull(enabledAgentProtocols)) { enabled.add(p.trim()); } for (AgentProtocol p : AgentProtocol.all()) { String name = p.getName(); if (name != null && (p.isRequired() || (!disabled.contains(name) && (!p.isOptIn() || enabled.contains(name))))) { result.add(name); } } agentProtocols = result; return result; } return agentProtocols; } /** * Sets the enabled agent protocols. * * @param protocols the enabled agent protocols. * @since 2.16 */ public void setAgentProtocols(Set<String> protocols) { Set<String> disabled = new TreeSet<>(); Set<String> enabled = new TreeSet<>(); for (AgentProtocol p : AgentProtocol.all()) { String name = p.getName(); if (name != null && !p.isRequired()) { // we want to record the protocols where the admin has made a conscious decision // thus, if a protocol is opt-in, we record the admin enabling it // if a protocol is opt-out, we record the admin disabling it // We should not transition rapidly from opt-in -> opt-out -> opt-in // the scenario we want to have work is: // 1. We introduce a new protocol, it starts off as opt-in. Some admins decide to test and opt-in // 2. We decide that the protocol is ready for general use. It gets marked as opt-out. Any admins // that took part in early testing now have their config switched to not mention the new protocol // at all when they save their config as the protocol is now opt-out. Any admins that want to // disable it can do so and will have their preference recorded. // 3. We decide that the protocol needs to be retired. It gets switched back to opt-in. At this point // the initial opt-in admins, assuming they visited an upgrade to a master with step 2, will // have the protocol disabled for them. This is what we want. If they didn't upgrade to a master // with step 2, well there is not much we can do to differentiate them from somebody who is upgrading // from a previous step 3 master and had needed to keep the protocol turned on. // // What we should never do is flip-flop: opt-in -> opt-out -> opt-in -> opt-out as that will basically // clear any preference that an admin has set, but this should be ok as we only ever will be // adding new protocols and retiring old ones. if (p.isOptIn()) { if (protocols.contains(name)) { enabled.add(name); } } else { if (!protocols.contains(name)) { disabled.add(name); } } } } disabledAgentProtocols = disabled.isEmpty() ? null : new ArrayList<>(disabled); enabledAgentProtocols = enabled.isEmpty() ? null : new ArrayList<>(enabled); agentProtocols = null; } private void launchTcpSlaveAgentListener() throws IOException { synchronized(tcpSlaveAgentListenerLock) { // shutdown previous agent if the port has changed if (tcpSlaveAgentListener != null && tcpSlaveAgentListener.configuredPort != slaveAgentPort) { tcpSlaveAgentListener.shutdown(); tcpSlaveAgentListener = null; } if (slaveAgentPort != -1 && tcpSlaveAgentListener == null) { String administrativeMonitorId = getClass().getName() + ".tcpBind"; try { tcpSlaveAgentListener = new TcpSlaveAgentListener(slaveAgentPort); // remove previous monitor in case of previous error for (Iterator<AdministrativeMonitor> it = AdministrativeMonitor.all().iterator(); it.hasNext(); ) { AdministrativeMonitor am = it.next(); if (administrativeMonitorId.equals(am.id)) { it.remove(); } } } catch (BindException e) { LOGGER.log(Level.WARNING, String.format("Failed to listen to incoming agent connections through JNLP port %s. Change the JNLP port number", slaveAgentPort), e); new AdministrativeError(administrativeMonitorId, "Failed to listen to incoming agent connections through JNLP", "Failed to listen to incoming agent connections through JNLP. <a href='configureSecurity'>Change the JNLP port number</a> to solve the problem.", e); } } } } public void setNodeName(String name) { throw new UnsupportedOperationException(); // not allowed } public String getNodeDescription() { return Messages.Hudson_NodeDescription(); } @Exported public String getDescription() { return systemMessage; } public PluginManager getPluginManager() { return pluginManager; } public 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); } /** * @since 1.484 */ public View.AsynchPeople getAsynchPeople() { return new View.AsynchPeople(this); } /** * Does this {@link View} has any associated user information recorded? * @deprecated Potentially very expensive call; do not use from Jelly views. */ @Deprecated public boolean hasPeople() { return View.People.isApplicable(items.values()); } public Api getApi() { return new Api(this); } /** * Returns a secret key that survives across container start/stop. * <p> * This value is useful for implementing some of the security features. * * @deprecated * Due to the past security advisory, this value should not be used any more to protect sensitive information. * See {@link ConfidentialStore} and {@link ConfidentialKey} for how to store secrets. */ @Deprecated public String getSecretKey() { return secretKey; } /** * Gets {@linkplain #getSecretKey() the secret key} as a key for AES-128. * @since 1.308 * @deprecated * See {@link #getSecretKey()}. */ @Deprecated public SecretKey getSecretKeyAsAES128() { return Util.toAes128Key(secretKey); } /** * Returns the unique identifier of this Jenkins that has been historically used to identify * this Jenkins to the outside world. * * <p> * This form of identifier is weak in that it can be impersonated by others. See * https://wiki.jenkins-ci.org/display/JENKINS/Instance+Identity for more modern form of instance ID * that can be challenged and verified. * * @since 1.498 */ @SuppressWarnings("deprecation") public String getLegacyInstanceId() { return Util.getDigestOf(getSecretKey()); } /** * 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; } /** * @deprecated * UI method. Not meant to be used programatically. */ @Deprecated public ComputerSet getComputer() { return new ComputerSet(); } /** * Exposes {@link Descriptor} by its name to URL. * * After doing all the {@code getXXX(shortClassName)} methods, I finally realized that * this just doesn't scale. * * @param id * Either {@link Descriptor#getId()} (recommended) or the short name of a {@link Describable} subtype (for compatibility) * @throws IllegalArgumentException if a short name was passed which matches multiple IDs (fail fast) */ @SuppressWarnings({"unchecked", "rawtypes"}) // too late to fix public Descriptor getDescriptor(String id) { // legacy descriptors that are reigstered manually doesn't show up in getExtensionList, so check them explicitly. Iterable<Descriptor> descriptors = Iterators.sequence(getExtensionList(Descriptor.class), DescriptorExtensionList.listLegacyInstances()); for (Descriptor d : descriptors) { if (d.getId().equals(id)) { return d; } } Descriptor candidate = null; for (Descriptor d : descriptors) { String name = d.getId(); if (name.substring(name.lastIndexOf('.') + 1).equals(id)) { if (candidate == null) { candidate = d; } else { throw new IllegalArgumentException(id + " is ambiguous; matches both " + name + " and " + candidate.getId()); } } } return candidate; } /** * Alias for {@link #getDescriptor(String)}. */ public Descriptor getDescriptorByName(String id) { return getDescriptor(id); } /** * Gets the {@link Descriptor} that corresponds to the given {@link Describable} type. * <p> * If you have an instance of {@code type} and call {@link Describable#getDescriptor()}, * you'll get the same instance that this method returns. */ public Descriptor getDescriptor(Class<? extends Describable> type) { for( Descriptor d : getExtensionList(Descriptor.class) ) if(d.clazz==type) return d; return null; } /** * Works just like {@link #getDescriptor(Class)} but don't take no for an answer. * * @throws AssertionError * If the descriptor is missing. * @since 1.326 */ public Descriptor getDescriptorOrDie(Class<? extends Describable> type) { Descriptor d = getDescriptor(type); if (d==null) throw new AssertionError(type+" is missing its descriptor"); return d; } /** * Gets the {@link Descriptor} instance in the current Jenkins 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; } protected void updateComputerList() { updateComputerList(AUTOMATIC_SLAVE_LAUNCH); } /** @deprecated Use {@link SCMListener#all} instead. */ @Deprecated public CopyOnWriteList<SCMListener> getSCMListeners() { return scmListeners; } /** * 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 jpi 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 for {@link #getDescription}. */ public String getSystemMessage() { return systemMessage; } /** * Gets the markup formatter used in the system. * * @return * never null. * @since 1.391 */ public @Nonnull MarkupFormatter getMarkupFormatter() { MarkupFormatter f = markupFormatter; return f != null ? f : new EscapedMarkupFormatter(); } /** * Sets the markup formatter used in the system globally. * * @since 1.391 */ public void setMarkupFormatter(MarkupFormatter f) { this.markupFormatter = f; } /** * Sets the system message. */ public void setSystemMessage(String message) throws IOException { this.systemMessage = message; save(); } public FederatedLoginService getFederatedLoginService(String name) { for (FederatedLoginService fls : FederatedLoginService.all()) { if (fls.getUrlName().equals(name)) return fls; } return null; } public List<FederatedLoginService> getFederatedLoginServices() { return FederatedLoginService.all(); } public Launcher createLauncher(TaskListener listener) { return new LocalLauncher(listener).decorateFor(this); } 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 Jenkins.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 Jenkins}. * * @see #getAllItems(Class) */ @Exported(name="jobs") public List<TopLevelItem> getItems() { if (authorizationStrategy instanceof AuthorizationStrategy.Unsecured || authorizationStrategy instanceof FullControlOnceLoggedInAuthorizationStrategy) { return new ArrayList(items.values()); } 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 Jenkins} 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) { return Items.getAllItems(this, type); } /** * Gets all the items recursively. * * @since 1.402 */ public List<Item> getAllItems() { return getAllItems(Item.class); } /** * Gets a list of simple top-level projects. * @deprecated This method will ignore Maven and matrix projects, as well as projects inside containers such as folders. * You may prefer to call {@link #getAllItems(Class)} on {@link AbstractProject}, * perhaps also using {@link Util#createSubList} to consider only {@link TopLevelItem}s. * (That will also consider the caller's permissions.) * If you really want to get just {@link Project}s at top level, ignoring permissions, * you can filter the values from {@link #getItemMap} using {@link Util#createSubList}. */ @Deprecated 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; } public List<Action> getViewActions() { return getActions(); } /** * 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 View getView(String name) { return viewGroupMixIn.getView(name); } /** * Gets the read-only list of all {@link View}s. */ @Exported public Collection<View> getViews() { return viewGroupMixIn.getViews(); } @Override public void addView(View v) throws IOException { viewGroupMixIn.addView(v); } /** * Completely replaces views. * * <p> * This operation is NOT provided as an atomic operation, but rather * the sole purpose of this is to define a setter for this to help * introspecting code, such as system-config-dsl plugin */ // even if we want to offer this atomic operation, CopyOnWriteArrayList // offers no such operation public void setViews(Collection<View> views) throws IOException { BulkChange bc = new BulkChange(this); try { this.views.clear(); for (View v : views) { addView(v); } } finally { bc.commit(); } } public boolean canDelete(View view) { return viewGroupMixIn.canDelete(view); } public synchronized void deleteView(View view) throws IOException { viewGroupMixIn.deleteView(view); } public void onViewRenamed(View view, String oldName, String newName) { viewGroupMixIn.onViewRenamed(view, oldName, newName); } /** * Returns the primary {@link View} that renders the top-page of Jenkins. */ @Exported public View getPrimaryView() { return viewGroupMixIn.getPrimaryView(); } public void setPrimaryView(View v) { this.primaryView = v.getViewName(); } public ViewsTabBar getViewsTabBar() { return viewsTabBar; } public void setViewsTabBar(ViewsTabBar viewsTabBar) { this.viewsTabBar = viewsTabBar; } public Jenkins getItemGroup() { return this; } public MyViewsTabBar getMyViewsTabBar() { return myViewsTabBar; } public void setMyViewsTabBar(MyViewsTabBar myViewsTabBar) { this.myViewsTabBar = myViewsTabBar; } /** * Returns true if the current running Jenkins 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 Jenkins 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>() { @Override public int compare(Computer lhs, Computer rhs) { if(lhs.getNode()==Jenkins.this) return -1; if(rhs.getNode()==Jenkins.this) return 1; return lhs.getName().compareTo(rhs.getName()); } }); return r; } @CLIResolver public @CheckForNull Computer getComputer(@Argument(required=true,metaVar="NAME",usage="Node name") @Nonnull String name) { if(name.equals("(master)")) name = ""; for (Computer c : computers.values()) { if(c.getName().equals(name)) return c; } return null; } /** * Gets the label that exists on this system by the name. * * @return null if name is null. * @see Label#parseExpression(String) (String) */ public Label getLabel(String expr) { if(expr==null) return null; expr = hudson.util.QuotedStringTokenizer.unquote(expr); while(true) { Label l = labels.get(expr); if(l!=null) return l; // non-existent try { labels.putIfAbsent(expr,Label.parseExpression(expr)); } catch (ANTLRException e) { // laxly accept it as a single label atom for backward compatibility return getLabelAtom(expr); } } } /** * Returns the label atom of the given name. * @return non-null iff name is non-null */ public @Nullable LabelAtom getLabelAtom(@CheckForNull String name) { if (name==null) return null; while(true) { Label l = labels.get(name); if(l!=null) return (LabelAtom)l; // non-existent LabelAtom la = new LabelAtom(name); if (labels.putIfAbsent(name, la)==null) la.load(); } } /** * Gets all the active labels in the current system. */ public Set<Label> getLabels() { Set<Label> r = new TreeSet<Label>(); for (Label l : labels.values()) { if(!l.isEmpty()) r.add(l); } return r; } public Set<LabelAtom> getLabelAtoms() { Set<LabelAtom> r = new TreeSet<LabelAtom>(); for (Label l : labels.values()) { if(!l.isEmpty() && l instanceof LabelAtom) r.add((LabelAtom)l); } return r; } public Queue getQueue() { return queue; } @Override public String getDisplayName() { return Messages.Hudson_DisplayName(); } public List<JDK> getJDKs() { return jdks; } /** * Replaces all JDK installations with those from the given collection. * * Use {@link hudson.model.JDK.DescriptorImpl#setInstallations(JDK...)} to * set JDK installations from external code. */ @Restricted(NoExternalUse.class) public void setJDKs(Collection<? extends JDK> jdks) { this.jdks = new ArrayList<JDK>(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 agent node of the give name, hooked under this Jenkins. */ public @CheckForNull Node getNode(String name) { return nodes.getNode(name); } /** * Gets a {@link Cloud} by {@link Cloud#name its name}, or null. */ public Cloud getCloud(String name) { return clouds.getByName(name); } protected Map<Node,Computer> getComputerMap() { return computers; } /** * Returns all {@link Node}s in the system, excluding {@link Jenkins} instance itself which * represents the master. */ public List<Node> getNodes() { return nodes.getNodes(); } /** * Get the {@link Nodes} object that handles maintaining individual {@link Node}s. * @return The Nodes object. */ @Restricted(NoExternalUse.class) public Nodes getNodesObject() { // TODO replace this with something better when we properly expose Nodes. return nodes; } /** * Adds one more {@link Node} to Jenkins. */ public void addNode(Node n) throws IOException { nodes.addNode(n); } /** * Removes a {@link Node} from Jenkins. */ public void removeNode(@Nonnull Node n) throws IOException { nodes.removeNode(n); } /** * Saves an existing {@link Node} on disk, called by {@link Node#save()}. This method is preferred in those cases * where you need to determine atomically that the node being saved is actually in the list of nodes. * * @param n the node to be updated. * @return {@code true}, if the node was updated. {@code false}, if the node was not in the list of nodes. * @throws IOException if the node could not be persisted. * @see Nodes#updateNode * @since 1.634 */ public boolean updateNode(Node n) throws IOException { return nodes.updateNode(n); } public void setNodes(final List<? extends Node> n) throws IOException { nodes.setNodes(n); } public DescribableList<NodeProperty<?>, NodePropertyDescriptor> getNodeProperties() { return nodeProperties; } public DescribableList<NodeProperty<?>, NodePropertyDescriptor> getGlobalNodeProperties() { return globalNodeProperties; } /** * Resets all labels and remove invalid ones. * * This should be called when the assumptions behind label cache computation changes, * but we also call this periodically to self-heal any data out-of-sync issue. */ /*package*/ void trimLabels() { for (Iterator<Label> itr = labels.values().iterator(); itr.hasNext();) { Label l = itr.next(); resetLabel(l); 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(); @Override public boolean isInstantiable() { return false; } public FormValidation doCheckNumExecutors(@QueryParameter String value) { return FormValidation.validateNonNegativeInteger(value); } public FormValidation doCheckRawBuildsDir(@QueryParameter String value) { // do essentially what expandVariablesForDirectory does, without an Item String replacedValue = expandVariablesForDirectory(value, "doCheckRawBuildsDir-Marker:foo", Jenkins.getInstance().getRootDir().getPath() + "/jobs/doCheckRawBuildsDir-Marker$foo"); File replacedFile = new File(replacedValue); if (!replacedFile.isAbsolute()) { return FormValidation.error(value + " does not resolve to an absolute path"); } if (!replacedValue.contains("doCheckRawBuildsDir-Marker")) { return FormValidation.error(value + " does not contain ${ITEM_FULL_NAME} or ${ITEM_ROOTDIR}, cannot distinguish between projects"); } if (replacedValue.contains("doCheckRawBuildsDir-Marker:foo")) { // make sure platform can handle colon try { File tmp = File.createTempFile("Jenkins-doCheckRawBuildsDir", "foo:bar"); tmp.delete(); } catch (IOException e) { return FormValidation.error(value + " contains ${ITEM_FULLNAME} but your system does not support it (JENKINS-12251). Use ${ITEM_FULL_NAME} instead"); } } File d = new File(replacedValue); if (!d.isDirectory()) { // if dir does not exist (almost guaranteed) need to make sure nearest existing ancestor can be written to d = d.getParentFile(); while (!d.exists()) { d = d.getParentFile(); } if (!d.canWrite()) { return FormValidation.error(value + " does not exist and probably cannot be created"); } } return FormValidation.ok(); } // to route /descriptor/FQCN/xxx to getDescriptor(FQCN).xxx public Object getDynamic(String token) { return Jenkins.getInstance().getDescriptor(token); } } /** * Gets the system default quiet period. */ public int getQuietPeriod() { return quietPeriod!=null ? quietPeriod : 5; } /** * Sets the global quiet period. * * @param quietPeriod * null to the default value. */ public void setQuietPeriod(Integer quietPeriod) throws IOException { this.quietPeriod = quietPeriod; save(); } /** * Gets the global SCM check out retry count. */ public int getScmCheckoutRetryCount() { return scmCheckoutRetryCount; } public void setScmCheckoutRetryCount(int scmCheckoutRetryCount) throws IOException { this.scmCheckoutRetryCount = scmCheckoutRetryCount; save(); } @Override public String getSearchUrl() { return ""; } @Override public SearchIndexBuilder makeSearchIndex() { return super.makeSearchIndex() .add("configure", "config","configure") .add("manage") .add("log") .add(new CollectionSearchIndex<TopLevelItem>() { protected SearchItem get(String key) { return getItemByFullName(key, TopLevelItem.class); } protected Collection<TopLevelItem> all() { return getAllItems(TopLevelItem.class); } }) .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; } }); } public String getUrlChildPrefix() { return "job"; } /** * Gets the absolute URL of Jenkins, such as {@code http://localhost/jenkins/}. * * <p> * This method first tries to use the manually configured value, then * fall back to {@link #getRootUrlFromRequest}. * It is done in this order so that it can work correctly even in the face * of a reverse proxy. * * @return null if this parameter is not configured by the user and the calling thread is not in an HTTP request; otherwise the returned URL will always have the trailing {@code /} * @since 1.66 * @see <a href="https://wiki.jenkins-ci.org/display/JENKINS/Hyperlinks+in+HTML">Hyperlinks in HTML</a> */ public @Nullable String getRootUrl() { String url = JenkinsLocationConfiguration.get().getUrl(); if(url!=null) { return Util.ensureEndsWith(url,"/"); } StaplerRequest req = Stapler.getCurrentRequest(); if(req!=null) return getRootUrlFromRequest(); return null; } /** * Is Jenkins running in HTTPS? * * Note that we can't really trust {@link StaplerRequest#isSecure()} because HTTPS might be terminated * in the reverse proxy. */ public boolean isRootUrlSecure() { String url = getRootUrl(); return url!=null && url.startsWith("https"); } /** * Gets the absolute URL of Jenkins top page, such as {@code http://localhost/jenkins/}. * * <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. * * <p>Please note that this will not work in all cases if Jenkins is running behind a * reverse proxy which has not been fully configured. * Specifically the {@code Host} and {@code X-Forwarded-Proto} headers must be set. * <a href="https://wiki.jenkins-ci.org/display/JENKINS/Running+Jenkins+behind+Apache">Running Jenkins behind Apache</a> * shows some examples of configuration. * @since 1.263 */ public @Nonnull String getRootUrlFromRequest() { StaplerRequest req = Stapler.getCurrentRequest(); if (req == null) { throw new IllegalStateException("cannot call getRootUrlFromRequest from outside a request handling thread"); } StringBuilder buf = new StringBuilder(); String scheme = getXForwardedHeader(req, "X-Forwarded-Proto", req.getScheme()); buf.append(scheme).append("://"); String host = getXForwardedHeader(req, "X-Forwarded-Host", req.getServerName()); int index = host.indexOf(':'); int port = req.getServerPort(); if (index == -1) { // Almost everyone else except Nginx put the host and port in separate headers buf.append(host); } else { // Nginx uses the same spec as for the Host header, i.e. hostanme:port buf.append(host.substring(0, index)); if (index + 1 < host.length()) { try { port = Integer.parseInt(host.substring(index + 1)); } catch (NumberFormatException e) { // ignore } } // but if a user has configured Nginx with an X-Forwarded-Port, that will win out. } String forwardedPort = getXForwardedHeader(req, "X-Forwarded-Port", null); if (forwardedPort != null) { try { port = Integer.parseInt(forwardedPort); } catch (NumberFormatException e) { // ignore } } if (port != ("https".equals(scheme) ? 443 : 80)) { buf.append(':').append(port); } buf.append(req.getContextPath()).append('/'); return buf.toString(); } /** * Gets the originating "X-Forwarded-..." header from the request. If there are multiple headers the originating * header is the first header. If the originating header contains a comma separated list, the originating entry * is the first one. * @param req the request * @param header the header name * @param defaultValue the value to return if the header is absent. * @return the originating entry of the header or the default value if the header was not present. */ private static String getXForwardedHeader(StaplerRequest req, String header, String defaultValue) { String value = req.getHeader(header); if (value != null) { int index = value.indexOf(','); return index == -1 ? value.trim() : value.substring(0,index).trim(); } return defaultValue; } public File getRootDir() { return root; } public FilePath getWorkspaceFor(TopLevelItem item) { for (WorkspaceLocator l : WorkspaceLocator.all()) { FilePath workspace = l.locate(item, this); if (workspace != null) { return workspace; } } return new FilePath(expandVariablesForDirectory(workspaceDir, item)); } public File getBuildDirFor(Job job) { return expandVariablesForDirectory(buildsDir, job); } private File expandVariablesForDirectory(String base, Item item) { return new File(expandVariablesForDirectory(base, item.getFullName(), item.getRootDir().getPath())); } @Restricted(NoExternalUse.class) static String expandVariablesForDirectory(String base, String itemFullName, String itemRootDir) { return Util.replaceMacro(base, ImmutableMap.of( "JENKINS_HOME", Jenkins.getInstance().getRootDir().getPath(), "ITEM_ROOTDIR", itemRootDir, "ITEM_FULLNAME", itemFullName, // legacy, deprecated "ITEM_FULL_NAME", itemFullName.replace(':','$'))); // safe, see JENKINS-12251 } public String getRawWorkspaceDir() { return workspaceDir; } public String getRawBuildsDir() { return buildsDir; } @Restricted(NoExternalUse.class) public void setRawBuildsDir(String buildsDir) { this.buildsDir = buildsDir; } @Override public @Nonnull FilePath getRootPath() { return new FilePath(getRootDir()); } @Override public FilePath createPath(String absolutePath) { return new FilePath((VirtualChannel)null,absolutePath); } public ClockDifference getClockDifference() { return ClockDifference.ZERO; } @Override public Callable<ClockDifference, IOException> getClockDifferenceCallable() { return new MasterToSlaveCallable<ClockDifference, IOException>() { public ClockDifference call() throws IOException { return new ClockDifference(0); } }; } /** * 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; } public boolean isUseProjectNamingStrategy(){ return projectNamingStrategy != DefaultProjectNamingStrategy.DEFAULT_NAMING_STRATEGY; } /** * If true, all the POST requests to Jenkins would have to have crumb in it to protect * Jenkins from CSRF vulnerabilities. */ @Exported public boolean isUseCrumbs() { return crumbIssuer!=null; } /** * Returns the constant that captures the three basic security modes in Jenkins. */ 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.useSecurity = true; IdStrategy oldUserIdStrategy = this.securityRealm == null ? securityRealm.getUserIdStrategy() // don't trigger rekey on Jenkins load : this.securityRealm.getUserIdStrategy(); 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"); } if (!oldUserIdStrategy.equals(this.securityRealm.getUserIdStrategy())) { User.rekey(); } } 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; useSecurity = true; authorizationStrategy = a; } public boolean isDisableRememberMe() { return disableRememberMe; } public void setDisableRememberMe(boolean disableRememberMe) { this.disableRememberMe = disableRememberMe; } public void disableSecurity() { useSecurity = null; setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); authorizationStrategy = AuthorizationStrategy.UNSECURED; } public void setProjectNamingStrategy(ProjectNamingStrategy ns) { if(ns == null){ ns = DefaultProjectNamingStrategy.DEFAULT_NAMING_STRATEGY; } projectNamingStrategy = ns; } public Lifecycle getLifecycle() { return Lifecycle.get(); } /** * Gets the dependency injection container that hosts all the extension implementations and other * components in Jenkins. * * @since 1.433 */ public Injector getInjector() { return lookup(Injector.class); } /** * 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. * @see ExtensionList#lookup */ @SuppressWarnings({"unchecked"}) public <T> ExtensionList<T> getExtensionList(Class<T> extensionType) { return extensionLists.get(extensionType); } /** * Used to bind {@link ExtensionList}s to URLs. * * @since 1.349 */ public ExtensionList getExtensionList(String extensionType) throws ClassNotFoundException { return getExtensionList(pluginManager.uberClassLoader.loadClass(extensionType)); } /** * Returns {@link ExtensionList} that retains the discovered {@link Descriptor} instances for the given * kind of {@link Describable}. * * @return * Can be an empty list but never null. */ @SuppressWarnings({"unchecked"}) public <T extends Describable<T>,D extends Descriptor<T>> DescriptorExtensionList<T,D> getDescriptorList(Class<T> type) { return descriptorLists.get(type); } /** * Refresh {@link ExtensionList}s by adding all the newly discovered extensions. * * Exposed only for {@link PluginManager#dynamicLoad(File)}. */ public void refreshExtensions() throws ExtensionRefreshException { ExtensionList<ExtensionFinder> finders = getExtensionList(ExtensionFinder.class); for (ExtensionFinder ef : finders) { if (!ef.isRefreshable()) throw new ExtensionRefreshException(ef+" doesn't support refresh"); } List<ExtensionComponentSet> fragments = Lists.newArrayList(); for (ExtensionFinder ef : finders) { fragments.add(ef.refresh()); } ExtensionComponentSet delta = ExtensionComponentSet.union(fragments).filtered(); // if we find a new ExtensionFinder, we need it to list up all the extension points as well List<ExtensionComponent<ExtensionFinder>> newFinders = Lists.newArrayList(delta.find(ExtensionFinder.class)); while (!newFinders.isEmpty()) { ExtensionFinder f = newFinders.remove(newFinders.size()-1).getInstance(); ExtensionComponentSet ecs = ExtensionComponentSet.allOf(f).filtered(); newFinders.addAll(ecs.find(ExtensionFinder.class)); delta = ExtensionComponentSet.union(delta, ecs); } for (ExtensionList el : extensionLists.values()) { el.refresh(delta); } for (ExtensionList el : descriptorLists.values()) { el.refresh(delta); } // TODO: we need some generalization here so that extension points can be notified when a refresh happens? for (ExtensionComponent<RootAction> ea : delta.find(RootAction.class)) { Action a = ea.getInstance(); if (!actions.contains(a)) actions.add(a); } } /** * Returns the root {@link ACL}. * * @see AuthorizationStrategy#getRootACL() */ @Override public ACL getACL() { return authorizationStrategy.getRootACL(); } /** * @return * never null. */ public AuthorizationStrategy getAuthorizationStrategy() { return authorizationStrategy; } /** * The strategy used to check the project names. * @return never <code>null</code> */ public ProjectNamingStrategy getProjectNamingStrategy() { return projectNamingStrategy == null ? ProjectNamingStrategy.DEFAULT_NAMING_STRATEGY : projectNamingStrategy; } /** * Returns true if Jenkins is quieting down. * <p> * No further jobs will be executed unless it * can be finished while other current pending builds * are still in progress. */ @Exported public boolean isQuietingDown() { return isQuietingDown; } /** * Returns true if the container initiated the termination of the web application. */ public boolean isTerminating() { return terminating; } /** * Gets the initialization milestone that we've already reached. * * @return * {@link InitMilestone#STARTED} even if the initialization hasn't been started, so that this method * never returns null. */ public InitMilestone getInitLevel() { return initLevel; } public void setNumExecutors(int n) throws IOException { if (this.numExecutors != n) { this.numExecutors = n; updateComputerList(); save(); } } /** * {@inheritDoc}. * * Note that the look up is case-insensitive. */ @Override public TopLevelItem getItem(String name) throws AccessDeniedException { if (name==null) return null; TopLevelItem item = items.get(name); if (item==null) return null; if (!item.hasPermission(Item.READ)) { if (item.hasPermission(Item.DISCOVER)) { throw new AccessDeniedException("Please login to access job " + name); } return null; } return item; } /** * Gets the item by its path name from the given context * * <h2>Path Names</h2> * <p> * If the name starts from '/', like "/foo/bar/zot", then it's interpreted as absolute. * Otherwise, the name should be something like "foo/bar" and it's interpreted like * relative path name in the file system is, against the given context. * <p>For compatibility, as a fallback when nothing else matches, a simple path * like {@code foo/bar} can also be treated with {@link #getItemByFullName}. * @param context * null is interpreted as {@link Jenkins}. Base 'directory' of the interpretation. * @since 1.406 */ public Item getItem(String pathName, ItemGroup context) { if (context==null) context = this; if (pathName==null) return null; if (pathName.startsWith("/")) // absolute return getItemByFullName(pathName); Object/*Item|ItemGroup*/ ctx = context; StringTokenizer tokens = new StringTokenizer(pathName,"/"); while (tokens.hasMoreTokens()) { String s = tokens.nextToken(); if (s.equals("..")) { if (ctx instanceof Item) { ctx = ((Item)ctx).getParent(); continue; } ctx=null; // can't go up further break; } if (s.equals(".")) { continue; } if (ctx instanceof ItemGroup) { ItemGroup g = (ItemGroup) ctx; Item i = g.getItem(s); if (i==null || !i.hasPermission(Item.READ)) { // TODO consider DISCOVER ctx=null; // can't go up further break; } ctx=i; } else { return null; } } if (ctx instanceof Item) return (Item)ctx; // fall back to the classic interpretation return getItemByFullName(pathName); } public final Item getItem(String pathName, Item context) { return getItem(pathName,context!=null?context.getParent():null); } public final <T extends Item> T getItem(String pathName, ItemGroup context, @Nonnull Class<T> type) { Item r = getItem(pathName, context); if (type.isInstance(r)) return type.cast(r); return null; } public final <T extends Item> T getItem(String pathName, Item context, Class<T> type) { return getItem(pathName,context!=null?context.getParent():null,type); } 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. * @throws AccessDeniedException as per {@link ItemGroup#getItem} */ public @CheckForNull <T extends Item> T getItemByFullName(String fullName, Class<T> type) throws AccessDeniedException { StringTokenizer tokens = new StringTokenizer(fullName,"/"); ItemGroup parent = this; if(!tokens.hasMoreTokens()) return null; // for example, empty full name. while(true) { Item item = parent.getItem(tokens.nextToken()); if(!tokens.hasMoreTokens()) { if(type.isInstance(item)) return type.cast(item); else return null; } if(!(item instanceof ItemGroup)) return null; // this item can't have any children if (!item.hasPermission(Item.READ)) return null; // TODO consider DISCOVER parent = (ItemGroup) item; } } public @CheckForNull Item getItemByFullName(String fullName) { return getItemByFullName(fullName,Item.class); } /** * Gets the user of the given name. * * @return the user of the given name (which may or may not be an id), if that person exists or the invoker {@link #hasPermission} on {@link #ADMINISTER}; else null * @see User#get(String,boolean), {@link User#getById(String, boolean)} */ public @CheckForNull User getUser(String name) { return User.get(name,hasPermission(ADMINISTER)); } public synchronized TopLevelItem createProject( TopLevelItemDescriptor type, String name ) throws IOException { return createProject(type, name, true); } public synchronized TopLevelItem createProject( TopLevelItemDescriptor type, String name, boolean notify ) throws IOException { return itemGroupMixIn.createProject(type,name,notify); } /** * Overwrites the existing item by new one. * * <p> * This is a short cut for deleting an existing job and adding a new one. */ public synchronized void putItem(TopLevelItem item) throws IOException, InterruptedException { String name = item.getName(); TopLevelItem old = items.get(name); if (old ==item) return; // noop checkPermission(Item.CREATE); if (old!=null) old.delete(); items.put(name,item); ItemListener.fireOnCreated(item); } /** * Creates a new job. * * <p> * This version infers the descriptor from the type of the top-level item. * * @throws IllegalArgumentException * if the project of the given name already exists. */ public synchronized <T extends TopLevelItem> T createProject( Class<T> type, String name ) throws IOException { return type.cast(createProject((TopLevelItemDescriptor)getDescriptor(type),name)); } /** * Called by {@link Job#renameTo(String)} to update relevant data structure. * assumed to be synchronized on Jenkins by the caller. */ public void onRenamed(TopLevelItem job, String oldName, String newName) throws IOException { items.remove(oldName); items.put(newName,job); // For compatibility with old views: for (View v : views) v.onJobRenamed(job, oldName, newName); } /** * Called in response to {@link Job#doDoDelete(StaplerRequest, StaplerResponse)} */ public void onDeleted(TopLevelItem item) throws IOException { ItemListener.fireOnDeleted(item); items.remove(item.getName()); // For compatibility with old views: for (View v : views) v.onJobRenamed(item, item.getName(), null); } @Override public boolean canAdd(TopLevelItem item) { return true; } @Override synchronized public <I extends TopLevelItem> I add(I item, String name) throws IOException, IllegalArgumentException { if (items.containsKey(name)) { throw new IllegalArgumentException("already an item '" + name + "'"); } items.put(name, item); return item; } @Override public void remove(TopLevelItem item) throws IOException, IllegalArgumentException { items.remove(item.getName()); } 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 void setMode(Mode m) throws IOException { this.mode = m; save(); } public String getLabelString() { return fixNull(label).trim(); } @Override public void setLabelString(String label) throws IOException { this.label = label; save(); } @Override public LabelAtom getSelfLabel() { return getLabelAtom("master"); } public Computer createComputer() { return new Hudson.MasterComputer(); } private void loadConfig() throws IOException { XmlFile cfg = getConfigFile(); if (cfg.exists()) { // reset some data that may not exist in the disk file // so that we can take a proper compensation action later. primaryView = null; views.clear(); // load from disk cfg.unmarshal(Jenkins.this); } } private synchronized TaskBuilder loadTasks() throws IOException { File projectsDir = new File(root,"jobs"); if(!projectsDir.getCanonicalFile().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(); final Set<String> loadedNames = Collections.synchronizedSet(new HashSet<String>()); TaskGraphBuilder g = new TaskGraphBuilder(); Handle loadJenkins = g.requires(EXTENSIONS_AUGMENTED).attains(JOB_LOADED).add("Loading global config", new Executable() { public void run(Reactor session) throws Exception { loadConfig(); // if we are loading old data that doesn't have this field if (slaves != null && !slaves.isEmpty() && nodes.isLegacy()) { nodes.setNodes(slaves); slaves = null; } else { nodes.load(); } clouds.setOwner(Jenkins.this); } }); for (final File subdir : subdirs) { g.requires(loadJenkins).attains(JOB_LOADED).notFatal().add("Loading job "+subdir.getName(),new Executable() { public void run(Reactor session) throws Exception { if(!Items.getConfigFile(subdir).exists()) { //Does not have job config file, so it is not a jenkins job hence skip it return; } TopLevelItem item = (TopLevelItem) Items.load(Jenkins.this, subdir); items.put(item.getName(), item); loadedNames.add(item.getName()); } }); } g.requires(JOB_LOADED).add("Cleaning up old builds",new Executable() { public void run(Reactor reactor) throws Exception { // anything we didn't load from disk, throw them away. // doing this after loading from disk allows newly loaded items // to inspect what already existed in memory (in case of reloading) // retainAll doesn't work well because of CopyOnWriteMap implementation, so remove one by one // hopefully there shouldn't be too many of them. for (String name : items.keySet()) { if (!loadedNames.contains(name)) items.remove(name); } } }); g.requires(JOB_LOADED).add("Finalizing set up",new Executable() { public void run(Reactor session) throws Exception { rebuildDependencyGraph(); {// recompute label objects - populates the labels mapping. for (Node slave : nodes.getNodes()) // Note that not all labels are visible until the agents have connected. slave.getAssignedLabels(); getAssignedLabels(); } // initialize views by inserting the default view if necessary // this is both for clean Jenkins and for backward compatibility. if(views.size()==0 || primaryView==null) { View v = new AllView(Messages.Hudson_ViewName()); setViewOwner(v); views.add(0,v); primaryView = v.getViewName(); } 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); } else { // read in old data that doesn't have the security field set if(authorizationStrategy==null) { if(useSecurity==null) authorizationStrategy = AuthorizationStrategy.UNSECURED; else authorizationStrategy = new LegacyAuthorizationStrategy(); } if(securityRealm==null) { if(useSecurity==null) setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); else setSecurityRealm(new LegacySecurityRealm()); } else { // force the set to proxy setSecurityRealm(securityRealm); } } // Initialize the filter with the crumb issuer setCrumbIssuer(crumbIssuer); // auto register root actions for (Action a : getExtensionList(RootAction.class)) if (!actions.contains(a)) actions.add(a); } }); return g; } /** * Save the settings to a file. */ public synchronized void save() throws IOException { if(BulkChange.contains(this)) return; getConfigFile().write(this); SaveableListener.fireOnChange(this, getConfigFile()); } /** * Called to shut down the system. */ @edu.umd.cs.findbugs.annotations.SuppressWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD") public void cleanUp() { if (theInstance != this && theInstance != null) { LOGGER.log(Level.WARNING, "This instance is no longer the singleton, ignoring cleanUp()"); return; } synchronized (Jenkins.class) { if (cleanUpStarted) { LOGGER.log(Level.WARNING, "Jenkins.cleanUp() already started, ignoring repeated cleanUp()"); return; } cleanUpStarted = true; } try { LOGGER.log(Level.INFO, "Stopping Jenkins"); final List<Throwable> errors = new ArrayList<>(); fireBeforeShutdown(errors); _cleanUpRunTerminators(errors); terminating = true; final Set<Future<?>> pending = _cleanUpDisconnectComputers(errors); _cleanUpShutdownUDPBroadcast(errors); _cleanUpCloseDNSMulticast(errors); _cleanUpInterruptReloadThread(errors); _cleanUpShutdownTriggers(errors); _cleanUpShutdownTimer(errors); _cleanUpShutdownTcpSlaveAgent(errors); _cleanUpShutdownPluginManager(errors); _cleanUpPersistQueue(errors); _cleanUpShutdownThreadPoolForLoad(errors); _cleanUpAwaitDisconnects(errors, pending); _cleanUpPluginServletFilters(errors); _cleanUpReleaseAllLoggers(errors); LOGGER.log(Level.INFO, "Jenkins stopped"); if (!errors.isEmpty()) { StringBuilder message = new StringBuilder("Unexpected issues encountered during cleanUp: "); Iterator<Throwable> iterator = errors.iterator(); message.append(iterator.next().getMessage()); while (iterator.hasNext()) { message.append("; "); message.append(iterator.next().getMessage()); } iterator = errors.iterator(); RuntimeException exception = new RuntimeException(message.toString(), iterator.next()); while (iterator.hasNext()) { exception.addSuppressed(iterator.next()); } throw exception; } } finally { theInstance = null; if (JenkinsJVM.isJenkinsJVM()) { JenkinsJVMAccess._setJenkinsJVM(oldJenkinsJVM); } } } private void fireBeforeShutdown(List<Throwable> errors) { LOGGER.log(Level.FINE, "Notifying termination"); for (ItemListener l : ItemListener.all()) { try { l.onBeforeShutdown(); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(Level.WARNING, "ItemListener " + l + ": " + e.getMessage(), e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(Level.WARNING, "ItemListener " + l + ": " + e.getMessage(), e); // save for later errors.add(e); } } } private void _cleanUpRunTerminators(List<Throwable> errors) { try { final TerminatorFinder tf = new TerminatorFinder( pluginManager != null ? pluginManager.uberClassLoader : Thread.currentThread().getContextClassLoader()); new Reactor(tf).execute(new Executor() { @Override public void execute(Runnable command) { command.run(); } }, new ReactorListener() { final Level level = Level.parse(Configuration.getStringConfigParameter("termLogLevel", "FINE")); public void onTaskStarted(Task t) { LOGGER.log(level, "Started {0}", InitReactorRunner.getDisplayName(t)); } public void onTaskCompleted(Task t) { LOGGER.log(level, "Completed {0}", InitReactorRunner.getDisplayName(t)); } public void onTaskFailed(Task t, Throwable err, boolean fatal) { LOGGER.log(SEVERE, "Failed " + InitReactorRunner.getDisplayName(t), err); } public void onAttained(Milestone milestone) { Level lv = level; String s = "Attained " + milestone.toString(); if (milestone instanceof TermMilestone) { lv = Level.INFO; // noteworthy milestones --- at least while we debug problems further s = milestone.toString(); } LOGGER.log(lv, s); } }); } catch (InterruptedException | ReactorException | IOException e) { LOGGER.log(SEVERE, "Failed to execute termination",e); errors.add(e); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(SEVERE, "Failed to execute termination", e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(SEVERE, "Failed to execute termination", e); // save for later errors.add(e); } } private Set<Future<?>> _cleanUpDisconnectComputers(final List<Throwable> errors) { LOGGER.log(Level.INFO, "Starting node disconnection"); final Set<Future<?>> pending = new HashSet<Future<?>>(); // JENKINS-28840 we know we will be interrupting all the Computers so get the Queue lock once for all Queue.withLock(new Runnable() { @Override public void run() { for( Computer c : computers.values() ) { try { c.interrupt(); killComputer(c); pending.add(c.disconnect(null)); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(Level.WARNING, "Could not disconnect " + c + ": " + e.getMessage(), e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(Level.WARNING, "Could not disconnect " + c + ": " + e.getMessage(), e); // save for later errors.add(e); } } } }); return pending; } private void _cleanUpShutdownUDPBroadcast(List<Throwable> errors) { if(udpBroadcastThread!=null) { LOGGER.log(Level.FINE, "Shutting down {0}", udpBroadcastThread.getName()); try { udpBroadcastThread.shutdown(); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(SEVERE, "Failed to shutdown UDP Broadcast Thread", e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(SEVERE, "Failed to shutdown UDP Broadcast Thread", e); // save for later errors.add(e); } } } private void _cleanUpCloseDNSMulticast(List<Throwable> errors) { if(dnsMultiCast!=null) { LOGGER.log(Level.FINE, "Closing DNS Multicast service"); try { dnsMultiCast.close(); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(SEVERE, "Failed to close DNS Multicast service", e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(SEVERE, "Failed to close DNS Multicast service", e); // save for later errors.add(e); } } } private void _cleanUpInterruptReloadThread(List<Throwable> errors) { LOGGER.log(Level.FINE, "Interrupting reload thread"); try { interruptReloadThread(); } catch (SecurityException e) { LOGGER.log(WARNING, "Not permitted to interrupt reload thread", e); errors.add(e); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(SEVERE, "Failed to interrupt reload thread", e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(SEVERE, "Failed to interrupt reload thread", e); // save for later errors.add(e); } } private void _cleanUpShutdownTriggers(List<Throwable> errors) { LOGGER.log(Level.FINE, "Shutting down triggers"); try { final java.util.Timer timer = Trigger.timer; if (timer != null) { final CountDownLatch latch = new CountDownLatch(1); timer.schedule(new TimerTask() { @Override public void run() { timer.cancel(); latch.countDown(); } }, 0); if (latch.await(10, TimeUnit.SECONDS)) { LOGGER.log(Level.FINE, "Triggers shut down successfully"); } else { timer.cancel(); LOGGER.log(Level.INFO, "Gave up waiting for triggers to finish running"); } } Trigger.timer = null; } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(SEVERE, "Failed to shut down triggers", e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(SEVERE, "Failed to shut down triggers", e); // save for later errors.add(e); } } private void _cleanUpShutdownTimer(List<Throwable> errors) { LOGGER.log(Level.FINE, "Shutting down timer"); try { Timer.shutdown(); } catch (SecurityException e) { LOGGER.log(WARNING, "Not permitted to shut down Timer", e); errors.add(e); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(SEVERE, "Failed to shut down Timer", e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(SEVERE, "Failed to shut down Timer", e); // save for later errors.add(e); } } private void _cleanUpShutdownTcpSlaveAgent(List<Throwable> errors) { if(tcpSlaveAgentListener!=null) { LOGGER.log(FINE, "Shutting down TCP/IP slave agent listener"); try { tcpSlaveAgentListener.shutdown(); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(SEVERE, "Failed to shut down TCP/IP slave agent listener", e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(SEVERE, "Failed to shut down TCP/IP slave agent listener", e); // save for later errors.add(e); } } } private void _cleanUpShutdownPluginManager(List<Throwable> errors) { if(pluginManager!=null) {// be defensive. there could be some ugly timing related issues LOGGER.log(Level.INFO, "Stopping plugin manager"); try { pluginManager.stop(); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(SEVERE, "Failed to stop plugin manager", e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(SEVERE, "Failed to stop plugin manager", e); // save for later errors.add(e); } } } private void _cleanUpPersistQueue(List<Throwable> errors) { if(getRootDir().exists()) { // if we are aborting because we failed to create JENKINS_HOME, // don't try to save. Issue #536 LOGGER.log(Level.INFO, "Persisting build queue"); try { getQueue().save(); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(SEVERE, "Failed to persist build queue", e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(SEVERE, "Failed to persist build queue", e); // save for later errors.add(e); } } } private void _cleanUpShutdownThreadPoolForLoad(List<Throwable> errors) { LOGGER.log(FINE, "Shuting down Jenkins load thread pool"); try { threadPoolForLoad.shutdown(); } catch (SecurityException e) { LOGGER.log(WARNING, "Not permitted to shut down Jenkins load thread pool", e); errors.add(e); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(SEVERE, "Failed to shut down Jenkins load thread pool", e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(SEVERE, "Failed to shut down Jenkins load thread pool", e); // save for later errors.add(e); } } private void _cleanUpAwaitDisconnects(List<Throwable> errors, Set<Future<?>> pending) { if (!pending.isEmpty()) { LOGGER.log(Level.INFO, "Waiting for node disconnection completion"); } 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 remote computer connection cleanly", e); } catch (TimeoutException e) { LOGGER.log(Level.WARNING, "Failed to shut down remote computer connection within 10 seconds", e); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(Level.WARNING, "Failed to shut down remote computer connection", e); } catch (Throwable e) { LOGGER.log(Level.SEVERE, "Unexpected error while waiting for remote computer connection disconnect", e); errors.add(e); } } } private void _cleanUpPluginServletFilters(List<Throwable> errors) { LOGGER.log(Level.FINE, "Stopping filters"); try { PluginServletFilter.cleanUp(); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(SEVERE, "Failed to stop filters", e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(SEVERE, "Failed to stop filters", e); // save for later errors.add(e); } } private void _cleanUpReleaseAllLoggers(List<Throwable> errors) { LOGGER.log(Level.FINE, "Releasing all loggers"); try { LogFactory.releaseAll(); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { LOGGER.log(SEVERE, "Failed to release all loggers", e); // safe to ignore and continue for this one } catch (Throwable e) { LOGGER.log(SEVERE, "Failed to release all loggers", e); // save for later errors.add(e); } } public Object getDynamic(String token) { for (Action a : getActions()) { String url = a.getUrlName(); if (url==null) continue; if (url.equals(token) || url.equals('/' + token)) return a; } for (Action a : getManagementLinks()) if (Objects.equals(a.getUrlName(), 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); JSONObject json = req.getSubmittedForm(); workspaceDir = json.getString("rawWorkspaceDir"); buildsDir = json.getString("rawBuildsDir"); systemMessage = Util.nullify(req.getParameter("system_message")); boolean result = true; for (Descriptor<?> d : Functions.getSortedDescriptorsForGlobalConfigUnclassified()) result &= configureDescriptor(req,json,d); version = VERSION; save(); updateComputerList(); if(result) FormApply.success(req.getContextPath()+'/').generateResponse(req, rsp, null); else FormApply.success("configure").generateResponse(req, rsp, null); // back to config } finally { bc.commit(); } } /** * Gets the {@link CrumbIssuer} currently in use. * * @return null if none is in use. */ public CrumbIssuer getCrumbIssuer() { return crumbIssuer; } public void setCrumbIssuer(CrumbIssuer issuer) { crumbIssuer = issuer; } public synchronized void doTestPost( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { rsp.sendRedirect("foo"); } private boolean configureDescriptor(StaplerRequest req, JSONObject json, Descriptor<?> d) throws FormException { // collapse the structure to remain backward compatible with the JSON structure before 1. String name = d.getJsonSafeClassName(); JSONObject js = json.has(name) ? json.getJSONObject(name) : new JSONObject(); // if it doesn't have the property, the method returns invalid null object. json.putAll(js); return d.configure(req, js); } /** * Accepts submission from the node configuration page. */ @RequirePOST public synchronized void doConfigExecutorsSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException { checkPermission(ADMINISTER); BulkChange bc = new BulkChange(this); try { JSONObject json = req.getSubmittedForm(); MasterBuildConfiguration mbc = MasterBuildConfiguration.all().get(MasterBuildConfiguration.class); if (mbc!=null) mbc.configure(req,json); getNodeProperties().rebuild(req, json.optJSONObject("nodeProperties"), NodeProperty.all()); } finally { bc.commit(); } updateComputerList(); rsp.sendRedirect(req.getContextPath()+'/'+toComputer().getUrl()); // back to the computer page } /** * Accepts the new description. */ @RequirePOST public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { getPrimaryView().doSubmitDescription(req, rsp); } @RequirePOST // TODO does not seem to work on _either_ overload! public synchronized HttpRedirect doQuietDown() throws IOException { try { return doQuietDown(false,0); } catch (InterruptedException e) { throw new AssertionError(); // impossible } } /** * Quiet down Jenkins - preparation for a restart * * @param block Block until the system really quiets down and no builds are running * @param timeout If non-zero, only block up to the specified number of milliseconds */ @RequirePOST public HttpRedirect doQuietDown(@QueryParameter boolean block, @QueryParameter int timeout) throws InterruptedException, IOException { synchronized (this) { checkPermission(ADMINISTER); isQuietingDown = true; } if (block) { long waitUntil = timeout; if (timeout > 0) waitUntil += System.currentTimeMillis(); while (isQuietingDown && (timeout <= 0 || System.currentTimeMillis() < waitUntil) && !RestartListener.isAllReady()) { Thread.sleep(1000); } } return new HttpRedirect("."); } /** * Cancel previous quiet down Jenkins - preparation for a restart */ @RequirePOST // TODO the cancel link needs to be updated accordingly public synchronized HttpRedirect doCancelQuietDown() { checkPermission(ADMINISTER); isQuietingDown = false; getQueue().scheduleMaintenance(); return new HttpRedirect("."); } public HttpResponse doToggleCollapse() throws ServletException, IOException { final StaplerRequest request = Stapler.getCurrentRequest(); final String paneId = request.getParameter("paneId"); PaneStatusProperties.forCurrentUser().toggleCollapsed(paneId); return HttpResponses.forwardToPreviousPage(); } /** * Backward compatibility. Redirect to the thread dump. */ public void doClassicThreadDump(StaplerResponse rsp) throws IOException, ServletException { rsp.sendRedirect2("threadDump"); } /** * Obtains the thread dump of all agents (including the master.) * * <p> * Since this is for diagnostics, it has a built-in precautionary measure against hang agents. */ public Map<String,Map<String,String>> getAllThreadDumps() throws IOException, InterruptedException { checkPermission(ADMINISTER); // issue the requests all at once Map<String,Future<Map<String,String>>> future = new HashMap<String, Future<Map<String, String>>>(); for (Computer c : getComputers()) { try { future.put(c.getName(), RemotingDiagnostics.getThreadDumpAsync(c.getChannel())); } catch(Exception e) { LOGGER.info("Failed to get thread dump for node " + c.getName() + ": " + e.getMessage()); } } if (toComputer() == null) { future.put("master", RemotingDiagnostics.getThreadDumpAsync(FilePath.localChannel)); } // if the result isn't available in 5 sec, ignore that. // this is a precaution against hang nodes long endTime = System.currentTimeMillis() + 5000; Map<String,Map<String,String>> r = new HashMap<String, Map<String, String>>(); for (Entry<String, Future<Map<String, String>>> e : future.entrySet()) { try { r.put(e.getKey(), e.getValue().get(endTime-System.currentTimeMillis(), TimeUnit.MILLISECONDS)); } catch (Exception x) { StringWriter sw = new StringWriter(); x.printStackTrace(new PrintWriter(sw,true)); r.put(e.getKey(), Collections.singletonMap("Failed to retrieve thread dump",sw.toString())); } } return Collections.unmodifiableSortedMap(new TreeMap<String, Map<String, String>>(r)); } @RequirePOST public synchronized TopLevelItem doCreateItem( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { return itemGroupMixIn.createTopLevelItem(req, rsp); } /** * @since 1.319 */ public TopLevelItem createProjectFromXML(String name, InputStream xml) throws IOException { return itemGroupMixIn.createProjectFromXML(name, xml); } @SuppressWarnings({"unchecked"}) public <T extends TopLevelItem> T copy(T src, String name) throws IOException { return itemGroupMixIn.copy(src, name); } // 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); } @RequirePOST public synchronized void doCreateView( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException { checkPermission(View.CREATE); addView(View.create(req,rsp, this)); } /** * Check if the given name is suitable as a name * for job, view, etc. * * @throws Failure * if the given name is not good */ public static void checkGoodName(String name) throws Failure { if(name==null || name.length()==0) throw new Failure(Messages.Hudson_NoName()); if(".".equals(name.trim())) throw new Failure(Messages.Jenkins_NotAllowedName(".")); if("..".equals(name.trim())) throw new Failure(Messages.Jenkins_NotAllowedName("..")); for( int i=0; i<name.length(); i++ ) { char ch = name.charAt(i); if(Character.isISOControl(ch)) { throw new Failure(Messages.Hudson_ControlCodeNotAllowed(toPrintableName(name))); } if("?*/\\%!@#$^&|<>[]:;".indexOf(ch)!=-1) throw new Failure(Messages.Hudson_UnsafeChar(ch)); } // looks good } private static 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 { // TODO fire something in SecurityListener? (seems to be used only for REST calls when LegacySecurityRealm is active) if(req.getUserPrincipal()==null) { // authentication must have failed rsp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } // the user is now authenticated, so send him back to the target String path = req.getContextPath()+req.getOriginalRestOfPath(); String q = req.getQueryString(); if(q!=null) path += '?'+q; rsp.sendRedirect2(path); } /** * Called once the user logs in. Just forward to the top page. * Used only by {@link LegacySecurityRealm}. */ public void doLoginEntry( StaplerRequest req, StaplerResponse rsp ) throws IOException { if(req.getUserPrincipal()==null) { rsp.sendRedirect2("noPrincipal"); return; } // TODO fire something in SecurityListener? 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 { String user = getAuthentication().getName(); securityRealm.doLogout(req, rsp); SecurityListener.fireLoggedOut(user); } /** * Serves jar files for JNLP agents. */ public Slave.JnlpJar getJnlpJars(String fileName) { return new Slave.JnlpJar(fileName); } public Slave.JnlpJar doJnlpJars(StaplerRequest req) { return new Slave.JnlpJar(req.getRestOfPath().substring(1)); } /** * Reloads the configuration. */ @RequirePOST public synchronized HttpResponse doReload() throws IOException { checkPermission(ADMINISTER); LOGGER.log(Level.WARNING, "Reloading Jenkins as requested by {0}", getAuthentication().getName()); // engage "loading ..." UI and then run the actual task in a separate thread servletContext.setAttribute("app", new HudsonIsLoading()); new Thread("Jenkins config reload thread") { @Override public void run() { try { ACL.impersonate(ACL.SYSTEM); reload(); } catch (Exception e) { LOGGER.log(SEVERE,"Failed to reload Jenkins config",e); new JenkinsReloadFailed(e).publish(servletContext,root); } } }.start(); return HttpResponses.redirectViaContextPath("/"); } /** * Reloads the configuration synchronously. * Beware that this calls neither {@link ItemListener#onLoaded} nor {@link Initializer}s. */ public void reload() throws IOException, InterruptedException, ReactorException { queue.save(); executeReactor(null, loadTasks()); User.reload(); queue.load(); servletContext.setAttribute("app", this); } /** * Do a finger-print check. */ public void doDoFingerprintCheck( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { // Parse the request MultipartFormDataParser p = new MultipartFormDataParser(req); if(isUseCrumbs() && !getCrumbIssuer().validateCrumb(req, p)) { rsp.sendError(HttpServletResponse.SC_FORBIDDEN,"No crumb found"); } try { rsp.sendRedirect2(req.getContextPath()+"/fingerprint/"+ Util.getDigestOf(p.getFileItem("name").getInputStream())+'/'); } finally { p.cleanUp(); } } /** * For debugging. Expose URL to perform GC. */ @edu.umd.cs.findbugs.annotations.SuppressWarnings("DM_GC") @RequirePOST public void doGc(StaplerResponse rsp) throws IOException { checkPermission(Jenkins.ADMINISTER); System.gc(); rsp.setStatus(HttpServletResponse.SC_OK); rsp.setContentType("text/plain"); rsp.getWriter().println("GCed"); } /** * End point that intentionally throws an exception to test the error behaviour. * @since 1.467 */ public void doException() { throw new RuntimeException(); } public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws IOException, JellyException { ContextMenu menu = new ContextMenu().from(this, request, response); for (MenuItem i : menu.items) { if (i.url.equals(request.getContextPath() + "/manage")) { // add "Manage Jenkins" subitems i.subMenu = new ContextMenu().from(this, request, response, "manage"); } } return menu; } public ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { ContextMenu menu = new ContextMenu(); for (View view : getViews()) { menu.add(view.getViewUrl(),view.getDisplayName()); } return menu; } /** * Obtains the heap dump. */ public HeapDump getHeapDump() throws IOException { return new HeapDump(this,FilePath.localChannel); } /** * Simulates OutOfMemoryError. * Useful to make sure OutOfMemoryHeapDump setting. */ @RequirePOST public void doSimulateOutOfMemory() throws IOException { checkPermission(ADMINISTER); System.out.println("Creating artificial OutOfMemoryError situation"); List<Object> args = new ArrayList<Object>(); while (true) args.add(new byte[1024*1024]); } /** * Binds /userContent/... to $JENKINS_HOME/userContent. */ public DirectoryBrowserSupport doUserContent() { return new DirectoryBrowserSupport(this,getRootPath().child("userContent"),"User content","folder.png",true); } /** * Perform a restart of Jenkins, if we can. * * This first replaces "app" to {@link HudsonIsRestarting} */ @CLIMethod(name="restart") public void doRestart(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, RestartNotSupportedException { checkPermission(ADMINISTER); if (req != null && req.getMethod().equals("GET")) { req.getView(this,"_restart.jelly").forward(req,rsp); return; } restart(); if (rsp != null) // null for CLI rsp.sendRedirect2("."); } /** * Queues up a restart of Jenkins for when there are no builds running, if we can. * * This first replaces "app" to {@link HudsonIsRestarting} * * @since 1.332 */ @CLIMethod(name="safe-restart") public HttpResponse doSafeRestart(StaplerRequest req) throws IOException, ServletException, RestartNotSupportedException { checkPermission(ADMINISTER); if (req != null && req.getMethod().equals("GET")) return HttpResponses.forwardToView(this,"_safeRestart.jelly"); safeRestart(); return HttpResponses.redirectToDot(); } /** * Performs a restart. */ public void restart() throws RestartNotSupportedException { final Lifecycle lifecycle = Lifecycle.get(); lifecycle.verifyRestartable(); // verify that Jenkins is restartable servletContext.setAttribute("app", new HudsonIsRestarting()); new Thread("restart thread") { final String exitUser = getAuthentication().getName(); @Override public void run() { try { ACL.impersonate(ACL.SYSTEM); // give some time for the browser to load the "reloading" page Thread.sleep(5000); LOGGER.severe(String.format("Restarting VM as requested by %s",exitUser)); for (RestartListener listener : RestartListener.all()) listener.onRestart(); lifecycle.restart(); } catch (InterruptedException | IOException e) { LOGGER.log(Level.WARNING, "Failed to restart Jenkins",e); } } }.start(); } /** * Queues up a restart to be performed once there are no builds currently running. * @since 1.332 */ public void safeRestart() throws RestartNotSupportedException { final Lifecycle lifecycle = Lifecycle.get(); lifecycle.verifyRestartable(); // verify that Jenkins is restartable // Quiet down so that we won't launch new builds. isQuietingDown = true; new Thread("safe-restart thread") { final String exitUser = getAuthentication().getName(); @Override public void run() { try { ACL.impersonate(ACL.SYSTEM); // Wait 'til we have no active executors. doQuietDown(true, 0); // Make sure isQuietingDown is still true. if (isQuietingDown) { servletContext.setAttribute("app",new HudsonIsRestarting()); // give some time for the browser to load the "reloading" page LOGGER.info("Restart in 10 seconds"); Thread.sleep(10000); LOGGER.severe(String.format("Restarting VM as requested by %s",exitUser)); for (RestartListener listener : RestartListener.all()) listener.onRestart(); lifecycle.restart(); } else { LOGGER.info("Safe-restart mode cancelled"); } } catch (Throwable e) { LOGGER.log(Level.WARNING, "Failed to restart Jenkins",e); } } }.start(); } @Extension @Restricted(NoExternalUse.class) public static class MasterRestartNotifyier extends RestartListener { @Override public void onRestart() { Computer computer = Jenkins.getInstance().toComputer(); if (computer == null) return; RestartCause cause = new RestartCause(); for (ComputerListener listener: ComputerListener.all()) { listener.onOffline(computer, cause); } } @Override public boolean isReadyToRestart() throws IOException, InterruptedException { return true; } private static class RestartCause extends OfflineCause.SimpleOfflineCause { protected RestartCause() { super(Messages._Jenkins_IsRestarting()); } } } /** * Shutdown the system. * @since 1.161 */ @CLIMethod(name="shutdown") @RequirePOST 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().getName(), req!=null?req.getRemoteAddr():"???")); if (rsp!=null) { rsp.setStatus(HttpServletResponse.SC_OK); rsp.setContentType("text/plain"); PrintWriter w = rsp.getWriter(); w.println("Shutting down"); w.close(); } System.exit(0); } /** * Shutdown the system safely. * @since 1.332 */ @CLIMethod(name="safe-shutdown") @RequirePOST public HttpResponse doSafeExit(StaplerRequest req) throws IOException { checkPermission(ADMINISTER); isQuietingDown = true; final String exitUser = getAuthentication().getName(); final String exitAddr = req!=null ? req.getRemoteAddr() : "unknown"; new Thread("safe-exit thread") { @Override public void run() { try { ACL.impersonate(ACL.SYSTEM); LOGGER.severe(String.format("Shutting down VM as requested by %s from %s", exitUser, exitAddr)); // Wait 'til we have no active executors. doQuietDown(true, 0); // Make sure isQuietingDown is still true. if (isQuietingDown) { cleanUp(); System.exit(0); } } catch (Exception e) { LOGGER.log(Level.WARNING, "Failed to shut down Jenkins", e); } } }.start(); return HttpResponses.plainText("Shutting down as soon as all jobs are complete"); } /** * Gets the {@link Authentication} object that represents the user * associated with the current request. */ public static @Nonnull 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 = 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"), FilePath.localChannel, getACL()); } /** * 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"), FilePath.localChannel, getACL()); } /** * @since 1.509.1 */ public static void _doScript(StaplerRequest req, StaplerResponse rsp, RequestDispatcher view, VirtualChannel channel, ACL acl) throws IOException, ServletException { // ability to run arbitrary script is dangerous acl.checkPermission(RUN_SCRIPTS); String text = req.getParameter("script"); if (text != null) { if (!"POST".equals(req.getMethod())) { throw HttpResponses.error(HttpURLConnection.HTTP_BAD_METHOD, "requires POST"); } if (channel == null) { throw HttpResponses.error(HttpURLConnection.HTTP_NOT_FOUND, "Node is offline"); } try { req.setAttribute("output", RemotingDiagnostics.executeGroovy(text, channel)); } 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. */ @RequirePOST public void doEval(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { checkPermission(RUN_SCRIPTS); 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 { if (getSecurityRealm().allowsSignup()) { req.getView(getSecurityRealm(), "signup.jelly").forward(req, rsp); return; } req.getView(SecurityRealm.class, "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) throw new ServletException(); Cookie cookie = new Cookie("iconSize", Functions.validateIconSize(qs)); cookie.setMaxAge(/* ~4 mo. */9999999); // #762 rsp.addCookie(cookie); String ref = req.getHeader("Referer"); if(ref==null) ref="."; rsp.sendRedirect2(ref); } @RequirePOST public void doFingerprintCleanup(StaplerResponse rsp) throws IOException { FingerprintCleanupThread.invoke(); rsp.setStatus(HttpServletResponse.SC_OK); rsp.setContentType("text/plain"); rsp.getWriter().println("Invoked"); } @RequirePOST 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(!JDK.isDefaultName(value)) // 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(Jenkins.this)) return FormValidation.ok(); else return FormValidation.errorWithMarkup(Messages.Hudson_NoJavaInPath(request.getContextPath())); } /** * Checks if a top-level view with the given name exists and * make sure that the name is good as a view name. */ public FormValidation doCheckViewName(@QueryParameter String value) { checkPermission(View.CREATE); String name = fixEmpty(value); if (name == null) return FormValidation.ok(); // already exists? if (getView(name) != null) return FormValidation.error(Messages.Hudson_ViewAlreadyExists(name)); // good view name? try { checkGoodName(name); } catch (Failure e) { return FormValidation.error(e.getMessage()); } return FormValidation.ok(); } /** * Checks if a top-level view with the given name exists. * @deprecated 1.512 */ @Deprecated 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)); } /** * Serves static resources placed along with Jelly view files. * <p> * This method can serve a lot of files, so care needs to be taken * to make this method secure. It's not clear to me what's the best * strategy here, though the current implementation is based on * file extensions. */ public void doResources(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { String path = req.getRestOfPath(); // cut off the "..." portion of /resources/.../path/to/file // as this is only used to make path unique (which in turn // allows us to set a long expiration date path = path.substring(path.indexOf('/',1)+1); int idx = path.lastIndexOf('.'); String extension = path.substring(idx+1); if(ALLOWED_RESOURCE_EXTENSIONS.contains(extension)) { URL url = pluginManager.uberClassLoader.getResource(path); if(url!=null) { long expires = MetaClass.NO_CACHE ? 0 : 365L * 24 * 60 * 60 * 1000; /*1 year*/ rsp.serveFile(req,url,expires); return; } } rsp.sendError(HttpServletResponse.SC_NOT_FOUND); } /** * Extension list that {@link #doResources(StaplerRequest, StaplerResponse)} can serve. * This set is mutable to allow plugins to add additional extensions. */ public static final Set<String> ALLOWED_RESOURCE_EXTENSIONS = new HashSet<String>(Arrays.asList( "js|css|jpeg|jpg|png|gif|html|htm".split("\\|") )); /** * Checks if container uses UTF-8 to decode URLs. See * http://wiki.jenkins-ci.org/display/JENKINS/Tomcat#Tomcat-i18n */ public FormValidation doCheckURIEncoding(StaplerRequest request) throws IOException { // expected is non-ASCII String final String expected = "\u57f7\u4e8b"; final String value = fixEmpty(request.getParameter("value")); if (!expected.equals(value)) return FormValidation.warningWithMarkup(Messages.Hudson_NotUsesUTF8ToDecodeURL()); return FormValidation.ok(); } /** * Does not check when system default encoding is "ISO-8859-1". */ public static boolean isCheckURIEncodingEnabled() { return !"ISO-8859-1".equalsIgnoreCase(System.getProperty("file.encoding")); } /** * Rebuilds the dependency map. */ public void rebuildDependencyGraph() { DependencyGraph graph = new DependencyGraph(); graph.build(); // volatile acts a as a memory barrier here and therefore guarantees // that graph is fully build, before it's visible to other threads dependencyGraph = graph; dependencyGraphDirty.set(false); } /** * Rebuilds the dependency map asynchronously. * * <p> * This would keep the UI thread more responsive and helps avoid the deadlocks, * as dependency graph recomputation tends to touch a lot of other things. * * @since 1.522 */ public Future<DependencyGraph> rebuildDependencyGraphAsync() { dependencyGraphDirty.set(true); return Timer.get().schedule(new java.util.concurrent.Callable<DependencyGraph>() { @Override public DependencyGraph call() throws Exception { if (dependencyGraphDirty.get()) { rebuildDependencyGraph(); } return dependencyGraph; } }, 500, TimeUnit.MILLISECONDS); } public DependencyGraph getDependencyGraph() { return dependencyGraph; } // for Jelly public List<ManagementLink> getManagementLinks() { return ManagementLink.all(); } /** * If set, a currently active setup wizard - e.g. installation * * @since 2.0 */ @Restricted(NoExternalUse.class) public SetupWizard getSetupWizard() { return setupWizard; } /** * 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(); for (String name : ALWAYS_READABLE_PATHS) { if (rest.startsWith(name)) { return this; } } for (String name : getUnprotectedRootActions()) { if (rest.startsWith("/" + name + "/") || rest.equals("/" + name)) { return this; } } // TODO SlaveComputer.doSlaveAgentJnlp; there should be an annotation to request unprotected access if (rest.matches("/computer/[^/]+/slave-agent[.]jnlp") && "true".equals(Stapler.getCurrentRequest().getParameter("encrypt"))) { return this; } throw e; } return this; } /** * Gets a list of unprotected root actions. * These URL prefixes should be exempted from access control checks by container-managed security. * Ideally would be synchronized with {@link #getTarget}. * @return a list of {@linkplain Action#getUrlName URL names} * @since 1.495 */ public Collection<String> getUnprotectedRootActions() { Set<String> names = new TreeSet<String>(); names.add("jnlpJars"); // TODO cleaner to refactor doJnlpJars into a URA // TODO consider caching (expiring cache when actions changes) for (Action a : getActions()) { if (a instanceof UnprotectedRootAction) { String url = a.getUrlName(); if (url == null) continue; names.add(url); } } return names; } /** * Fallback to the primary view. */ public View getStaplerFallback() { return getPrimaryView(); } /** * This method checks all existing jobs to see if displayName is * unique. It does not check the displayName against the displayName of the * job that the user is configuring though to prevent a validation warning * if the user sets the displayName to what it currently is. * @param displayName * @param currentJobName */ boolean isDisplayNameUnique(String displayName, String currentJobName) { Collection<TopLevelItem> itemCollection = items.values(); // if there are a lot of projects, we'll have to store their // display names in a HashSet or something for a quick check for(TopLevelItem item : itemCollection) { if(item.getName().equals(currentJobName)) { // we won't compare the candidate displayName against the current // item. This is to prevent an validation warning if the user // sets the displayName to what the existing display name is continue; } else if(displayName.equals(item.getDisplayName())) { return false; } } return true; } /** * True if there is no item in Jenkins that has this name * @param name The name to test * @param currentJobName The name of the job that the user is configuring */ boolean isNameUnique(String name, String currentJobName) { Item item = getItem(name); if(null==item) { // the candidate name didn't return any items so the name is unique return true; } else if(item.getName().equals(currentJobName)) { // the candidate name returned an item, but the item is the item // that the user is configuring so this is ok return true; } else { // the candidate name returned an item, so it is not unique return false; } } /** * Checks to see if the candidate displayName collides with any * existing display names or project names * @param displayName The display name to test * @param jobName The name of the job the user is configuring */ public FormValidation doCheckDisplayName(@QueryParameter String displayName, @QueryParameter String jobName) { displayName = displayName.trim(); if(LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Current job name is " + jobName); } if(!isNameUnique(displayName, jobName)) { return FormValidation.warning(Messages.Jenkins_CheckDisplayName_NameNotUniqueWarning(displayName)); } else if(!isDisplayNameUnique(displayName, jobName)){ return FormValidation.warning(Messages.Jenkins_CheckDisplayName_DisplayNameNotUniqueWarning(displayName)); } else { return FormValidation.ok(); } } public static class MasterComputer extends Computer { protected MasterComputer() { super(Jenkins.getInstance()); } /** * Returns "" to match with {@link Jenkins#getNodeName()}. */ @Override public String getName() { return ""; } @Override public boolean isConnecting() { return false; } @Override public String getDisplayName() { return Messages.Hudson_Computer_DisplayName(); } @Override public String getCaption() { return Messages.Hudson_Computer_Caption(); } @Override public String getUrl() { return "computer/(master)/"; } public RetentionStrategy getRetentionStrategy() { return RetentionStrategy.NOOP; } /** * Will always keep this guy alive so that it can function as a fallback to * execute {@link FlyweightTask}s. See JENKINS-7291. */ @Override protected boolean isAlive() { return true; } @Override public Boolean isUnix() { return !Functions.isWindows(); } /** * Report an error. */ @Override public HttpResponse doDoDelete() throws IOException { throw HttpResponses.status(SC_BAD_REQUEST); } @Override public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException { Jenkins.getInstance().doConfigExecutorsSubmit(req, rsp); } @WebMethod(name="config.xml") @Override public void doConfigDotXml(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { throw HttpResponses.status(SC_BAD_REQUEST); } @Override public boolean hasPermission(Permission permission) { // no one should be allowed to delete the master. // this hides the "delete" link from the /computer/(master) page. if(permission==Computer.DELETE) return false; // Configuration of master node requires ADMINISTER permission return super.hasPermission(permission==Computer.CONFIGURE ? Jenkins.ADMINISTER : permission); } @Override public VirtualChannel getChannel() { return FilePath.localChannel; } @Override public Charset getDefaultCharset() { return Charset.defaultCharset(); } public List<LogRecord> getLogRecords() throws IOException, InterruptedException { return logRecords; } @RequirePOST 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); } protected Future<?> _connect(boolean forceReconnect) { return Futures.precomputed(null); } /** * {@link LocalChannel} instance that can be used to execute programs locally. * * @deprecated as of 1.558 * Use {@link FilePath#localChannel} */ @Deprecated public static final LocalChannel localChannel = FilePath.localChannel; } /** * Shortcut for {@code Jenkins.getInstanceOrNull()?.lookup.get(type)} */ public static @CheckForNull <T> T lookup(Class<T> type) { Jenkins j = Jenkins.getInstanceOrNull(); return j != null ? j.lookup.get(type) : null; } /** * Live view of recent {@link LogRecord}s produced by Jenkins. */ public static List<LogRecord> logRecords = Collections.emptyList(); // initialized to dummy value to avoid NPE /** * Thread-safe reusable {@link XStream}. */ public static final XStream XSTREAM; /** * Alias to {@link #XSTREAM} so that one can access additional methods on {@link XStream2} more easily. */ public static final XStream2 XSTREAM2; private static final int TWICE_CPU_NUM = Math.max(4, 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 NamingThreadFactory(new DaemonThreadFactory(), "Jenkins load")); private static void computeVersion(ServletContext context) { // set the version Properties props = new Properties(); InputStream is = null; try { is = Jenkins.class.getResourceAsStream("jenkins-version.properties"); if(is!=null) props.load(is); } catch (IOException e) { e.printStackTrace(); // if the version properties is missing, that's OK. } finally { IOUtils.closeQuietly(is); } String ver = props.getProperty("version"); if(ver==null) ver = UNCOMPUTED_VERSION; if(Main.isDevelopmentMode && "${build.version}".equals(ver)) { // in dev mode, unable to get version (ahem Eclipse) try { File dir = new File(".").getAbsoluteFile(); while(dir != null) { File pom = new File(dir, "pom.xml"); if (pom.exists() && "pom".equals(XMLUtils.getValue("/project/artifactId", pom))) { pom = pom.getCanonicalFile(); LOGGER.info("Reading version from: " + pom.getAbsolutePath()); ver = XMLUtils.getValue("/project/version", pom); break; } dir = dir.getParentFile(); } LOGGER.info("Jenkins is in dev mode, using version: " + ver); } catch (Exception e) { LOGGER.log(Level.WARNING, "Unable to read Jenkins version: " + e.getMessage(), e); } } VERSION = ver; context.setAttribute("version",ver); VERSION_HASH = Util.getDigestOf(ver).substring(0, 8); SESSION_HASH = Util.getDigestOf(ver+System.currentTimeMillis()).substring(0, 8); if(ver.equals(UNCOMPUTED_VERSION) || SystemProperties.getBoolean("hudson.script.noCache")) RESOURCE_PATH = ""; else RESOURCE_PATH = "/static/"+SESSION_HASH; VIEW_RESOURCE_PATH = "/resources/"+ SESSION_HASH; } /** * The version number before it is "computed" (by a call to computeVersion()). * @since 2.0 */ @Restricted(NoExternalUse.class) public static final String UNCOMPUTED_VERSION = "?"; /** * Version number of this Jenkins. */ public static String VERSION = UNCOMPUTED_VERSION; /** * Parses {@link #VERSION} into {@link VersionNumber}, or null if it's not parseable as a version number * (such as when Jenkins is run with "mvn hudson-dev:run") */ public @CheckForNull static VersionNumber getVersion() { return toVersion(VERSION); } /** * Get the stored version of Jenkins, as stored by * {@link #doConfigSubmit(org.kohsuke.stapler.StaplerRequest, org.kohsuke.stapler.StaplerResponse)}. * <p> * Parses the version into {@link VersionNumber}, or null if it's not parseable as a version number * (such as when Jenkins is run with "mvn hudson-dev:run") * @since 2.0 */ @Restricted(NoExternalUse.class) public @CheckForNull static VersionNumber getStoredVersion() { return toVersion(Jenkins.getActiveInstance().version); } /** * Parses a version string into {@link VersionNumber}, or null if it's not parseable as a version number * (such as when Jenkins is run with "mvn hudson-dev:run") */ private static @CheckForNull VersionNumber toVersion(@CheckForNull String versionString) { if (versionString == null) { return null; } try { return new VersionNumber(versionString); } catch (NumberFormatException e) { try { // for non-released version of Jenkins, this looks like "1.345 (private-foobar), so try to approximate. int idx = versionString.indexOf(' '); if (idx > 0) { return new VersionNumber(versionString.substring(0,idx)); } } catch (NumberFormatException _) { // fall through } // totally unparseable return null; } catch (IllegalArgumentException e) { // totally unparseable return null; } } /** * Hash of {@link #VERSION}. */ public static String VERSION_HASH; /** * Unique random token that identifies the current session. * Used to make {@link #RESOURCE_PATH} unique so that we can set long "Expires" header. * * We used to use {@link #VERSION_HASH}, but making this session local allows us to * reuse the same {@link #RESOURCE_PATH} for static resources in plugins. */ public static String SESSION_HASH; /** * Prefix to static resources like images and javascripts in the war file. * Either "" or strings like "/static/VERSION", which avoids Jenkins 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 Jenkins 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 = Configuration.getBooleanConfigParameter("parallelLoad", true); public static boolean KILL_AFTER_LOAD = Configuration.getBooleanConfigParameter("killAfterLoad", false); /** * @deprecated No longer used. */ @Deprecated public static boolean FLYWEIGHT_SUPPORT = true; /** * 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() * @deprecated as of 1.464 * This flag will have no effect. */ @Restricted(NoExternalUse.class) @Deprecated public static boolean CONCURRENT_BUILD = true; /** * Switch to enable people to use a shorter workspace name. */ private static final String WORKSPACE_DIRNAME = Configuration.getStringConfigParameter("workspaceDirName", "workspace"); /** * Automatically try to launch an agent when Jenkins is initialized or a new agent computer is created. */ public static boolean AUTOMATIC_SLAVE_LAUNCH = true; private static final Logger LOGGER = Logger.getLogger(Jenkins.class.getName()); 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,PermissionScope.JENKINS); public static final Permission RUN_SCRIPTS = new Permission(PERMISSIONS, "RunScripts", Messages._Hudson_RunScriptsPermission_Description(),ADMINISTER,PermissionScope.JENKINS); /** * Urls that are always visible without READ permission. * * <p>See also:{@link #getUnprotectedRootActions}. */ private static final ImmutableSet<String> ALWAYS_READABLE_PATHS = ImmutableSet.of( "/login", "/logout", "/accessDenied", "/adjuncts/", "/error", "/oops", "/signup", "/tcpSlaveAgentListener", "/federatedLoginService/", "/securityRealm", "/instance-identity" ); /** * {@link Authentication} object that represents the anonymous user. * Because Acegi creates its own {@link AnonymousAuthenticationToken} instances, the code must not * expect the singleton semantics. This is just a convenient instance. * * @since 1.343 */ public static final Authentication ANONYMOUS; static { try { ANONYMOUS = new AnonymousAuthenticationToken( "anonymous", "anonymous", new GrantedAuthority[]{new GrantedAuthorityImpl("anonymous")}); XSTREAM = XSTREAM2 = new XStream2(); XSTREAM.alias("jenkins", Jenkins.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); XSTREAM.addImplicitCollection(Jenkins.class, "disabledAgentProtocols", "disabledAgentProtocol", String.class); XSTREAM.addImplicitCollection(Jenkins.class, "enabledAgentProtocols", "enabledAgentProtocol", String.class); XSTREAM2.addCriticalField(Jenkins.class, "securityRealm"); XSTREAM2.addCriticalField(Jenkins.class, "authorizationStrategy"); // this seems to be necessary to force registration of converter early enough Mode.class.getEnumConstants(); // double check that initialization order didn't do any harm assert PERMISSIONS != null; assert ADMINISTER != null; } catch (RuntimeException | Error e) { // when loaded on an agent and this fails, subsequent NoClassDefFoundError will fail to chain the cause. // see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8051847 // As we don't know where the first exception will go, let's also send this to logging so that // we have a known place to look at. LOGGER.log(SEVERE, "Failed to load Jenkins.class", e); throw e; } } private static final class JenkinsJVMAccess extends JenkinsJVM { private static void _setJenkinsJVM(boolean jenkinsJVM) { JenkinsJVM.setJenkinsJVM(jenkinsJVM); } } }