Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nodes persistence cleanup, APIs to control loading #8979

Merged
merged 3 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 37 additions & 10 deletions core/src/main/java/hudson/model/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,26 @@
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.BulkChange;
import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.FilePath;
import hudson.FileSystemProvisioner;
import hudson.Launcher;
import hudson.Util;
import hudson.XmlFile;
import hudson.model.Descriptor.FormException;
import hudson.model.Queue.Task;
import hudson.model.labels.LabelAtom;
import hudson.model.listeners.SaveableListener;
import hudson.model.queue.CauseOfBlockage;
import hudson.remoting.Callable;
import hudson.remoting.VirtualChannel;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.slaves.Cloud;
import hudson.slaves.ComputerListener;
import hudson.slaves.EphemeralNode;
import hudson.slaves.NodeDescriptor;
import hudson.slaves.NodeProperty;
import hudson.slaves.NodePropertyDescriptor;
Expand All @@ -53,6 +57,7 @@
import hudson.util.DescribableList;
import hudson.util.EnumConverter;
import hudson.util.TagCloud;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Collections;
Expand All @@ -63,6 +68,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import jenkins.model.Nodes;
import jenkins.util.SystemProperties;
import jenkins.util.io.OnMaster;
import net.sf.json.JSONObject;
Expand Down Expand Up @@ -96,7 +102,7 @@
* @see Computer
*/
@ExportedBean
public abstract class Node extends AbstractModelObject implements ReconfigurableDescribable<Node>, ExtensionPoint, AccessControlled, OnMaster, Saveable {
public abstract class Node extends AbstractModelObject implements ReconfigurableDescribable<Node>, ExtensionPoint, AccessControlled, OnMaster, PersistenceRoot {

private static final Logger LOGGER = Logger.getLogger(Node.class.getName());

Expand All @@ -110,6 +116,8 @@
*/
protected transient volatile boolean holdOffLaunchUntilSave;

private transient Nodes parent;

@Override
public String getDisplayName() {
return getNodeName(); // default implementation
Expand All @@ -133,16 +141,18 @@
*/
@Override
public void save() throws IOException {
// this should be a no-op unless this node instance is the node instance in Jenkins' list of nodes
// thus where Jenkins.get() == null there is no list of nodes, so we do a no-op
// Nodes.updateNode(n) will only persist the node record if the node instance is in the list of nodes
// so either path results in the same behaviour: the node instance is only saved if it is in the list of nodes
// for all other cases we do not know where to persist the node record and hence we follow the default
// no-op of a Saveable.NOOP
final Jenkins jenkins = Jenkins.getInstanceOrNull();
if (jenkins != null) {
jenkins.updateNode(this);
if (parent == null) return;
if (this instanceof EphemeralNode) {

Check warning on line 145 in core/src/main/java/hudson/model/Node.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 145 is only partially covered, one branch is missing
Util.deleteRecursive(getRootDir());
return;

Check warning on line 147 in core/src/main/java/hudson/model/Node.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 146-147 are not covered by tests
}
if (BulkChange.contains(this)) return;

Check warning on line 149 in core/src/main/java/hudson/model/Node.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 149 is only partially covered, one branch is missing
getConfigFile().write(this);
SaveableListener.fireOnChange(this, getConfigFile());
}

protected XmlFile getConfigFile() {
return parent.getConfigFile(this);
}

/**
Expand Down Expand Up @@ -248,6 +258,11 @@
return true;
}

public void onLoad(Nodes parent, String name) {
this.parent = parent;
setNodeName(name);
}

/**
* Let Nodes be aware of the lifecycle of their own {@link Computer}.
*/
Expand Down Expand Up @@ -629,4 +644,16 @@
}
}

@Override
public File getRootDir() {
return getParent().getRootDirFor(this);
}

@NonNull
private Nodes getParent() {
if (parent == null) {

Check warning on line 654 in core/src/main/java/hudson/model/Node.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 654 is only partially covered, one branch is missing
throw new IllegalStateException("no parent set on " + getClass().getName() + "[" + getNodeName() + "]");

Check warning on line 655 in core/src/main/java/hudson/model/Node.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 655 is not covered by tests
}
return parent;
}
}
22 changes: 21 additions & 1 deletion core/src/main/java/jenkins/model/Jenkins.java
Original file line number Diff line number Diff line change
Expand Up @@ -540,1732 +540,1751 @@
*/
public final Hudson.CloudList clouds = new Hudson.CloudList(this);

@Restricted(Beta.class)
public void loadNode(File dir) throws IOException {
getNodesObject().load(dir);
}

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.get().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 final transient 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<>();

/**
* 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 final transient ViewGroupMixIn viewGroupMixIn = new ViewGroupMixIn(this) {
@Override
protected List<View> views() { return views; }

@Override
protected String primaryView() { return primaryView; }

@Override
protected void primaryView(String name) { primaryView = name; }
};


private final transient FingerprintMap fingerprintMap = new FingerprintMap();

/**
* Loaded plugins.
*/
public final transient PluginManager pluginManager;

public transient volatile TcpSlaveAgentListener tcpSlaveAgentListener;

private final transient Object tcpSlaveAgentListenerLock = new Object();

/**
* List of registered {@link SCMListener}s.
*/
private final transient CopyOnWriteList<SCMListener> scmListeners = new CopyOnWriteList<>();

/**
* TCP agent port.
* 0 for random, -1 to disable.
*/
private int slaveAgentPort = getSlaveAgentPortInitialValue(0);

private static int getSlaveAgentPortInitialValue(int def) {
return SystemProperties.getInteger(Jenkins.class.getName() + ".slaveAgentPort", def);
}

/**
* If -Djenkins.model.Jenkins.slaveAgentPort is defined, enforce it on every start instead of only the first one.
*/
private static final boolean SLAVE_AGENT_PORT_ENFORCE = SystemProperties.getBoolean(Jenkins.class.getName() + ".slaveAgentPortEnforce", false);

/**
* 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
@GuardedBy("this")
private List<String> disabledAgentProtocols;
/**
* @deprecated Just a temporary buffer for XSTream migration code from JENKINS-39465, do not use
*/
@Deprecated
private transient 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
@GuardedBy("this")
private List<String> enabledAgentProtocols;
/**
* @deprecated Just a temporary buffer for XSTream migration code from JENKINS-39465, do not use
*/
@Deprecated
private transient String[] _enabledAgentProtocols;

/**
* The TCP agent protocols that are enabled. Built from {@link #disabledAgentProtocols} and
* {@link #enabledAgentProtocols}.
*
* @since 2.16
* @see #setAgentProtocols(Set)
* @see #getAgentProtocols()
*/
@GuardedBy("this")
private transient Set<String> agentProtocols;

/**
* Whitespace-separated labels assigned to the built-in node as a {@link Node}.
*/
private String label = "";

private static /* non-final for Groovy */ String nodeNameAndSelfLabelOverride = SystemProperties.getString(Jenkins.class.getName() + ".nodeNameAndSelfLabelOverride");

/**
* {@link hudson.security.csrf.CrumbIssuer}
*/
private volatile CrumbIssuer crumbIssuer = GlobalCrumbIssuerConfiguration.createDefaultCrumbIssuer();

/**
* 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 final transient ConcurrentHashMap<String, Label> labels = new ConcurrentHashMap<>();

/**
* Load statistics of the entire system.
*
* This includes every executor and every job in the system.
*/
@Exported
public final transient 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 final transient LoadStatistics unlabeledLoad = new UnlabeledLoadStatistics();

/**
* {@link NodeProvisioner} that reacts to {@link #unlabeledLoad}.
* @since 1.467
*/
public final transient 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 final transient NodeProvisioner overallNodeProvisioner = unlabeledNodeProvisioner;


public final transient ServletContext servletContext;

/**
* Transient action list. Useful for adding navigation items to the navigation bar
* on the left.
*/
private final transient List<Action> actions = new CopyOnWriteArrayList<>();

/**
* List of built-in node-specific node properties
*/
private DescribableList<NodeProperty<?>, NodePropertyDescriptor> nodeProperties = new DescribableList<>(this);

/**
* List of global properties
*/
private DescribableList<NodeProperty<?>, NodePropertyDescriptor> globalNodeProperties = new DescribableList<>(this);

/**
* {@link AdministrativeMonitor}s installed on this system.
*
* @see AdministrativeMonitor
*/
public final transient List<AdministrativeMonitor> administrativeMonitors = getExtensionList(AdministrativeMonitor.class);

/**
* Widgets on Jenkins.
*/
private final transient List<Widget> widgets = getExtensionList(Widget.class);

/**
* {@link AdjunctManager}
*/
private final transient AdjunctManager adjuncts;

/**
* Code that handles {@link ItemGroup} work.
*/
private final transient 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.get()
*
* Do not use in the production code as the signature may change.
*/
public interface JenkinsHolder {
@CheckForNull Jenkins getInstance();
}

static JenkinsHolder HOLDER = new JenkinsHolder() {
@Override
public @CheckForNull Jenkins getInstance() {
return theInstance;
}
};

/**
* Gets the {@link Jenkins} singleton.
* @return {@link Jenkins} instance
* @throws IllegalStateException for the reasons that {@link #getInstanceOrNull} might return null
* @since 2.98
*/
@NonNull
public static Jenkins get() throws IllegalStateException {
Jenkins instance = getInstanceOrNull();
if (instance == null) {
throw new IllegalStateException("Jenkins.instance is missing. Read the documentation of Jenkins.getInstanceOrNull to see what you are doing wrong.");
}
return instance;
}

/**
* @deprecated This is a verbose historical alias for {@link #get}.
* @since 1.590
*/
@Deprecated
@NonNull
public static Jenkins getActiveInstance() throws IllegalStateException {
return get();
}

/**
* Gets the {@link Jenkins} singleton.
* {@link #get} is what you normally want.
* <p>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 this method.
* <p>In other cases you may have code that might end up running on a remote JVM and not on the Jenkins controller or built-in node.
* For those cases you really should rewrite your code so that when the {@link Callable} is sent over the remoting channel
* it can do whatever it needs without ever referring to {@link Jenkins};
* for example, gather any information you need on the controller side before constructing the callable.
* If you must do a runtime check whether you are in the controller or agent, use {@link JenkinsJVM} rather than this method,
* as merely loading the {@link Jenkins} class file into an agent JVM can cause linkage errors under some conditions.
* @return The instance. Null if the {@link Jenkins} service has not been started, or was already shut down,
* or we are running on an unrelated JVM, typically an agent.
* @since 1.653
*/
@CLIResolver
@CheckForNull
public static Jenkins getInstanceOrNull() {
return HOLDER.getInstance();
}

/**
* @deprecated This is a historical alias for {@link #getInstanceOrNull} but with ambiguous nullability. Use {@link #get} in typical cases.
*/
@Nullable
@Deprecated
public static Jenkins getInstance() {
return getInstanceOrNull();
}

/**
* Secret key generated once and used for a long time, beyond
* container start/stop. Persisted outside {@code config.xml} to avoid
* accidental exposure.
*/
private final transient String secretKey;

private final transient 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;

/**
* If this is false, no migration is needed to reconfigure the built-in node (formerly 'master', now 'built-in').
* Otherwise, {@link BuiltInNodeMigration} will show up.
*/
// See #readResolve for null -> true transition and #save for null -> false transition
@Restricted(NoExternalUse.class)
/* package-private */ Boolean nodeRenameMigrationNeeded;

/**
* HTTP proxy configuration.
*/
public transient volatile ProxyConfiguration proxy;

/**
* Bound to "/log".
*/
private transient LogRecorderManager log = new LogRecorderManager();


private final transient 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.
*/
@SuppressFBWarnings({
"ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", // Trigger.timer
"DM_EXIT" // Exit is wanted here
})
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();
STARTUP_MARKER_FILE = new FileBoolean(new File(root, ".lastStarted"));
// As Jenkins is starting, grant this process full control
try (ACLContext ctx = ACL.as2(ACL.SYSTEM2)) {
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 = DEFAULT_WORKSPACES_DIR;
}

// 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);
labelAtomSet = Collections.unmodifiableSet(Label.parse(label));
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 {
byte[] random = new byte[32];
RANDOM.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;
WebApp webApp = WebApp.get(servletContext);
// JSON binding needs to be able to see all the classes from all the plugins
webApp.setClassLoader(pluginManager.uberClassLoader);
webApp.setJsonInErrorMessageSanitizer(RedactSecretJsonInErrorMessageSanitizer.INSTANCE);

TypedFilter typedFilter = new TypedFilter();
webApp.setFilterForGetMethods(typedFilter);
webApp.setFilterForFields(typedFilter);
webApp.setFilterForDoActions(new DoActionFilter());

StaplerFilteredActionListener actionListener = new StaplerFilteredActionListener();
webApp.setFilteredGetterTriggerListener(actionListener);
webApp.setFilteredDoActionTriggerListener(actionListener);
webApp.setFilteredFieldTriggerListener(actionListener);

webApp.setDispatchValidator(new StaplerDispatchValidator());
webApp.setFilteredDispatchTriggerListener(actionListener);

adjuncts = new AdjunctManager(servletContext, pluginManager.uberClassLoader, "adjuncts/" + SESSION_HASH, TimeUnit.DAYS.toMillis(365));

ClassFilterImpl.register();

// initialization consists of ...
executeReactor(is,
pluginManager.initTasks(is), // loading and preparing plugins
loadTasks(), // load jobs
InitMilestone.ordering() // forced ordering among key milestones
);

// Ensure we reached the final initialization state. Log the error otherwise
if (initLevel != InitMilestone.COMPLETED) {
LOGGER.log(SEVERE, "Jenkins initialization has not reached the COMPLETED initialization milestone after the startup. " +
"Current state: {0}. " +
"It may cause undefined incorrect behavior in Jenkins plugin relying on this state. " +
"It is likely an issue with the Initialization task graph. " +
"Example: usage of @Initializer(after = InitMilestone.COMPLETED) in a plugin (JENKINS-37759). " +
"Please create a bug in Jenkins bugtracker. ",
initLevel);
}


if (KILL_AFTER_LOAD)
// TODO cleanUp?
System.exit(0);
save();

launchTcpSlaveAgentListener();

Timer.get().scheduleAtFixedRate(new SafeTimerTask() {
@Override
protected void doRun() throws Exception {
trimLabels();
}
}, TimeUnit.MINUTES.toMillis(5), TimeUnit.MINUTES.toMillis(5), TimeUnit.MILLISECONDS);

updateComputerList();

{ // built-in node is online now, its instance must always exist
final Computer c = toComputer();
if (c != null) {
for (ComputerListener cl : ComputerListener.all()) {
try {
cl.onOnline(c, new LogTaskListener(LOGGER, INFO));
} catch (Exception e) {
// Per Javadoc log exceptions but still go online.
// NOTE: this does not include Errors, which indicate a fatal problem
LOGGER.log(WARNING, String.format("Exception in onOnline() for the computer listener %s on the built-in node",
cl.getClass()), e);
}
}
}
}

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));

