diff --git a/changelog.html b/changelog.html index 580151deeadf6ab71416e03bae1b7db98aeb9ef4..f268d49ab7038f2c31472414220397aed43a96fe 100644 --- a/changelog.html +++ b/changelog.html @@ -55,6 +55,12 @@ Upcoming changes diff --git a/core/src/main/java/hudson/ExtensionFinder.java b/core/src/main/java/hudson/ExtensionFinder.java index 7ba61f23652c4a4a53c6305e4b361e9ad2d411a0..72169a17917f3c665d37eef351903d0a5c8fc7f0 100644 --- a/core/src/main/java/hudson/ExtensionFinder.java +++ b/core/src/main/java/hudson/ExtensionFinder.java @@ -414,7 +414,7 @@ public abstract class ExtensionFinder implements ExtensionPoint { * Instead, we should just drop the failing plugins. */ public static final Scope FAULT_TOLERANT_SCOPE = new Scope() { - public Provider scope(Key key, Provider unscoped) { + public Provider scope(final Key key, final Provider unscoped) { final Provider base = Scopes.SINGLETON.scope(key,unscoped); return new Provider() { public T get() { @@ -423,6 +423,9 @@ public abstract class ExtensionFinder implements ExtensionPoint { } catch (Exception e) { LOGGER.log(Level.WARNING,"Failed to instantiate. Skipping this component",e); return null; + } catch (LinkageError e) { + LOGGER.log(Level.WARNING,"Failed to instantiate. Skipping this component",e); + return null; } } }; diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 8d8254ec250a729b9ace5fc659417a06d5d411d1..8c771dcdd1a45c4590967b741a745a96bf3bdbe4 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -27,7 +27,6 @@ package hudson; import hudson.Launcher.LocalLauncher; import hudson.Launcher.RemoteLauncher; -import hudson.model.AbstractDescribableImpl; import jenkins.model.Jenkins; import hudson.model.TaskListener; import hudson.model.AbstractProject; @@ -840,7 +839,7 @@ public final class FilePath implements Serializable { return channel.call(wrapper); } catch (TunneledInterruptedException e) { - throw (InterruptedException)new InterruptedException().initCause(e); + throw (InterruptedException)new InterruptedException(e.getMessage()).initCause(e); } catch (AbortException e) { throw e; // pass through so that the caller can catch it as AbortException } catch (IOException e) { @@ -1976,8 +1975,26 @@ public final class FilePath implements Serializable { * @see #validateFileMask(FilePath, String) */ public String validateAntFileMask(final String fileMasks) throws IOException, InterruptedException { + return validateAntFileMask(fileMasks, Integer.MAX_VALUE); + } + + /** + * Like {@link #validateAntFileMask(String)} but performing only a bounded number of operations. + *

Whereas the unbounded overload is appropriate for calling from cancelable, long-running tasks such as build steps, + * this overload should be used when an answer is needed quickly, such as for {@link #validateFileMask(String)} + * or anything else returning {@link FormValidation}. + *

If a positive match is found, {@code null} is returned immediately. + * A message is returned in case the file pattern can definitely be determined to not match anything in the directory within the alloted time. + * If the time runs out without finding a match but without ruling out the possibility that there might be one, {@link InterruptedException} is thrown, + * in which case the calling code should give the user the benefit of the doubt and use {@link hudson.util.FormValidation.Kind#OK} (with or without a message). + * @param bound a maximum number of negative operations (deliberately left vague) to perform before giving up on a precise answer; 10_000 is a reasonable pick + * @throws InterruptedException not only in case of a channel failure, but also if too many operations were performed without finding any matches + * @since 1.484 + */ + public String validateAntFileMask(final String fileMasks, final int bound) throws IOException, InterruptedException { return act(new FileCallable() { - public String invoke(File dir, VirtualChannel channel) throws IOException { + private static final long serialVersionUID = 1; + public String invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { if(fileMasks.startsWith("~")) return Messages.FilePath_TildaDoesntWork(); @@ -2074,10 +2091,28 @@ public final class FilePath implements Serializable { return null; // no error } - private boolean hasMatch(File dir, String pattern) { - FileSet fs = Util.createFileSet(dir,pattern); - DirectoryScanner ds = fs.getDirectoryScanner(new Project()); - + private boolean hasMatch(File dir, String pattern) throws InterruptedException { + class Cancel extends RuntimeException {} + DirectoryScanner ds = bound == Integer.MAX_VALUE ? new DirectoryScanner() : new DirectoryScanner() { + int ticks; + @Override public synchronized boolean isCaseSensitive() { + if (!filesIncluded.isEmpty() || !dirsIncluded.isEmpty() || ticks++ > bound) { + throw new Cancel(); + } + return super.isCaseSensitive(); + } + }; + ds.setBasedir(dir); + ds.setIncludes(new String[] {pattern}); + try { + ds.scan(); + } catch (Cancel c) { + if (ds.getIncludedFilesCount()!=0 || ds.getIncludedDirsCount()!=0) { + return true; + } else { + throw new InterruptedException("no matches found within " + bound); + } + } return ds.getIncludedFilesCount()!=0 || ds.getIncludedDirsCount()!=0; } @@ -2126,11 +2161,11 @@ public final class FilePath implements Serializable { if(!exists()) // no workspace. can't check return FormValidation.ok(); - String msg = validateAntFileMask(value); + String msg = validateAntFileMask(value, 10000); if(errorIfNotExist) return FormValidation.error(msg); else return FormValidation.warning(msg); } catch (InterruptedException e) { - return FormValidation.ok(); + return FormValidation.ok(Messages.FilePath_did_not_manage_to_validate_may_be_too_sl(value)); } } diff --git a/core/src/main/java/hudson/logging/LogRecorder.java b/core/src/main/java/hudson/logging/LogRecorder.java index 094896f84f763871a6c3e6e147561bb851084bf7..1326f93c0f58b8ff4f347aa9445857b45a20cbee 100644 --- a/core/src/main/java/hudson/logging/LogRecorder.java +++ b/core/src/main/java/hudson/logging/LogRecorder.java @@ -87,6 +87,7 @@ public class LogRecorder extends AbstractModelObject implements Saveable { public static final class Target { public final String name; private final int level; + private transient /* almost final*/ Logger logger; public Target(String name, Level level) { this(name,level.intValue()); @@ -118,7 +119,10 @@ public class LogRecorder extends AbstractModelObject implements Saveable { } public Logger getLogger() { - return Logger.getLogger(name); + if (logger == null) { + logger = Logger.getLogger(name); + } + return logger; } /** diff --git a/core/src/main/java/hudson/model/AperiodicWork.java b/core/src/main/java/hudson/model/AperiodicWork.java index b46ac8fc0e4782c28ce0df3c581906da66fe1d35..512db0efc0e0f4cf3af11f003790c2c03c6f6171 100644 --- a/core/src/main/java/hudson/model/AperiodicWork.java +++ b/core/src/main/java/hudson/model/AperiodicWork.java @@ -30,6 +30,7 @@ import hudson.triggers.Trigger; import jenkins.model.Jenkins; import java.util.Random; +import java.util.Timer; import java.util.logging.Logger; @@ -84,7 +85,10 @@ public abstract class AperiodicWork extends SafeTimerTask implements ExtensionPo @Override public final void doRun() throws Exception{ doAperiodicRun(); - Trigger.timer.schedule(getNewInstance(), getRecurrencePeriod()); + Timer timer = Trigger.timer; + if (timer != null) { + timer.schedule(getNewInstance(), getRecurrencePeriod()); + } } protected abstract void doAperiodicRun(); diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index 2fde52bfd8b0413e030b93ca0bbd6402f91a1333..e7e0142bc11fbe64c0bbf0522324a6e68d7c091b 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -85,6 +85,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; +import java.util.Timer; import java.util.TreeSet; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicInteger; @@ -93,7 +94,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; -import javax.management.timer.Timer; import javax.servlet.ServletException; import jenkins.model.Jenkins; @@ -1746,8 +1746,11 @@ public class Queue extends ResourceController implements Saveable { MaintainTask(Queue queue) { this.queue = new WeakReference(queue); - long interval = 5 * Timer.ONE_SECOND; - Trigger.timer.schedule(this, interval, interval); + long interval = 5000; + Timer timer = Trigger.timer; + if (timer != null) { + timer.schedule(this, interval, interval); + } } protected void doRun() { diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java index f2de9a2fb5dcd4e37b0a47e848a7ff69ffd55e92..ded2b9872fc402d095b4639f7648245d95a1ac8a 100644 --- a/core/src/main/java/hudson/model/View.java +++ b/core/src/main/java/hudson/model/View.java @@ -32,12 +32,8 @@ import hudson.Extension; import hudson.ExtensionPoint; import hudson.Indenter; import hudson.Util; -import hudson.XmlFile; import hudson.model.Descriptor.FormException; -import hudson.model.Node.Mode; -import hudson.model.labels.LabelAtom; import hudson.model.labels.LabelAtomPropertyDescriptor; -import hudson.model.listeners.SaveableListener; import hudson.scm.ChangeLogSet.Entry; import hudson.search.CollectionSearchIndex; import hudson.search.SearchIndexBuilder; @@ -48,11 +44,9 @@ import hudson.security.PermissionGroup; import hudson.security.PermissionScope; import hudson.util.AlternativeUiTextProvider; import hudson.util.AlternativeUiTextProvider.Message; -import hudson.util.AtomicFileWriter; import hudson.util.DescribableList; import hudson.util.DescriptorList; import hudson.util.IOException2; -import hudson.util.IOUtils; import hudson.util.RunList; import hudson.util.XStream2; import hudson.views.ListViewColumn; @@ -77,7 +71,6 @@ import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; @@ -92,7 +85,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -645,7 +637,7 @@ public abstract class View extends AbstractModelObject implements AccessControll @Exported public final List users; - public final Object parent; + public final ModelObject parent; public People(Jenkins parent) { this.parent = parent; diff --git a/core/src/main/java/hudson/node_monitors/AbstractNodeMonitorDescriptor.java b/core/src/main/java/hudson/node_monitors/AbstractNodeMonitorDescriptor.java index 4d20fb2c9e5ae9aa13889ea4463695497c4e6751..0db9f64c2b166dcec4d58cf5e5812db190d1976a 100644 --- a/core/src/main/java/hudson/node_monitors/AbstractNodeMonitorDescriptor.java +++ b/core/src/main/java/hudson/node_monitors/AbstractNodeMonitorDescriptor.java @@ -36,6 +36,7 @@ import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.Timer; import java.util.logging.Level; import java.util.logging.Logger; @@ -70,11 +71,14 @@ public abstract class AbstractNodeMonitorDescriptor extends Descriptor implements Describable> * * If plugins want to run periodic jobs, they should implement {@link PeriodicWork}. */ - public static Timer timer; + @SuppressWarnings("MS_SHOULD_BE_FINAL") + public static @CheckForNull Timer timer; @Initializer(after=JOB_LOADED) public static void init() { new DoubleLaunchChecker().schedule(); - // start all PeridocWorks - for(PeriodicWork p : PeriodicWork.all()) - timer.scheduleAtFixedRate(p,p.getInitialDelay(),p.getRecurrencePeriod()); - - // start all AperidocWorks - for(AperiodicWork p : AperiodicWork.all()) - timer.schedule(p,p.getInitialDelay()); - - // start monitoring nodes, although there's no hurry. - timer.schedule(new SafeTimerTask() { - public void doRun() { - ComputerSet.initialize(); + Timer _timer = timer; + if (_timer != null) { + // start all PeridocWorks + for(PeriodicWork p : PeriodicWork.all()) { + _timer.scheduleAtFixedRate(p,p.getInitialDelay(),p.getRecurrencePeriod()); } - }, 1000*10); + + // start all AperidocWorks + for(AperiodicWork p : AperiodicWork.all()) { + _timer.schedule(p,p.getInitialDelay()); + } + + // start monitoring nodes, although there's no hurry. + _timer.schedule(new SafeTimerTask() { + public void doRun() { + ComputerSet.initialize(); + } + }, 1000*10); + } } /** diff --git a/core/src/main/java/hudson/util/DoubleLaunchChecker.java b/core/src/main/java/hudson/util/DoubleLaunchChecker.java index bcdb07c74a38a18e13555340c4d3f46b62e9a2fe..3abf89b2eeb95474815fdbf6543dcf62331b061f 100644 --- a/core/src/main/java/hudson/util/DoubleLaunchChecker.java +++ b/core/src/main/java/hudson/util/DoubleLaunchChecker.java @@ -40,6 +40,7 @@ import java.util.logging.Logger; import java.util.logging.Level; import java.lang.management.ManagementFactory; import java.lang.reflect.Method; +import java.util.Timer; /** * Makes sure that no other Hudson uses our JENKINS_HOME directory, @@ -145,11 +146,14 @@ public class DoubleLaunchChecker { public void schedule() { // randomize the scheduling so that multiple Hudson instances will write at the file at different time long MINUTE = 1000*60; - Trigger.timer.schedule(new SafeTimerTask() { - protected void doRun() { - execute(); - } - },(random.nextInt(30)+60)*MINUTE); + Timer timer = Trigger.timer; + if (timer != null) { + timer.schedule(new SafeTimerTask() { + protected void doRun() { + execute(); + } + },(random.nextInt(30)+60)*MINUTE); + } } /** diff --git a/core/src/main/java/hudson/util/FormFieldValidator.java b/core/src/main/java/hudson/util/FormFieldValidator.java index 9261588bf86dd2bcde8990a8421686573efe2015..e644e028bede5046a5f90d3ebbff9815fc1a9e6d 100644 --- a/core/src/main/java/hudson/util/FormFieldValidator.java +++ b/core/src/main/java/hudson/util/FormFieldValidator.java @@ -378,11 +378,11 @@ public abstract class FormFieldValidator { return; } - String msg = ws.validateAntFileMask(value); + String msg = ws.validateAntFileMask(value, 10000); if(errorIfNotExist) error(msg); else warning(msg); } catch (InterruptedException e) { - ok(); // coundn't check + ok(Messages.FormFieldValidator_did_not_manage_to_validate_may_be_too_sl(value)); } } diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index d3dd6a3a2a5bcd81035c31b069ca59fda38d35c4..360f52dba246915a78cb459f1176ba4797895862 100755 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -727,7 +727,10 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro * @param pluginManager * If non-null, use existing plugin manager. create a new one. */ - @edu.umd.cs.findbugs.annotations.SuppressWarnings("SC_START_IN_CTOR") // bug in FindBugs. It flags UDPBroadcastThread.start() call but that's for another class + @edu.umd.cs.findbugs.annotations.SuppressWarnings({ + "SC_START_IN_CTOR", // bug in FindBugs. It flags UDPBroadcastThread.start() call but that's for another class + "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD" // Trigger.timer + }) protected Jenkins(File root, ServletContext context, PluginManager pluginManager) throws IOException, InterruptedException, ReactorException { long start = System.currentTimeMillis(); @@ -816,12 +819,15 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro } dnsMultiCast = new DNSMultiCast(this); - Trigger.timer.scheduleAtFixedRate(new SafeTimerTask() { - @Override - protected void doRun() throws Exception { - trimLabels(); - } - }, TimeUnit2.MINUTES.toMillis(5), TimeUnit2.MINUTES.toMillis(5)); + Timer timer = Trigger.timer; + if (timer != null) { + timer.scheduleAtFixedRate(new SafeTimerTask() { + @Override + protected void doRun() throws Exception { + trimLabels(); + } + }, TimeUnit2.MINUTES.toMillis(5), TimeUnit2.MINUTES.toMillis(5)); + } updateComputerList(); @@ -2563,6 +2569,7 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro /** * Called to shut down the system. */ + @edu.umd.cs.findbugs.annotations.SuppressWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD") public void cleanUp() { for (ItemListener l : ItemListener.all()) l.onBeforeShutdown(); @@ -2579,7 +2586,10 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro if(dnsMultiCast!=null) dnsMultiCast.close(); interruptReloadThread(); - Trigger.timer.cancel(); + Timer timer = Trigger.timer; + if (timer != null) { + timer.cancel(); + } // TODO: how to wait for the completion of the last job? Trigger.timer = null; if(tcpSlaveAgentListener!=null) diff --git a/core/src/main/resources/hudson/Messages.properties b/core/src/main/resources/hudson/Messages.properties index 38ec4e855eb6c3d5aafe4b4cab1107c4d43cc530..a9fcae1043f596223d8b954b809b7c21c9b84fd1 100644 --- a/core/src/main/resources/hudson/Messages.properties +++ b/core/src/main/resources/hudson/Messages.properties @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +FilePath.did_not_manage_to_validate_may_be_too_sl=Did not manage to validate {0} (may be too slow) FilePath.validateAntFileMask.whitespaceSeprator=\ Whitespace can no longer be used as the separator. Please Use '','' as the separator instead. FilePath.validateAntFileMask.doesntMatchAndSuggest=\ diff --git a/core/src/main/resources/hudson/triggers/Messages_fr.properties b/core/src/main/resources/hudson/triggers/Messages_fr.properties index 58f50abe592b3ed65652af4bc0c4ece97b8145c3..f58f36fb9a268c438264d14b4e1522e0db054dbd 100644 --- a/core/src/main/resources/hudson/triggers/Messages_fr.properties +++ b/core/src/main/resources/hudson/triggers/Messages_fr.properties @@ -20,8 +20,11 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -SCMTrigger.DisplayName=Scruter l''outil de gestion de version +SCMTrigger.DisplayName=Scrutation de l''outil de gestion de version SCMTrigger.getDisplayName=Log du dernier accès à {0} -SCMTrigger.SCMTriggerCause.ShortDescription=Un changement dans la base de code a provoqué le lancement de ce job +SCMTrigger.BuildAction.DisplayName=Log de scrutation +SCMTrigger.SCMTriggerCause.ShortDescription=Lancé par un changement dans la base de code TimerTrigger.DisplayName=Construire périodiquement -TimerTrigger.TimerTriggerCause.ShortDescription=L''\u00e9\u00e9ch\u00e9ance d''une alarme p\u00e9riodique a provoqu\u00e9 le lancement de ce job +TimerTrigger.MissingWhitespace=Il semble manquer un espace entre * et *. +TimerTrigger.TimerTriggerCause.ShortDescription=Lancé par une alarme périodique +Trigger.init=Initialisation des minuteurs des déclencheurs diff --git a/core/src/main/resources/hudson/util/Messages.properties b/core/src/main/resources/hudson/util/Messages.properties index 3a677120185e1b355fb5999f75a4d89e8a6c3321..5f40d7f1c373c95b48f38e539349cb517853aa1b 100644 --- a/core/src/main/resources/hudson/util/Messages.properties +++ b/core/src/main/resources/hudson/util/Messages.properties @@ -24,6 +24,7 @@ ClockDifference.InSync=In sync ClockDifference.Ahead=\ ahead ClockDifference.Behind=\ behind ClockDifference.Failed=Failed to check +FormFieldValidator.did_not_manage_to_validate_may_be_too_sl=Did not manage to validate {0} (may be too slow) FormValidation.ValidateRequired=Required FormValidation.Error.Details=(show details) -HttpResponses.Saved=Saved \ No newline at end of file +HttpResponses.Saved=Saved diff --git a/core/src/test/java/hudson/FilePathTest.java b/core/src/test/java/hudson/FilePathTest.java index 8f5d301d4030212231a1a87df24d1d41768993f1..4d2a9fab23423f3dfccbe2b7226b98c78a2c7968 100644 --- a/core/src/test/java/hudson/FilePathTest.java +++ b/core/src/test/java/hudson/FilePathTest.java @@ -424,4 +424,55 @@ public class FilePathTest extends ChannelTestCase { assertEquals("/opt/jenkins/workspace/foo/bar/manchu", new FilePath(nixPath, "foo/bar\\manchu").getRemote()); assertEquals("/opt/jenkins/workspace/foo/bar/manchu", new FilePath(nixPath, "foo/bar/manchu").getRemote()); } + + public void testValidateAntFileMask() throws Exception { + File tmp = Util.createTempDir(); + try { + FilePath d = new FilePath(french, tmp.getPath()); + d.child("d1/d2/d3").mkdirs(); + d.child("d1/d2/d3/f.txt").touch(0); + d.child("d1/d2/d3/f.html").touch(0); + d.child("d1/d2/f.txt").touch(0); + assertValidateAntFileMask(null, d, "**/*.txt"); + assertValidateAntFileMask(null, d, "d1/d2/d3/f.txt"); + assertValidateAntFileMask(null, d, "**/*.html"); + assertValidateAntFileMask(Messages.FilePath_validateAntFileMask_portionMatchButPreviousNotMatchAndSuggest("**/*.js", "**", "**/*.js"), d, "**/*.js"); + assertValidateAntFileMask(Messages.FilePath_validateAntFileMask_doesntMatchAnything("index.htm"), d, "index.htm"); + assertValidateAntFileMask(Messages.FilePath_validateAntFileMask_doesntMatchAndSuggest("f.html", "d1/d2/d3/f.html"), d, "f.html"); + // XXX lots more to test, e.g. multiple patterns separated by commas; ought to have full code coverage for this method + } finally { + Util.deleteRecursive(tmp); + } + } + + private static void assertValidateAntFileMask(String expected, FilePath d, String fileMasks) throws Exception { + assertEquals(expected, d.validateAntFileMask(fileMasks)); + } + + @Bug(7214) + public void testValidateAntFileMaskBounded() throws Exception { + File tmp = Util.createTempDir(); + try { + FilePath d = new FilePath(french, tmp.getPath()); + FilePath d2 = d.child("d1/d2"); + d2.mkdirs(); + for (int i = 0; i < 100; i++) { + FilePath d3 = d2.child("d" + i); + d3.mkdirs(); + d3.child("f.txt").touch(0); + } + assertEquals(null, d.validateAntFileMask("d1/d2/**/f.txt")); + assertEquals(null, d.validateAntFileMask("d1/d2/**/f.txt", 10)); + assertEquals(Messages.FilePath_validateAntFileMask_portionMatchButPreviousNotMatchAndSuggest("**/*.js", "**", "**/*.js"), d.validateAntFileMask("**/*.js", 1000)); + try { + d.validateAntFileMask("**/*.js", 10); + fail(); + } catch (InterruptedException x) { + // good + } + } finally { + Util.deleteRecursive(tmp); + } + } + }