Merge branch 'master' into rename-configuration-to-configure

This commit is contained in:
Jan Faracik 2025-09-15 12:02:47 +01:00 committed by GitHub
commit 3ef51444c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
269 changed files with 5733 additions and 5976 deletions

View File

@ -54,7 +54,7 @@ jobs:
repositories: >-
["jenkins.io"]
- name: Check out
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Publish jenkins.io changelog draft

View File

@ -11,7 +11,7 @@ jobs:
steps:
- name: Check if PR targets LTS branch
if: startsWith(github.event.pull_request.base.ref, 'stable-')
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@ -15,9 +15,9 @@ jobs:
is-lts: ${{ steps.set-version.outputs.is-lts }}
is-rc: ${{ steps.set-version.outputs.is-rc }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: "temurin"
java-version: 17
@ -73,7 +73,7 @@ jobs:
wget -q https://get.jenkins.io/${REPO}/${PROJECT_VERSION}/${FILE_NAME}
- name: Upload Release Asset
id: upload-war
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@ -108,7 +108,7 @@ jobs:
- name: Upload Release Asset
id: upload-deb
if: always()
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@ -144,7 +144,7 @@ jobs:
- name: Upload Release Asset
id: upload-rpm
if: always()
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@ -180,7 +180,7 @@ jobs:
- name: Upload Release Asset
id: upload-msi
if: always()
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@ -216,7 +216,7 @@ jobs:
- name: Upload Release Asset
id: upload-suse-rpm
if: always()
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'jenkinsci' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Run update-since-todo.py

View File

@ -2,6 +2,6 @@
<extension>
<groupId>io.jenkins.tools.incrementals</groupId>
<artifactId>git-changelist-maven-extension</artifactId>
<version>1.10</version>
<version>1.13</version>
</extension>
</extensions>

2
ath.sh
View File

@ -6,7 +6,7 @@ set -o xtrace
cd "$(dirname "$0")"
# https://github.com/jenkinsci/acceptance-test-harness/releases
export ATH_VERSION=6300.v12732144c83f
export ATH_VERSION=6361.vcb_036a_7ffb_a_5
if [[ $# -eq 0 ]]; then
export JDK=17

View File

@ -40,8 +40,8 @@ THE SOFTWARE.
<properties>
<commons-fileupload2.version>2.0.0-M4</commons-fileupload2.version>
<groovy.version>2.4.21</groovy.version>
<jelly.version>1.1-jenkins-20250616</jelly.version>
<stapler.version>1997.v356365fb_929e</stapler.version>
<jelly.version>1.1-jenkins-20250731</jelly.version>
<stapler.version>2030.v88a_855365981</stapler.version>
</properties>
<dependencyManagement>
@ -63,7 +63,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>6.2.9</version>
<version>6.2.10</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@ -71,7 +71,7 @@ THE SOFTWARE.
<!-- https://docs.spring.io/spring-security/reference/6.3/getting-spring-security.html#getting-maven-no-boot -->
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>6.5.2</version>
<version>6.5.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@ -240,7 +240,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.jenkins-ci</groupId>
<artifactId>crypto-util</artifactId>
<version>1.10</version>
<version>1.11</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci</groupId>
@ -305,7 +305,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.kohsuke.stapler</groupId>
<artifactId>json-lib</artifactId>
<version>2.4-jenkins-8</version>
<version>2.4-jenkins-15</version>
</dependency>
<dependency>
<groupId>org.kohsuke.stapler</groupId>

View File

@ -15,7 +15,7 @@
<url>https://github.com/jenkinsci/jenkins</url>
<properties>
<mina-sshd.version>2.15.0</mina-sshd.version>
<mina-sshd.version>2.16.0</mina-sshd.version>
<!-- Filled in by jacoco-maven-plugin -->
<jacocoSurefireArgs />
</properties>
@ -38,11 +38,6 @@
<artifactId>commons-io</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-common</artifactId>

View File

@ -36,6 +36,7 @@
package hudson.util;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
@ -139,6 +140,9 @@ public class QuotedStringTokenizer
/* ------------------------------------------------------------ */
@Override
@SuppressFBWarnings(
value = {"SF_DEAD_STORE_DUE_TO_SWITCH_FALLTHROUGH", "SF_SWITCH_FALLTHROUGH"},
justification = "TODO needs triage")
public boolean hasMoreTokens()
{
// Already found a token

View File

@ -117,11 +117,6 @@ THE SOFTWARE.
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.infradna.tool</groupId>
<artifactId>bridge-method-annotation</artifactId>
<version>${bridge-method-injector.version}</version>
</dependency>
<dependency>
<groupId>com.sun.xml.txw2</groupId>
<artifactId>txw2</artifactId>
@ -168,6 +163,11 @@ THE SOFTWARE.
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
<dependency>
<groupId>io.jenkins.tools</groupId>
<artifactId>bridge-method-annotation</artifactId>
<version>${bridge-method-injector.version}</version>
</dependency>
<dependency>
<!-- needed by Jelly -->
<groupId>jakarta.servlet.jsp.jstl</groupId>
@ -446,11 +446,6 @@ THE SOFTWARE.
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
@ -568,7 +563,7 @@ THE SOFTWARE.
</executions>
</plugin>
<plugin>
<groupId>com.infradna.tool</groupId>
<groupId>io.jenkins.tools</groupId>
<artifactId>bridge-method-injector</artifactId>
<executions>
<execution>

View File

@ -33,6 +33,7 @@ import hudson.PluginWrapper.Dependency;
import hudson.model.Hudson;
import hudson.util.CyclicGraphDetector;
import hudson.util.CyclicGraphDetector.CycleDetectedException;
import hudson.util.DelegatingClassLoader;
import hudson.util.IOUtils;
import hudson.util.MaskingClassLoader;
import java.io.File;
@ -559,7 +560,7 @@ public class ClassicPluginStrategy implements PluginStrategy {
/**
* Used to load classes from dependency plugins.
*/
static final class DependencyClassLoader extends ClassLoader {
static final class DependencyClassLoader extends DelegatingClassLoader {
/**
* This classloader is created for this plugin. Useful during debugging.
*/
@ -574,10 +575,6 @@ public class ClassicPluginStrategy implements PluginStrategy {
*/
private volatile List<PluginWrapper> transitiveDependencies;
static {
registerAsParallelCapable();
}
DependencyClassLoader(ClassLoader parent, File archive, List<Dependency> dependencies, PluginManager pluginManager) {
super("dependency ClassLoader for " + archive.getPath(), parent);
this._for = archive;

View File

@ -1,15 +0,0 @@
package hudson;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* @deprecated No longer does anything. Only here to prevent errors from old versions of tools like {@code JenkinsRule}.
*/
@Deprecated
@Restricted(NoExternalUse.class)
public class DNSMultiCast {
public static boolean disabled = true;
}

View File

@ -58,8 +58,10 @@ import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.security.Permission;
import hudson.security.PermissionScope;
import hudson.util.CachingClassLoader;
import hudson.util.CyclicGraphDetector;
import hudson.util.CyclicGraphDetector.CycleDetectedException;
import hudson.util.ExistenceCheckingClassLoader;
import hudson.util.FormValidation;
import hudson.util.PersistedList;
import hudson.util.Retrier;
@ -106,13 +108,11 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.function.Supplier;
@ -218,6 +218,14 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
*/
/* private final */ static int CHECK_UPDATE_ATTEMPTS;
/**
* Class name prefixes to skip in the class loading
*/
private static final String[] CLASS_PREFIXES_TO_SKIP = {
"SimpleTemplateScript", // cf. groovy.text.SimpleTemplateEngine
"groovy.tmp.templates.GStringTemplateScript", // Leaks on classLoader in some cases, see JENKINS-75879
};
static {
try {
// Secure initialization
@ -2392,43 +2400,50 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
/**
* {@link ClassLoader} that can see all plugins.
*/
public static final class UberClassLoader extends ClassLoader {
public static final class UberClassLoader extends CachingClassLoader {
private final List<PluginWrapper> activePlugins;
/** Cache of loaded, or known to be unloadable, classes. */
private final ConcurrentMap<String, Optional<Class<?>>> loaded = new ConcurrentHashMap<>();
static {
registerAsParallelCapable();
}
/**
* The servlet container's {@link ClassLoader} (the parent of Jenkins core) is
* parallel-capable and maintains its own growing {@link Map} of {@link
* ClassLoader#getClassLoadingLock} objects per class name for every load attempt (including
* misses), and we cannot override this behavior. Wrap the servlet container {@link
* ClassLoader} in {@link ExistenceCheckingClassLoader} to avoid calling {@link
* ClassLoader#getParent}'s {@link ClassLoader#loadClass(String, boolean)} at all for misses
* by first checking if the resource exists. If the resource does not exist, we immediately
* throw {@link ClassNotFoundException}. As a result, the servlet container's {@link
* ClassLoader} is never asked to try and fail, and it never creates/retains lock objects
* for those misses.
*/
public UberClassLoader(List<PluginWrapper> activePlugins) {
super("UberClassLoader", PluginManager.class.getClassLoader());
super("UberClassLoader", new ExistenceCheckingClassLoader(PluginManager.class.getClassLoader()));
this.activePlugins = activePlugins;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (name.startsWith("SimpleTemplateScript")) { // cf. groovy.text.SimpleTemplateEngine
throw new ClassNotFoundException("ignoring " + name);
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
for (String namePrefixToSkip : CLASS_PREFIXES_TO_SKIP) {
if (name.startsWith(namePrefixToSkip)) {
throw new ClassNotFoundException("ignoring " + name);
}
}
return loaded.computeIfAbsent(name, this::computeValue).orElseThrow(() -> new ClassNotFoundException(name));
return super.loadClass(name, resolve);
}
private Optional<Class<?>> computeValue(String name) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
for (PluginWrapper p : activePlugins) {
try {
if (FAST_LOOKUP) {
return Optional.of(ClassLoaderReflectionToolkit.loadClass(p.classLoader, name));
return ClassLoaderReflectionToolkit.loadClass(p.classLoader, name);
} else {
return Optional.of(p.classLoader.loadClass(name));
return p.classLoader.loadClass(name);
}
} catch (ClassNotFoundException e) {
// Not found. Try the next class loader.
}
}
// Not found in any of the class loaders. Delegate.
return Optional.empty();
throw new ClassNotFoundException(name);
}
@Override
@ -2460,10 +2475,6 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
return Collections.enumeration(resources);
}
void clearCacheMisses() {
loaded.values().removeIf(Optional::isEmpty);
}
@Override
public String toString() {
// only for debugging purpose

View File

@ -497,7 +497,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
this.encoding = encoding;
}
protected @NonNull Charset getClientCharset() throws IOException, InterruptedException {
public @NonNull Charset getClientCharset() throws IOException, InterruptedException {
if (encoding != null) {
return encoding;
}

View File

@ -66,6 +66,11 @@ public class ReverseProxySetupMonitor extends AdministrativeMonitor {
return true;
}
@Override
public boolean isActivationFake() {
return true;
}
@Restricted(DoNotUse.class) // WebOnly
@RestrictedSince("2.235")
public HttpResponse doTest(StaplerRequest2 request, @QueryParameter boolean testWithContext) {

View File

@ -129,6 +129,7 @@ public abstract class AbstractItem extends Actionable implements Loadable, Item,
doSetName(name);
}
@NonNull
@Override
@Exported(visibility = 999)
public String getName() {
@ -470,6 +471,7 @@ public abstract class AbstractItem extends Actionable implements Loadable, Item,
@Override
public abstract Collection<? extends Job> getAllJobs();
@NonNull
@Override
@Exported
public final String getFullName() {

View File

@ -160,6 +160,11 @@ public abstract class AdministrativeMonitor extends AbstractModelObject implemen
*/
public abstract boolean isActivated();
@Restricted(NoExternalUse.class)
public boolean isActivationFake() {
return false;
}
/**
* Returns true if this monitor is security related.
*
@ -186,8 +191,7 @@ public abstract class AdministrativeMonitor extends AbstractModelObject implemen
* By default {@link Jenkins#ADMINISTER}, but {@link Jenkins#SYSTEM_READ} or {@link Jenkins#MANAGE} are also supported.
* <p>
* Changing this permission check to return {@link Jenkins#SYSTEM_READ} will make the active
* administrative monitor appear on {@code manage.jelly} and on the globally visible
* {@link jenkins.management.AdministrativeMonitorsDecorator} to users without Administer permission.
* administrative monitor appear on {@link ManageJenkinsAction} to users without Administer permission.
* {@link #doDisable(StaplerRequest2, StaplerResponse2)} will still always require Administer permission.
* </p>
* <p>

View File

@ -625,6 +625,19 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
return !isOffline();
}
/**
* {@inheritDoc}
* <p>
* Uses {@link #getChannel()} to check the connection.
* A connected agent may still be offline for scheduling if marked temporarily offline.
* @return {@code true} if the agent is connected, {@code false} otherwise.
* @see #isOffline()
*/
@Override
public boolean isConnected() {
return getChannel() != null;
}
/**
* This method is called to determine whether manual launching of the agent is allowed at this point in time.
* @return {@code true} if manual launching of the agent is allowed at this point in time.

View File

@ -38,7 +38,10 @@ import hudson.security.PermissionScope;
import hudson.util.Secret;
import java.io.IOException;
import java.util.Collection;
import jenkins.model.FullyNamed;
import jenkins.model.FullyNamedModelObject;
import jenkins.model.Jenkins;
import jenkins.model.Named;
import jenkins.search.SearchGroup;
import jenkins.util.SystemProperties;
import jenkins.util.io.OnMaster;
@ -73,7 +76,7 @@ import org.kohsuke.stapler.StaplerRequest2;
* @see Items
* @see ItemVisitor
*/
public interface Item extends PersistenceRoot, SearchableModelObject, AccessControlled, OnMaster {
public interface Item extends PersistenceRoot, FullyNamedModelObject, SearchableModelObject, FullyNamed, Named, AccessControlled, OnMaster {
/**
* Gets the parent that contains this item.
*/
@ -97,6 +100,8 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
*
* @see #getFullName()
*/
@NonNull
@Override
String getName();
/**
@ -110,6 +115,8 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
*
* @see jenkins.model.Jenkins#getItemByFullName(String,Class)
*/
@NonNull
@Override
String getFullName();
/**
@ -127,13 +134,6 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
@Override
String getDisplayName();
/**
* Works like {@link #getDisplayName()} but return
* the full path that includes all the display names
* of the ancestors.
*/
String getFullDisplayName();
/**
* Gets the relative name to this item from the specified group.
*

View File

@ -33,6 +33,8 @@ import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jenkins.model.FullyNamed;
import jenkins.model.FullyNamedModelObject;
import org.springframework.security.access.AccessDeniedException;
/**
@ -41,19 +43,7 @@ import org.springframework.security.access.AccessDeniedException;
* @author Kohsuke Kawaguchi
* @see ItemGroupMixIn
*/
public interface ItemGroup<T extends Item> extends PersistenceRoot, ModelObject {
/**
* Gets the full name of this {@link ItemGroup}.
*
* @see Item#getFullName()
*/
String getFullName();
/**
* @see Item#getFullDisplayName()
*/
String getFullDisplayName();
public interface ItemGroup<T extends Item> extends FullyNamed, FullyNamedModelObject, PersistenceRoot {
/**
* Gets all the items in this collection in a read-only view.
*/

View File

@ -77,6 +77,7 @@ import java.awt.Paint;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
@ -210,9 +211,11 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
// This code can be deleted after several Jenkins releases,
// when it is likely that everyone is running a version equal or higher to this version.
var buildDirPath = getBuildDir().toPath();
if (Files.deleteIfExists(buildDirPath.resolve("legacyIds"))) {
Path legacyIds = buildDirPath.resolve("legacyIds");
if (Files.exists(legacyIds)) {
LOGGER.info("Deleting legacyIds file in " + buildDirPath + ". See https://issues.jenkins"
+ ".io/browse/JENKINS-75465 for more information.");
Files.delete(legacyIds);
}
TextFile f = getNextBuildNumberFile();

View File

@ -26,11 +26,11 @@ package hudson.model;
import hudson.Extension;
import hudson.Util;
import hudson.util.HudsonIsLoading;
import hudson.util.HudsonIsRestarting;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import jenkins.management.AdministrativeMonitorsDecorator;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.management.Badge;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithContextMenu;
@ -50,6 +50,9 @@ import org.kohsuke.stapler.StaplerResponse2;
*/
@Extension(ordinal = 998) @Symbol("manageJenkins")
public class ManageJenkinsAction implements RootAction, StaplerFallback, ModelObjectWithContextMenu {
private static final Logger LOGGER = Logger.getLogger(ManageJenkinsAction.class.getName());
@Override
public String getIconFileName() {
if (Jenkins.get().hasAnyPermission(Jenkins.MANAGE, Jenkins.SYSTEM_READ))
@ -98,28 +101,36 @@ public class ManageJenkinsAction implements RootAction, StaplerFallback, ModelOb
menu.add("manage/" + url, icon, iconXml, text, post, requiresConfirmation, badge, message);
}
/** Unlike {@link Jenkins#getActiveAdministrativeMonitors} this checks for activation lazily. */
@Override
public Badge getBadge() {
Jenkins jenkins = Jenkins.get();
AdministrativeMonitorsDecorator decorator = jenkins.getExtensionList(PageDecorator.class)
.get(AdministrativeMonitorsDecorator.class);
if (decorator == null) {
if (!(AdministrativeMonitor.hasPermissionToDisplay())) {
return null;
}
Collection<AdministrativeMonitor> activeAdministrativeMonitors = Optional.ofNullable(decorator.getMonitorsToDisplay()).orElse(Collections.emptyList());
boolean anySecurity = activeAdministrativeMonitors.stream().anyMatch(AdministrativeMonitor::isSecurity);
if (activeAdministrativeMonitors.isEmpty()) {
var app = Jenkins.get().getServletContext().getAttribute("app");
if (app instanceof HudsonIsLoading || app instanceof HudsonIsRestarting) {
return null;
}
int size = activeAdministrativeMonitors.size();
String tooltip = size > 1 ? Messages.ManageJenkinsAction_notifications(size) : Messages.ManageJenkinsAction_notification(size);
return new Badge(String.valueOf(size),
tooltip,
anySecurity ? Badge.Severity.DANGER : Badge.Severity.WARNING);
if (Jenkins.get().administrativeMonitors.stream().anyMatch(m -> m.isSecurity() && isActive(m))) {
return new Badge("1+", Messages.ManageJenkinsAction_notifications(),
Badge.Severity.DANGER);
} else if (Jenkins.get().administrativeMonitors.stream().anyMatch(m -> !m.isSecurity() && isActive(m))) {
return new Badge("1+", Messages.ManageJenkinsAction_notifications(),
Badge.Severity.WARNING);
} else {
return null;
}
}
private static boolean isActive(AdministrativeMonitor m) {
try {
return !m.isActivationFake() && m.hasRequiredPermission() && m.isEnabled() && m.isActivated();
} catch (Throwable x) {
LOGGER.log(Level.WARNING, null, x);
return false;
}
}
}

View File

@ -109,6 +109,8 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import jenkins.console.WithConsoleUrl;
import jenkins.model.FullyNamed;
import jenkins.model.FullyNamedModelObject;
import jenkins.model.Jenkins;
import jenkins.model.queue.AsynchronousExecution;
import jenkins.model.queue.CompositeCauseOfBlockage;
@ -1882,7 +1884,7 @@ public class Queue extends ResourceController implements Saveable {
* design, a {@link Task} must have at least one sub-task.)
* Most of the time, the primary subtask is the only sub task.
*/
public interface Task extends ModelObject, SubTask {
public interface Task extends FullyNamedModelObject, SubTask {
/**
* Returns true if the execution should be blocked
* for temporary reasons.
@ -1928,21 +1930,22 @@ public class Queue extends ResourceController implements Saveable {
*/
String getName();
/**
* @see hudson.model.Item#getFullDisplayName()
*/
String getFullDisplayName();
/**
* Returns task-specific key which is used by the {@link LoadBalancer} to choose one particular executor
* amongst all the free executors on all possibly suitable nodes.
* NOTE: To be able to re-use the same node during the next run this key should not change from one run to
* another. You probably want to compute that key based on the job's name.
*
* @return by default: {@link #getFullDisplayName()}
* @return by default: {@link FullyNamed#getFullName()} if implements {@link FullyNamed} or {@link #getFullDisplayName()} otherwise.
* @see hudson.model.LoadBalancer
*/
default String getAffinityKey() { return getFullDisplayName(); }
default String getAffinityKey() {
if (this instanceof FullyNamed fullyNamed) {
return fullyNamed.getFullName();
} else {
return getFullDisplayName();
}
}
/**
* Checks the permission to see if the current user can abort this executable.

View File

@ -29,7 +29,7 @@ package hudson.model;
*
* @author Kohsuke Kawaguchi
*/
public interface ResourceActivity {
public interface ResourceActivity extends ModelObject {
/**
* Gets the list of {@link Resource}s that this task requires.
* Used to make sure no two conflicting tasks run concurrently.
@ -47,9 +47,4 @@ public interface ResourceActivity {
default ResourceList getResourceList() {
return ResourceList.EMPTY;
}
/**
* Used for rendering HTML.
*/
String getDisplayName();
}

View File

@ -95,7 +95,7 @@ public final class RunMap<R extends Run<?, R>> extends AbstractLazyLoadRunMap<R>
* Used to create new instance of {@link Run}.
* @since 2.451
*/
public RunMap(@NonNull Job<?, ?> job, Constructor cons) {
public RunMap(@NonNull Job<?, ?> job, Constructor<R> cons) {
this.job = Objects.requireNonNull(job);
this.cons = cons;
initBaseDir(job.getBuildDir());
@ -105,7 +105,7 @@ public final class RunMap<R extends Run<?, R>> extends AbstractLazyLoadRunMap<R>
* @deprecated Use {@link #RunMap(Job, Constructor)}.
*/
@Deprecated
public RunMap(File baseDir, Constructor cons) {
public RunMap(File baseDir, Constructor<R> cons) {
job = null;
this.cons = cons;
initBaseDir(baseDir);
@ -187,6 +187,8 @@ public final class RunMap<R extends Run<?, R>> extends AbstractLazyLoadRunMap<R>
*/
public interface Constructor<R extends Run<?, R>> {
R create(File dir) throws IOException;
Class<R> getBuildClass();
}
@Override
@ -201,7 +203,7 @@ public final class RunMap<R extends Run<?, R>> extends AbstractLazyLoadRunMap<R>
/**
* Add a <em>new</em> build to the map.
* Do not use when loading existing builds (use {@link #put(Integer, Object)}).
* Do not use when loading existing builds (use {@link #putAll(Map)}).
*/
@Override
public R put(R r) {
@ -298,6 +300,11 @@ public final class RunMap<R extends Run<?, R>> extends AbstractLazyLoadRunMap<R>
return null;
}
@Override
protected Class<R> getBuildClass() {
return cons.getBuildClass();
}
/**
* Backward compatibility method that notifies {@link RunMap} of who the owner is.
*

View File

@ -52,12 +52,15 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@ -67,12 +70,14 @@ import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import jenkins.model.IdStrategy;
import jenkins.model.Jenkins;
import jenkins.model.Loadable;
import jenkins.model.ModelObjectWithContextMenu;
import jenkins.scm.RunWithSCM;
import jenkins.search.SearchGroup;
import jenkins.security.HMACConfidentialKey;
import jenkins.security.ImpersonatingUserDetailsService2;
import jenkins.security.LastGrantedAuthoritiesProperty;
import jenkins.security.UserDetailsCache;
@ -174,7 +179,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
@SuppressFBWarnings(value = "SS_SHOULD_BE_STATIC", justification = "Reserved for future use")
private final int version = 10; // Not currently used, but it may be helpful in the future to store a version.
private String id;
String id;
private volatile String fullName;
private volatile String description;
@ -185,6 +190,8 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
XSTREAM.alias("user", User.class);
}
private User() {}
private User(String id, String fullName) {
this.id = id;
this.fullName = fullName;
@ -199,6 +206,10 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
private void load(String userId) {
clearExistingProperties();
loadFromUserConfigFile(userId);
fixUpAfterLoad();
}
private void fixUpAfterLoad() {
removeNullsThatFailedToLoad();
allocateDefaultPropertyInstancesAsNeeded();
setUserToProperties();
@ -225,9 +236,10 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
}
private void loadFromUserConfigFile(String userId) {
AllUsers.getInstance().migrateUserIdMapper();
XmlFile config = getConfigFile();
try {
if (config != null && config.exists()) {
if (config.exists()) {
config.unmarshal(this);
this.id = userId;
}
@ -241,8 +253,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
}
private XmlFile getConfigFile() {
File existingUserFolder = getExistingUserFolder();
return existingUserFolder == null ? null : new XmlFile(XSTREAM, new File(existingUserFolder, CONFIG_XML));
return new XmlFile(XSTREAM, new File(getUserFolderFor(id), CONFIG_XML));
}
/**
@ -571,10 +582,10 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
*/
private static @Nullable User getOrCreateById(@NonNull String id, @NonNull String fullName, boolean create) {
User u = AllUsers.get(id);
if (u == null && (create || UserIdMapper.getInstance().isMapped(id))) {
if (u == null && create) {
u = new User(id, fullName);
AllUsers.put(id, u);
if (!id.equals(fullName) && !UserIdMapper.getInstance().isMapped(id)) {
if (!id.equals(fullName)) {
try {
u.save();
} catch (IOException x) {
@ -691,7 +702,6 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
*/
@Restricted(Beta.class)
public static void reload() throws IOException {
UserIdMapper.getInstance().reload();
AllUsers.reload();
}
@ -708,6 +718,32 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
or greater issues in the realm change, could affect currently logged
in users and even the user making the change. */
try {
var subdirectories = getRootDir().listFiles();
if (subdirectories != null) {
for (var oldDirectory : subdirectories) {
var dirName = oldDirectory.getName();
if (!HASHED_DIRNAMES.matcher(dirName).matches()) {
continue;
}
var xml = new XmlFile(XSTREAM, new File(oldDirectory, CONFIG_XML));
if (!xml.exists()) {
continue;
}
try {
var user = (User) xml.read();
if (user.id == null) {
continue;
}
var newDirectory = getUserFolderFor(user.id);
if (!oldDirectory.equals(newDirectory)) {
Files.move(oldDirectory.toPath(), newDirectory.toPath(), StandardCopyOption.REPLACE_EXISTING);
LOGGER.info(() -> "migrated " + oldDirectory + " to " + newDirectory);
}
} catch (Exception x) {
LOGGER.log(Level.WARNING, "failed to migrate " + xml, x);
}
}
}
reload();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Failed to perform rekey operation.", e);
@ -777,17 +813,9 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
if (ExtensionList.lookup(AllUsers.class).isEmpty()) {
return;
}
UserIdMapper.getInstance().clear();
AllUsers.clear();
}
private static File getConfigFileFor(String id) {
return new File(getUserFolderFor(id), "config.xml");
}
private static File getUserFolderFor(String id) {
return new File(getRootDir(), idStrategy().filenameOf(id));
}
/**
* Returns the folder that store all the user information.
* Useful for plugins to save a user-specific file aside the config.xml.
@ -799,11 +827,8 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
*/
public @CheckForNull File getUserFolder() {
return getExistingUserFolder();
}
private @CheckForNull File getExistingUserFolder() {
return UserIdMapper.getInstance().getDirectory(id);
var d = getUserFolderFor(id);
return d.isDirectory() ? d : null;
}
/**
@ -813,6 +838,21 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
return new File(Jenkins.get().getRootDir(), "users");
}
private static final int PREFIX_MAX = 14;
private static final Pattern DISALLOWED_PREFIX_CHARS = Pattern.compile("[^A-Za-z0-9]");
static final Pattern HASHED_DIRNAMES = Pattern.compile("[a-z0-9]{0," + PREFIX_MAX + "}_[a-f0-9]{64}");
private static final HMACConfidentialKey DIRNAMES = new HMACConfidentialKey(User.class, "DIRNAMES");
private static String getUserFolderNameFor(String id) {
var fullPrefix = DISALLOWED_PREFIX_CHARS.matcher(id).replaceAll("").toLowerCase(Locale.ROOT);
return (fullPrefix.length() > PREFIX_MAX ? fullPrefix.substring(0, PREFIX_MAX) : fullPrefix) + '_' + DIRNAMES.mac(idStrategy().keyFor(id));
}
@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "sanitized")
static File getUserFolderFor(String id) {
return new File(getRootDir(), getUserFolderNameFor(id));
}
/**
* Is the ID allowed? Some are prohibited for security reasons. See SECURITY-166.
* <p>
@ -852,19 +892,11 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
if (BulkChange.contains(this)) {
return;
}
XmlFile xmlFile = new XmlFile(XSTREAM, constructUserConfigFile());
XmlFile xmlFile = getConfigFile();
xmlFile.write(this);
SaveableListener.fireOnChange(this, xmlFile);
}
private File constructUserConfigFile() throws IOException {
return new File(putUserFolderIfAbsent(), CONFIG_XML);
}
private File putUserFolderIfAbsent() throws IOException {
return UserIdMapper.getInstance().putIfAbsent(id, true);
}
/**
* Deletes the data directory and removes this user from Hudson.
*
@ -872,19 +904,11 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
*/
public void delete() throws IOException {
String idKey = idStrategy().keyFor(id);
File existingUserFolder = getExistingUserFolder();
UserIdMapper.getInstance().remove(id);
AllUsers.remove(id);
deleteExistingUserFolder(existingUserFolder);
Util.deleteRecursive(getUserFolderFor(id));
UserDetailsCache.get().invalidate(idKey);
}
private void deleteExistingUserFolder(File existingUserFolder) throws IOException {
if (existingUserFolder != null && existingUserFolder.exists()) {
Util.deleteRecursive(existingUserFolder);
}
}
/**
* Exposed remote API.
*/
@ -947,7 +971,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
public boolean canDelete() {
final IdStrategy strategy = idStrategy();
return hasPermission(Jenkins.ADMINISTER) && !strategy.equals(id, Jenkins.getAuthentication2().getName())
&& UserIdMapper.getInstance().isMapped(id);
&& getUserFolder() != null;
}
/**
@ -1074,16 +1098,70 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
@Restricted(NoExternalUse.class)
public static final class AllUsers {
private boolean migratedUserIdMapper;
private final ConcurrentMap<String, User> byName = new ConcurrentHashMap<>();
@Initializer(after = InitMilestone.JOB_CONFIG_ADAPTED)
public static void scanAll() {
for (String userId : UserIdMapper.getInstance().getConvertedUserIds()) {
User user = new User(userId, userId);
getInstance().byName.putIfAbsent(idStrategy().keyFor(userId), user);
@SuppressWarnings("deprecation")
synchronized void migrateUserIdMapper() {
if (!migratedUserIdMapper) {
try {
UserIdMapper.migrate();
} catch (IOException x) {
LOGGER.log(Level.WARNING, null, x);
}
migratedUserIdMapper = true;
}
}
@Initializer(after = InitMilestone.JOB_CONFIG_ADAPTED)
public static void scanAll() throws IOException {
DIRNAMES.createMac(); // force the key to be saved during startup
var instance = getInstance();
instance.migrateUserIdMapper();
var subdirectories = getRootDir().listFiles();
if (subdirectories == null) {
return;
}
var byName = instance.byName;
var idStrategy = idStrategy();
for (var dir : subdirectories) {
var dirName = dir.getName();
if (!HASHED_DIRNAMES.matcher(dirName).matches()) {
LOGGER.fine(() -> "ignoring unrecognized dir " + dir);
continue;
}
var xml = new XmlFile(XSTREAM, new File(dir, CONFIG_XML));
if (!xml.exists()) {
LOGGER.fine(() -> "ignoring dir " + dir + " with no " + CONFIG_XML);
continue;
}
var user = new User();
try {
xml.unmarshal(user);
} catch (Exception x) {
LOGGER.log(Level.WARNING, "failed to load " + xml, x);
continue;
}
if (user.id == null) {
LOGGER.warning(() -> "ignoring " + xml + " with no <id>");
continue;
}
var expectedFolderName = getUserFolderNameFor(user.id);
if (!dirName.equals(expectedFolderName)) {
LOGGER.warning(() -> "ignoring " + xml + " with <id> " + user.id + " expected to be in " + expectedFolderName);
continue;
}
user.fixUpAfterLoad();
var old = byName.put(idStrategy.keyFor(user.id), user);
if (old != null) {
LOGGER.warning(() -> "entry for " + user.id + " in " + dir + " duplicates one seen earlier for " + old.id);
} else {
LOGGER.fine(() -> "successfully loaded " + user.id + " from " + xml);
}
}
LOGGER.fine(() -> "loaded " + byName.size() + " entries");
}
/**
* Keyed by {@link User#id}. This map is used to ensure
* singleton-per-id semantics of {@link User} objects.
@ -1094,7 +1172,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
return ExtensionList.lookupSingleton(AllUsers.class);
}
private static void reload() {
private static void reload() throws IOException {
getInstance().byName.clear();
UserDetailsCache.get().invalidateAll();
scanAll();
@ -1252,7 +1330,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
UserDetails userDetails = UserDetailsCache.get().loadUserByUsername(idOrFullName);
return userDetails.getUsername();
} catch (UsernameNotFoundException x) {
LOGGER.log(Level.FINE, "not sure whether " + idOrFullName + " is a valid username or not", x);
LOGGER.log(Level.FINER, "not sure whether " + idOrFullName + " is a valid username or not", x);
} catch (ExecutionException x) {
LOGGER.log(Level.FINE, "could not look up " + idOrFullName, x);
} finally {

View File

@ -24,179 +24,90 @@
package hudson.model;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.Util;
import hudson.XmlFile;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.util.XStream2;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Collections;
import java.nio.file.StandardCopyOption;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import jenkins.model.IdStrategy;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* @deprecated Formerly used to track which directory held each user configuration.
* Now that is deterministic based on a hash of {@link IdStrategy#keyFor}.
*/
@Deprecated
@Restricted(NoExternalUse.class)
@Extension
public class UserIdMapper {
private static final XStream2 XSTREAM = new XStream2();
static final String MAPPING_FILE = "users.xml";
private static final Logger LOGGER = Logger.getLogger(UserIdMapper.class.getName());
private static final int PREFIX_MAX = 15;
private static final Pattern PREFIX_PATTERN = Pattern.compile("[^A-Za-z0-9]");
@SuppressFBWarnings(value = "SS_SHOULD_BE_STATIC", justification = "Reserved for future use")
private final int version = 1; // Not currently used, but it may be helpful in the future to store a version.
private transient File usersDirectory;
// contrary to the name, the keys were actually IdStrategy.keyFor, not necessarily ids
private Map<String, String> idToDirectoryNameMap = new ConcurrentHashMap<>();
static UserIdMapper getInstance() {
return ExtensionList.lookupSingleton(UserIdMapper.class);
private UserIdMapper() {
}
public UserIdMapper() {
}
@Initializer(after = InitMilestone.PLUGINS_STARTED, before = InitMilestone.JOB_LOADED)
public File init() throws IOException {
usersDirectory = createUsersDirectoryAsNeeded();
load();
return usersDirectory;
}
@CheckForNull File getDirectory(String userId) {
String directoryName = idToDirectoryNameMap.get(getIdStrategy().keyFor(userId));
return directoryName == null ? null : new File(usersDirectory, directoryName);
}
File putIfAbsent(String userId, boolean saveToDisk) throws IOException {
String idKey = getIdStrategy().keyFor(userId);
String directoryName = idToDirectoryNameMap.get(idKey);
File directory = null;
if (directoryName == null) {
synchronized (this) {
directoryName = idToDirectoryNameMap.get(idKey);
if (directoryName == null) {
directory = createDirectoryForNewUser(userId);
directoryName = directory.getName();
idToDirectoryNameMap.put(idKey, directoryName);
if (saveToDisk) {
save();
@SuppressWarnings("deprecation")
static void migrate() throws IOException {
var idStrategy = User.idStrategy();
var usersDirectory = User.getRootDir();
var data = new UserIdMapper();
var mapperXml = new XmlFile(XSTREAM, new File(usersDirectory, "users.xml"));
if (mapperXml.exists()) { // need to migrate
// Load it, and trust ids it defines over <id></id> in users//config.xml which UserIdMigrator neglected to resave.
LOGGER.info(() -> "migrating " + mapperXml);
mapperXml.unmarshal(data);
for (var entry : data.idToDirectoryNameMap.entrySet()) {
var idKey = entry.getKey();
var directoryName = entry.getValue();
try {
var oldDirectory = new File(usersDirectory, directoryName);
var userXml = new XmlFile(User.XSTREAM, new File(oldDirectory, User.CONFIG_XML));
var user = (User) userXml.read();
if (user.id == null || !idKey.equals(idStrategy.keyFor(user.id))) {
user.id = idKey; // not quite right but hoping for the best
userXml.write(user);
}
var newDirectory = User.getUserFolderFor(user.id);
Files.move(oldDirectory.toPath(), newDirectory.toPath(), StandardCopyOption.REPLACE_EXISTING);
LOGGER.info(() -> "migrated " + oldDirectory + " to " + newDirectory);
} catch (Exception x) {
LOGGER.log(Level.WARNING, "failed to migrate " + entry, x);
}
}
mapperXml.delete();
}
return directory == null ? new File(usersDirectory, directoryName) : directory;
}
boolean isMapped(String userId) {
return idToDirectoryNameMap.containsKey(getIdStrategy().keyFor(userId));
}
Set<String> getConvertedUserIds() {
return Collections.unmodifiableSet(idToDirectoryNameMap.keySet());
}
void remove(String userId) throws IOException {
idToDirectoryNameMap.remove(getIdStrategy().keyFor(userId));
save();
}
void clear() {
idToDirectoryNameMap.clear();
}
void reload() throws IOException {
clear();
load();
}
protected IdStrategy getIdStrategy() {
return User.idStrategy();
}
protected File getUsersDirectory() {
return User.getRootDir();
}
private XmlFile getXmlConfigFile() {
File file = getConfigFile(usersDirectory);
return new XmlFile(XSTREAM, file);
}
static File getConfigFile(File usersDirectory) {
return new File(usersDirectory, MAPPING_FILE);
}
private File createDirectoryForNewUser(String userId) throws IOException {
try {
Path tempDirectory = Files.createTempDirectory(Util.fileToPath(usersDirectory), generatePrefix(userId));
return tempDirectory.toFile();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Error creating directory for user: " + userId, e);
throw e;
}
}
private String generatePrefix(String userId) {
String fullPrefix = PREFIX_PATTERN.matcher(userId).replaceAll("");
return fullPrefix.length() > PREFIX_MAX - 1 ? fullPrefix.substring(0, PREFIX_MAX - 1) + '_' : fullPrefix + '_';
}
private File createUsersDirectoryAsNeeded() throws IOException {
File usersDirectory = getUsersDirectory();
if (!usersDirectory.exists()) {
try {
Files.createDirectory(usersDirectory.toPath());
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Unable to create users directory: " + usersDirectory, e);
throw e;
}
}
return usersDirectory;
}
synchronized void save() throws IOException {
try {
getXmlConfigFile().write(this);
} catch (IOException ioe) {
LOGGER.log(Level.WARNING, "Error saving userId mapping file.", ioe);
throw ioe;
}
}
private void load() throws IOException {
UserIdMigrator migrator = new UserIdMigrator(usersDirectory, getIdStrategy());
if (migrator.needsMigration()) {
try {
migrator.migrateUsers(this);
} catch (IOException ioe) {
LOGGER.log(Level.SEVERE, "Error migrating users.", ioe);
throw ioe;
}
} else {
XmlFile config = getXmlConfigFile();
try {
config.unmarshal(this);
} catch (NoSuchFileException e) {
LOGGER.log(Level.FINE, "User id mapping file does not exist. It will be created when a user is saved.");
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to load " + config, e);
throw e;
// Also look for any remaining user dirs, such as those predating even UserIdMapper, or PresetData or incomplete @LocalData.
var subdirectories = usersDirectory.listFiles();
if (subdirectories != null) {
for (var oldDirectory : subdirectories) {
if (!User.HASHED_DIRNAMES.matcher(oldDirectory.getName()).matches()) {
var userXml = new XmlFile(User.XSTREAM, new File(oldDirectory, User.CONFIG_XML));
if (userXml.exists()) {
try {
var user = (User) userXml.read();
var id = user.id;
if (id == null) {
id = idStrategy.idFromFilename(oldDirectory.getName());
user.id = id;
userXml.write(user);
}
var newDirectory = User.getUserFolderFor(id);
Files.move(oldDirectory.toPath(), newDirectory.toPath(), StandardCopyOption.REPLACE_EXISTING);
LOGGER.info(() -> "migrated " + oldDirectory + " to " + newDirectory);
} catch (Exception x) {
LOGGER.log(Level.WARNING, "failed to migrate " + oldDirectory, x);
}
}
}
}
}
}

View File

@ -1,103 +0,0 @@
/*
* The MIT License
*
* Copyright (c) 2018 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.model;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.IdStrategy;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@Restricted(NoExternalUse.class)
class UserIdMigrator {
private static final Logger LOGGER = Logger.getLogger(UserIdMigrator.class.getName());
private static final String EMPTY_USERNAME_DIRECTORY_NAME = "emptyUsername";
private final File usersDirectory;
private final IdStrategy idStrategy;
UserIdMigrator(File usersDirectory, IdStrategy idStrategy) {
this.usersDirectory = usersDirectory;
this.idStrategy = idStrategy;
}
boolean needsMigration() {
File mappingFile = UserIdMapper.getConfigFile(usersDirectory);
if (mappingFile.exists() && mappingFile.isFile()) {
LOGGER.finest("User mapping file already exists. No migration needed.");
return false;
}
File[] userDirectories = listUserDirectories();
return userDirectories != null && userDirectories.length > 0;
}
private File[] listUserDirectories() {
return usersDirectory.listFiles(file -> file.isDirectory() && new File(file, User.CONFIG_XML).exists());
}
Map<String, File> scanExistingUsers() throws IOException {
Map<String, File> users = new HashMap<>();
File[] userDirectories = listUserDirectories();
if (userDirectories != null) {
for (File directory : userDirectories) {
String userId = idStrategy.idFromFilename(directory.getName());
users.put(userId, directory);
}
}
addEmptyUsernameIfExists(users);
return users;
}
private void addEmptyUsernameIfExists(Map<String, File> users) throws IOException {
File emptyUsernameConfigFile = new File(usersDirectory, User.CONFIG_XML);
if (emptyUsernameConfigFile.exists()) {
File newEmptyUsernameDirectory = new File(usersDirectory, EMPTY_USERNAME_DIRECTORY_NAME);
Files.createDirectory(newEmptyUsernameDirectory.toPath());
File newEmptyUsernameConfigFile = new File(newEmptyUsernameDirectory, User.CONFIG_XML);
Files.move(emptyUsernameConfigFile.toPath(), newEmptyUsernameConfigFile.toPath());
users.put("", newEmptyUsernameDirectory);
}
}
void migrateUsers(UserIdMapper mapper) throws IOException {
LOGGER.fine("Beginning migration of users to userId mapping.");
Map<String, File> existingUsers = scanExistingUsers();
for (Map.Entry<String, File> existingUser : existingUsers.entrySet()) {
File newDirectory = mapper.putIfAbsent(existingUser.getKey(), false);
LOGGER.log(Level.INFO, "Migrating user '" + existingUser.getKey() + "' from 'users/" + existingUser.getValue().getName() + "/' to 'users/" + newDirectory.getName() + "/'");
Files.move(existingUser.getValue().toPath(), newDirectory.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
mapper.save();
LOGGER.fine("Completed migration of users to userId mapping.");
}
}

View File

@ -27,6 +27,7 @@ package hudson.model.queue;
import static java.lang.Math.max;
import com.google.common.collect.Iterables;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.Computer;
import hudson.model.Executor;
import hudson.model.Label;
@ -48,6 +49,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import jenkins.model.Named;
/**
* Defines a mapping problem for answering "where do we execute this task?"
@ -114,7 +116,7 @@ public class MappingWorksheet {
}
}
public final class ExecutorChunk extends ReadOnlyList<ExecutorSlot> {
public final class ExecutorChunk extends ReadOnlyList<ExecutorSlot> implements Named {
public final int index;
public final Computer computer;
public final Node node;
@ -150,6 +152,8 @@ public class MappingWorksheet {
/**
* Node name.
*/
@NonNull
@Override
public String getName() {
return node.getNodeName();
}

View File

@ -251,7 +251,7 @@ public class NodeProvisioner {
LOGGER.log(Level.INFO,
"{0} provisioning successfully completed. "
+ "We have now {1,number,integer} computer(s)",
new Object[]{f.displayName, jenkins.getComputers().length});
new Object[]{f.displayName, jenkins.getComputersCollection().size()});
fireOnCommit(f, node);
} catch (IOException e) {
LOGGER.log(Level.WARNING,

View File

@ -66,6 +66,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.Charset;
import java.security.Security;
import java.util.ArrayList;
@ -641,9 +642,11 @@ public class SlaveComputer extends Computer {
// Orderly shutdown will have null exception
if (cause != null) {
offlineCause = new ChannelTermination(cause);
Functions.printStackTrace(cause, taskListener.error("Connection terminated"));
} else {
}
if (cause == null || cause instanceof ClosedChannelException) {
taskListener.getLogger().println("Connection terminated");
} else {
Functions.printStackTrace(cause, taskListener.error("Connection terminated"));
}
closeChannel();
try {

View File

@ -0,0 +1,56 @@
package hudson.util;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
*
* ClassLoader with internal caching of class loading results.
*
* <p>
* Caches both successful and failed class lookups to avoid redundant delegation
* and repeated class resolution attempts. Designed for performance optimization
* in systems that repeatedly query class presence (e.g., plugin environments,
* reflective loading, optional dependencies).
* </p>
*
* Useful for classloaders that have heavy-weight loadClass() implementations
*
* @author Dmytro Ukhlov
*/
@Restricted(NoExternalUse.class)
public class CachingClassLoader extends DelegatingClassLoader {
private final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
public CachingClassLoader(String name, ClassLoader parent) {
super(name, parent);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Object classOrEmpty = cache.computeIfAbsent(name, key -> {
try {
return super.loadClass(name, false);
} catch (ClassNotFoundException e) {
// Not found.
return Optional.empty();
}
});
if (classOrEmpty == Optional.empty()) {
throw new ClassNotFoundException(name);
}
Class<?> clazz = (Class<?>) classOrEmpty;
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
public void clearCacheMisses() {
cache.values().removeIf(v -> v == Optional.empty());
}
}

View File

@ -37,7 +37,10 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/**
@ -64,15 +67,23 @@ public abstract class CopyOnWriteMap<K, V> implements Map<K, V> {
update(Collections.emptyMap());
}
protected Map<K, V> getView() {
return view;
}
protected Map<K, V> createView() {
return Collections.unmodifiableMap(core);
}
protected void update(Map<K, V> m) {
core = m;
view = Collections.unmodifiableMap(core);
view = createView();
}
/**
* Atomically replaces the entire map by the copy of the specified map.
*/
public void replaceBy(Map<? extends K, ? extends V> data) {
public synchronized void replaceBy(Map<? extends K, ? extends V> data) {
Map<K, V> d = copy();
d.clear();
d.putAll(data);
@ -214,7 +225,7 @@ public abstract class CopyOnWriteMap<K, V> implements Map<K, V> {
/**
* {@link CopyOnWriteMap} backed by {@link TreeMap}.
*/
public static final class Tree<K, V> extends CopyOnWriteMap<K, V> {
public static final class Tree<K, V> extends CopyOnWriteMap<K, V> implements NavigableMap<K, V> {
private final Comparator<K> comparator;
public Tree(Map<K, V> core, Comparator<K> comparator) {
@ -232,7 +243,7 @@ public abstract class CopyOnWriteMap<K, V> implements Map<K, V> {
}
@Override
protected Map<K, V> copy() {
protected TreeMap<K, V> copy() {
TreeMap<K, V> m = new TreeMap<>(comparator);
m.putAll(core);
return m;
@ -243,6 +254,142 @@ public abstract class CopyOnWriteMap<K, V> implements Map<K, V> {
update(new TreeMap<>(comparator));
}
@Override
protected NavigableMap<K, V> createView() {
return Collections.unmodifiableNavigableMap((NavigableMap<K, V>) core);
}
@Override
protected NavigableMap<K, V> getView() {
return (NavigableMap<K, V>) super.getView();
}
@Override
public synchronized Entry<K, V> pollFirstEntry() {
TreeMap<K, V> d = copy();
Entry<K, V> res = d.pollFirstEntry();
update(d);
return res;
}
@Override
public synchronized Entry<K, V> pollLastEntry() {
TreeMap<K, V> d = copy();
Entry<K, V> res = d.pollLastEntry();
update(d);
return res;
}
@Override
public Entry<K, V> lowerEntry(K key) {
return getView().lowerEntry(key);
}
@Override
public K lowerKey(K key) {
return getView().lowerKey(key);
}
@Override
public Entry<K, V> floorEntry(K key) {
return getView().floorEntry(key);
}
@Override
public K floorKey(K key) {
return getView().floorKey(key);
}
@Override
public Entry<K, V> ceilingEntry(K key) {
return getView().ceilingEntry(key);
}
@Override
public K ceilingKey(K key) {
return getView().ceilingKey(key);
}
@Override
public Entry<K, V> higherEntry(K key) {
return getView().higherEntry(key);
}
@Override
public K higherKey(K key) {
return getView().higherKey(key);
}
@Override
public Entry<K, V> firstEntry() {
return getView().firstEntry();
}
@Override
public Entry<K, V> lastEntry() {
return getView().lastEntry();
}
@Override
public NavigableMap<K, V> descendingMap() {
return getView().descendingMap();
}
@Override
public NavigableSet<K> navigableKeySet() {
return getView().navigableKeySet();
}
@Override
public NavigableSet<K> descendingKeySet() {
return getView().descendingKeySet();
}
@Override
public NavigableMap<K, V> subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) {
return getView().subMap(fromKey, fromInclusive, toKey, toInclusive);
}
@Override
public NavigableMap<K, V> headMap(K toKey, boolean inclusive) {
return getView().headMap(toKey, inclusive);
}
@Override
public NavigableMap<K, V> tailMap(K fromKey, boolean inclusive) {
return getView().tailMap(fromKey, inclusive);
}
@Override
public Comparator<? super K> comparator() {
return getView().comparator();
}
@Override
public SortedMap<K, V> subMap(K fromKey, K toKey) {
return getView().subMap(fromKey, toKey);
}
@Override
public SortedMap<K, V> headMap(K toKey) {
return getView().headMap(toKey);
}
@Override
public SortedMap<K, V> tailMap(K fromKey) {
return getView().tailMap(fromKey);
}
@Override
public K firstKey() {
return getView().firstKey();
}
@Override
public K lastKey() {
return getView().lastKey();
}
public static class ConverterImpl extends TreeMapConverter {
public ConverterImpl(Mapper mapper) {
super(mapper);

View File

@ -0,0 +1,83 @@
package hudson.util;
import java.util.Objects;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* A {@link ClassLoader} that does not define any classes itself but delegates class loading to
* other class loaders to avoid the JDK's per-class-name locking and lock retention.
*
* <p>This class first attempts to load classes via its {@link ClassLoader#getParent} class loader,
* then falls back to {@link ClassLoader#findClass} to allow for custom delegation logic.
*
* <p>In a parallel-capable {@link ClassLoader}, the JDK maintains a per-name lock object
* indefinitely. In Jenkins, many class loading misses across many loaders can accumulate hundreds
* of thousands of such locks, retaining significant memory. This loader never defines classes and
* bypasses {@link ClassLoader}'s default {@code loadClass} locking; it delegates to the parent
* first and then to {@code findClass} for custom delegation.
*
* <p>The actual defining loader (parent or a delegate) still performs the necessary synchronization
* and class definition. A runtime guard ({@link #verify}) throws if this loader ever becomes the
* defining loader.
*
* <p>Subclasses must not call {@code defineClass}; implement delegation in {@code findClass} if
* needed and do not mark subclasses as parallel-capable.
*
* @author Dmytro Ukhlov
*/
@Restricted(NoExternalUse.class)
public abstract class DelegatingClassLoader extends ClassLoader {
protected DelegatingClassLoader(String name, ClassLoader parent) {
super(name, Objects.requireNonNull(parent));
}
public DelegatingClassLoader(ClassLoader parent) {
super(Objects.requireNonNull(parent));
}
/**
* Parent-first delegation without synchronizing on {@link #getClassLoadingLock(String)}. This
* prevents creation/retention of per-name lock objects in a loader that does not define
* classes. The defining loader downstream still serializes class definition as required.
*
* @param name The binary name of the class
* @param resolve If {@code true} then resolve the class
* @return The resulting {@link Class} object
* @throws ClassNotFoundException If the class could not be found
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = null;
try {
c = getParent().loadClass(name);
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
verify(c);
if (resolve) {
resolveClass(c);
}
return c;
}
/**
* Safety check to ensure this delegating loader never becomes the defining loader.
*
* <p>Fails fast if a subclass erroneously defines a class here, which would violate the
* delegation-only contract and could reintroduce locking/retention issues.
*/
private void verify(Class<?> clazz) {
if (clazz.getClassLoader() == this) {
throw new IllegalStateException("DelegatingClassLoader must not be the defining loader: " + clazz.getName());
}
}
}

View File

@ -0,0 +1,60 @@
package hudson.util;
import java.util.Objects;
import jenkins.util.URLClassLoader2;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* A {@link ClassLoader} that verifies the existence of a {@code .class} resource before attempting
* to load the class. Intended to sit in front of servlet container loaders we do not control.
*
* <p>This implementation overrides {@link #loadClass(String, boolean)} and uses {@link
* #getResource(String)} to check whether the corresponding <code>.class</code> file is available in
* the classpath. If the resource is not found, a {@link ClassNotFoundException} is thrown
* immediately.
*
* <p>Parallel-capable parent loaders retain a per-class-name lock object for every load attempt,
* including misses. By checking getResource(name + ".class") first and throwing {@link
* ClassNotFoundException} on absence, we avoid calling {@code loadClass} on misses, thus preventing
* the parent from populating its lock map for nonexistent classes.
*
* <p>This class is only needed in {@link hudson.PluginManager.UberClassLoader}. It is unnecessary
* for plugin {@link ClassLoader}s (because {@link URLClassLoader2} mitigates lock retention via
* {@link ClassLoader#getClassLoadingLock}) and redundant for delegators (because {@link
* DelegatingClassLoader} already avoids base locking).
*
* @author Dmytro Ukhlov
* @see ClassLoader
* @see #getResource(String)
*/
@Restricted(NoExternalUse.class)
public final class ExistenceCheckingClassLoader extends DelegatingClassLoader {
public ExistenceCheckingClassLoader(String name, ClassLoader parent) {
super(name, Objects.requireNonNull(parent));
}
public ExistenceCheckingClassLoader(ClassLoader parent) {
super(Objects.requireNonNull(parent));
}
/**
* Short-circuits misses by checking for the {@code .class} resource prior to delegation.
* Successful loads behave exactly as the parent would; misses do not touch the parent's
* per-name lock map.
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// Add support for loading of JaCoCo dynamic instrumentation classes
if (name.equals("java.lang.$JaCoCo")) {
return super.loadClass(name, resolve);
}
if (getResource(name.replace('.', '/') + ".class") == null) {
throw new ClassNotFoundException(name);
}
return super.loadClass(name, resolve);
}
}

View File

@ -35,6 +35,7 @@ import java.util.ListIterator;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@ -326,6 +327,18 @@ public class Iterators {
return com.google.common.collect.Iterators.filter(itr, Objects::nonNull);
}
/**
* Wraps another iterator and map iterable objects.
*/
public static <T, U> Iterator<U> map(final Iterator<T> itr, Function<T, U> mapper) {
return new AdaptedIterator<>(itr) {
@Override
protected U adapt(T item) {
return mapper.apply(item);
}
};
}
/**
* Returns an {@link Iterable} that iterates over all the given {@link Iterable}s.
*

View File

@ -42,7 +42,7 @@ import java.util.stream.Collectors;
*
* @author Kohsuke Kawaguchi
*/
public class MaskingClassLoader extends ClassLoader {
public class MaskingClassLoader extends DelegatingClassLoader {
/**
* Prefix of the packages that should be hidden.
*/
@ -50,10 +50,6 @@ public class MaskingClassLoader extends ClassLoader {
private final List<String> masksResources;
static {
registerAsParallelCapable();
}
public MaskingClassLoader(ClassLoader parent, String... masks) {
this(parent, Arrays.asList(masks));
}

View File

@ -17,6 +17,7 @@ Lesser General Public License for more details.
package hudson.util.jna;
import com.sun.jna.ptr.IntByReference;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.TreeMap;
@ -89,6 +90,7 @@ public class RegistryKey implements AutoCloseable {
return convertBufferToInt(getValue(valueName));
}
@SuppressFBWarnings(value = "SF_SWITCH_FALLTHROUGH", justification = "TODO needs triage")
private byte[] getValue(String valueName) {
IntByReference pType, lpcbData;
byte[] lpData = new byte[1];
@ -149,6 +151,7 @@ public class RegistryKey implements AutoCloseable {
/**
* Does a specified value exist?
*/
@SuppressFBWarnings(value = "SF_SWITCH_FALLTHROUGH", justification = "TODO needs triage")
public boolean valueExists(String name) {
IntByReference pType, lpcbData;
byte[] lpData = new byte[1];
@ -223,6 +226,7 @@ public class RegistryKey implements AutoCloseable {
*
* @return TreeMap with name and value pairs
*/
@SuppressFBWarnings(value = "SF_SWITCH_FALLTHROUGH", justification = "TODO needs triage")
public TreeMap<String, Object> getValues() {
int dwIndex, result;
char[] lpValueName;

View File

@ -141,7 +141,7 @@ public class CloudSet extends AbstractModelObject implements Describable<CloudSe
@Override
public ModelObjectWithContextMenu.ContextMenu doChildrenContextMenu(StaplerRequest2 request, StaplerResponse2 response) throws Exception {
ModelObjectWithContextMenu.ContextMenu m = new ModelObjectWithContextMenu.ContextMenu();
Jenkins.get().clouds.stream().forEach(m::add);
Jenkins.get().clouds.forEach(m::add);
return m;
}

View File

@ -137,10 +137,10 @@ public final class WebSocketAgents extends InvisibleAction implements Unprotecte
@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED_BAD_PRACTICE", justification = "method signature does not permit plumbing through the return value")
@Override
protected void opened() {
transport = new Transport();
Computer.threadPoolForRemoting.submit(() -> {
LOGGER.fine(() -> "setting up channel for " + agent);
state.fireBeforeChannel(new ChannelBuilder(agent, Computer.threadPoolForRemoting));
transport = new Transport();
try {
state.fireAfterChannel(state.getChannelBuilder().build(transport));
LOGGER.fine(() -> "set up channel for " + agent);

View File

@ -31,6 +31,11 @@ public class URICheckEncodingMonitor extends AdministrativeMonitor {
return true;
}
@Override
public boolean isActivationFake() {
return true;
}
@Override
public String getDisplayName() {
return Messages.URICheckEncodingMonitor_DisplayName();

View File

@ -1,50 +0,0 @@
package jenkins.management;
import hudson.Extension;
import hudson.model.PageDecorator;
import hudson.model.RootAction;
import jakarta.servlet.ServletException;
import java.io.IOException;
import jenkins.model.Jenkins;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.StaplerRequest2;
import org.kohsuke.stapler.StaplerResponse2;
import org.kohsuke.stapler.verb.GET;
@Extension
@Restricted(NoExternalUse.class)
public class AdministrativeMonitorsApi implements RootAction {
@GET
public void doNonSecurityPopupContent(StaplerRequest2 req, StaplerResponse2 resp) throws IOException, ServletException {
AdministrativeMonitorsApiData viewData = new AdministrativeMonitorsApiData(getDecorator().getNonSecurityAdministrativeMonitors());
req.getView(viewData, "monitorsList.jelly").forward(req, resp);
}
@GET
public void doSecurityPopupContent(StaplerRequest2 req, StaplerResponse2 resp) throws IOException, ServletException {
AdministrativeMonitorsApiData viewData = new AdministrativeMonitorsApiData(getDecorator().getSecurityAdministrativeMonitors());
req.getView(viewData, "monitorsList.jelly").forward(req, resp);
}
@Override
public String getIconFileName() {
return null;
}
@Override
public String getDisplayName() {
return null;
}
@Override
public String getUrlName() {
return "administrativeMonitorsApi";
}
private AdministrativeMonitorsDecorator getDecorator() {
return Jenkins.get()
.getExtensionList(PageDecorator.class)
.get(AdministrativeMonitorsDecorator.class);
}
}

View File

@ -1,24 +0,0 @@
package jenkins.management;
import hudson.model.AdministrativeMonitor;
import java.util.ArrayList;
import java.util.List;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@Restricted(NoExternalUse.class)
public class AdministrativeMonitorsApiData {
private final List<AdministrativeMonitor> monitorsList = new ArrayList<>();
AdministrativeMonitorsApiData(List<AdministrativeMonitor> monitors) {
monitorsList.addAll(monitors);
}
public List<AdministrativeMonitor> getMonitorsList() {
return this.monitorsList;
}
public boolean hasActiveMonitors() {
return !this.monitorsList.isEmpty();
}
}

View File

@ -1,173 +0,0 @@
/*
* The MIT License
*
* Copyright (c) 2016, Daniel Beck, CloudBees, 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.management;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.diagnosis.ReverseProxySetupMonitor;
import hudson.model.AdministrativeMonitor;
import hudson.model.PageDecorator;
import hudson.util.HudsonIsLoading;
import hudson.util.HudsonIsRestarting;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import jenkins.diagnostics.URICheckEncodingMonitor;
import jenkins.model.Jenkins;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest2;
/**
* Show notifications and popups for active administrative monitors on all pages.
*/
@Extension
@Restricted(NoExternalUse.class)
public class AdministrativeMonitorsDecorator extends PageDecorator {
private final Collection<String> ignoredJenkinsRestOfUrls = new ArrayList<>();
public AdministrativeMonitorsDecorator() {
// otherwise this would be added to every internal context menu building request
ignoredJenkinsRestOfUrls.add("contextMenu");
}
@NonNull
@Override
public String getDisplayName() {
return Messages.AdministrativeMonitorsDecorator_DisplayName();
}
// Used by Jelly
public Collection<AdministrativeMonitor> filterNonSecurityAdministrativeMonitors(Collection<AdministrativeMonitor> activeMonitors) {
return this.filterActiveAdministrativeMonitors(activeMonitors, false);
}
// Used by Jelly
public Collection<AdministrativeMonitor> filterSecurityAdministrativeMonitors(Collection<AdministrativeMonitor> activeMonitors) {
return this.filterActiveAdministrativeMonitors(activeMonitors, true);
}
/**
* Prevent us to compute multiple times the {@link AdministrativeMonitor#isActivated()} by re-using the same list
*/
private Collection<AdministrativeMonitor> filterActiveAdministrativeMonitors(Collection<AdministrativeMonitor> activeMonitors, boolean isSecurity) {
Collection<AdministrativeMonitor> active = new ArrayList<>();
for (AdministrativeMonitor am : activeMonitors) {
if (am.isSecurity() == isSecurity) {
active.add(am);
}
}
return active;
}
// Used by API
public List<AdministrativeMonitor> getNonSecurityAdministrativeMonitors() {
Collection<AdministrativeMonitor> allowedMonitors = getMonitorsToDisplay();
if (allowedMonitors == null) {
return Collections.emptyList();
}
return allowedMonitors.stream()
.filter(administrativeMonitor -> !administrativeMonitor.isSecurity())
.collect(Collectors.toList());
}
// Used by API
public List<AdministrativeMonitor> getSecurityAdministrativeMonitors() {
Collection<AdministrativeMonitor> allowedMonitors = getMonitorsToDisplay();
if (allowedMonitors == null) {
return Collections.emptyList();
}
return allowedMonitors.stream()
.filter(AdministrativeMonitor::isSecurity)
.collect(Collectors.toList());
}
private Collection<AdministrativeMonitor> getAllActiveAdministrativeMonitors() {
Collection<AdministrativeMonitor> active = new ArrayList<>();
for (AdministrativeMonitor am : Jenkins.get().getActiveAdministrativeMonitors()) {
if (am instanceof ReverseProxySetupMonitor) {
// TODO make reverse proxy monitor work when shown on any URL
continue;
}
if (am instanceof URICheckEncodingMonitor) {
// TODO make URI encoding monitor work when shown on any URL
continue;
}
active.add(am);
}
return active;
}
/**
* Compute the administrative monitors that are active and should be shown.
* This is done only when the instance is currently running and the user has the permission to read them.
*
* @return the list of active monitors if we should display them, otherwise null.
*/
public Collection<AdministrativeMonitor> getMonitorsToDisplay() {
if (!(AdministrativeMonitor.hasPermissionToDisplay())) {
return null;
}
StaplerRequest2 req = Stapler.getCurrentRequest2();
if (req == null) {
return null;
}
List<Ancestor> ancestors = req.getAncestors();
if (ancestors == null || ancestors.isEmpty()) {
// ???
return null;
}
Ancestor a = ancestors.get(ancestors.size() - 1);
Object o = a.getObject();
// don't show while Jenkins is loading
if (o instanceof HudsonIsLoading || o instanceof HudsonIsRestarting) {
return null;
}
// don't show for some URLs served directly by Jenkins
if (o instanceof Jenkins) {
String url = a.getRestOfUrl();
if (ignoredJenkinsRestOfUrls.contains(url)) {
return null;
}
}
return getAllActiveAdministrativeMonitors();
}
}

View File

@ -66,7 +66,6 @@ public class Badge {
*
* @param text The text to be shown in the badge.
* Keep it short, ideally just a number. More than 6 or 7 characters do not look good. Avoid spaces as they will lead to line breaks.
* as this might lead to line breaks.
* @param tooltip The tooltip to show for the badge.
* Do not include html tags.
* @param severity The severity of the badge (danger, warning, info)

View File

@ -0,0 +1,17 @@
package jenkins.model;
import edu.umd.cs.findbugs.annotations.NonNull;
/**
* An interface for objects that have a name and a parent, so exposing a full name.
*/
public interface FullyNamed {
/**
* Returns the full name of this object, which is a qualified name
* that includes the names of all its ancestors, separated by '/'.
*
* @return the full name of this object.
*/
@NonNull
String getFullName();
}

View File

@ -0,0 +1,23 @@
package jenkins.model;
import hudson.model.ModelObject;
import jenkins.security.stapler.StaplerAccessibleType;
/**
* A model object that has a human-readable full name. This is usually valid when nested as part of an object hierarchy.
*
* <p>
* This interface is used to mark objects that can be qualified in the context of a Jenkins instance.
* It is typically used for objects that are part of the Jenkins model and can be referenced by their names.
*
* @see ModelObject
*/
@StaplerAccessibleType
public interface FullyNamedModelObject extends ModelObject {
/**
* Works like {@link #getDisplayName()} but return
* the full path that includes all the display names
* of the ancestors in an unspecified format.
*/
String getFullDisplayName();
}

View File

@ -29,7 +29,6 @@ import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.markup.MarkupFormatter;
import hudson.model.BallColor;
import hudson.model.BuildBadgeAction;
import hudson.model.ModelObject;
import hudson.model.ParameterValue;
import hudson.model.ParametersAction;
import hudson.model.Queue;
@ -48,7 +47,7 @@ import org.kohsuke.accmod.restrictions.Beta;
* @since 2.477
*/
@Restricted(Beta.class)
public interface HistoricalBuild extends ModelObject {
public interface HistoricalBuild extends FullyNamedModelObject {
/**
* @return A build number
@ -74,12 +73,6 @@ public interface HistoricalBuild extends ModelObject {
@CheckForNull
String getDescription();
/**
* @return a human-readable full display name of this build.
*/
@NonNull
String getFullDisplayName();
/**
* Get the {@link Queue.Item#getId()} of the original queue item from where this {@link HistoricalBuild} instance
* originated.

View File

@ -27,7 +27,7 @@ package jenkins.model;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Util;
import hudson.model.Computer;
import hudson.model.Node;
import hudson.model.ModelObject;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import java.util.List;
@ -45,13 +45,7 @@ import org.kohsuke.accmod.restrictions.Beta;
* @since 2.480
*/
@Restricted(Beta.class)
public interface IComputer extends AccessControlled, IconSpec {
/**
* Returns {@link Node#getNodeName() the name of the node}.
*/
@NonNull
String getName();
public interface IComputer extends AccessControlled, IconSpec, ModelObject, Named {
/**
* Used to render the list of executors.
* @return a snapshot of the executor display information
@ -60,16 +54,14 @@ public interface IComputer extends AccessControlled, IconSpec {
List<? extends IDisplayExecutor> getDisplayExecutors();
/**
* @return {@code true} if the node is offline. {@code false} if it is online.
* Returns whether the agent is offline for scheduling new tasks.
* Even if {@code true}, the agent may still be connected to the controller and executing a task,
* but is considered offline for scheduling.
* @return {@code true} if the agent is offline; {@code false} if online.
* @see #isConnected()
*/
boolean isOffline();
/**
* @return the node name for UI purposes.
*/
@NonNull
String getDisplayName();
/**
* Returns {@code true} if the computer is accepting tasks. Needed to allow agents programmatic suspension of task
* scheduling that does not overlap with being offline.
@ -192,10 +184,21 @@ public interface IComputer extends AccessControlled, IconSpec {
int countExecutors();
/**
* @return true if the computer is online.
* Indicates whether the agent can accept a new task when it becomes idle.
* {@code false} does not necessarily mean the agent is disconnected.
* @return {@code true} if the agent is online.
* @see #isConnected()
*/
boolean isOnline();
/**
* Indicates whether the agent is actually connected to the controller.
* @return {@code true} if the agent is connected to the controller.
*/
default boolean isConnected() {
return isOnline();
}
/**
* @return the number of {@link IExecutor}s that are idle right now.
*/

View File

@ -25,6 +25,7 @@
package jenkins.model;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.ModelObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
@ -34,13 +35,7 @@ import org.kohsuke.accmod.restrictions.Beta;
* @since 2.480
*/
@Restricted(Beta.class)
public interface IDisplayExecutor {
/**
* @return The UI label for this executor.
*/
@NonNull
String getDisplayName();
public interface IDisplayExecutor extends ModelObject {
/**
* @return the URL where to reach specifically this executor, relative to Jenkins URL.
*/

View File

@ -1731,6 +1731,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
}
@NonNull
@Override
public String getFullName() {
return "";

View File

@ -0,0 +1,19 @@
package jenkins.model;
import edu.umd.cs.findbugs.annotations.NonNull;
/**
* An object that has a name.
* <p>
* This interface is used to provide a consistent way to retrieve the name of an object in Jenkins.
* It is typically implemented by objects that need to be identified by a name, such as tasks, nodes, or other model objects.
*/
public interface Named {
/**
* Returns the name of this object.
*
* @return the name of this object, never null.
*/
@NonNull
String getName();
}

View File

@ -24,6 +24,7 @@
package jenkins.model;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_CONFLICT;
import static jakarta.servlet.http.HttpServletResponse.SC_CREATED;
@ -201,7 +202,7 @@ public abstract class ParameterizedJobMixIn<JobT extends Job<JobT, RunT> & Param
}
if (!asJob().isBuildable()) {
throw HttpResponses.error(SC_CONFLICT, new IOException(asJob().getFullName() + " is not buildable"));
throw HttpResponses.errorWithoutStack(SC_CONFLICT, asJob().getFullName() + " is not buildable");
}
// if a build is parameterized, let that take over
@ -238,12 +239,12 @@ public abstract class ParameterizedJobMixIn<JobT extends Job<JobT, RunT> & Param
ParametersDefinitionProperty pp = asJob().getProperty(ParametersDefinitionProperty.class);
if (!asJob().isBuildable()) {
throw HttpResponses.error(SC_CONFLICT, new IOException(asJob().getFullName() + " is not buildable!"));
throw HttpResponses.errorWithoutStack(SC_CONFLICT, asJob().getFullName() + " is not buildable");
}
if (pp != null) {
pp.buildWithParameters(req, rsp, delay);
} else {
throw new IllegalStateException("This build is not parameterized!");
throw HttpResponses.errorWithoutStack(SC_BAD_REQUEST, asJob().getFullName() + " is not parameterized");
}
}

View File

@ -26,30 +26,26 @@ package jenkins.model.lazy;
import static jenkins.model.lazy.AbstractLazyLoadRunMap.Direction.ASC;
import static jenkins.model.lazy.AbstractLazyLoadRunMap.Direction.DESC;
import static jenkins.model.lazy.AbstractLazyLoadRunMap.Direction.EXACT;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.model.Job;
import hudson.model.Run;
import hudson.model.RunMap;
import hudson.model.listeners.RunListener;
import hudson.util.CopyOnWriteMap;
import java.io.File;
import java.io.IOException;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedMap;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.TreeMap;
import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -87,203 +83,44 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
*
* <p>
* Some of the {@link SortedMap} operations are inefficiently implemented, by
* {@linkplain #all() loading all the build records eagerly}. We hope to replace
* loading all the build records eagerly. We hope to replace
* these implementations by more efficient lazy-loading ones as we go.
*
* <p>
* Object lock of {@code this} is used to make sure mutation occurs sequentially.
* That is, ensure that only one thread is actually calling {@link #retrieve(File)} and
* updating {@link jenkins.model.lazy.AbstractLazyLoadRunMap.Index#byNumber}.
* updating {@link jenkins.model.lazy.AbstractLazyLoadRunMap#core}.
*
* @author Kohsuke Kawaguchi
* @since 1.485
*/
public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R> implements SortedMap<Integer, R> {
/**
* Used in {@link #all()} to quickly determine if we've already loaded everything.
*/
private volatile boolean fullyLoaded;
/**
* Currently visible index.
* Updated atomically. Once set to this field, the index object may not be modified.
*/
private volatile Index index = new Index();
private LazyLoadRunMapEntrySet<R> entrySet = new LazyLoadRunMapEntrySet<>(this);
private transient volatile Set<Integer> keySet;
private transient volatile Collection<R> values;
private final CopyOnWriteMap.Tree<Integer, BuildReference<R>> core = new CopyOnWriteMap.Tree<>(
Collections.reverseOrder());
private final BuildReferenceMapAdapter.Resolver<R> buildResolver = new BuildReferenceMapAdapterResolver();
private final BuildReferenceMapAdapter<R> adapter = new BuildReferenceMapAdapter<>(core, buildResolver) {
@Override
protected boolean removeValue(R value) {
return AbstractLazyLoadRunMap.this.removeValue(value);
}
};
@Override
public Set<Integer> keySet() {
Set<Integer> ks = keySet;
if (ks == null) {
ks = new AbstractSet<>() {
@Override
public Iterator<Integer> iterator() {
return new Iterator() {
private final Iterator<Entry<Integer, R>> it = entrySet().iterator();
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public Integer next() {
return it.next().getKey();
}
@Override
public void remove() {
it.remove();
}
};
}
@Override
public Spliterator<Integer> spliterator() {
return new Spliterators.AbstractIntSpliterator(
Long.MAX_VALUE,
Spliterator.DISTINCT | Spliterator.ORDERED | Spliterator.SORTED) {
private final Iterator<Integer> it = iterator();
@Override
public boolean tryAdvance(IntConsumer action) {
if (action == null) {
throw new NullPointerException();
}
if (it.hasNext()) {
action.accept(it.next());
return true;
}
return false;
}
@Override
public Comparator<Integer> getComparator() {
return Collections.reverseOrder();
}
};
}
@Override
public int size() {
return AbstractLazyLoadRunMap.this.size();
}
@Override
public boolean isEmpty() {
return AbstractLazyLoadRunMap.this.isEmpty();
}
@Override
public void clear() {
AbstractLazyLoadRunMap.this.clear();
}
@Override
public boolean contains(Object k) {
return AbstractLazyLoadRunMap.this.containsKey(k);
}
};
keySet = ks;
}
return ks;
return adapter.keySet();
}
@Override
public Collection<R> values() {
Collection<R> vals = values;
if (vals == null) {
vals = new AbstractCollection<>() {
@Override
public Iterator<R> iterator() {
return new Iterator<>() {
private final Iterator<Entry<Integer, R>> it = entrySet().iterator();
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public R next() {
return it.next().getValue();
}
@Override
public void remove() {
it.remove();
}
};
}
@Override
public Spliterator<R> spliterator() {
return Spliterators.spliteratorUnknownSize(
iterator(), Spliterator.DISTINCT | Spliterator.ORDERED);
}
@Override
public int size() {
return AbstractLazyLoadRunMap.this.size();
}
@Override
public boolean isEmpty() {
return AbstractLazyLoadRunMap.this.isEmpty();
}
@Override
public void clear() {
AbstractLazyLoadRunMap.this.clear();
}
@Override
public boolean contains(Object v) {
return AbstractLazyLoadRunMap.this.containsValue(v);
}
};
values = vals;
}
return vals;
return adapter.values();
}
/**
* Historical holder for map.
*
* TODO all this mess including {@link #numberOnDisk} could probably be simplified to a single {@code TreeMap<Integer,BuildReference<R>>}
* where a null value means not yet loaded and a broken entry just uses {@code NoHolder}.
*
* The idiom is that you put yourself in a synchronized block, {@linkplain #copy() make a copy of this},
* update the copy, then set it to {@link #index}.
*/
private class Index {
/**
* Stores the mapping from build number to build, for builds that are already loaded.
*
* If we have known load failure of the given ID, we record that in the map
* by using the null value (not to be confused with a non-null {@link BuildReference}
* with null referent, which just means the record was GCed.)
*/
private final TreeMap<Integer, BuildReference<R>> byNumber;
private Index() {
byNumber = new TreeMap<>(Collections.reverseOrder());
}
private Index(Index rhs) {
byNumber = new TreeMap<>(rhs.byNumber);
}
@Override
public Set<Entry<Integer, R>> entrySet() {
assert baseDirInitialized();
return adapter.entrySet();
}
/**
* Build numbers found on disk, in the ascending order.
*/
// copy on write
private volatile SortedIntList numberOnDisk = new SortedIntList(0);
/**
* Base directory for data.
* In effect this is treated as a final field, but can't mark it final
@ -330,8 +167,6 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
* @since 1.507
*/
public synchronized void purgeCache() {
index = new Index();
fullyLoaded = false;
loadNumberOnDisk();
}
@ -343,7 +178,7 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
// the job may have just been created
kids = MemoryReductionUtil.EMPTY_STRING_ARRAY;
}
SortedIntList list = new SortedIntList(kids.length / 2);
TreeMap<Integer, BuildReference<R>> newBuildRefsMap = new TreeMap<>();
var allower = createLoadAllower();
for (String s : kids) {
if (!BUILD_NUMBER.matcher(s).matches()) {
@ -353,7 +188,7 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
try {
int buildNumber = Integer.parseInt(s);
if (allower.test(buildNumber)) {
list.add(buildNumber);
newBuildRefsMap.put(buildNumber, new BuildReference<>(s));
} else {
LOGGER.fine(() -> "declining to consider " + buildNumber + " in " + dir);
}
@ -361,8 +196,7 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
// matched BUILD_NUMBER but not an int?
}
}
list.sort();
numberOnDisk = list;
core.replaceBy(newBuildRefsMap);
}
@Restricted(NoExternalUse.class)
@ -384,13 +218,10 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
public final void recognizeNumber(int buildNumber) {
if (new File(dir, Integer.toString(buildNumber)).isDirectory()) {
synchronized (this) {
SortedIntList list = new SortedIntList(numberOnDisk);
if (list.contains(buildNumber)) {
if (this.core.containsKey(buildNumber)) {
LOGGER.fine(() -> "already knew about " + buildNumber + " in " + dir);
} else {
list.add(buildNumber);
list.sort();
numberOnDisk = list;
core.put(buildNumber, new BuildReference<>(String.valueOf(buildNumber)));
LOGGER.fine(() -> "recognizing " + buildNumber + " in " + dir);
}
}
@ -401,25 +232,36 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
@Override
public Comparator<? super Integer> comparator() {
return Collections.reverseOrder();
return core.comparator();
}
@Override
public boolean isEmpty() {
return search(Integer.MAX_VALUE, DESC) == null;
return adapter.isEmpty();
}
@Override
public Set<Entry<Integer, R>> entrySet() {
assert baseDirInitialized();
return entrySet;
public boolean containsKey(Object value) {
return adapter.containsKey(value);
}
@Override
public boolean containsValue(Object value) {
return adapter.containsValue(value);
}
/**
* Returns a read-only view of records that has already been loaded.
*/
public SortedMap<Integer, R> getLoadedBuilds() {
return Collections.unmodifiableSortedMap(new BuildReferenceMapAdapter<>(this, index.byNumber));
TreeMap<Integer, BuildReference<R>> res = new TreeMap<>(Comparator.reverseOrder());
for (var entry : this.core.entrySet()) {
BuildReference<R> buildRef = entry.getValue();
if (buildRef.isSet() && !buildRef.isUnloadable()) {
res.put(entry.getKey(), buildRef);
}
}
return new BuildReferenceMapAdapter<>(res, buildResolver);
}
/**
@ -430,47 +272,27 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
*/
@Override
public SortedMap<Integer, R> subMap(Integer fromKey, Integer toKey) {
// TODO: if this method can produce a lazy map, that'd be wonderful
// because due to the lack of floor/ceil/higher/lower kind of methods
// to look up keys in SortedMap, various places of Jenkins rely on
// subMap+firstKey/lastKey combo.
R start = search(fromKey, DESC);
if (start == null) return EMPTY_SORTED_MAP;
R end = search(toKey, ASC);
if (end == null) return EMPTY_SORTED_MAP;
for (R i = start; i != end; ) {
i = search(getNumberOf(i) - 1, DESC);
assert i != null;
}
return Collections.unmodifiableSortedMap(new BuildReferenceMapAdapter<>(this, index.byNumber.subMap(fromKey, toKey)));
return adapter.subMap(fromKey, toKey);
}
@Override
public SortedMap<Integer, R> headMap(Integer toKey) {
return subMap(Integer.MAX_VALUE, toKey);
return adapter.headMap(toKey);
}
@Override
public SortedMap<Integer, R> tailMap(Integer fromKey) {
return subMap(fromKey, Integer.MIN_VALUE);
return adapter.tailMap(fromKey);
}
@Override
public Integer firstKey() {
R r = newestBuild();
if (r == null) throw new NoSuchElementException();
return getNumberOf(r);
return adapter.firstKey();
}
@Override
public Integer lastKey() {
R r = oldestBuild();
if (r == null) throw new NoSuchElementException();
return getNumberOf(r);
return adapter.lastKey();
}
public R newestBuild() {
@ -483,11 +305,7 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
@Override
public R get(Object key) {
if (key instanceof Integer) {
int n = (Integer) key;
return get(n);
}
return super.get(key);
return adapter.get(key);
}
public R get(int n) {
@ -503,7 +321,7 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
* @since 2.14
*/
public boolean runExists(int number) {
return numberOnDisk.contains(number);
return this.core.containsKey(number);
}
/**
@ -518,72 +336,51 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
* If DESC, finds the closest #M that satisfies M N.
*/
public @CheckForNull R search(final int n, final Direction d) {
switch (d) {
case EXACT:
return getByNumber(n);
case ASC:
for (int m : numberOnDisk) {
if (m < n) {
// TODO could be made more efficient with numberOnDisk.find
continue;
}
R r = getByNumber(m);
if (r != null) {
return r;
}
}
return null;
case DESC:
// TODO again could be made more efficient
ListIterator<Integer> iterator = numberOnDisk.listIterator(numberOnDisk.size());
while (iterator.hasPrevious()) {
int m = iterator.previous();
if (m > n) {
continue;
}
R r = getByNumber(m);
if (r != null) {
return r;
}
}
return null;
default:
throw new AssertionError();
if (d == EXACT) {
return this.adapter.get(n);
}
// prepare sub map, where we need to find first resolvable entry
NavigableMap<Integer, BuildReference<R>> subCore = (d == ASC)
? core.headMap(n, true).descendingMap()
: core.tailMap(n, true);
// wrap with BuildReferenceMapAdapter to skip unresolvable entries
return new BuildReferenceMapAdapter<>(subCore, buildResolver).values().stream().findFirst().orElse(null);
}
public R getById(String id) {
return getByNumber(Integer.parseInt(id));
}
public R getByNumber(int n) {
Index snapshot = index;
if (snapshot.byNumber.containsKey(n)) {
BuildReference<R> ref = snapshot.byNumber.get(n);
if (ref == null) {
LOGGER.fine(() -> "known failure of #" + n + " in " + dir);
return null;
}
R v = unwrap(ref);
if (v != null) {
/**
* Ensure loading referent object if needed, cache it and return
* Save that object as 'unloadable' in case of failure to avoid next load attempts
*
* @param ref reference object to be resolved
* @return R referent build object, or null if it can't be resolved
*/
private R resolveBuildRef(BuildReference<R> ref) {
if (ref == null || ref.isUnloadable()) {
return null;
}
R v;
if ((v = ref.get()) != null) {
return v; // already in memory
}
// otherwise fall through to load
synchronized (this) {
if ((v = ref.get()) != null) {
return v; // already in memory
}
// otherwise fall through to load
}
synchronized (this) {
if (index.byNumber.containsKey(n)) { // JENKINS-22767: recheck inside lock
BuildReference<R> ref = index.byNumber.get(n);
if (ref == null) {
LOGGER.fine(() -> "known failure of #" + n + " in " + dir);
int n = ref.number;
if (allowLoad(n)) {
v = load(n);
// save if build unloadable
if (v == null) {
ref.setUnloadable();
return null;
}
R v = unwrap(ref);
if (v != null) {
return v;
}
}
if (allowLoad(n)) {
return load(n, null);
ref.set(v);
return v;
} else {
LOGGER.fine(() -> "declining to load " + n + " in " + dir);
return null;
@ -591,17 +388,25 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
}
}
public R getByNumber(int n) {
return adapter.get(n);
}
/**
* @return the highest recorded build number, or 0 if there are none
*/
@Restricted(NoExternalUse.class)
public synchronized int maxNumberOnDisk() {
return numberOnDisk.max();
try {
return this.core.firstKey();
} catch (NoSuchElementException ignored) {
return 0;
}
}
protected final synchronized void proposeNewNumber(int number) throws IllegalStateException {
if (number <= maxNumberOnDisk()) {
throw new IllegalStateException("JENKINS-27530: cannot create a build with number " + number + " since that (or higher) is already in use among " + numberOnDisk);
throw new IllegalStateException("JENKINS-27530: cannot create a build with number " + number + " since that (or higher) is already in use among " + keySet());
}
}
@ -616,95 +421,36 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
@Override
public synchronized R put(Integer key, R r) {
int n = getNumberOf(r);
Index copy = copy();
BuildReference<R> ref = createReference(r);
BuildReference<R> old = copy.byNumber.put(n, ref);
index = copy;
if (!numberOnDisk.contains(n)) {
SortedIntList a = new SortedIntList(numberOnDisk);
a.add(n);
a.sort();
numberOnDisk = a;
}
entrySet.clearCache();
return unwrap(old);
}
private R unwrap(BuildReference<R> ref) {
return ref != null ? ref.get() : null;
BuildReference<R> old = core.put(n, createReference(r));
return resolveBuildRef(old);
}
@Override
public synchronized void putAll(Map<? extends Integer, ? extends R> rhs) {
Index copy = copy();
for (R r : rhs.values()) {
BuildReference<R> ref = createReference(r);
copy.byNumber.put(getNumberOf(r), ref);
public synchronized void putAll(Map<? extends Integer, ? extends R> newData) {
TreeMap<Integer, BuildReference<R>> newWrapperData = new TreeMap<>();
for (Map.Entry<? extends Integer, ? extends R> entry : newData.entrySet()) {
newWrapperData.put(entry.getKey(), createReference(entry.getValue()));
}
index = copy;
core.putAll(newWrapperData);
}
/**
* Loads all the build records to fully populate the map.
* Calling this method results in eager loading everything,
* so the whole point of this class is to avoid this call as much as possible
* for typical code path.
*
* @return
* fully populated map.
*/
/*package*/ TreeMap<Integer, BuildReference<R>> all() {
if (!fullyLoaded) {
synchronized (this) {
if (!fullyLoaded) {
Index copy = copy();
for (Integer number : numberOnDisk) {
if (!copy.byNumber.containsKey(number))
load(number, copy);
}
index = copy;
fullyLoaded = true;
}
}
}
return index.byNumber;
@Override
public R remove(Object key) {
return adapter.remove(key);
}
/**
* Creates a duplicate for the COW data structure in preparation for mutation.
*/
private Index copy() {
return new Index(index);
}
/**
* Tries to load the record #N.
*
* @return null if the data failed to load.
*/
private R load(int n, Index editInPlace) {
private R load(int n) {
assert Thread.holdsLock(this);
assert dir != null;
R v = load(new File(dir, String.valueOf(n)), editInPlace);
if (v == null && editInPlace != null) {
// remember the failure.
// if editInPlace==null, we can create a new copy for this, but not sure if it's worth doing,
// TODO should we also update numberOnDisk?
editInPlace.byNumber.put(n, null);
}
return v;
return load(new File(dir, String.valueOf(n)));
}
/**
* @param editInPlace
* If non-null, update this data structure.
* Otherwise do a copy-on-write of {@link #index}
*/
private R load(File dataDir, Index editInPlace) {
private R load(File dataDir) {
assert Thread.holdsLock(this);
try {
R r = retrieve(dataDir);
@ -712,15 +458,6 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
LOGGER.fine(() -> "nothing in " + dataDir);
return null;
}
Index copy = editInPlace != null ? editInPlace : new Index(index);
BuildReference<R> ref = createReference(r);
BuildReference<R> old = copy.byNumber.put(getNumberOf(r), ref);
assert old == null || old.get() == null : "tried to overwrite " + old + " with " + ref;
if (editInPlace == null) index = copy;
return r;
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to load " + dataDir, e);
@ -747,7 +484,6 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
return new BuildReference<>(getIdOf(r), r);
}
/**
* Parses {@code R} instance from data in the specified directory.
*
@ -759,31 +495,22 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
*/
protected abstract R retrieve(File dir) throws IOException;
protected abstract Class<R> getBuildClass();
public synchronized boolean removeValue(R run) {
Index copy = copy();
int n = getNumberOf(run);
BuildReference<R> old = copy.byNumber.remove(n);
SortedIntList a = new SortedIntList(numberOnDisk);
a.removeValue(n);
numberOnDisk = a;
this.index = copy;
entrySet.clearCache();
return old != null;
return core.remove(getNumberOf(run)) != null;
}
/**
* Replaces all the current loaded Rs with the given ones.
*/
public synchronized void reset(TreeMap<Integer, R> builds) {
Index index = new Index();
public synchronized void reset(Map<Integer, R> builds) {
TreeMap<Integer, BuildReference<R>> copy = new TreeMap<>();
for (R r : builds.values()) {
BuildReference<R> ref = createReference(r);
index.byNumber.put(getNumberOf(r), ref);
copy.put(getNumberOf(r), createReference(r));
}
this.index = index;
this.core.replaceBy(copy);
}
@Override
@ -800,7 +527,22 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer, R>
ASC, DESC, EXACT
}
private static final SortedMap EMPTY_SORTED_MAP = Collections.unmodifiableSortedMap(new TreeMap());
private class BuildReferenceMapAdapterResolver implements BuildReferenceMapAdapter.Resolver<R> {
@Override
public R resolveBuildRef(BuildReference<R> buildRef) {
return AbstractLazyLoadRunMap.this.resolveBuildRef(buildRef);
}
@Override
public Integer getNumberOf(R build) {
return AbstractLazyLoadRunMap.this.getNumberOf(build);
}
@Override
public Class<R> getBuildClass() {
return AbstractLazyLoadRunMap.this.getBuildClass();
}
}
static final Logger LOGGER = Logger.getLogger(AbstractLazyLoadRunMap.class.getName());
}

View File

@ -36,13 +36,57 @@ public final class BuildReference<R> {
private static final Logger LOGGER = Logger.getLogger(BuildReference.class.getName());
final String id;
final int number;
private volatile Holder<R> holder;
public BuildReference(String id, R referent) {
public BuildReference(String id) {
this.id = id;
this.holder = findHolder(referent);
int num;
try {
num = Integer.parseInt(id);
} catch (NumberFormatException ignored) {
num = Integer.MAX_VALUE;
}
this.number = num;
}
public BuildReference(String id, R referent) {
this(id);
set(referent);
}
/**
* Set referent if loaded
*/
/*package*/ void set(R referent) {
holder = findHolder(referent);
}
/**
* check if reference marked as unloadable
*/
/*package*/ boolean isUnloadable() {
return DefaultHolderFactory.UnloadableHolder.getInstance() == holder;
}
/**
* check if reference holder set.
* means there war a try to load build object and we have some result of that try
*
* @return true if there was a try to
*/
/*package*/ boolean isSet() {
return holder != null;
}
/**
* Set referent as unloadable
*/
/*package*/ void setUnloadable() {
holder = DefaultHolderFactory.UnloadableHolder.getInstance();
}
/**
* Gets the build if still in memory.
* @return the actual build, or null if it has been collected
@ -155,7 +199,7 @@ public final class BuildReference<R> {
} else if (mode.equals("strong")) {
return new StrongHolder<>(referent);
} else if (mode.equals("none")) {
return new NoHolder<>();
return NoHolder.getInstance();
} else {
throw new IllegalStateException("unrecognized value of " + MODE_PROPERTY + ": " + mode);
}
@ -186,6 +230,32 @@ public final class BuildReference<R> {
}
private static final class NoHolder<R> implements Holder<R> {
static final NoHolder<?> INSTANCE = new NoHolder<>();
static <R> NoHolder<R> getInstance() {
//noinspection unchecked
return (NoHolder<R>) INSTANCE;
}
private NoHolder() {
}
@Override public R get() {
return null;
}
}
private static final class UnloadableHolder<R> implements Holder<R> {
static final UnloadableHolder<?> INSTANCE = new UnloadableHolder<>();
static <R> UnloadableHolder<R> getInstance() {
//noinspection unchecked
return (UnloadableHolder<R>) INSTANCE;
}
private UnloadableHolder() {
}
@Override public R get() {
return null;
}

View File

@ -1,54 +1,61 @@
package jenkins.model.lazy;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.util.AdaptedIterator;
import hudson.util.Iterators;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.IntConsumer;
/**
* Take {@code SortedMap<Integer,BuildReference<R>>} and make it look like {@code SortedMap<Integer,R>}.
*
* When {@link BuildReference} lost the build object, we'll use {@link AbstractLazyLoadRunMap#getById(String)}
* to obtain one.
* <p>
* When {@link BuildReference} lost the build object, we'll use {@link Resolver} to obtain one.
* </p>
*
* <p>
* By default, this adapter provides a read-only view of the underlying {@link #core} map,
* which may be modified externally to change the adapter's state.
* Support for removal operations through {@link #entrySet()}, {@link #values()},
* {@link #keySet()}, and their iterators can be enabled by overriding
* {@link #removeValue(Object)}. This method is invoked by the internal collection views
* to perform the actual removal logic (such as updating {@link #core} or performing other actions).
* </p>
*
* <p>
* Some operations are weakly implemented (for example, {@link #size()} may be approximate).
* This adapter implements {@link SortedMap}, which does not allow {@code null} keys; however,
* methods such as {@code get(null)} or {@code containsKey(null)} do not throw a {@link NullPointerException}
* and instead return {@code null} or {@code false}, respectively, indicating that the key was not found.
* </p>
*
* @author Kohsuke Kawaguchi
*/
class BuildReferenceMapAdapter<R> implements SortedMap<Integer, R> {
private final AbstractLazyLoadRunMap<R> loader;
class BuildReferenceMapAdapter<R> extends AbstractMap<Integer, R> implements SortedMap<Integer, R> {
private final NavigableMap<Integer, BuildReference<R>> core;
private final Resolver<R> resolver;
private final SortedMap<Integer, BuildReference<R>> core;
private final Set<Integer> keySet = new KeySetAdapter();
private final Collection<R> values = new ValuesAdapter();
private final Set<Map.Entry<Integer, R>> entrySet = new EntrySetAdapter();
BuildReferenceMapAdapter(AbstractLazyLoadRunMap<R> loader, SortedMap<Integer, BuildReference<R>> core) {
this.loader = loader;
BuildReferenceMapAdapter(NavigableMap<Integer, BuildReference<R>> core, Resolver<R> resolver) {
this.core = core;
this.resolver = resolver;
}
private R unwrap(@Nullable BuildReference<R> ref) {
if (ref == null) return null;
R v = ref.get();
if (v == null)
v = loader.getById(ref.id);
return v;
}
private BuildReference<R> wrap(@Nullable R value) {
if (value == null) return null;
return loader.createReference(value);
}
@Override
public Comparator<? super Integer> comparator() {
return core.comparator();
@ -56,330 +63,274 @@ class BuildReferenceMapAdapter<R> implements SortedMap<Integer, R> {
@Override
public SortedMap<Integer, R> subMap(Integer fromKey, Integer toKey) {
return new BuildReferenceMapAdapter<>(loader, core.subMap(fromKey, toKey));
return new BuildReferenceMapAdapter<>(core.subMap(fromKey, true, toKey, false), resolver);
}
@Override
public SortedMap<Integer, R> headMap(Integer toKey) {
return new BuildReferenceMapAdapter<>(loader, core.headMap(toKey));
return new BuildReferenceMapAdapter<>(core.headMap(toKey, false), resolver);
}
@Override
public SortedMap<Integer, R> tailMap(Integer fromKey) {
return new BuildReferenceMapAdapter<>(loader, core.tailMap(fromKey));
return new BuildReferenceMapAdapter<>(core.tailMap(fromKey, true), resolver);
}
@Override
public Integer firstKey() {
return core.firstKey();
return keySet.stream().findFirst().orElseThrow(NoSuchElementException::new);
}
@Override
public Integer lastKey() {
return core.lastKey();
return new BuildReferenceMapAdapter<>(core.descendingMap(), resolver).firstKey();
}
@Override
public Set<Integer> keySet() {
return core.keySet();
return keySet;
}
@Override
public Collection<R> values() {
return new CollectionAdapter(core.values());
return values;
}
@Override
public Set<Entry<Integer, R>> entrySet() {
return new SetAdapter(core.entrySet());
}
@Override
public int size() {
return core.size();
return entrySet;
}
@Override
public boolean isEmpty() {
return core.isEmpty();
return entrySet().isEmpty();
}
@Override
public boolean containsKey(Object key) {
return core.containsKey(key);
BuildReference<R> ref = key instanceof Integer ? core.get(key) : null;
if (ref == null) {
return false;
}
// if found, check if value is loadable
if (!ref.isSet()) {
resolver.resolveBuildRef(ref);
}
return !ref.isUnloadable();
}
@Override
public boolean containsValue(Object value) {
return core.containsValue(value); // TODO should this be core.containsValue(wrap(value))?
if (!resolver.getBuildClass().isInstance(value)) {
return false;
}
R val = resolver.getBuildClass().cast(value);
return val.equals(get(resolver.getNumberOf(val)));
}
@Override
public R get(Object key) {
return unwrap(core.get(key));
}
@Override
public R put(Integer key, R value) {
return unwrap(core.put(key, wrap(value)));
return key instanceof Integer ? resolver.resolveBuildRef(core.get(key)) : null;
}
@Override
public R remove(Object key) {
return unwrap(core.remove(key));
}
@Override
public void putAll(Map<? extends Integer, ? extends R> m) {
for (Entry<? extends Integer, ? extends R> e : m.entrySet())
put(e.getKey(), e.getValue());
}
@Override
public void clear() {
core.clear();
}
@Override
public boolean equals(Object o) {
return core.equals(o); // TODO this is wrong
}
@Override
public int hashCode() {
return core.hashCode();
}
@Override public String toString() {
return new LinkedHashMap<>(this).toString();
}
private class CollectionAdapter implements Collection<R> {
private final Collection<BuildReference<R>> core;
private CollectionAdapter(Collection<BuildReference<R>> core) {
this.core = core;
R val = get(key);
if (val == null) {
return null;
}
return removeValue(val) ? val : null;
}
protected boolean removeValue(R value) {
throw new UnsupportedOperationException();
}
private class KeySetAdapter extends AbstractSet<Integer> {
@Override
public int size() {
return core.size();
return BuildReferenceMapAdapter.this.size();
}
@Override
public boolean isEmpty() {
return core.isEmpty();
return BuildReferenceMapAdapter.this.isEmpty();
}
@Override
public boolean contains(Object o) {
// TODO: to properly pass this onto core, we need to wrap o into BuildReference but also needs to figure out ID.
throw new UnsupportedOperationException();
public boolean contains(Object k) {
return BuildReferenceMapAdapter.this.containsKey(k);
}
@Override
public boolean remove(Object o) {
return BuildReferenceMapAdapter.this.remove(o) != null;
}
@Override
public Iterator<Integer> iterator() {
return new AdaptedIterator<>(BuildReferenceMapAdapter.this.entrySet().iterator()) {
@Override
protected Integer adapt(Entry<Integer, R> e) {
return e.getKey();
}
};
}
@Override
public Spliterator<Integer> spliterator() {
return new Spliterators.AbstractIntSpliterator(Long.MAX_VALUE,
Spliterator.DISTINCT | Spliterator.ORDERED | Spliterator.SORTED) {
private final Iterator<Integer> it = KeySetAdapter.this.iterator();
@Override
public boolean tryAdvance(IntConsumer action) {
Objects.requireNonNull(action);
if (it.hasNext()) {
action.accept(it.next());
return true;
}
return false;
}
@Override
public Comparator<? super Integer> getComparator() {
return BuildReferenceMapAdapter.this.comparator();
}
};
}
}
private class ValuesAdapter extends AbstractCollection<R> {
@Override
public int size() {
return BuildReferenceMapAdapter.this.size();
}
@Override
public boolean isEmpty() {
return BuildReferenceMapAdapter.this.isEmpty();
}
@Override
public boolean contains(Object v) {
return BuildReferenceMapAdapter.this.containsValue(v);
}
@Override
public boolean remove(Object o) {
return resolver.getBuildClass().isInstance(o) &&
BuildReferenceMapAdapter.this.removeValue(resolver.getBuildClass().cast(o));
}
@Override
public Iterator<R> iterator() {
// silently drop null, as if we didn't have them in this collection in the first place
// this shouldn't be indistinguishable from concurrent modifications to the collection
return Iterators.removeNull(new AdaptedIterator<>(core.iterator()) {
return new AdaptedIterator<>(BuildReferenceMapAdapter.this.entrySet().iterator()) {
@Override
protected R adapt(BuildReference<R> ref) {
return unwrap(ref);
protected R adapt(Entry<Integer, R> e) {
return e.getValue();
}
});
};
}
@Override
public Object[] toArray() {
List<Object> list = new ArrayList<>(size());
for (var e : this) {
list.add(e);
}
return list.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return new ArrayList<>(this).toArray(a);
}
@Override
public boolean add(R value) {
return core.add(wrap(value));
}
@Override
public boolean remove(Object o) {
// return core.remove(o);
// TODO: to properly pass this onto core, we need to wrap o into BuildReference but also needs to figure out ID.
throw new UnsupportedOperationException();
}
@Override
public boolean containsAll(Collection<?> c) {
for (Object o : c) {
if (!contains(o))
return false;
}
return true;
}
@Override
public boolean addAll(Collection<? extends R> c) {
boolean b = false;
for (R r : c) {
b |= add(r);
}
return b;
}
@Override
public boolean removeAll(Collection<?> c) {
boolean b = false;
for (Object o : c) {
b |= remove(o);
}
return b;
}
@Override
public boolean retainAll(Collection<?> c) {
// TODO: to properly pass this onto core, we need to wrap o into BuildReference but also needs to figure out ID.
throw new UnsupportedOperationException();
}
@Override
public void clear() {
core.clear();
}
@Override
public boolean equals(Object o) {
return core.equals(o);
}
@Override
public int hashCode() {
return core.hashCode();
public Spliterator<R> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), Spliterator.DISTINCT | Spliterator.ORDERED);
}
}
private class SetAdapter implements Set<Entry<Integer, R>> {
private final Set<Entry<Integer, BuildReference<R>>> core;
private SetAdapter(Set<Entry<Integer, BuildReference<R>>> core) {
this.core = core;
}
private class EntrySetAdapter extends AbstractSet<Entry<Integer, R>> {
@Override
public int size() {
return core.size();
return BuildReferenceMapAdapter.this.core.size();
}
@Override
public boolean isEmpty() {
return core.isEmpty();
return this.stream().findFirst().isEmpty();
}
@Override
public boolean contains(Object o) {
// TODO: to properly pass this onto core, we need to wrap o into BuildReference but also needs to figure out ID.
throw new UnsupportedOperationException();
}
@Override
public Iterator<Entry<Integer, R>> iterator() {
return Iterators.removeNull(new AdaptedIterator<>(core.iterator()) {
@Override
protected Entry<Integer, R> adapt(Entry<Integer, BuildReference<R>> e) {
return _unwrap(e);
}
});
}
@Override
public Object[] toArray() {
List<Object> list = new ArrayList<>(size());
for (var e : this) {
list.add(e);
if (o instanceof Map.Entry<?, ?> e && e.getKey() instanceof Integer key) {
return e.getValue() != null && e.getValue().equals(BuildReferenceMapAdapter.this.get(key));
}
return list.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return new ArrayList<>(this).toArray(a);
}
@Override
public boolean add(Entry<Integer, R> value) {
return core.add(_wrap(value));
return false;
}
@Override
public boolean remove(Object o) {
// return core.remove(o);
// TODO: to properly pass this onto core, we need to wrap o into BuildReference but also needs to figure out ID.
throw new UnsupportedOperationException();
}
@Override
public boolean containsAll(Collection<?> c) {
for (Object o : c) {
if (!contains(o))
return false;
if (o instanceof Map.Entry<?, ?> e) {
return resolver.getBuildClass().isInstance(e.getValue()) &&
BuildReferenceMapAdapter.this.removeValue(resolver.getBuildClass().cast(e.getValue()));
}
return true;
return false;
}
@Override
public boolean addAll(Collection<? extends Entry<Integer, R>> c) {
boolean b = false;
for (Entry<Integer, R> r : c) {
b |= add(r);
}
return b;
public Iterator<Entry<Integer, R>> iterator() {
return new Iterator<>() {
private Entry<Integer, R> current;
private final Iterator<Entry<Integer, R>> it = Iterators.removeNull(Iterators.map(
BuildReferenceMapAdapter.this.core.entrySet().iterator(), coreEntry -> {
R v = BuildReferenceMapAdapter.this.resolver.resolveBuildRef(coreEntry.getValue());
return v == null ? null : new AbstractMap.SimpleEntry<>(coreEntry.getKey(), v);
}));
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public Entry<Integer, R> next() {
return current = it.next();
}
@Override
public void remove() {
if (current == null) {
throw new IllegalStateException();
}
BuildReferenceMapAdapter.this.removeValue(current.getValue());
}
};
}
@Override
public boolean removeAll(Collection<?> c) {
boolean b = false;
for (Object o : c) {
b |= remove(o);
}
return b;
}
@Override
public boolean retainAll(Collection<?> c) {
// TODO: to properly pass this onto core, we need to wrap o into BuildReference but also needs to figure out ID.
throw new UnsupportedOperationException();
}
@Override
public void clear() {
core.clear();
}
@Override
public boolean equals(Object o) {
return core.equals(o);
}
@Override
public int hashCode() {
return core.hashCode();
}
private Entry<Integer, BuildReference<R>> _wrap(Entry<Integer, R> e) {
return new AbstractMap.SimpleEntry<>(e.getKey(), wrap(e.getValue()));
}
private Entry<Integer, R> _unwrap(Entry<Integer, BuildReference<R>> e) {
R v = unwrap(e.getValue());
if (v == null)
return null;
return new AbstractMap.SimpleEntry<>(e.getKey(), v);
public Spliterator<Map.Entry<Integer, R>> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), Spliterator.DISTINCT | Spliterator.ORDERED);
}
}
/**
* An interface for resolving build references into actual build instances
* and extracting basic metadata from them.
**/
public interface Resolver<R> {
/**
* Resolves the given build reference into an actual build instance.
*
* @param buildRef the reference to a build to resolve, can be {@code null}
* @return the resolved build instance, or {@code null} if the reference is {@code null}
* or could not be resolved
*/
R resolveBuildRef(BuildReference<R> buildRef);
/**
* Returns the build number associated with the given build instance.
*
* @param build the build instance, cannot be null
* @return the build number
*/
Integer getNumberOf(R build);
/**
* Returns the class of the build type handled by this resolver.
*
* @return the {@link Class} of the build type {@code R}
*/
Class<R> getBuildClass();
}
}

View File

@ -47,6 +47,7 @@ import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.kohsuke.accmod.Restricted;
@ -128,13 +129,15 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT, RunT> & Queue.Task &
}
if (currentBuilds != null) {
// if we are reloading, keep all those that are still building intact
TreeMap<Integer, RunT> stillBuildingBuilds = new TreeMap<>();
for (RunT r : currentBuilds.getLoadedBuilds().values()) {
if (r.isBuilding()) {
// Do not use RunMap.put(Run):
_builds.put(r.getNumber(), r);
stillBuildingBuilds.put(r.getNumber(), r);
LOGGER.log(Level.FINE, "keeping reloaded {0}", r);
}
}
_builds.putAll(stillBuildingBuilds);
}
this.builds = _builds;
}
@ -145,6 +148,11 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT, RunT> & Queue.Task &
public RunT create(File dir) throws IOException {
return loadBuild(dir);
}
@Override
public Class<RunT> getBuildClass() {
return LazyBuildMixIn.this.getBuildClass();
}
});
return r;
}

View File

@ -1,125 +0,0 @@
package jenkins.model.lazy;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import jenkins.model.lazy.AbstractLazyLoadRunMap.Direction;
/**
* Set that backs {@link AbstractLazyLoadRunMap#entrySet()}.
*
* @author Kohsuke Kawaguchi
*/
class LazyLoadRunMapEntrySet<R> extends AbstractSet<Map.Entry<Integer, R>> {
private final AbstractLazyLoadRunMap<R> owner;
/**
* Lazily loaded all entries.
*/
private Set<Map.Entry<Integer, R>> all;
LazyLoadRunMapEntrySet(AbstractLazyLoadRunMap<R> owner) {
this.owner = owner;
}
private synchronized Set<Map.Entry<Integer, R>> all() {
if (all == null)
all = new BuildReferenceMapAdapter<>(owner, owner.all()).entrySet();
return all;
}
synchronized void clearCache() {
all = null;
}
@Override
public int size() {
return all().size();
}
@Override
public boolean isEmpty() {
return owner.newestBuild() == null;
}
@Override
public boolean contains(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
Object k = e.getKey();
if (k instanceof Integer) {
return owner.getByNumber((Integer) k).equals(e.getValue());
}
}
return false;
}
@Override
public Iterator<Map.Entry<Integer, R>> iterator() {
return new Iterator<>() {
R last = null;
R next = owner.newestBuild();
@Override
public boolean hasNext() {
return next != null;
}
@Override
public Map.Entry<Integer, R> next() {
last = next;
if (last != null) {
next = owner.search(owner.getNumberOf(last) - 1, Direction.DESC);
} else
throw new NoSuchElementException();
return entryOf(last);
}
private Map.Entry<Integer, R> entryOf(R r) {
return new AbstractMap.SimpleImmutableEntry<>(owner.getNumberOf(r), r);
}
@Override
public void remove() {
if (last == null)
throw new UnsupportedOperationException();
owner.removeValue(last);
}
};
}
@Override
public Spliterator<Map.Entry<Integer, R>> spliterator() {
return Spliterators.spliteratorUnknownSize(
iterator(), Spliterator.DISTINCT | Spliterator.ORDERED);
}
@Override
public Object[] toArray() {
return all().toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return all().toArray(a);
}
@Override
public boolean add(Map.Entry<Integer, R> integerREntry) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry e = (Map.Entry) o;
return owner.removeValue((R) e.getValue());
}
return false;
}
}

View File

@ -1,156 +0,0 @@
/*
* The MIT License
*
* Copyright (c) 2012, CloudBees, 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.lazy;
import java.util.AbstractList;
import java.util.Arrays;
/**
* {@code ArrayList&lt;Integer>} that uses {@code int} for storage.
*
* Plus a number of binary-search related methods that assume the array is sorted in the ascending order.
*
* @author Kohsuke Kawaguchi
*/
class SortedIntList extends AbstractList<Integer> {
private int[] data;
private int size;
SortedIntList(int capacity) {
this.data = new int[capacity];
this.size = 0;
}
/**
* Internal copy constructor.
*/
SortedIntList(SortedIntList that) {
this.data = new int[that.size + 8];
System.arraycopy(that.data, 0, this.data, 0,
that.size);
this.size = that.size;
}
/**
* Binary search to find the position of the given string.
*
* @return
* -(insertionPoint+1) if the exact string isn't found.
* That is, -1 means the probe would be inserted at the very beginning.
*/
public int find(int probe) {
return Arrays.binarySearch(data, 0, size, probe);
}
@Override
public boolean contains(Object o) {
return o instanceof Integer && contains(((Integer) o).intValue());
}
public boolean contains(int i) {
return find(i) >= 0;
}
@Override
public Integer get(int index) {
if (size <= index) throw new IndexOutOfBoundsException();
return data[index];
}
@Override
public int size() {
return size;
}
public int max() {
return size > 0 ? data[size - 1] : 0;
}
@Override
public boolean add(Integer i) {
return add(i.intValue());
}
public boolean add(int i) {
ensureCapacity(size + 1);
data[size++] = i;
return true;
}
private void ensureCapacity(int i) {
if (data.length < i) {
int[] r = new int[Math.max(data.length * 2, i)];
System.arraycopy(data, 0, r, 0, size);
data = r;
}
}
/**
* Finds the index of the entry lower than v.
*/
public int lower(int v) {
return Boundary.LOWER.apply(find(v));
}
/**
* Finds the index of the entry greater than v.
*/
public int higher(int v) {
return Boundary.HIGHER.apply(find(v));
}
/**
* Finds the index of the entry lower or equal to v.
*/
public int floor(int v) {
return Boundary.FLOOR.apply(find(v));
}
/**
* Finds the index of the entry greater or equal to v.
*/
public int ceil(int v) {
return Boundary.CEIL.apply(find(v));
}
public boolean isInRange(int idx) {
return 0 <= idx && idx < size;
}
public void sort() {
Arrays.sort(data, 0, size);
}
public void copyInto(int[] dest) {
System.arraycopy(data, 0, dest, 0, size);
}
public void removeValue(int n) {
int idx = find(n);
if (idx < 0) return;
System.arraycopy(data, idx + 1, data, idx, size - (idx + 1));
size--;
}
}

View File

@ -1,135 +0,0 @@
/*
* The MIT License
*
* Copyright (c) 2012, CloudBees, 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.lazy;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* {@link List} decorator that provides a number of binary-search related methods
* by assuming that the array is sorted in the ascending order.
*
* @author Kohsuke Kawaguchi
*/
class SortedList<T extends Comparable<T>> extends AbstractList<T> {
private List<T> data;
SortedList(List<T> data) {
this.data = new ArrayList<>(data);
assert isSorted();
}
/**
* Binary search to find the position of the given string.
*
* @return
* -(insertionPoint+1) if the exact string isn't found.
* That is, -1 means the probe would be inserted at the very beginning.
*/
public int find(T probe) {
return Collections.binarySearch(data, probe);
}
@Override
public boolean contains(Object o) {
return find((T) o) >= 0;
}
@Override
public T get(int idx) {
return data.get(idx);
}
@Override
public int size() {
return data.size();
}
@Override
public T remove(int index) {
return data.remove(index);
}
@Override
public boolean remove(Object o) {
return data.remove(o);
}
/**
* Finds the index of the entry lower than v.
*
* @return
* return value will be in the [-1,size) range
*/
public int lower(T v) {
return Boundary.LOWER.apply(find(v));
}
/**
* Finds the index of the entry greater than v.
*
* @return
* return value will be in the [0,size] range
*/
public int higher(T v) {
return Boundary.HIGHER.apply(find(v));
}
/**
* Finds the index of the entry lower or equal to v.
*
* @return
* return value will be in the [-1,size) range
*/
public int floor(T v) {
return Boundary.FLOOR.apply(find(v));
}
/**
* Finds the index of the entry greater or equal to v.
*
* @return
* return value will be in the [0,size] range
*/
public int ceil(T v) {
return Boundary.CEIL.apply(find(v));
}
public boolean isInRange(int idx) {
return 0 <= idx && idx < data.size();
}
private boolean isSorted() {
for (int i = 1; i < data.size(); i++) {
if (data.get(i).compareTo(data.get(i - 1)) < 0) {
return false;
}
}
return true;
}
}

View File

@ -26,15 +26,15 @@ package jenkins.model.queue;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.model.Item;
import hudson.model.ModelObject;
import hudson.security.AccessControlled;
import jenkins.model.FullyNamedModelObject;
/**
* A task that can be displayed in the executors widget.
*
* @since 2.480
*/
public interface ITask extends ModelObject {
public interface ITask extends FullyNamedModelObject {
/**
* @return {@code true} if the current user can cancel the current task.
*

View File

@ -3,9 +3,9 @@ package jenkins.model.queue;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.Cause;
import hudson.model.ModelObject;
import hudson.model.Queue;
import hudson.model.Run;
import jenkins.model.FullyNamedModelObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
@ -14,7 +14,7 @@ import org.kohsuke.accmod.restrictions.Beta;
* @since 2.405
*/
@Restricted(Beta.class)
public interface QueueItem extends ModelObject {
public interface QueueItem extends FullyNamedModelObject {
/**
* @return true if the item is starving for an executor for too long.
*/
@ -73,6 +73,15 @@ public interface QueueItem extends ModelObject {
@CheckForNull
@Override
default String getDisplayName() {
// TODO review usage of this method and replace with getFullDisplayName() where appropriate
return getTask().getFullDisplayName();
}
/**
* @return the full display name for this queue item; by default, {@link Queue.Task#getFullDisplayName()}
*/
@Override
default String getFullDisplayName() {
return getTask().getFullDisplayName();
}
}

View File

@ -2,6 +2,8 @@ package jenkins.util;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import jenkins.ClassLoaderReflectionToolkit;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@ -12,6 +14,10 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
*/
@Restricted(NoExternalUse.class)
public class URLClassLoader2 extends URLClassLoader implements JenkinsClassLoader {
private static final AtomicInteger NEXT_INSTANCE_NUMBER = new AtomicInteger(0);
private final String lockObjectPrefixName = String.format(
"%s@%x-loadClassLock:", URLClassLoader2.class.getName(), NEXT_INSTANCE_NUMBER.getAndIncrement());
static {
registerAsParallelCapable();
@ -69,8 +75,25 @@ public class URLClassLoader2 extends URLClassLoader implements JenkinsClassLoade
return super.findLoadedClass(name);
}
/**
* Replace the JDK's per-name lock map with a GC-collectable lock object. This is a workaround
* for JDK-8005233. When JDK-8005233 is resolved, this should be deleted. See also the
* discussion in <a
* href="https://mail.openjdk.org/pipermail/core-libs-dev/2025-May/146392.html">this OpenJDK
* thread</a>.
*
* <p>Parallel-capable {@link ClassLoader} implementations keep a distinct lock object per class
* name indefinitely, which can retain huge maps when there are many misses. Returning an
* interned {@link String} keyed by this loader and the class name preserves mutual exclusion
* for a given (loader, name) pair but allows the JVM to reclaim the lock when no longer
* referenced. Interned Strings are heap objects and GC-eligible on modern JDKs (7+).
*
* @param className the binary name of the class being loaded (must not be null)
* @return a lock object unique to this classloader/class pair
*/
@Override
public Object getClassLoadingLock(String className) {
return super.getClassLoadingLock(className);
Objects.requireNonNull(className);
return (lockObjectPrefixName + className).intern();
}
}

View File

@ -24,36 +24,70 @@
package org.acegisecurity.util;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.lang.reflect.Field;
/**
* @deprecated use {@link org.apache.commons.lang.reflect.FieldUtils}
* @deprecated Add a dependency to commons-lang3-api plugin and use {@code org.apache.commons.lang3.reflect.FieldUtils}
*/
@Deprecated
public final class FieldUtils {
public static Object getProtectedFieldValue(String protectedField, Object object) {
public static Object getProtectedFieldValue(@NonNull String protectedField, @NonNull Object object) {
try {
return org.apache.commons.lang.reflect.FieldUtils.readField(object, protectedField, true);
Field field = getField(object.getClass(), protectedField);
return field.get(object);
} catch (IllegalAccessException x) {
throw new RuntimeException(x);
}
}
public static void setProtectedFieldValue(String protectedField, Object object, Object newValue) {
public static void setProtectedFieldValue(@NonNull String protectedField, @NonNull Object object, @NonNull Object newValue) {
try {
// acgegi would silently fail to write to final fields
// FieldUtils.writeField(Object, field, true) only sets accessible on *non* public fields
// and then fails with IllegalAccessException (even if you make the field accessible in the interim!
// for backwards compatability we need to use a few steps
Field field = org.apache.commons.lang.reflect.FieldUtils.getField(object.getClass(), protectedField, true);
field.setAccessible(true);
Field field = getField(object.getClass(), protectedField);
field.set(object, newValue);
} catch (Exception x) {
} catch (IllegalAccessException x) {
throw new RuntimeException(x);
}
}
/**
* Return the field with the given name from the class or its superclasses.
* If the field is not found, an {@link IllegalArgumentException} is thrown.
*
* @param clazz the class to search for the field
* @param fieldName the name of the field to find
* @return the {@link Field} object representing the field
* @throws IllegalArgumentException if the field is not found
*/
private static Field getField(@NonNull final Class<?> clazz, @NonNull final String fieldName) {
// Check class and its superclasses
Class<?> current = clazz;
while (current != null) {
try {
Field field = current.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException e) {
// Continue to check superclass
}
current = current.getSuperclass();
}
// Check interfaces
for (Class<?> iface : clazz.getInterfaces()) {
try {
Field field = iface.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException e) {
// Continue to check next interface
}
}
throw new IllegalArgumentException("Field '" + fieldName + "' not found in class " + clazz.getName());
}
// TODO other methods as needed
private FieldUtils() {}

View File

@ -1,6 +1,6 @@
# The MIT License
#
# Copyright (c) 2004-2019, Sun Microsystems, Inc., Damian Szczepanik
# Copyright (c) 2004-2025, Sun Microsystems, Inc., Damian Szczepanik
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -20,3 +20,10 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
Welcome\ to\ Jenkins!=Witamy w Jenkinsie!
noJobDescription=To jest miejsce, gdzie projekty Jenkinsa będą wyświetlane. Aby rozpocząć, możesz skonfigurować rozproszone zadania albo zacząć budować swoje oprogramowanie.
setUpDistributedBuilds=Skonfiguruj rozproszone zadania
setUpAgent=Skonfiguruj agenta
setUpCloud=Skonfiguruj chmurę
learnMoreDistributedBuilds=Dowiedz się więcej o rozproszonych zadaniach
startBuilding=Zacznij budować swoje oprogramowanie
createJob=Utwórz projekt

View File

@ -28,6 +28,7 @@ THE SOFTWARE.
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry description="${it.formattedDescription}">
<f:checkbox title="${h.escape(it.name)}" name="value" checked="${it.value}" readonly="true" />
<j:set var="readOnlyMode" value="true"/>
<f:checkbox title="${h.escape(it.name)}" name="value" checked="${it.value}"/>
</f:entry>
</j:jelly>

View File

@ -199,8 +199,7 @@ LabelExpression.LabelLink=<a href="{0}{2}">Label {1}</a> matches {3,choice,0#no
LabelExpression.NoMatch=No agent/cloud matches this label expression.
LabelExpression.NoMatch_DidYouMean=No agent/cloud matches this label expression. Did you mean {1} instead of {0}?
ManageJenkinsAction.DisplayName=Manage Jenkins
ManageJenkinsAction.notification={0} notification
ManageJenkinsAction.notifications={0} notifications
ManageJenkinsAction.notifications=One or more notifications
MultiStageTimeSeries.EMPTY_STRING=
ParametersDefinitionProperty.BuildButtonText=Build
Queue.AllNodesOffline=All nodes of label {0} are offline

View File

@ -35,6 +35,7 @@ THE SOFTWARE.
<l:main-panel>
<t:buildCaption it="${build}">${title}</t:buildCaption>
<j:set var="escapeEntryTitleAndDescription" value="true" /> <!-- SECURITY-353 defense unless overridden -->
<j:set var="readOnlyMode" value="true"/>
<j:forEach var="parameterValue" items="${it.parameters}">
<st:include it="${parameterValue}" page="value.jelly" />
</j:forEach>

View File

@ -1,6 +1,6 @@
# The MIT License
#
# Copyright (c) 2004-2010, Sun Microsystems, Inc.
# Copyright (c) 2004-2025, Sun Microsystems, 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
@ -24,3 +24,4 @@ Description=Opis
DisplayName=Wyświetlana nazwa
LOADING=ŁADOWANIE
Save=Zapisz
Edit\ Build\ Information=Edycja informacji o zadaniu

View File

@ -1,6 +1,6 @@
# The MIT License
#
# Copyright (c) 2004-2016, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
# Copyright (c) 2004-2025, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -21,4 +21,7 @@
# THE SOFTWARE.
Console\ Output=Logi konsoli
Download=Pobierz
Copy=Kopiuj
skipSome=Pominięto {0,number,integer} KB.. <a href="{1}">Pokaż wszystko</a>
View\ as\ plain\ text=Wyświetl bez formatowania

View File

@ -24,10 +24,11 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<f:textbox name="value" value="${it.value}" readonly="true" />
</f:entry>
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<j:set var="readOnlyMode" value="true"/>
<f:textbox name="value" value="${it.value}"/>
</f:entry>
</j:jelly>

View File

@ -26,8 +26,9 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<f:textarea name="value" value="${it.value}" readonly="readonly" />
</f:entry>
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<j:set var="readOnlyMode" value="true"/>
<f:textarea name="value" value="${it.value}"/>
</f:entry>
</j:jelly>

View File

@ -0,0 +1,27 @@
<!--
The MIT License
Copyright (c) 2025 Jan Faracik
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.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout">
<l:app-bar title="${it.fullName}" icon="${h.getUserAvatar(it, '96x96')}" />
</j:jelly>

View File

@ -27,9 +27,7 @@ THE SOFTWARE.
<st:include page="sidepanel.jelly" />
<l:breadcrumb title="${%Builds}" />
<l:main-panel>
<h1>
${%title(it)}
</h1>
<l:app-bar title="${%Builds}" />
<t:buildListTable builds="${it.builds}"/>
</l:main-panel>

View File

@ -28,12 +28,7 @@ THE SOFTWARE.
<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
<l:main-panel>
<div class="jenkins-app-bar">
<div class="jenkins-app-bar__content jenkins-build-caption">
<l:icon src="${h.getUserAvatar(it, '96x96')}" class="jenkins-avatar" />
<h1>
${it.fullName}
</h1>
</div>
<div class="jenkins-app-bar__content" />
<div class="jenkins-app-bar__controls">
<t:editDescriptionButton permission="${app.ADMINISTER}"/>
</div>

View File

@ -30,8 +30,10 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:header />
<l:side-panel>
<st:include page="app-bar.jelly" />
<l:tasks>
<l:task contextMenu="false" href="${rootURL}/${it.url}/" icon="symbol-person-circle" title="${%Status}"/>
<l:task contextMenu="false" href="${rootURL}/${it.url}/" icon="symbol-person" title="${%Profile}"/>
<l:task href="${rootURL}/${it.url}/builds" icon="symbol-build-history" title="${%Builds}"/>
<t:actions actions="${it.propertyActions}"/>
<t:actions actions="${it.transientActions}"/>

View File

@ -1,6 +1,6 @@
# The MIT License
#
# Copyright (c) 2004-2016, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
# Copyright (c) 2004-2025, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -25,3 +25,4 @@ Configure=Konfiguracja
Delete=Usuń
People=Użytkownicy
Status=Status
Profile=Profil

View File

@ -1,6 +1,6 @@
# The MIT License
#
# Copyright (c) 2004-2016, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
# Copyright (c) 2004-2025, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -25,6 +25,7 @@ ItemName.help=Pole wymagane
ItemName.label=Podaj nazwę projektu
ItemName.validation.required=To pole nie może być puste, podaj nazwę projektu
ItemType.validation.required=Wybierz rodzaj projektu
ItemType.label=Wybierz rodzaj projektu
CopyOption.placeholder=Podaj nazwę
CopyOption.description=Jeśli chcesz stworzyć nowy projekt na podstawie istniejącego, możesz użyć tej opcji:
CopyOption.label=Kopiuj z

View File

@ -35,6 +35,9 @@ THE SOFTWARE.
<link rel="alternate" title="Jenkins:${it.viewName} (failed builds) (RSS 2.0)" href="${rootURL}/${it.url}rssFailed?flavor=rss20" type="application/rss+xml" />
</l:header>
<l:side-panel>
<!-- Display the user's name for their views -->
<st:include page="app-bar.jelly" it="${it.owner.user}" optional="true" />
<l:tasks>
<st:include page="tasks-top.jelly" it="${it.owner}" optional="true" />

View File

@ -0,0 +1,32 @@
# The MIT License
#
# Copyright 2025 Damian Szczepanik
#
# 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.
UserPropertyCategory.Account.DisplayName=Konto
UserPropertyCategory.Preferences.DisplayName=Ustawienia
UserPropertyCategory.Experimental.DisplayName=Eksperymenty
UserPropertyCategory.Appearance.DisplayName=Wygląd
UserPropertyCategory.Security.DisplayName=Bezpieczeństwo
UserPropertyCategory.Invisible.DisplayName=Niewidoczny
UserPropertyCategoryAccountAction.DisplayName=Konto
UserPropertyCategoryAppearanceAction.DisplayName=Wygląd
UserPropertyCategoryExperimentalAction.DisplayName=Eksperymenty
UserPropertyCategoryPreferencesAction.DisplayName=Ustawienia
UserPropertyCategorySecurityAction.DisplayName=Bezpieczeństwo

View File

@ -30,16 +30,14 @@ THE SOFTWARE.
<l:layout permission="${app.ADMINISTER}" title="${%title}">
<st:include page="sidepanel.jelly" it="${it.targetUser}" />
<l:main-panel>
<f:form method="post" action="configSubmit" name="config" class="jenkins-form">
<h1>
${%title}
</h1>
<l:app-bar title="${%title}" />
<f:form method="post" action="configSubmit" name="config" class="jenkins-form">
<j:set var="thisAction" value="${it}" />
<j:set var="it" value="${thisAction.targetUser}" />
<j:set var="instance" value="${it}"/>
<f:section>
<f:section title="${%General}">
<f:entry title="${%Full name}" description="${%Full name.Description}">
<f:textbox field="fullName" />
</f:entry>

View File

@ -30,11 +30,9 @@ THE SOFTWARE.
<l:layout permission="${app.ADMINISTER}" title="${%title}">
<st:include page="sidepanel.jelly" it="${it.targetUser}" />
<l:main-panel>
<l:app-bar title="${%title}" />
<f:form method="post" action="configSubmit" name="config" class="jenkins-form">
<h1>
${%title}
</h1>
<j:set var="instance" value="${it}"/>
<j:set var="descriptors" value="${it.myCategoryDescriptors}" />
<j:set var="instances" value="${it.targetUser.properties}" />

View File

@ -30,11 +30,9 @@ THE SOFTWARE.
<l:layout permission="${app.ADMINISTER}" title="${%title}">
<st:include page="sidepanel.jelly" it="${it.targetUser}" />
<l:main-panel>
<l:app-bar title="${%title}" />
<f:form method="post" action="configSubmit" name="config">
<h1>
${%title}
</h1>
<j:set var="instance" value="${it}"/>
<j:set var="descriptors" value="${it.myCategoryDescriptors}" />
<j:set var="instances" value="${it.targetUser.properties}" />

View File

@ -30,11 +30,9 @@ THE SOFTWARE.
<l:layout permission="${app.ADMINISTER}" title="${%title}">
<st:include page="sidepanel.jelly" it="${it.targetUser}" />
<l:main-panel>
<f:form method="post" action="configSubmit" name="config">
<h1>
${%title}
</h1>
<l:app-bar title="${%title}" />
<f:form method="post" action="configSubmit" name="config">
<j:set var="instance" value="${it}"/>
<j:set var="descriptors" value="${it.myCategoryDescriptors}" />
<j:set var="instances" value="${it.targetUser.properties}" />

View File

@ -30,11 +30,9 @@ THE SOFTWARE.
<l:layout permission="${app.ADMINISTER}" title="${%title}">
<st:include page="sidepanel.jelly" it="${it.targetUser}" />
<l:main-panel>
<f:form method="post" action="configSubmit" name="config">
<h1>
${%title}
</h1>
<l:app-bar title="${%title}" />
<f:form method="post" action="configSubmit" name="config">
<j:set var="instance" value="${it}"/>
<j:set var="descriptors" value="${it.myCategoryDescriptors}" />
<j:set var="instances" value="${it.targetUser.properties}" />

View File

@ -1,6 +1,6 @@
# The MIT License
#
# Copyright (c) 2004-2022, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
# Copyright (c) 2004-2025, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -25,3 +25,4 @@ for\ failures=dla nieudanych
Clear=wyczyść
trend=trend
find=szukaj
No\ builds=Brak zadań

View File

@ -1 +1,23 @@
IOfflineCause.offline=rozłączony
# The MIT License
#
# Copyright (c) 2025, Damian Szczepanik
#
# 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.
CloudsLink.DisplayName=Chmura
IOfflineCause.offline=Offline

View File

@ -0,0 +1,25 @@
# The MIT License
#
# Copyright 2025 Damian Szczepanik
#
# 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.
SetupWizard_ConfigureInstance_ValidationErrors=Niektóre ustawienia są niepoprawne. Sprawdź komunikat błędu, aby zobaczyć szczegóły.
SetupWizard_ConfigureInstance_RootUrl_Empty=Adres URL nie może być pusty
SetupWizard_ConfigureInstance_RootUrl_Invalid=Adres URL jest niepoprawny, upewnij się, że używasz http:// lub https:// wraz z poprawnym adresem domeny.
SetupWizard.DisplayName=Kreator konfiguracji

View File

@ -66,5 +66,3 @@ ShutdownLink.Description=Stops executing new builds, so that the system can be e
ShutdownLink.ShuttingDownInProgressDescription=Jenkins is currently shutting down. New builds are not executing.
ShutdownLink.ShutDownReason_title=Reason
ShutdownLink.ShutDownReason_update=Update reason
AdministrativeMonitorsDecorator.DisplayName=Administrative Monitors Notifier

View File

@ -74,6 +74,3 @@ ShutdownLink.Description=\
# Configure tools, their locations and automatic installers.
ConfigureTools.Description=\
Настройване на инструментите, местоположенията и автоматичното инсталиране.
# Administrative Monitors Notifier
AdministrativeMonitorsDecorator.DisplayName=\
Известия за предупреждения

View File

@ -49,5 +49,4 @@ NodesLink.Description=Knoten hinzufügen, entfernen, steuern und überwachen, au
CliLink.Description=Jenkins aus der Kommandozeile oder skriptgesteuert nutzen und verwalten.
CliLink.DisplayName=Jenkins CLI
SystemLogLink.DisplayName=Systemlog
AdministrativeMonitorsDecorator.DisplayName=Anzeige aktiver Administrator-Warnungen
ConfigureTools.Description=Hilfsprogramme, ihre Installationsverzeichnisse und Installationsverfahren konfigurieren

View File

@ -21,8 +21,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
AdministrativeMonitorsDecorator.DisplayName=Componente di notifica monitor \
amministrativi
CliLink.Description=Accedi/gestisci Jenkins dal terminale o da uno script.
CliLink.DisplayName=Interfaccia a riga di comando di Jenkins
ConfigureLink.Description=Configura le impostazioni e i percorsi globali.

View File

@ -45,7 +45,6 @@ NodesLink.Description=Adiciona, remove, controla e monitora o vários nós
CliLink.DisplayName=Interface de Linha de Commando do Jenkins (CLI)
ShutdownLink.DisplayName_update=Atualizar preparação de desligamento
ShutdownLink.ShutDownReason_update=Atualizar razão
AdministrativeMonitorsDecorator.DisplayName=Notificador de monitorações administrativas
ConfigureTools.Description=Configurar ferramentas, suas localizações e instaladores automáticos.
ShutdownLink.ShuttingDownInProgressDescription=O Jenkins está sendo desligado no momento. Novas construções não serão executadas.
ShutdownLink.ShutDownReason_title=Razão

View File

@ -41,4 +41,3 @@ NodesLink.Description=Позволяет добавлять, удалять, к
PluginsLink.Description=Добавить, удалить, отключить или включить плагины, расширяющие функционональные возможности Jenkins.
ConfigureTools.Description=Конфигурация инструментов, их расположение и автоматическая инсталяция.
SystemLogLink.DisplayName=Системный журнал
AdministrativeMonitorsDecorator.DisplayName=Системные уведомления

View File

@ -66,5 +66,3 @@ ShutdownLink.Description=Slutar köra nya byggen så att systemet eventuellt kan
ShutdownLink.ShuttingDownInProgressDescription=Jenkins stängs ned för tillfället. Nya byggen körs inte.
ShutdownLink.ShutDownReason_title=Anledning
ShutdownLink.ShutDownReason_update=Uppdatera anledning
AdministrativeMonitorsDecorator.DisplayName=Avisering om administrativ övervakning

View File

@ -58,5 +58,3 @@ ShutdownLink.Description=不再執行新的建置作業,讓系統可以安全
ShutdownLink.ShuttingDownInProgressDescription=Jenkins 正在停機,不會執行新的建置作業。
ShutdownLink.ShutDownReason_title=原因
ShutdownLink.ShutDownReason_update=更新原因
AdministrativeMonitorsDecorator.DisplayName=管理監視器通知

View File

@ -41,7 +41,7 @@ THE SOFTWARE.
<h2>Restarting Jenkins</h2>
<p>
Jenkins will enter into the "quiet down" mode by sending a POST request with optional <code>reason</code> query parameter to <a href="../quietDown">this URL</a>.
Jenkins will enter into the "quiet down" mode by sending a POST request with optional <code>message</code> query parameter as the reason to <a href="../quietDown">this URL</a>.
You can also send another request to this URL to update the reason.
You can cancel this mode by sending a POST request to <a href="../cancelQuietDown">this URL</a>. On environments
where Jenkins can restart itself (such as when Jenkins is installed as a Windows service), POSTing to

View File

@ -1,6 +1,6 @@
# The MIT License
#
# Copyright (c) 2013-2019, Sun Microsystems, Inc., Kohsuke Kawaguchi
# Copyright (c) 2013-2025, Sun Microsystems, Inc., Kohsuke Kawaguchi
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -23,6 +23,7 @@
Password=Hasło
signUp=Zaloguj się poniżej lub <a href="signup">załóż konto</a>.
signIn=Zaloguj
Sign\ in\ to\ Jenkins=Zaloguj się do Jenkinsa
Keep\ me\ signed\ in=Zapamiętaj mnie
Username=Użytkownik
Invalid\ username\ or\ password=Niepoprawny użytkownik lub hasło

View File

@ -24,12 +24,14 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<select class="jenkins-select__input" name="[${it.flagKey}]">
<f:option selected="${flagValue == null}" value="">
<j:if test="${it.getDefaultValue() == true}">${%Default_True}</j:if>
<j:if test="${it.getDefaultValue() == false}">${%Default_False}</j:if>
</f:option>
<f:option selected="${flagValue == true}" value="true">${%True}</f:option>
<f:option selected="${flagValue == false}" value="false">${%False}</f:option>
</select>
<div class="jenkins-select">
<select class="jenkins-select__input" name="[${it.flagKey}]">
<f:option selected="${flagValue == null}" value="">
<j:if test="${it.getDefaultValue() == true}">${%Default_True}</j:if>
<j:if test="${it.getDefaultValue() == false}">${%Default_False}</j:if>
</f:option>
<f:option selected="${flagValue == true}" value="true">${%True}</f:option>
<f:option selected="${flagValue == false}" value="false">${%False}</f:option>
</select>
</div>
</j:jelly>

View File

@ -23,20 +23,20 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form" xmlns:l="/lib/layout">
<j:set var="userProperty" value="${instance}"/>
<f:entry field="experimentalFlags">
<j:invokeStatic var="flagConfigs" className="jenkins.model.experimentalflags.UserExperimentalFlag"
method="all"/>
<j:if test="${empty(flagConfigs)}">
<div class="jenkins-form-item">
${%NoFlagInfo}
</div>
</j:if>
<j:if test="${!empty(flagConfigs)}">
<j:invokeStatic var="flagConfigs" className="jenkins.model.experimentalflags.UserExperimentalFlag"
method="all"/>
<j:if test="${empty(flagConfigs)}">
<l:notice icon="symbol-flask"
title="${%NoFlagInfo}" />
</j:if>
<j:if test="${!empty(flagConfigs)}">
<f:entry field="experimentalFlags">
<f:rowSet name="flags">
<table class="jenkins-table sortable">
<table class="jenkins-table sortable jenkins-!-margin-0">
<thead>
<tr>
<th>${%FlagDisplayName}</th>
@ -62,6 +62,6 @@ THE SOFTWARE.
</tbody>
</table>
</f:rowSet>
</j:if>
</f:entry>
</f:entry>
</j:if>
</j:jelly>

Some files were not shown because too many files have changed in this diff Show More