STARTUP_MARKER_FILE.on();
}
}

/**
* Maintains backwards compatibility. Invoked by XStream when this object is de-serialized.
*/
@SuppressWarnings("unused")
protected Object readResolve() {
if (jdks == null) {
jdks = new ArrayList<>();
}
if (SLAVE_AGENT_PORT_ENFORCE) {
slaveAgentPort = getSlaveAgentPortInitialValue(slaveAgentPort);
}
synchronized (this) {
if (disabledAgentProtocols == null && _disabledAgentProtocols != null) {
disabledAgentProtocols = Arrays.asList(_disabledAgentProtocols);
_disabledAgentProtocols = null;
}
if (enabledAgentProtocols == null && _enabledAgentProtocols != null) {
enabledAgentProtocols = Arrays.asList(_enabledAgentProtocols);
_enabledAgentProtocols = null;
}
// Invalidate the protocols cache after the reload
agentProtocols = null;
}

// no longer persisted
installStateName = null;

if (nodeRenameMigrationNeeded == null) {
/* deserializing without a value set means we need to migrate */
nodeRenameMigrationNeeded = true;
}
_setLabelString(label);

return this;
}

/**
* Retrieve the proxy configuration.
*
* @return the proxy configuration
* @since 2.205
*/
@CheckForNull
public ProxyConfiguration getProxy() {
return proxy;
}

/**
* Set the proxy configuration.
*
* @param proxy the proxy to set
* @since 2.205
*/
public void setProxy(@CheckForNull ProxyConfiguration proxy) {
this.proxy = proxy;
}

/**
* Get the Jenkins {@link jenkins.install.InstallState install state}.
* @return The Jenkins {@link jenkins.install.InstallState install state}.
*/
@NonNull
public InstallState getInstallState() {
if (installState != null) {
installStateName = installState.name();
installState = null;
}
InstallState is = installStateName != null ? InstallState.valueOf(installStateName) : InstallState.UNKNOWN;
return is != null ? is : InstallState.UNKNOWN;
}

/**
* Update the current install state. This will invoke state.initializeState()
* when the state has been transitioned.
*/
public void setInstallState(@NonNull InstallState newState) {
String prior = installStateName;
installStateName = newState.name();
LOGGER.log(Main.isDevelopmentMode ? Level.INFO : Level.FINE, "Install state transitioning from: {0} to: {1}", new Object[] { prior, installStateName });
if (!installStateName.equals(prior)) {
getSetupWizard().onInstallStateUpdate(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;

String taskName = InitReactorRunner.getDisplayName(task);

Thread t = Thread.currentThread();
String name = t.getName();
if (taskName != null)
t.setName(taskName);
try (ACLContext ctx = ACL.as2(ACL.SYSTEM2)) { // full access in the initialization thread
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);
}
}

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;
getLifecycle().onExtendTimeout(EXTEND_TIMEOUT_SECONDS, TimeUnit.SECONDS);
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;
}

/**
* @since 2.24
*/
public boolean isSlaveAgentPortEnforced() {
return Jenkins.SLAVE_AGENT_PORT_ENFORCE;
}

/**
* @param port
* 0 to indicate random available TCP port. -1 to disable this service.
*/
public void setSlaveAgentPort(int port) throws IOException {
if (SLAVE_AGENT_PORT_ENFORCE) {
LOGGER.log(Level.WARNING, "setSlaveAgentPort({0}) call ignored because system property {1} is true", new String[] { Integer.toString(port), Jenkins.class.getName() + ".slaveAgentPortEnforce" });
} else {
forceSetSlaveAgentPort(port);
}
}

private void forceSetSlaveAgentPort(int port) throws IOException {
this.slaveAgentPort = port;
launchTcpSlaveAgentListener();
}

/**
* Returns the enabled agent protocols.
*
* @return the enabled agent protocols.
* @since 2.16
*/
@NonNull
public synchronized Set<String> getAgentProtocols() {
if (agentProtocols == null) {
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);
}
}
/*
* An empty result is almost never valid, but it can happen due to JENKINS-70206. Since we know the result
* is likely incorrect, at least decline to cache it so that a correct result can be computed later on
* rather than continuing to deliver the incorrect result indefinitely.
*/
if (!result.isEmpty()) {
agentProtocols = result;
}
return result;
}
return agentProtocols;
}

/**
* Sets the enabled agent protocols.
*
* @param protocols the enabled agent protocols.
* @since 2.16
*/
public synchronized void setAgentProtocols(@NonNull 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 controller with step 2, will
// have the protocol disabled for them. This is what we want. If they didn't upgrade to a controller
// with step 2, well there is not much we can do to differentiate them from somebody who is upgrading
// from a previous step 3 controller 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) {
final String administrativeMonitorId = getClass().getName() + ".tcpBind";
try {
tcpSlaveAgentListener = new TcpSlaveAgentListener(slaveAgentPort);
// remove previous monitor in case of previous error
AdministrativeMonitor toBeRemoved = null;
ExtensionList<AdministrativeMonitor> all = AdministrativeMonitor.all();
for (AdministrativeMonitor am : all) {
if (administrativeMonitorId.equals(am.id)) {
toBeRemoved = am;
break;
}
}
all.remove(toBeRemoved);
} catch (BindException e) {
LOGGER.log(Level.WARNING, String.format("Failed to listen to incoming agent connections through port %s. Change the port number", slaveAgentPort), e);
new AdministrativeError(administrativeMonitorId,
"Failed to listen to incoming agent connections",
"Failed to listen to incoming agent connections. <a href='configureSecurity'>Change the inbound TCP port number</a> to solve the problem.", e);
}
}
}
}

@Extension
@Restricted(NoExternalUse.class)
public static class EnforceSlaveAgentPortAdministrativeMonitor extends AdministrativeMonitor {
@Inject
Jenkins j;

@Override
public String getDisplayName() {
return jenkins.model.Messages.EnforceSlaveAgentPortAdministrativeMonitor_displayName();
}

public String getSystemPropertyName() {
return Jenkins.class.getName() + ".slaveAgentPort";
}

public int getExpectedPort() {
int slaveAgentPort = j.slaveAgentPort;
return Jenkins.getSlaveAgentPortInitialValue(slaveAgentPort);
}

@RequirePOST
public void doAct(StaplerRequest req, StaplerResponse rsp) throws IOException {
j.forceSetSlaveAgentPort(getExpectedPort());
rsp.sendRedirect2(req.getContextPath() + "/manage");
}

@Override
public boolean isActivated() {
int slaveAgentPort = Jenkins.get().slaveAgentPort;
return SLAVE_AGENT_PORT_ENFORCE && slaveAgentPort != Jenkins.getSlaveAgentPortInitialValue(slaveAgentPort);
}
}

@Override
public void setNodeName(String name) {
throw new UnsupportedOperationException(); // not allowed
}

@Override
public String getNodeDescription() {
return Messages.Hudson_NodeDescription();
}

@Exported
public String getDescription() {
return systemMessage;
}

@NonNull
public PluginManager getPluginManager() {
return pluginManager;
}

public UpdateCenter getUpdateCenter() {
return updateCenter;
}

/**
* If usage statistics has been disabled
*
* @since 2.226
*/
@CheckForNull
public Boolean isNoUsageStatistics() {
return noUsageStatistics;
}

/**
* If usage statistics are being collected
*
* @return {@code true} if usage statistics should be collected.
* Defaults to {@code true} when {@link #noUsageStatistics} is not set.
*/
public boolean isUsageStatisticsCollected() {
return noUsageStatistics == null || !noUsageStatistics;
}

/**
* Sets the noUsageStatistics flag
*
*/
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() {
/* Do not show "REST API" link in footer when on 404 error page */
final StaplerRequest req = Stapler.getCurrentRequest();
if (req != null) {
final Object attribute = req.getAttribute("javax.servlet.error.message");
if (attribute != null) {
return null;
}
}
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
* <a href="https://github.com/jenkinsci/instance-identity-plugin">the Instance Identity plugin</a> for more modern form of instance ID
* that can be challenged and verified.
*
* @since 1.498
*/
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 JENKINS-610
Descriptor d = findDescriptor(shortClassName, JobPropertyDescriptor.all());
return (JobPropertyDescriptor) d;
}

/**
* @deprecated
* UI method. Not meant to be used programmatically.
*/
@Deprecated
public ComputerSet getComputer() {
return new ComputerSet();
}

/**
* Only there to bind to /cloud/ URL. Otherwise /cloud/new gets resolved to getCloud("new") by stapler which is not what we want.
*/
@Restricted(DoNotUse.class)
public CloudSet getCloud() {
return new CloudSet();
}

/**
* 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("rawtypes") // too late to fix
public Descriptor getDescriptor(String id) {
// legacy descriptors that are registered 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)}.
*/
@Override
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.
*/
@CheckForNull
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
*/
@NonNull
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 updateNewComputer(Node n) {
updateNewComputer(n, AUTOMATIC_AGENT_LAUNCH);
}

protected void updateComputerList() {
updateComputerList(AUTOMATIC_AGENT_LAUNCH);
}

/** @deprecated Use {@link SCMListener#all} instead. */
@Deprecated
public CopyOnWriteList<SCMListener> getSCMListeners() {
return scmListeners;
}

/**
* Gets the plugin object from its short name.
* This allows URL {@code hudson/plugin/ID} to be served by the views
* of the plugin class.
* @param shortName Short name of the plugin
* @return The plugin singleton or {@code null} if for some reason the plugin is not loaded.
* The fact the plugin is loaded does not mean it is enabled and fully initialized for the current Jenkins session.
* Use {@link Plugin#getWrapper()} and then {@link PluginWrapper#isActive()} to check it.
*/
@CheckForNull
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 <P> Class of the plugin
* @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 singleton or {@code null} if for some reason the plugin is not loaded.
* The fact the plugin is loaded does not mean it is enabled and fully initialized for the current Jenkins session.
* Use {@link Plugin#getWrapper()} and then {@link PluginWrapper#isActive()} to check it.
*/
@SuppressWarnings("unchecked")
@CheckForNull
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<>();
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();
}

@StaplerDispatchable
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();
}

@Override
public Launcher createLauncher(TaskListener listener) {
return new LocalLauncher(listener).decorateFor(this);
}


@Override
public String getFullName() {
return "";
}

@Override
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.get().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)
*/
@Override
@Exported(name = "jobs")
public List<TopLevelItem> getItems() {
return getItems(t -> true);
}

/**
* Gets just the immediate children of {@link Jenkins} based on supplied predicate.
*
* @see #getAllItems(Class)
* @since 2.221
*/
@Override
public List<TopLevelItem> getItems(Predicate<TopLevelItem> pred) {
List<TopLevelItem> viewableItems = new ArrayList<>();
for (TopLevelItem item : items.values()) {
if (pred.test(item) && 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<>();
for (TopLevelItem i : getItems(type::isInstance)) {
r.add(type.cast(i));
}
return r;
}

/**
* 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<>();
for (Job j : allItems(Job.class))
names.add(j.getFullName());
names.sort(String.CASE_INSENSITIVE_ORDER);
return names;
}

@Override
public List<Action> getViewActions() {
return getActions();
}

/**
* Gets the names of all the {@link TopLevelItem}s.
*/
public Collection<String> getTopLevelItemNames() {
List<String> names = new ArrayList<>();
for (TopLevelItem j : items.values())
names.add(j.getName());
return names;
}

/**
* Gets a view by the specified name.
* The method iterates through {@link hudson.model.ViewGroup}s if required.
* @param name Name of the view
* @return View instance or {@code null} if it is missing
*/
@Override
@CheckForNull
public View getView(@CheckForNull String name) {
return viewGroupMixIn.getView(name);
}

/**
* Gets the read-only list of all {@link View}s.
*/
@Override
@Exported
public Collection<View> getViews() {
return viewGroupMixIn.getViews();
}

@Override
public void addView(@NonNull 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 {
try (BulkChange bc = new BulkChange(this)) {
this.views.clear();
for (View v : views) {
addView(v);
}
bc.commit();
}
}

@Override
public boolean canDelete(View view) {
return viewGroupMixIn.canDelete(view);
}

@Override
public synchronized void deleteView(View view) throws IOException {
viewGroupMixIn.deleteView(view);
}

@Override
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
@Override
public View getPrimaryView() {
return viewGroupMixIn.getPrimaryView();
}

public void setPrimaryView(@NonNull View v) {
this.primaryView = v.getViewName();
}

@Override
public ViewsTabBar getViewsTabBar() {
return viewsTabBar;
}

public void setViewsTabBar(ViewsTabBar viewsTabBar) {
this.viewsTabBar = viewsTabBar;
}

@Override
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() {
return computers.values().stream().sorted(Comparator.comparing(Computer::getName)).toArray(Computer[]::new);
}

@CLIResolver
public @CheckForNull Computer getComputer(@Argument(required = true, metaVar = "NAME", usage = "Node name") @NonNull String name) {
if (name.equals("(built-in)")
|| name.equals("(master)")) // backwards compatibility for URLs
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)
*/
@CheckForNull
public Label getLabel(String expr) {
if (expr == null) return null;
expr = QuotedStringTokenizer.unquote(expr);
while (true) {
Label l = labels.get(expr);
if (l != null)
return l;

// non-existent
try {
// For the record, this method creates temporary labels but there is a periodic task
// calling "trimLabels" to remove unused labels running every 5 minutes.
labels.putIfAbsent(expr, Label.parseExpression(expr));
} catch (IllegalArgumentException 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);
// For the record, this method creates temporary labels but there is a periodic task
// calling "trimLabels" to remove unused labels running every 5 minutes.
if (labels.putIfAbsent(name, la) == null)
la.load();
}
}

/**
* Returns the label atom of the given name, only if it already exists.
* @return non-null if the label atom already exists.
*/
@Restricted(NoExternalUse.class)
public @Nullable LabelAtom tryGetLabelAtom(@NonNull String name) {
Label label = labels.get(name);
if (label instanceof LabelAtom) {
return (LabelAtom) label;
}
return null;
}


/**
* Gets all the active labels in the current system.
*/
public Set<Label> getLabels() {
Set<Label> r = new TreeSet<>();
for (Label l : labels.values()) {
if (!l.isEmpty())
r.add(l);
}
return r;
}

@NonNull
private transient Set<LabelAtom> labelAtomSet;

@Override
protected Set<LabelAtom> getLabelAtomSet() {
return labelAtomSet;
}

public Set<LabelAtom> getLabelAtoms() {
Set<LabelAtom> r = new TreeSet<>();
for (Label l : labels.values()) {
if (!l.isEmpty() && l instanceof LabelAtom)
r.add((LabelAtom) l);
}
return r;
}

@Override
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<>(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);
}

@CheckForNull
@Restricted(Beta.class)
public Node getOrLoadNode(String nodeName) {
return getNodesObject().getOrLoad(nodeName);
}

/**
* Gets a {@link Cloud} by {@link Cloud#name its name}, or null.
*/
public Cloud getCloud(String name) {
return clouds.getByName(name);
}

@Override
protected ConcurrentMap<Node, Computer> getComputerMap() {
return computers;
}

/**
* Returns all {@link Node}s in the system, excluding {@link Jenkins} instance itself which
* represents the built-in node (in other words, this only returns agents).
*/
@Override
@NonNull
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.
* If a node of the same name already exists then that node will be replaced.
*/
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);
}

/**
* Unload a node from Jenkins without touching its configuration file.
*/
@Restricted(Beta.class)
public void unloadNode(@NonNull Node n) {
nodes.unload(n);
}

Check warning on line 2286 in core/src/main/java/jenkins/model/Jenkins.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 545-2286 are not covered by tests

/**
* 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.
Expand Down Expand Up @@ -3298,7 +3317,8 @@
/**
* The file we save our configuration.
*/
private XmlFile getConfigFile() {
@Restricted(NoExternalUse.class)
protected XmlFile getConfigFile() {
return new XmlFile(XSTREAM, new File(root, "config.xml"));
}

Expand Down
12 changes: 12 additions & 0 deletions core/src/main/java/jenkins/model/NodeListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import java.util.List;
import java.util.logging.Logger;
import jenkins.util.Listeners;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;

/**
* Listen to {@link Node} CRUD operations.
Expand All @@ -42,6 +44,16 @@ public abstract class NodeListener implements ExtensionPoint {

private static final Logger LOGGER = Logger.getLogger(NodeListener.class.getName());

/**
* Allows to veto node loading.
* @param node the node being loaded. Not yet attached to Jenkins.
* @return false to veto node loading.
*/
@Restricted(Beta.class)
protected boolean allowLoad(@NonNull Node node) {
return true;
}

/**
* Node is being created.
*/
Expand Down
Loading
Loading