提交 8d771bc2 编写于 作者: K kohsuke

Merged revisions...

Merged revisions 11754-11755,11763-11766,11770,11773-11777,11791-11797,11828-11829,11831-11839,11841-11843,11850,11854,11994,12774,12778-12793,12820-12822,12831-12841,12854-12855,12860-12882,12896-12905,12914-12920,12938-12941,12950,13045-13046,13048,13063-13064,13066,13072-13076,13111,13122-13147,13150,13153-13158,13487-13488,13851-13852,13854-13856,13859,13866-13867,13869,13872-13876,13878-13879,13883-13885,13887-13890,13896-13919 via svnmerge from 
https://www.dev.java.net/svn/hudson/branches/multiple-computer-per-node

................
  r11754 | kohsuke | 2008-08-25 17:59:14 -0700 (Mon, 25 Aug 2008) | 3 lines
  
  Adding EphemeralNode for non-persisted dynamically-allocated expandable/shrinkable slave pool.
  
  Tweaked the serialization of Hudson.slaves accordingly.
................
  r11755 | kohsuke | 2008-08-25 18:10:42 -0700 (Mon, 25 Aug 2008) | 1 line
  
  making a small improvements
................
  r11763 | kohsuke | 2008-08-26 13:57:25 -0700 (Tue, 26 Aug 2008) | 1 line
  
  adding more convenience methods
................
  r11764 | kohsuke | 2008-08-26 14:24:20 -0700 (Tue, 26 Aug 2008) | 1 line
  
  ported mask capability from the TFS plugin to the core.
................
  r11765 | kohsuke | 2008-08-26 14:26:25 -0700 (Tue, 26 Aug 2008) | 1 line
  
  making it non-final to allow for subclassing
................
  r11766 | kohsuke | 2008-08-26 14:35:20 -0700 (Tue, 26 Aug 2008) | 1 line
  
  test case for NodeList
................
  r11770 | kohsuke | 2008-08-26 15:15:21 -0700 (Tue, 26 Aug 2008) | 3 lines
  
  Wrote a simple tool that monitors a file system change (in a poor way) and triggers a build.
................
  r11773 | kohsuke | 2008-08-26 17:20:30 -0700 (Tue, 26 Aug 2008) | 1 line
  
  made more structured
................
  r11774 | kohsuke | 2008-08-26 17:47:41 -0700 (Tue, 26 Aug 2008) | 1 line
  
  indentation fix
................
  r11775 | kohsuke | 2008-08-26 17:48:22 -0700 (Tue, 26 Aug 2008) | 1 line
  
  remove the use of a deprecated feature.
................
  r11776 | kohsuke | 2008-08-26 17:51:54 -0700 (Tue, 26 Aug 2008) | 1 line
  
  renamed to 'ALL' to be consistent with recent use of DescriptorList
................
  r11777 | kohsuke | 2008-08-26 18:04:17 -0700 (Tue, 26 Aug 2008) | 1 line
  
  adding NodeFactory list and its configuration mechanism
................
  r11791 | kohsuke | 2008-08-27 09:43:38 -0700 (Wed, 27 Aug 2008) | 1 line
  
  picked up the latest jelly with Iterable support in <j:forEach>
................
  r11792 | kohsuke | 2008-08-27 09:44:08 -0700 (Wed, 27 Aug 2008) | 1 line
  
  Iterable support added in  commons-jelly 1.1-hudson-20080826
................
  r11793 | kohsuke | 2008-08-27 10:15:14 -0700 (Wed, 27 Aug 2008) | 2 lines
  
  - added name to NodeFactory for binding NodeFactory to URL tree.
  - NodeFactory should be access controlled
................
  r11794 | kohsuke | 2008-08-27 10:41:11 -0700 (Wed, 27 Aug 2008) | 1 line
  
  added a method to add one Node at a time
................
  r11795 | kohsuke | 2008-08-27 10:41:37 -0700 (Wed, 27 Aug 2008) | 1 line
  
  added permission constatnt
................
  r11796 | kohsuke | 2008-08-27 10:42:13 -0700 (Wed, 27 Aug 2008) | 1 line
  
  allowing the launch method to throw an exception.
................
  r11797 | kohsuke | 2008-08-27 10:44:13 -0700 (Wed, 27 Aug 2008) | 1 line
  
  improving the handling
................
  r11828 | kohsuke | 2008-08-29 11:07:16 -0700 (Fri, 29 Aug 2008) | 1 line
  
  added executor config to the sidebar.
................
  r11829 | kohsuke | 2008-08-29 11:16:11 -0700 (Fri, 29 Aug 2008) | 1 line
  
  "it" should be assigned to NodeFactory
................
  r11831 | kohsuke | 2008-08-29 11:44:41 -0700 (Fri, 29 Aug 2008) | 1 line
  
  formatting changes
................
  r11832 | kohsuke | 2008-08-29 11:59:21 -0700 (Fri, 29 Aug 2008) | 1 line
  
  added a script to create a flashing image from any picture
................
  r11833 | kohsuke | 2008-08-29 12:07:56 -0700 (Fri, 29 Aug 2008) | 1 line
  
  doc improvement.
................
  r11834 | kohsuke | 2008-08-29 13:37:59 -0700 (Fri, 29 Aug 2008) | 1 line
  
  added new images to indicate that a slave is launching
................
  r11835 | kohsuke | 2008-08-29 13:43:41 -0700 (Fri, 29 Aug 2008) | 1 line
  
  indicate a launching slave accordingly.
................
  r11836 | kohsuke | 2008-08-29 14:02:28 -0700 (Fri, 29 Aug 2008) | 1 line
  
  added a method to remove a Node.
................
  r11837 | kohsuke | 2008-08-29 14:02:38 -0700 (Fri, 29 Aug 2008) | 1 line
  
  disconnect
................
  r11838 | kohsuke | 2008-08-29 14:03:22 -0700 (Fri, 29 Aug 2008) | 1 line
  
  formatting changes
................
  r11839 | kohsuke | 2008-08-29 14:14:04 -0700 (Fri, 29 Aug 2008) | 1 line
  
  allowing subclasses.
................
  r11841 | kohsuke | 2008-08-29 14:28:29 -0700 (Fri, 29 Aug 2008) | 1 line
  
  constructor should be the first in the definition.
................
  r11842 | kohsuke | 2008-08-29 14:31:01 -0700 (Fri, 29 Aug 2008) | 1 line
  
  doc improvement.
................
  r11843 | kohsuke | 2008-08-29 14:32:48 -0700 (Fri, 29 Aug 2008) | 1 line
  
  fixed a problem of using partially constructed object during launch(), because the setNode() method is called from the Computer constructor.
................
  r11850 | kohsuke | 2008-08-29 14:50:36 -0700 (Fri, 29 Aug 2008) | 1 line
  
  serialize NodeFactory first so that references to them from NodeList will become references in XML.
................
  r11854 | kohsuke | 2008-08-29 14:56:56 -0700 (Fri, 29 Aug 2008) | 1 line
  
  cleaning up a bit.
................
  r11994 | kohsuke | 2008-09-03 14:42:11 -0700 (Wed, 03 Sep 2008) | 21 lines
  
  Fixed ArrayIndexOutOfBoundsException during replace(), when the title contains a '$' literal.
  
  The typical stack trace looks like:
  
  Caused by: java.lang.ArrayIndexOutOfBoundsException: 32
          at hudson.MarkupText$SubText.start(MarkupText.java:95)
          at hudson.MarkupText$SubText.group(MarkupText.java:131)
          at hudson.MarkupText$SubText.replace(MarkupText.java:154)
          at hudson.MarkupText$SubText.surroundWith(MarkupText.java:83)
          at 
  hudson.plugins.jira.JiraChangeLogAnnotator.annotate(JiraChangeLogAnnotator.java:37)
          at 
  hudson.scm.ChangeLogSet$Entry.getMsgAnnotated(ChangeLogSet.java:117)
          ... 148 more
  
  Vijayan Jayaraman and I found this problem while we were looking at the server log of Hudson for OpenJFX.
  
  Adding a unit test to verify this behavior, too.  
................
  r12774 | kohsuke | 2008-10-27 15:03:36 -0700 (Mon, 27 Oct 2008) | 3 lines
  
  preparing for heterogenousness in Node, so that different Node can be configured with different UIs, much like how different Jobs can be configured.
  
  Creating a slave works also like creating a new job.
................
  r12778 | kohsuke | 2008-10-27 15:06:51 -0700 (Mon, 27 Oct 2008) | 1 line
  
  We need to be able to create an emtpy Slave initially, so we have to do with the check in the form field validation.
................
  r12779 | kohsuke | 2008-10-27 15:48:04 -0700 (Mon, 27 Oct 2008) | 1 line
  
  making the new slave page a two-step process
................
  r12780 | kohsuke | 2008-10-27 15:48:24 -0700 (Mon, 27 Oct 2008) | 1 line
  
  fixed a compilation problem
................
  r12781 | kohsuke | 2008-10-27 15:53:30 -0700 (Mon, 27 Oct 2008) | 1 line
  
  bug fixes
................
  r12782 | kohsuke | 2008-10-27 15:58:20 -0700 (Mon, 27 Oct 2008) | 1 line
  
  bug fixes
................
  r12783 | kohsuke | 2008-10-27 16:08:56 -0700 (Mon, 27 Oct 2008) | 1 line
  
  making the configuration page work for slaves
................
  r12784 | kohsuke | 2008-10-27 16:35:17 -0700 (Mon, 27 Oct 2008) | 1 line
  
  added new-computer.svg
................
  r12785 | kohsuke | 2008-10-27 16:37:26 -0700 (Mon, 27 Oct 2008) | 1 line
  
  hooking up the config/create pages to the UI
................
  r12786 | kohsuke | 2008-10-27 16:44:40 -0700 (Mon, 27 Oct 2008) | 1 line
  
  fixed a copy method.
................
  r12787 | kohsuke | 2008-10-27 16:47:33 -0700 (Mon, 27 Oct 2008) | 1 line
  
  code was broken when a slave was renamed
................
  r12788 | kohsuke | 2008-10-27 16:58:04 -0700 (Mon, 27 Oct 2008) | 1 line
  
  moving out the commonality into a new tag
................
  r12789 | kohsuke | 2008-10-27 17:01:21 -0700 (Mon, 27 Oct 2008) | 1 line
  
  consistent term usage
................
  r12790 | kohsuke | 2008-10-27 17:03:19 -0700 (Mon, 27 Oct 2008) | 1 line
  
  retiring the configureExecutors. Slave configuration is now moved to individual slave page
................
  r12791 | kohsuke | 2008-10-27 17:10:32 -0700 (Mon, 27 Oct 2008) | 1 line
  
  retired configureExecutors.jelly and completed moved its contents to somewhere else
................
  r12792 | kohsuke | 2008-10-27 17:21:53 -0700 (Mon, 27 Oct 2008) | 1 line
  
  bug fix. request handling of "/descriptor/FQCN/..." was broken.
................
  r12793 | kohsuke | 2008-10-27 17:27:08 -0700 (Mon, 27 Oct 2008) | 1 line
  
  NodeFactory -> Cloud to encourage consistent term usage between the code and the UI.
................
  r12820 | kohsuke | 2008-10-29 10:58:20 -0700 (Wed, 29 Oct 2008) | 1 line
  
  formatting fix
................
  r12821 | kohsuke | 2008-10-29 11:17:49 -0700 (Wed, 29 Oct 2008) | 1 line
  
  added classes for handling a secret and prevent accidental exposure of a secret in the persisted form
................
  r12822 | kohsuke | 2008-10-29 11:32:39 -0700 (Wed, 29 Oct 2008) | 1 line
  
  added a base64 validator
................
  r12831 | kohsuke | 2008-10-29 15:58:45 -0700 (Wed, 29 Oct 2008) | 1 line
  
  secret key needs to be persisted outside config.xml (and it is, already!)
................
  r12832 | kohsuke | 2008-10-29 16:06:28 -0700 (Wed, 29 Oct 2008) | 1 line
  
  started working on Amazon EC2 plugin
................
  r12833 | kohsuke | 2008-10-29 16:09:20 -0700 (Wed, 29 Oct 2008) | 1 line
  
  brought the same enhancement as in <textbox />
................
  r12834 | kohsuke | 2008-10-29 16:15:05 -0700 (Wed, 29 Oct 2008) | 1 line
  
  copy over the onclick handler from the original button element
................
  r12835 | kohsuke | 2008-10-29 17:01:04 -0700 (Wed, 29 Oct 2008) | 1 line
  
  allow OK messages to be sent with some mark up.
................
  r12836 | kohsuke | 2008-10-29 17:06:52 -0700 (Wed, 29 Oct 2008) | 1 line
  
  added connection testing. This pattern needs to be generalized since it happens often
................
  r12837 | kohsuke | 2008-10-29 17:13:11 -0700 (Wed, 29 Oct 2008) | 1 line
  
  this is little better
................
  r12838 | kohsuke | 2008-10-29 17:17:44 -0700 (Wed, 29 Oct 2008) | 1 line
  
  added spinner
................
  r12839 | kohsuke | 2008-10-29 17:31:54 -0700 (Wed, 29 Oct 2008) | 1 line
  
  restructuring the validation button support in a form that can be readily moved to the core.
................
  r12840 | kohsuke | 2008-10-29 18:01:41 -0700 (Wed, 29 Oct 2008) | 1 line
  
  added <f:validateButton/> for multi-field server-side validation
................
  r12841 | kohsuke | 2008-10-29 18:03:12 -0700 (Wed, 29 Oct 2008) | 1 line
  
  logic moved to <f:validateButton/> in the core.
................
  r12854 | kohsuke | 2008-10-30 13:32:58 -0700 (Thu, 30 Oct 2008) | 1 line
  
  @QueryParameter.value won't be necessary any more
................
  r12855 | kohsuke | 2008-10-30 13:42:58 -0700 (Thu, 30 Oct 2008) | 1 line
  
  need a strongly-typed constructor.
................
  r12860 | kohsuke | 2008-10-30 15:24:18 -0700 (Thu, 30 Oct 2008) | 1 line
  
  adding more reflection support, which in turn we use in taglibs to raise the level of abstraction.
................
  r12861 | kohsuke | 2008-10-30 15:33:22 -0700 (Thu, 30 Oct 2008) | 1 line
  
  added a tag to create databinding to a<select> element from an enum property
................
  r12862 | kohsuke | 2008-10-30 15:37:44 -0700 (Thu, 30 Oct 2008) | 1 line
  
  adding a smarter bi-directional binding through <f:repeatable field="..."/>
................
  r12863 | kohsuke | 2008-10-30 15:47:48 -0700 (Thu, 30 Oct 2008) | 1 line
  
  making progress with the EC2 support
................
  r12864 | kohsuke | 2008-10-30 16:12:34 -0700 (Thu, 30 Oct 2008) | 1 line
  
  added form field validation for AMI ID
................
  r12865 | kohsuke | 2008-10-30 16:15:44 -0700 (Thu, 30 Oct 2008) | 1 line
  
  added simpler version
................
  r12866 | kohsuke | 2008-10-30 16:17:15 -0700 (Thu, 30 Oct 2008) | 1 line
  
  doc improvement
................
  r12867 | kohsuke | 2008-10-30 16:17:19 -0700 (Thu, 30 Oct 2008) | 1 line
  
  simplified a bit
................
  r12868 | kohsuke | 2008-10-30 16:40:25 -0700 (Thu, 30 Oct 2008) | 1 line
  
  doc improvement
................
  r12869 | kohsuke | 2008-10-30 17:00:57 -0700 (Thu, 30 Oct 2008) | 1 line
  
  adding the provisioning of the new slave
................
  r12870 | kohsuke | 2008-10-30 17:07:27 -0700 (Thu, 30 Oct 2008) | 1 line
  
  /** {@inheritDoc} */ is the default behavior for javadoc, so there's no point in making that explicit.
................
  r12871 | kohsuke | 2008-10-30 17:11:04 -0700 (Thu, 30 Oct 2008) | 1 line
  
  doc bug fix
................
  r12872 | kohsuke | 2008-10-30 17:17:20 -0700 (Thu, 30 Oct 2008) | 1 line
  
  simplified a bit
................
  r12873 | kohsuke | 2008-10-30 17:19:06 -0700 (Thu, 30 Oct 2008) | 1 line
  
  added RetentionStrategy for EC2 instances
................
  r12874 | kohsuke | 2008-10-30 17:21:17 -0700 (Thu, 30 Oct 2008) | 1 line
  
  allowing subclasses to override the disconnect behavior.
................
  r12875 | kohsuke | 2008-10-30 18:02:35 -0700 (Thu, 30 Oct 2008) | 1 line
  
  added termination
................
  r12876 | kohsuke | 2008-10-30 18:05:30 -0700 (Thu, 30 Oct 2008) | 1 line
  
  forgot to rename when NodeFactory was renamed to Cloud
................
  r12877 | kohsuke | 2008-10-30 18:06:08 -0700 (Thu, 30 Oct 2008) | 1 line
  
  using the console icon
................
  r12878 | kohsuke | 2008-10-30 18:17:20 -0700 (Thu, 30 Oct 2008) | 1 line
  
  adding UI to remove a slave.
................
  r12879 | kohsuke | 2008-10-30 18:48:57 -0700 (Thu, 30 Oct 2008) | 1 line
  
  commons-discovery now needed in stapler
................
  r12880 | kohsuke | 2008-10-30 18:50:27 -0700 (Thu, 30 Oct 2008) | 1 line
  
  needs to use a newer version of stapler
................
  r12881 | kohsuke | 2008-10-30 18:51:02 -0700 (Thu, 30 Oct 2008) | 1 line
  
  Bye bye CVS
................
  r12882 | kohsuke | 2008-10-30 18:51:43 -0700 (Thu, 30 Oct 2008) | 1 line
  
  Bye bye CVS
................
  r12896 | kohsuke | 2008-10-31 14:52:07 -0700 (Fri, 31 Oct 2008) | 1 line
  
  fixed the ordering between field and the mighty get(String) method.
................
  r12897 | kohsuke | 2008-10-31 15:08:41 -0700 (Fri, 31 Oct 2008) | 1 line
  
  fixed test compilation problems
................
  r12898 | kohsuke | 2008-10-31 15:11:25 -0700 (Fri, 31 Oct 2008) | 1 line
  
  marking this as a model object since it's always bound to URL.
................
  r12899 | kohsuke | 2008-10-31 15:13:42 -0700 (Fri, 31 Oct 2008) | 1 line
  
  adding UI hook up to manually provision a new node
................
  r12900 | kohsuke | 2008-10-31 15:14:00 -0700 (Fri, 31 Oct 2008) | 1 line
  
  after merging the trunk the version number is different
................
  r12901 | kohsuke | 2008-10-31 15:21:54 -0700 (Fri, 31 Oct 2008) | 1 line
  
  adding UI hook up for provisioning a new slave
................
  r12902 | kohsuke | 2008-10-31 15:45:10 -0700 (Fri, 31 Oct 2008) | 1 line
  
  adding a hook to decorate ComputerLauncher.
................
  r12903 | kohsuke | 2008-10-31 15:48:21 -0700 (Fri, 31 Oct 2008) | 1 line
  
  added filtering support
................
  r12904 | kohsuke | 2008-10-31 15:52:45 -0700 (Fri, 31 Oct 2008) | 1 line
  
  added view
................
  r12905 | kohsuke | 2008-10-31 15:53:49 -0700 (Fri, 31 Oct 2008) | 1 line
  
  doc improvement
................
  r12914 | kohsuke | 2008-11-01 17:45:57 -0700 (Sat, 01 Nov 2008) | 1 line
  
  adding code for connecting with SSH
................
  r12915 | kohsuke | 2008-11-01 17:49:28 -0700 (Sat, 01 Nov 2008) | 1 line
  
  working on launchers
................
  r12916 | kohsuke | 2008-11-01 18:33:12 -0700 (Sat, 01 Nov 2008) | 1 line
  
  added a method to fully read a stream
................
  r12917 | kohsuke | 2008-11-01 18:33:45 -0700 (Sat, 01 Nov 2008) | 1 line
  
  bumped up stapler
................
  r12918 | kohsuke | 2008-11-01 18:39:41 -0700 (Sat, 01 Nov 2008) | 1 line
  
  doc improvement.
................
  r12919 | kohsuke | 2008-11-01 18:45:24 -0700 (Sat, 01 Nov 2008) | 1 line
  
  added a launcher
................
  r12920 | kohsuke | 2008-11-01 18:55:02 -0700 (Sat, 01 Nov 2008) | 1 line
  
  implementing the actual ComputerLauncher
................
  r12938 | kohsuke | 2008-11-02 08:56:05 -0800 (Sun, 02 Nov 2008) | 1 line
  
  support the field notation
................
  r12939 | kohsuke | 2008-11-02 09:20:21 -0800 (Sun, 02 Nov 2008) | 1 line
  
  added the mechanism to execute the init script
................
  r12940 | kohsuke | 2008-11-02 09:21:38 -0800 (Sun, 02 Nov 2008) | 1 line
  
  simplification
................
  r12941 | kohsuke | 2008-11-02 09:36:21 -0800 (Sun, 02 Nov 2008) | 1 line
  
  adding key handling
................
  r12950 | kohsuke | 2008-11-03 11:00:58 -0800 (Mon, 03 Nov 2008) | 1 line
  
  formatting changes
................
  r13045 | kohsuke | 2008-11-06 15:24:32 -0800 (Thu, 06 Nov 2008) | 1 line
  
  adding time series datatype for retaining load average statistics in memory
................
  r13046 | kohsuke | 2008-11-06 15:59:02 -0800 (Thu, 06 Nov 2008) | 1 line
  
  added a convenience method.
................
  r13048 | kohsuke | 2008-11-06 16:21:54 -0800 (Thu, 06 Nov 2008) | 2 lines
  
  - started monitoring # of executor statistics.
  - exposed label to the remote API
................
  r13063 | kohsuke | 2008-11-07 13:05:15 -0800 (Fri, 07 Nov 2008) | 1 line
  
  added a convenience method.
................
  r13064 | kohsuke | 2008-11-07 13:11:08 -0800 (Fri, 07 Nov 2008) | 1 line
  
  monitor the length of the queue too
................
  r13066 | kohsuke | 2008-11-07 14:15:56 -0800 (Fri, 07 Nov 2008) | 1 line
  
  moved the stats to its own class.
................
  r13072 | kohsuke | 2008-11-07 15:40:29 -0800 (Fri, 07 Nov 2008) | 1 line
  
  adding provisioning logic based on load statistics
................
  r13073 | kohsuke | 2008-11-07 15:46:00 -0800 (Fri, 07 Nov 2008) | 1 line
  
  for effective testing, we need sub-sec precision
................
  r13074 | kohsuke | 2008-11-07 16:24:44 -0800 (Fri, 07 Nov 2008) | 1 line
  
  fixed a lie.
................
  r13075 | kohsuke | 2008-11-07 16:25:12 -0800 (Fri, 07 Nov 2008) | 1 line
  
  bug fix
................
  r13076 | kohsuke | 2008-11-07 16:30:55 -0800 (Fri, 07 Nov 2008) | 1 line
  
  avoid using deprecated methods
................
  r13111 | kohsuke | 2008-11-09 11:54:23 -0800 (Sun, 09 Nov 2008) | 1 line
  
  added the Future<?> return parameter to Computer.launch(). To do this w/o breaking compatibility, renamed launch to connect.
................
  r13122 | kohsuke | 2008-11-10 14:38:03 -0800 (Mon, 10 Nov 2008) | 1 line
  
  allow programmatic update of the assigned label
................
  r13123 | kohsuke | 2008-11-10 14:38:40 -0800 (Mon, 10 Nov 2008) | 1 line
  
  Iterator not needed because CopyOnWriteArrayList doesn't support removal via iterator.
................
  r13124 | kohsuke | 2008-11-10 14:39:24 -0800 (Mon, 10 Nov 2008) | 1 line
  
  added another Builder for tests
................
  r13125 | kohsuke | 2008-11-10 14:39:48 -0800 (Mon, 10 Nov 2008) | 1 line
  
  simplified a bit. This TestEnvironment stuff needs some clearer story.
................
  r13126 | kohsuke | 2008-11-10 14:46:40 -0800 (Mon, 10 Nov 2008) | 1 line
  
  IDEA complains about this.
................
  r13127 | kohsuke | 2008-11-10 14:51:53 -0800 (Mon, 10 Nov 2008) | 1 line
  
  improved the error diagnostics by displaying why a provisioned node failed to launch
................
  r13128 | kohsuke | 2008-11-10 14:52:12 -0800 (Mon, 10 Nov 2008) | 1 line
  
  Started a test case for NodeProvisioner
................
  r13129 | kohsuke | 2008-11-10 14:52:27 -0800 (Mon, 10 Nov 2008) | 1 line
  
  formatting changes
................
  r13130 | kohsuke | 2008-11-10 15:16:02 -0800 (Mon, 10 Nov 2008) | 1 line
  
  report the failure as a failure.
................
  r13131 | kohsuke | 2008-11-10 16:34:23 -0800 (Mon, 10 Nov 2008) | 1 line
  
  adding a parameter to the connect method to support joining to the pending launch activity.
................
  r13132 | kohsuke | 2008-11-10 16:38:31 -0800 (Mon, 10 Nov 2008) | 1 line
  
  making members public so that it can be accessed from other classes (DummyCloudImpl was the first to do this)
................
  r13133 | kohsuke | 2008-11-10 16:38:58 -0800 (Mon, 10 Nov 2008) | 1 line
  
  split the cloud implementation to a separate class to allow reuse.
................
  r13134 | kohsuke | 2008-11-10 16:56:27 -0800 (Mon, 10 Nov 2008) | 1 line
  
  assign unique names to support multiple jobs
................
  r13135 | kohsuke | 2008-11-10 17:11:31 -0800 (Mon, 10 Nov 2008) | 1 line
  
  avoid unnecessary interruption
................
  r13136 | kohsuke | 2008-11-10 17:11:41 -0800 (Mon, 10 Nov 2008) | 1 line
  
  adding another test case
................
  r13137 | kohsuke | 2008-11-10 17:16:28 -0800 (Mon, 10 Nov 2008) | 1 line
  
  cleaned up a test case
................
  r13138 | kohsuke | 2008-11-10 17:36:52 -0800 (Mon, 10 Nov 2008) | 1 line
  
  since the setNode method is called on every Computer just by adding a new node, don't force a new launch attempt.
................
  r13139 | kohsuke | 2008-11-10 17:37:38 -0800 (Mon, 10 Nov 2008) | 1 line
  
  perform orderly shutdown by giving computers enough time to disconnect.
................
  r13140 | kohsuke | 2008-11-10 17:38:08 -0800 (Mon, 10 Nov 2008) | 3 lines
  
  return Future for synchronization.
  
  This breaks binary compatibility, but I checked none of the plugins in Hudson SVN uses this, so I hope this is OK.
................
  r13141 | kohsuke | 2008-11-10 17:38:46 -0800 (Mon, 10 Nov 2008) | 1 line
  
  improved diagnostics
................
  r13142 | kohsuke | 2008-11-10 17:39:55 -0800 (Mon, 10 Nov 2008) | 1 line
  
  removed compiler warning
................
  r13143 | kohsuke | 2008-11-10 18:02:56 -0800 (Mon, 10 Nov 2008) | 1 line
  
  hide the rounding related problem from Cloud by passing int instead of float.
................
  r13144 | kohsuke | 2008-11-10 18:10:10 -0800 (Mon, 10 Nov 2008) | 1 line
  
  bug fix
................
  r13145 | kohsuke | 2008-11-10 18:11:51 -0800 (Mon, 10 Nov 2008) | 1 line
  
  formatting changes
................
  r13146 | kohsuke | 2008-11-10 18:15:08 -0800 (Mon, 10 Nov 2008) | 1 line
  
  moved the slave launch code to HudsonTestCase for reuse
................
  r13147 | kohsuke | 2008-11-10 18:20:19 -0800 (Mon, 10 Nov 2008) | 1 line
  
  adding another test case
................
  r13150 | kohsuke | 2008-11-11 07:28:53 -0800 (Tue, 11 Nov 2008) | 1 line
  
  bug fix
................
  r13153 | kohsuke | 2008-11-11 14:04:21 -0800 (Tue, 11 Nov 2008) | 1 line
  
  cutting down the test size to 5 to increase the test speed
................
  r13154 | kohsuke | 2008-11-11 14:04:37 -0800 (Tue, 11 Nov 2008) | 1 line
  
  improved debuggability
................
  r13155 | kohsuke | 2008-11-11 14:10:04 -0800 (Tue, 11 Nov 2008) | 1 line
  
  added another convenience method
................
  r13156 | kohsuke | 2008-11-11 14:10:21 -0800 (Tue, 11 Nov 2008) | 1 line
  
  further reduce the test turn around time
................
  r13157 | kohsuke | 2008-11-11 14:22:03 -0800 (Tue, 11 Nov 2008) | 1 line
  
  doc improvement and clean up
................
  r13158 | kohsuke | 2008-11-11 14:22:28 -0800 (Tue, 11 Nov 2008) | 1 line
  
  added a new metho
................
  r13487 | kohsuke | 2008-12-09 17:17:30 -0800 (Tue, 09 Dec 2008) | 1 line
  
  SlaveTemplate needs to be able to computer # of executors.
................
  r13488 | kohsuke | 2008-12-09 17:18:05 -0800 (Tue, 09 Dec 2008) | 1 line
  
  fixed a compilation problem with the latest head of the branch
................
  r13851 | kohsuke | 2008-12-24 13:40:43 -0800 (Wed, 24 Dec 2008) | 82 lines
  
  fixed a dead lock reported by Jesse.
  
  Found one Java-level deadlock:
  =============================
  "Executor #0 for master":
     waiting to lock monitor 0x09409bac (object 0x87474ca0, a hudson.util.CopyOnWriteMap$Hash),
     which is held by "main"
  "main":
     waiting to lock monitor 0x09409b48 (object 0x87474d48, a hudson.model.Hudson$MasterComputer),
     which is held by "Executor #0 for master"
  
  Java stack information for the threads listed above:
  ===================================================
  "Executor #0 for master":
  	at hudson.util.CopyOnWriteMap.remove(CopyOnWriteMap.java:78)
  	- waiting to lock <0x87474ca0> (a hudson.util.CopyOnWriteMap$Hash)
  	at hudson.model.Hudson.removeComputer(Hudson.java:717)
  	at hudson.model.Computer.removeExecutor(Computer.java:433)
  	- locked <0x87474d48> (a hudson.model.Hudson$MasterComputer)
  	at hudson.model.Executor.run(Executor.java:65)
  	- locked <0x87474d48> (a hudson.model.Hudson$MasterComputer)
  "main":
  	at hudson.model.Computer.setNumExecutors(Computer.java:338)
  	- waiting to lock <0x87474d48> (a hudson.model.Hudson$MasterComputer)
  	at hudson.model.Computer.setNode(Computer.java:327)
  	at hudson.model.Hudson.updateComputer(Hudson.java:704)
  	at hudson.model.Hudson.updateComputerList(Hudson.java:685)
  	- locked <0x87474ca0> (a hudson.util.CopyOnWriteMap$Hash)
  	at hudson.model.Hudson.setNodes(Hudson.java:1076)
  	at hudson.model.Hudson.addNode(Hudson.java:1060)
  	- locked <0x87474cb0> (a hudson.model.Hudson)
  	at org.jvnet.hudson.test.HudsonTestCase.createSlave(HudsonTestCase.java:247)
  	at hudson.slaves.NodeProvisionerTest.testBaselineSlaveUsage(NodeProvisionerTest.java:86)
  	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
  	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
  	at java.lang.reflect.Method.invoke(Method.java:597)
  	at junit.framework.TestCase.runTest(TestCase.java:154)
  	at org.jvnet.hudson.test.HudsonTestCase.runTest(HudsonTestCase.java:151)
  	at junit.framework.TestCase.runBare(TestCase.java:127)
  	at junit.framework.TestResult$1.protect(TestResult.java:106)
  	at junit.framework.TestResult.runProtected(TestResult.java:124)
  	at junit.framework.TestResult.run(TestResult.java:109)
  	at junit.framework.TestCase.run(TestCase.java:118)
  	at junit.framework.TestSuite.runTest(TestSuite.java:208)
  	at junit.framework.TestSuite.run(TestSuite.java:203)
  	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
  	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
  	at java.lang.reflect.Method.invoke(Method.java:597)
  	at org.apache.maven.surefire.junit.JUnitTestSet.execute(JUnitTestSet.java:213)
  	at org.apache.maven.surefire.suite.AbstractDirectoryTestSuite.executeTestSet(AbstractDirectoryTestSuite.java:140)
  	at org.apache.maven.surefire.suite.AbstractDirectoryTestSuite.execute(AbstractDirectoryTestSuite.java:127)
  	at org.apache.maven.surefire.Surefire.run(Surefire.java:177)
  	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
  	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
  	at java.lang.reflect.Method.invoke(Method.java:597)
  	at org.apache.maven.surefire.booter.SurefireBooter.runSuitesInProcess(SurefireBooter.java:345)
  	at org.apache.maven.surefire.booter.SurefireBooter.run(SurefireBooter.java:241)
  	at org.apache.maven.plugin.surefire.SurefirePlugin.execute(SurefirePlugin.java:537)
  	at org.apache.maven.plugin.DefaultPluginManager.executeMojo(DefaultPluginManager.java:447)
  	at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoals(DefaultLifecycleExecutor.java:539)
  	at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoalWithLifecycle(DefaultLifecycleExecutor.java:480)
  	at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoal(DefaultLifecycleExecutor.java:459)
  	at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoalAndHandleFailures(DefaultLifecycleExecutor.java:311)
  	at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeTaskSegments(DefaultLifecycleExecutor.java:278)
  	at org.apache.maven.lifecycle.DefaultLifecycleExecutor.execute(DefaultLifecycleExecutor.java:143)
  	at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:333)
  	at org.apache.maven.DefaultMaven.execute(DefaultMaven.java:126)
  	at org.apache.maven.cli.MavenCli.main(MavenCli.java:282)
  	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
  	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
  	at java.lang.reflect.Method.invoke(Method.java:597)
  	at org.codehaus.classworlds.Launcher.launchEnhanced(Launcher.java:315)
  	at org.codehaus.classworlds.Launcher.launch(Launcher.java:255)
  	at org.codehaus.classworlds.Launcher.mainWithExitCode(Launcher.java:430)
  	at org.codehaus.classworlds.Launcher.main(Launcher.java:375)
  
  Found 1 deadlock.
................
  r13852 | kohsuke | 2008-12-24 14:32:29 -0800 (Wed, 24 Dec 2008) | 1 line
  
  bug fix in the test case.
................
  r13854 | kohsuke | 2008-12-24 15:20:22 -0800 (Wed, 24 Dec 2008) | 2 lines
  
  Using a new version of Rhino to get a proper exception chaining.
................
  r13855 | kohsuke | 2008-12-24 15:22:13 -0800 (Wed, 24 Dec 2008) | 1 line
  
  typo
................
  r13856 | kohsuke | 2008-12-24 15:37:56 -0800 (Wed, 24 Dec 2008) | 1 line
  
  CR shouldn't be in the repository
................
  r13859 | kohsuke | 2008-12-24 15:50:32 -0800 (Wed, 24 Dec 2008) | 3 lines
  
  Fixed svn:eol-style.
................
  r13866 | kohsuke | 2008-12-24 21:26:43 -0800 (Wed, 24 Dec 2008) | 3 lines
  
  Copying rev.13860 to retry a merge that now looks suspicious.
................
  r13869 | kohsuke | 2008-12-24 22:03:46 -0800 (Wed, 24 Dec 2008) | 2 lines
  
  The 2nd merge was successful, so adopting the successful merge as the multiple-computer-per-node branch.
................
  r13872 | kohsuke | 2008-12-25 07:28:37 -0800 (Thu, 25 Dec 2008) | 1 line
  
  fixed a test failure
................
  r13873 | kohsuke | 2008-12-25 08:16:49 -0800 (Thu, 25 Dec 2008) | 1 line
  
  expanded NodeProvisioner to work on per-label basis
................
  r13874 | kohsuke | 2008-12-25 08:27:32 -0800 (Thu, 25 Dec 2008) | 1 line
  
  bug fix
................
  r13875 | kohsuke | 2008-12-25 08:33:17 -0800 (Thu, 25 Dec 2008) | 1 line
  
  bug fix
................
  r13876 | kohsuke | 2008-12-25 08:45:03 -0800 (Thu, 25 Dec 2008) | 3 lines
  
  [HUDSON-2605] Strangely, I discovered that on some File, which is new File("./target/hudson-for-test"), file.exists()==false but file.getAbsoluteFile().exists()==true.
  
  It looks like this happens when $PWD of the process at the OS level is different from System.getProperty("user.dir")
................
  r13878 | kohsuke | 2008-12-25 15:52:45 -0800 (Thu, 25 Dec 2008) | 1 line
  
  added a marker file
................
  r13879 | kohsuke | 2008-12-25 15:53:13 -0800 (Thu, 25 Dec 2008) | 1 line
  
  use marker file to find the hudson main workspace.
................
  r13883 | kohsuke | 2008-12-26 07:50:44 -0800 (Fri, 26 Dec 2008) | 1 line
  
  avoid using a deprecated method.
................
  r13884 | kohsuke | 2008-12-26 07:50:59 -0800 (Fri, 26 Dec 2008) | 1 line
  
  adding more probes
................
  r13885 | kohsuke | 2008-12-26 08:15:42 -0800 (Fri, 26 Dec 2008) | 1 line
  
  for analyzing test failures, capturing the output is crucial.
................
  r13887 | kohsuke | 2008-12-26 11:25:12 -0800 (Fri, 26 Dec 2008) | 1 line
  
  allow sub-types to intercept mutation
................
  r13888 | kohsuke | 2008-12-26 11:26:01 -0800 (Fri, 26 Dec 2008) | 1 line
  
  clouds need to be taken into account before marking a label as pointless.
................
  r13889 | kohsuke | 2008-12-26 11:26:40 -0800 (Fri, 26 Dec 2008) | 3 lines
  
  fixing bugs in NodeProvisioner.
  
  Conservative estimate on idle executors have to be max, not min.
................
  r13890 | kohsuke | 2008-12-26 11:42:53 -0800 (Fri, 26 Dec 2008) | 1 line
  
  turns out the problem was that we were adding multiple slaves under the same name, which confused Hudson to no end.
................
  r13896 | kohsuke | 2008-12-27 07:44:20 -0800 (Sat, 27 Dec 2008) | 1 line
  
  formatting changes
................
  r13897 | kohsuke | 2008-12-27 07:45:00 -0800 (Sat, 27 Dec 2008) | 1 line
  
  reprot the test name to stdout so that one can easily distinguish different tests in target/surefire-reports/xyz-output.txt
................
  r13898 | kohsuke | 2008-12-27 07:54:53 -0800 (Sat, 27 Dec 2008) | 1 line
  
  doc improvement
................
  r13899 | kohsuke | 2008-12-27 09:59:07 -0800 (Sat, 27 Dec 2008) | 1 line
  
  split the functionality into two classes
................
  r13900 | kohsuke | 2008-12-27 10:00:29 -0800 (Sat, 27 Dec 2008) | 1 line
  
  doc improvement
................
  r13901 | kohsuke | 2008-12-27 10:06:24 -0800 (Sat, 27 Dec 2008) | 1 line
  
  added 'tick'
................
  r13902 | kohsuke | 2008-12-27 10:08:04 -0800 (Sat, 27 Dec 2008) | 1 line
  
  adding graph rendering of the load statistics
................
  r13903 | kohsuke | 2008-12-27 10:19:41 -0800 (Sat, 27 Dec 2008) | 1 line
  
  improved graph layout
................
  r13904 | kohsuke | 2008-12-27 10:21:05 -0800 (Sat, 27 Dec 2008) | 1 line
  
  Picker -> TimeScale to better reflect what it is.
................
  r13905 | kohsuke | 2008-12-27 10:40:27 -0800 (Sat, 27 Dec 2008) | 1 line
  
  duplicate
................
  r13906 | kohsuke | 2008-12-27 14:39:36 -0800 (Sat, 27 Dec 2008) | 1 line
  
  renamed to a shorter name
................
  r13907 | kohsuke | 2008-12-27 15:03:25 -0800 (Sat, 27 Dec 2008) | 1 line
  
  added icons
................
  r13908 | kohsuke | 2008-12-27 16:20:37 -0800 (Sat, 27 Dec 2008) | 1 line
  
  renamed to make 'loadStatistics' the URL binding.
................
  r13909 | kohsuke | 2008-12-27 16:21:01 -0800 (Sat, 27 Dec 2008) | 1 line
  
  exposing loadStatistics for Computer for better URL binding
................
  r13910 | kohsuke | 2008-12-27 16:21:16 -0800 (Sat, 27 Dec 2008) | 1 line
  
  adding UI support
................
  r13911 | kohsuke | 2008-12-27 19:31:43 -0800 (Sat, 27 Dec 2008) | 1 line
  
  added some basic visualization
................
  r13912 | kohsuke | 2008-12-27 19:35:10 -0800 (Sat, 27 Dec 2008) | 1 line
  
  if there's no description, don't even show it.
................
  r13913 | kohsuke | 2008-12-27 19:39:26 -0800 (Sat, 27 Dec 2008) | 1 line
  
  added load statistics page for the label as well.
................
  r13914 | kohsuke | 2008-12-27 19:48:55 -0800 (Sat, 27 Dec 2008) | 1 line
  
  added one more convenience method.
................
  r13915 | kohsuke | 2008-12-27 19:54:29 -0800 (Sat, 27 Dec 2008) | 1 line
  
  refactored so that a different Dataset can be fed.
................
  r13916 | kohsuke | 2008-12-27 19:59:20 -0800 (Sat, 27 Dec 2008) | 1 line
  
  added global load statistics lnk to the management screen.
................
  r13917 | kohsuke | 2008-12-27 20:31:01 -0800 (Sat, 27 Dec 2008) | 1 line
  
  <dt>s should use the bold font to distinguish them from <dd>s
................
  r13918 | kohsuke | 2008-12-27 20:35:10 -0800 (Sat, 27 Dec 2008) | 1 line
  
  added description of what the graph means.
................
  r13919 | kohsuke | 2008-12-27 20:35:39 -0800 (Sat, 27 Dec 2008) | 1 line
  
  added description of what the graph means.
................


git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@14215 71c3de6d-444a-0410-be80-ed276b4c234a
上级 8ba8ae07
This file is used as a marker for the test harness to discover the root directory of Hudson.
......@@ -430,7 +430,7 @@
<dependency>
<groupId>org.jvnet.hudson</groupId>
<artifactId>commons-jexl</artifactId>
<version>1.1-hudson-20080725</version>
<version>1.1-hudson-20081031</version>
</dependency>
<dependency>
<groupId>org.jvnet.hudson</groupId>
......@@ -475,7 +475,7 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8</version>
<version>4.3.1</version>
</dependency>
<dependency><!-- needed by Jelly -->
<groupId>javax.servlet</groupId>
......
......@@ -25,6 +25,7 @@ import hudson.security.Permission;
import hudson.security.SecurityRealm;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.RetentionStrategy;
import hudson.slaves.Cloud;
import hudson.tasks.BuildStep;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildWrapper;
......@@ -33,6 +34,7 @@ import hudson.tasks.Builder;
import hudson.tasks.Publisher;
import hudson.util.Area;
import hudson.util.Iterators;
import hudson.util.DescriptorList;
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyTagException;
......@@ -996,6 +998,10 @@ public class Functions {
public static List<PageDecorator> getPageDecorators() {
return (List)PageDecorator.ALL;
}
public static DescriptorList<Cloud> getCloudDescriptors() {
return Cloud.ALL;
}
private static final Pattern SCHEME = Pattern.compile("[a-z]+://.+");
......
......@@ -577,6 +577,13 @@ public class Util {
}
}
/**
* Surrounds by a single-quote.
*/
public static String singleQuote(String s) {
return '\''+s+'\'';
}
/**
* Escapes HTML unsafe characters like &lt;, &amp;to the respective character entities.
*/
......
......@@ -144,7 +144,7 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
protected AbstractProject(ItemGroup parent, String name) {
super(parent,name);
if(!Hudson.getInstance().getSlaves().isEmpty()) {
if(!Hudson.getInstance().getNodes().isEmpty()) {
// if a new job is configured with Hudson that already has slave nodes
// make it roamable by default
canRoam = true;
......@@ -195,6 +195,21 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
return Hudson.getInstance().getLabel(assignedNode);
}
/**
* Sets the assigned label.
*/
public void setAssignedLabel(Label l) throws IOException {
if(l==null) {
canRoam = true;
assignedNode = null;
} else {
canRoam = false;
if(l==Hudson.getInstance().getSelfLabel()) assignedNode = null;
else assignedNode = l.getName();
}
save();
}
/**
* Get the term used in the UI to represent this kind of {@link AbstractProject}.
* Must start with a capital letter.
......
......@@ -2,24 +2,31 @@ package hudson.model;
import hudson.EnvVars;
import hudson.Util;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.RetentionStrategy;
import hudson.model.Descriptor.FormException;
import hudson.node_monitors.NodeMonitor;
import hudson.remoting.Channel;
import hudson.remoting.VirtualChannel;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.security.Permission;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.RetentionStrategy;
import hudson.tasks.BuildWrapper;
import hudson.tasks.Publisher;
import hudson.util.DaemonThreadFactory;
import hudson.util.ExceptionCatchingThreadFactory;
import hudson.util.RemotingDiagnostics;
import hudson.util.RunList;
import hudson.util.ExceptionCatchingThreadFactory;
import hudson.util.Futures;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
......@@ -27,16 +34,10 @@ import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.LogRecord;
import java.nio.charset.Charset;
import javax.servlet.ServletException;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
/**
* Represents the running state of a remote computer that holds {@link Executor}s.
*
......@@ -52,7 +53,8 @@ import org.kohsuke.stapler.export.ExportedBean;
*
* Also, even if you remove a {@link Node}, it takes time for the corresponding
* {@link Computer} to be removed, if some builds are already in progress on that
* node.
* node. Or when the node configuration is changed, unaffected {@link Computer} object
* remains intact, while all the {@link Node} objects will go away.
*
* <p>
* This object also serves UI (since {@link Node} is an interface and can't have
......@@ -135,20 +137,44 @@ public abstract class Computer extends AbstractModelObject implements AccessCont
*/
public abstract void doLaunchSlaveAgent( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException;
/**
* @deprecated Use {@link #connect(boolean)}
*/
public final void launch() {
connect(true);
}
/**
* Do the same as {@link #doLaunchSlaveAgent(StaplerRequest, StaplerResponse)}
* but outside the context of serving a request.
*
* If already connected, no-op.
* <p>
* If already connected or if this computer doesn't support proactive launching, no-op.
* This method may return immediately
* while the launch operation happens asynchronously.
*
* @see #disconnect()
*
* @param forceReconnect
* If true and a connect activity is already in progress, it will be cancelled and
* the new one will be started. If false, and a connect activity is already in progress, this method
* will do nothing and just return the pending connection operation.
* @return
* A {@link Future} representing pending completion of the task.
*/
public abstract void launch();
public abstract Future<?> connect(boolean forceReconnect);
/**
* Disconnect this computer.
*
* If this is the master, no-op
* If this is the master, no-op. This method may return immediately
* while the launch operation happens asynchronously.
*
* @return
* {@link Future} to track the asynchronous disconnect operation.
* @see #connect(boolean)
*/
public void disconnect() { }
public Future<?> disconnect() { return Futures.precomputed(null); }
/**
* Number of {@link Executor}s that are configured for this computer.
......@@ -176,7 +202,11 @@ public abstract class Computer extends AbstractModelObject implements AccessCont
public Node getNode() {
if(nodeName==null)
return Hudson.getInstance();
return Hudson.getInstance().getSlave(nodeName);
return Hudson.getInstance().getNode(nodeName);
}
public LoadStatistics getLoadStatistics() {
return getNode().getSelfLabel().loadStatistics;
}
/**
......@@ -205,6 +235,15 @@ public abstract class Computer extends AbstractModelObject implements AccessCont
return temporarilyOffline || getChannel()==null;
}
public final boolean isOnline() {
return !isOffline();
}
/**
* Is a {@link #connect(boolean)} operation in progress?
*/
public abstract boolean isConnecting();
/**
* Returns true if this computer is supposed to be launched via JNLP.
* @deprecated see {@linkplain #isLaunchSupported()} and {@linkplain ComputerLauncher}
......@@ -300,23 +339,28 @@ public abstract class Computer extends AbstractModelObject implements AccessCont
}
/**
* Called to notify {@link Computer} that it will be discarded.
* Called by {@link Hudson#updateComputerList()} to notify {@link Computer} that it will be discarded.
*/
protected void kill() {
setNumExecutors(0);
}
private synchronized void setNumExecutors(int n) {
this.numExecutors = n;
if(numExecutors==n) return; // no-op
// send signal to all idle executors to potentially kill them off
for( Executor e : executors )
if(e.isIdle())
e.interrupt();
int diff = n-numExecutors;
this.numExecutors = n;
// if the number is increased, add new ones
while(executors.size()<numExecutors)
executors.add(new Executor(this));
if(diff<0) {
// send signal to all idle executors to potentially kill them off
for( Executor e : executors )
if(e.isIdle())
e.interrupt();
} else {
// if the number is increased, add new ones
while(executors.size()<numExecutors)
executors.add(new Executor(this));
}
}
/**
......@@ -331,6 +375,17 @@ public abstract class Computer extends AbstractModelObject implements AccessCont
return n;
}
/**
* Returns the number of {@link Executor}s that are doing some work right now.
*/
public final int countBusy() {
return countExecutors()-countIdle();
}
public final int countExecutors() {
return executors.size();
}
/**
* Gets the read-only snapshot view of all {@link Executor}s.
*/
......@@ -351,7 +406,7 @@ public abstract class Computer extends AbstractModelObject implements AccessCont
}
/**
* Returns the time when this computer first became idle.
* Returns the time when this computer last became idle.
*
* <p>
* If this computer is already idle, the return value will point to the
......@@ -508,6 +563,46 @@ public abstract class Computer extends AbstractModelObject implements AccessCont
req.getView(this,"_script.jelly").forward(req,rsp);
}
/**
* Accepts the update to the node configuration.
*/
public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
try {
checkPermission(Hudson.CONFIGURE); // TODO: new permission?
final Hudson app = Hudson.getInstance();
Node result = getNode().getDescriptor().newInstance(req, req.getSubmittedForm());
// replace the old Node object by the new one
synchronized (app) {
List<Node> nodes = new ArrayList<Node>(app.getNodes());
int i = nodes.indexOf(getNode());
if(i<0) {
sendError("This slave appears to be removed while you were editing the configuration",req,rsp);
return;
}
nodes.set(i,result);
app.setNodes(nodes);
}
// take the user back to the slave top page.
rsp.sendRedirect2("../"+result.getNodeName()+'/');
} catch (FormException e) {
sendError(e,req,rsp);
}
}
/**
* Really deletes the slave.
*/
public void doDoDelete(StaplerResponse rsp) throws IOException {
checkPermission(DELETE);
Hudson.getInstance().removeNode(getNode());
rsp.sendRedirect("..");
}
/**
* Handles incremental log.
*/
......@@ -535,4 +630,10 @@ public abstract class Computer extends AbstractModelObject implements AccessCont
public boolean isAcceptingTasks() {
return true;
}
// TODO: define this as a separate permission?
public static final Permission CONFIGURE = Hudson.CONFIGURE;
public static final Permission DELETE = Hudson.DELETE;
}
package hudson.model;
import hudson.Util;
import hudson.slaves.NodeDescriptor;
import hudson.model.Descriptor.FormException;
import hudson.node_monitors.NodeMonitor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import java.io.IOException;
import java.text.ParseException;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import java.io.IOException;
import javax.servlet.ServletException;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.export.Exported;
/**
* Serves as the top of {@link Computer}s in the URL hierarchy.
......@@ -20,7 +27,7 @@ import org.kohsuke.stapler.export.Exported;
* @author Kohsuke Kawaguchi
*/
@ExportedBean
public final class ComputerSet implements ModelObject {
public final class ComputerSet extends AbstractModelObject {
private static final List<NodeMonitor> monitors;
@Exported
......@@ -37,6 +44,66 @@ public final class ComputerSet implements ModelObject {
return Hudson.getInstance().getComputers();
}
/**
* Gets all the slave names.
*/
public List<String> get_slaveNames() {
return new AbstractList<String>() {
final List<Node> nodes = Hudson.getInstance().getNodes();
public String get(int index) {
return nodes.get(index).getNodeName();
}
public int size() {
return nodes.size();
}
};
}
/**
* Number of total {@link Executor}s that belong to this label that are functioning.
* <p>
* This excludes executors that belong to offline nodes.
*/
@Exported
public int getTotalExecutors() {
int r=0;
for (Computer c : get_all()) {
if(c.isOnline())
r += c.countExecutors();
}
return r;
}
/**
* Number of busy {@link Executor}s that are carrying out some work right now.
*/
@Exported
public int getBusyExecutors() {
int r=0;
for (Computer c : get_all()) {
if(c.isOnline())
r += c.countBusy();
}
return r;
}
/**
* {@code getTotalExecutors()-getBusyExecutors()}, plus executors that are being brought online.
*/
public int getIdleExecutors() {
int r=0;
for (Computer c : get_all())
if(c.isOnline() || c.isConnecting())
r += c.countIdle();
return r;
}
public String getSearchUrl() {
return "/computers/";
}
public Computer getDynamic(String token, StaplerRequest req, StaplerResponse rsp) {
return Hudson.getInstance().getComputer(token);
}
......@@ -46,7 +113,7 @@ public final class ComputerSet implements ModelObject {
for(Computer c : get_all()) {
if(c.isLaunchSupported())
c.launch();
c.connect(true);
}
rsp.sendRedirect(".");
}
......@@ -66,6 +133,97 @@ public final class ComputerSet implements ModelObject {
rsp.forwardToPreviousPage(req);
}
/**
* First check point in creating a new slave.
*/
public synchronized void doCreateItem( StaplerRequest req, StaplerResponse rsp,
@QueryParameter("name") String name, @QueryParameter("mode") String mode,
@QueryParameter("from") String from ) throws IOException, ServletException {
final Hudson app = Hudson.getInstance();
app.checkPermission(Hudson.CONFIGURE); // TODO: new permission?
if (checkName(req, rsp, name)) return;
if(mode!=null && mode.equals("copy")) {
Node src = app.getNode(from);
if(src==null) {
rsp.setStatus(SC_BAD_REQUEST);
if(Util.fixEmpty(from)==null)
sendError(Messages.ComputerSet_SpecifySlaveToCopy(),req,rsp);
else
sendError(Messages.ComputerSet_NoSuchSlave(from),req,rsp);
return;
}
// copy through XStream
String xml = Hudson.XSTREAM.toXML(src);
Node result = (Node)Hudson.XSTREAM.fromXML(xml);
result.setNodeName(name);
app.addNode(result);
// send the browser to the config page
rsp.sendRedirect2(result.getNodeName()+"/configure");
} else {
// proceed to step 2
if(mode==null) {
rsp.sendError(SC_BAD_REQUEST);
return;
}
req.setAttribute("descriptor", NodeDescriptor.ALL.find(mode));
req.getView(this,"_new.jelly").forward(req,rsp);
}
}
/**
* Really creates a new slave.
*/
public synchronized void doDoCreateItem( StaplerRequest req, StaplerResponse rsp,
@QueryParameter("name") String name,
@QueryParameter("type") String type ) throws IOException, ServletException {
try {
final Hudson app = Hudson.getInstance();
app.checkPermission(Hudson.CONFIGURE); // TODO: new permission?
if (checkName(req, rsp, name)) return;
Node result = NodeDescriptor.ALL.find(type).newInstance(req, req.getSubmittedForm());
app.addNode(result);
// take the user back to the slave list top page
rsp.sendRedirect2(".");
} catch (FormException e) {
sendError(e,req,rsp);
}
}
/**
* Makes sure that the given name is good as a slave name.
*/
private boolean checkName(StaplerRequest req, StaplerResponse rsp, String name) throws IOException, ServletException {
if(name==null) {
rsp.sendError(HttpServletResponse.SC_BAD_REQUEST,"Query parameter 'name' is required");
return true;
}
name = name.trim();
try {
Hudson.checkGoodName(name);
} catch (ParseException e) {
rsp.setStatus(SC_BAD_REQUEST);
sendError(e,req,rsp);
return true;
}
if(Hudson.getInstance().getNode(name)!=null) {
rsp.setStatus(SC_BAD_REQUEST);
sendError(Messages.ComputerSet_SlaveAlreadyExists(name),req,rsp);
return true;
}
return false;
}
public Api getApi() {
return new Api(this);
}
......
......@@ -2,6 +2,7 @@ package hudson.model;
import hudson.XmlFile;
import hudson.BulkChange;
import static hudson.Util.singleQuote;
import hudson.util.CopyOnWriteList;
import hudson.scm.CVSSCM;
import net.sf.json.JSONArray;
......@@ -9,6 +10,7 @@ import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.Stapler;
import org.springframework.util.StringUtils;
import org.jvnet.tiger_types.Types;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
......@@ -18,11 +20,16 @@ import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.beans.Introspector;
/**
* Metadata about a configurable instance.
......@@ -63,6 +70,12 @@ import java.lang.reflect.Modifier;
* However, it is the responsibility of the derived type to properly
* invoke {@link #save()} and {@link #load()}.
*
* <h2>Reflection Enhancement</h2>
* {@link Descriptor} defines addition to the standard Java reflection
* and provides reflective information about its corresponding {@link Describable}.
* These are primarily used by tag libraries to
* keep the Jelly scripts concise.
*
* @author Kohsuke Kawaguchi
* @see Describable
*/
......@@ -84,6 +97,73 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable {
private transient final Map<String,Method> checkMethods = new ConcurrentHashMap<String,Method>();
/**
* Lazily computed list of properties on {@link #clazz}.
*/
private transient volatile Map<String, PropertyType> propertyTypes;
/**
* Represents a readable property on {@link Describable}.
*/
public static final class PropertyType {
public final Class clazz;
public final Type type;
private volatile Class itemType;
PropertyType(Class clazz, Type type) {
this.clazz = clazz;
this.type = type;
}
PropertyType(Field f) {
this(f.getType(),f.getGenericType());
}
PropertyType(Method getter) {
this(getter.getReturnType(),getter.getGenericReturnType());
}
public Enum[] getEnumConstants() {
return (Enum[])clazz.getEnumConstants();
}
/**
* If the property is a collection/array type, what is an item type?
*/
public Class getItemType() {
if(itemType==null)
itemType = computeItemType();
return itemType;
}
private Class computeItemType() {
if(clazz.isArray()) {
return clazz.getComponentType();
}
if(Collection.class.isAssignableFrom(clazz)) {
Type col = Types.getBaseClass(type, Collection.class);
if (col instanceof ParameterizedType)
return Types.erasure(Types.getTypeArgument(col,0));
else
return Object.class;
}
return null;
}
/**
* Returns {@link Descriptor} whose 'clazz' is the same as {@link #getItemType() the item type}.
*/
public Descriptor getItemTypeDescriptor() {
Class itemType = getItemType();
for( Descriptor d : Descriptor.ALL )
if(d.clazz==itemType)
return d;
return null;
}
}
protected Descriptor(Class<? extends T> clazz) {
this.clazz = clazz;
ALL.add(this);
......@@ -122,13 +202,31 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable {
if(method==NONE)
return null;
return '\''+Stapler.getCurrentRequest().getContextPath()+"/descriptor/"+clazz.getName()+"/check"+capitalizedFieldName+"?value='+encode(this.value)";
return singleQuote(Stapler.getCurrentRequest().getContextPath()+"/descriptor/"+clazz.getName()+"/check"+capitalizedFieldName+"?value=")+"+encode(this.value)";
}
/**
* Obtains the property type of the given field of {@link #clazz}
*/
public PropertyType getPropertyType(String field) {
if(propertyTypes ==null) {
Map<String, PropertyType> r = new HashMap<String, PropertyType>();
for (Field f : clazz.getFields())
r.put(f.getName(),new PropertyType(f));
for (Method m : clazz.getMethods())
if(m.getName().startsWith("get"))
r.put(Introspector.decapitalize(m.getName().substring(3)),new PropertyType(m));
propertyTypes = r;
}
return propertyTypes.get(field);
}
/**
* Gets the class name nicely escaped to be usable as a key in the structured form submission.
*/
public String getJsonSafeClassName() {
public final String getJsonSafeClassName() {
return clazz.getName().replace('.','-');
}
......@@ -374,6 +472,10 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable {
return null;
}
public static Descriptor find(String className) {
return find(ALL.getView(),className);
}
public static final class FormException extends Exception {
private final String formField;
......
package hudson.model;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter;
import hudson.BulkChange;
import com.trilead.ssh2.crypto.digest.SHA1;
import hudson.FeedAdapter;
import hudson.FilePath;
import hudson.Functions;
import hudson.Launcher;
......@@ -48,6 +52,11 @@ import hudson.security.SecurityMode;
import hudson.security.SecurityRealm;
import hudson.slaves.ComputerListener;
import hudson.slaves.RetentionStrategy;
import hudson.slaves.NodeList;
import hudson.slaves.Cloud;
import hudson.slaves.DumbSlave;
import hudson.slaves.NodeDescriptor;
import hudson.slaves.NodeProvisioner;
import hudson.tasks.BuildStep;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrappers;
......@@ -70,7 +79,19 @@ import hudson.util.MultipartFormDataParser;
import hudson.util.RemotingDiagnostics;
import hudson.util.TextFile;
import hudson.util.XStream2;
<<<<<<< .working
import hudson.util.HudsonIsRestarting;
=======
import hudson.util.DescribableList;
<<<<<<< .working
>>>>>>> .merge-right.r12882
=======
import hudson.util.Futures;
<<<<<<< .working
>>>>>>> .merge-right.r13488
=======
import hudson.util.HudsonIsRestarting;
>>>>>>> .merge-right.r13919
import hudson.widgets.Widget;
import net.sf.json.JSONObject;
import org.acegisecurity.*;
......@@ -135,6 +156,7 @@ import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
......@@ -235,15 +257,34 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
private transient volatile DependencyGraph dependencyGraph;
/**
* Active {@link Cloud}s.
*/
public final CloudList clouds = new CloudList(this);
public static class CloudList extends DescribableList<Cloud,Descriptor<Cloud>> {
public CloudList(Hudson h) {
super(h);
}
protected void onModified() throws IOException {
super.onModified();
Hudson.getInstance().trimLabels();
}
}
/**
* Set of installed cluster nodes.
*
* <p>
* We use this field with copy-on-write semantics.
* This field has mutable list (to keep the serialization look clean),
* but it shall never be modified. Only new completely populated slave
* list can be set here.
* <p>
* The field name should be really {@code nodes}, but again the backward compatibility
* prevents us from renaming.
*/
private volatile List<Slave> slaves;
private volatile NodeList slaves;
/**
* Quiet period.
......@@ -300,6 +341,16 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
private transient Set<Label> labelSet;
private transient volatile Set<Label> dynamicLabels = null;
/**
* Load statistics of the entire system.
*/
public transient final OverallLoadStatistics overallLoad = new OverallLoadStatistics();
/**
* {@link NodeProvisioner} that reacts to {@link OverallLoadStatistics}.
*/
public transient final NodeProvisioner overallNodeProvisioner = new NodeProvisioner(null,overallLoad);
public transient final ServletContext servletContext;
/**
......@@ -319,9 +370,10 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
/**
* Secrete key generated once and used for a long time, beyond
* container start/stop.
* container start/stop. Persisted outside <tt>config.xml</tt> to avoid
* accidental exposure.
*/
private final String secretKey;
private transient final String secretKey;
private transient final UpdateCenter updateCenter = new UpdateCenter();
......@@ -387,7 +439,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
pluginManager = new PluginManager(context);
// if we are loading old data that doesn't have this field
if(slaves==null) slaves = new ArrayList<Slave>();
if(slaves==null) slaves = new NodeList();
// work around to have MavenModule register itself until we either move it to a plugin
// or make it a part of the core.
......@@ -423,7 +475,16 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
l.onLoaded();
WindowsInstallerLink.registerIfApplicable();
<<<<<<< .working
UsageStatistics.register();
=======
LoadStatistics.register();
NodeProvisioner.launch();
<<<<<<< .working
>>>>>>> .merge-right.r13488
=======
UsageStatistics.register();
>>>>>>> .merge-right.r13919
}
public TcpSlaveAgentListener getTcpSlaveAgentListener() {
......@@ -454,6 +515,10 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
return "";
}
public void setNodeName(String name) {
throw new UnsupportedOperationException(); // not allowed
}
public String getNodeDescription() {
return "the master Hudson node";
}
......@@ -470,6 +535,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
return updateCenter;
}
<<<<<<< .working
public boolean isUsageStatisticsCollected() {
return noUsageStatistics==null || !noUsageStatistics;
}
......@@ -478,6 +544,12 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
return new View.People(this);
}
=======
public boolean isUsageStatisticsCollected() {
return noUsageStatistics==null || !noUsageStatistics;
}
>>>>>>> .merge-right.r13919
/**
* Does this {@link View} has any associated user information recorded?
*/
......@@ -495,7 +567,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
* This value is useful for implementing some of the security features.
*/
public String getSecretKey() {
return secretKey;
return secretKey;
}
/**
......@@ -681,6 +753,8 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
return new LocalLauncher(listener);
}
private final transient Object updateComputerLock = new Object();
/**
* Updates {@link #computers} by using {@link #getSlaves()}.
*
......@@ -689,7 +763,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
* so that we won't upset {@link Executor}s running in it.
*/
private void updateComputerList() throws IOException {
synchronized(computers) {// this synchronization is still necessary so that no two update happens concurrently
synchronized(updateComputerLock) {// just so that we don't have two code updating computer list at the same time
Map<String,Computer> byName = new HashMap<String,Computer>();
for (Computer c : computers.values()) {
if(c.getNode()==null)
......@@ -701,7 +775,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
Set<Computer> used = new HashSet<Computer>();
updateComputer(this, byName, used);
for (Slave s : getSlaves())
for (Node s : getNodes())
updateComputer(s, byName, used);
// find out what computers are removed, and kill off all executors.
......@@ -721,8 +795,10 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
if (c!=null) {
c.setNode(n); // reuse
} else {
if(n.getNumExecutors()>0)
if(n.getNumExecutors()>0) {
computers.put(n,c=n.createComputer());
c.connect(true);
}
}
used.add(c);
}
......@@ -929,7 +1005,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
public Set<Label> getLabels() {
Set<Label> r = new TreeSet<Label>();
for (Label l : labels.values()) {
if(!l.getNodes().isEmpty())
if(!l.isEmpty())
r.add(l);
}
return r;
......@@ -968,35 +1044,126 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
/**
* Gets the slave node of the give name, hooked under this Hudson.
*
* @deprecated
* Use {@link #getNode(String)}. Since 1.252.
*/
public Slave getSlave(String name) {
for (Slave s : getSlaves()) {
Node n = getNode(name);
if (n instanceof Slave)
return (Slave)n;
return null;
}
/**
* Gets the slave node of the give name, hooked under this Hudson.
*/
public Node getNode(String name) {
for (Node s : getSlaves()) {
if(s.getNodeName().equals(name))
return s;
}
return null;
}
/**
* Gets a {@link Cloud} by {@link Cloud#name its name}, or null.
*/
public Cloud getCloud(String name) {
for (Cloud nf : clouds)
if(nf.name.equals(name))
return nf;
return null;
}
/**
* @deprecated
* Use {@link #getNodes()}. Since 1.252.
*/
public List<Slave> getSlaves() {
return (List)Collections.unmodifiableList(slaves);
}
/**
* Returns all {@link Node}s in the system, excluding {@link Hudson} instance itself which
* represents the master.
*/
public List<Node> getNodes() {
return Collections.unmodifiableList(slaves);
}
/**
* Updates the slave list.
*
* @deprecated
* Use {@link #setNodes(List)}. Since 1.252.
*/
public void setSlaves(List<Slave> slaves) throws IOException {
this.slaves = new ArrayList<Slave>(slaves);
setNodes(slaves);
}
/**
* Adds one more {@link Node} to Hudson.
*/
public synchronized void addNode(Node n) throws IOException {
ArrayList<Node> nl = new ArrayList<Node>(this.slaves);
nl.add(n);
setNodes(nl);
}
/**
* Removes a {@link Node} from Hudson.
*/
public synchronized void removeNode(Node n) throws IOException {
n.toComputer().disconnect();
ArrayList<Node> nl = new ArrayList<Node>(this.slaves);
nl.remove(n);
setNodes(nl);
}
public void setNodes(List<? extends Node> nodes) throws IOException {
// make sure that all names are unique
Set<String> names = new HashSet<String>();
for (Node n : nodes)
if(!names.add(n.getNodeName()))
throw new IllegalArgumentException(n.getNodeName()+" is defined more than once");
this.slaves = new NodeList(nodes);
updateComputerList();
trimLabels();
save();
}
// label trim off
/**
* Resets all labels and remove invalid ones.
*/
private void trimLabels() {
for (Iterator<Label> itr = labels.values().iterator(); itr.hasNext();) {
Label l = itr.next();
l.reset();
if(l.getNodes().isEmpty())
if(l.isEmpty())
itr.remove();
}
}
save();
public NodeDescriptor getDescriptor() {
return DescriptorImpl.INSTANCE;
}
public static final class DescriptorImpl extends NodeDescriptor {
public static final DescriptorImpl INSTANCE = new DescriptorImpl();
private DescriptorImpl() {
super(Hudson.class);
}
public String getDisplayName() {
throw new UnsupportedOperationException();
}
// to route /descriptor/FQCN/xxx to getDescriptor(FQCN).xxx
public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) {
return Hudson.getInstance().getDescriptor(token);
}
}
/**
......@@ -1513,7 +1680,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
// recompute label objects
if (null != slaves) { // only if we have slaves
for (Slave slave : slaves)
for (Node slave : slaves)
slave.getAssignedLabels();
}
......@@ -1568,14 +1735,16 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
* Called to shut down the system.
*/
public void cleanUp() {
Set<Future<?>> pending = new HashSet<Future<?>>();
terminating = true;
for( Computer c : computers.values() ) {
c.interrupt();
c.kill();
c.disconnect();
pending.add(c.disconnect());
}
ExternalJob.reloadThread.interrupt();
Trigger.timer.cancel();
// TODO: how to wait for the completion of the last job?
Trigger.timer = null;
if(tcpSlaveAgentListener!=null)
tcpSlaveAgentListener.shutdown();
......@@ -1589,7 +1758,18 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
getQueue().save();
threadPoolForLoad.shutdown();
for (Future<?> f : pending)
try {
f.get(10, TimeUnit.SECONDS); // if clean up operation didn't complete in time, we fail the test
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break; // someone wants us to die now. quick!
} catch (ExecutionException e) {
LOGGER.log(Level.WARNING, "Failed to shut down properly",e);
} catch (TimeoutException e) {
LOGGER.log(Level.WARNING, "Failed to shut down properly",e);
}
LogFactory.releaseAll();
theInstance = null;
......@@ -1615,6 +1795,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
* Accepts submission from the configuration page.
*/
public synchronized void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
BulkChange bc = new BulkChange(this);
try {
checkPermission(ADMINISTER);
......@@ -1669,6 +1850,12 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
}
}
numExecutors = Integer.parseInt(req.getParameter("numExecutors"));
if(req.hasParameter("master.mode"))
mode = Mode.valueOf(req.getParameter("master.mode"));
else
mode = Mode.NORMAL;
quietPeriod = Integer.parseInt(req.getParameter("quiet_period"));
systemMessage = Util.nullify(req.getParameter("system_message"));
......@@ -1711,6 +1898,8 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
for( JSONObject o : StructuredForm.toList(json,"plugin"))
pluginManager.getPlugin(o.getString("name")).getPlugin().configure(o);
clouds.rebuildHetero(req,json, Cloud.ALL, "cloud");
save();
if(result)
rsp.sendRedirect(req.getContextPath()+'/'); // go to the top page
......@@ -1718,6 +1907,8 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
rsp.sendRedirect("configure"); // back to config
} catch (FormException e) {
sendError(e,req,rsp);
} finally {
bc.commit();
}
}
......@@ -1735,6 +1926,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
}
/**
<<<<<<< .working
* Accepts submission from the configuration page.
*/
public synchronized void doConfigExecutorsSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
......@@ -1759,6 +1951,8 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
}
/**
=======
>>>>>>> .merge-right.r12882
* Accepts the new description.
*/
public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
......@@ -1831,7 +2025,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
return null;
}
if(mode!=null && mode.equals("copyJob")) {
if(mode!=null && mode.equals("copy")) {
String from = req.getParameter("from");
TopLevelItem src = getItem(from);
if(src==null) {
......@@ -2174,6 +2368,20 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
doScript(req, rsp, req.getView(this, "_script.jelly"));
}
/**
* Run arbitrary Groovy script and return result as plain text.
*/
<<<<<<< .working
public void doScriptText(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
doScript(req, rsp, req.getView(this, "_scriptText.jelly"));
}
private void doScript(StaplerRequest req, StaplerResponse rsp, RequestDispatcher view) throws IOException, ServletException {
=======
public void doScript(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
doScript(req, rsp, req.getView(this, "_script.jelly"));
}
/**
* Run arbitrary Groovy script and return result as plain text.
*/
......@@ -2182,6 +2390,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
}
private void doScript(StaplerRequest req, StaplerResponse rsp, RequestDispatcher view) throws IOException, ServletException {
>>>>>>> .merge-right.r13919
// ability to run arbitrary script is dangerous
checkPermission(ADMINISTER);
......@@ -2593,6 +2802,11 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
return "";
}
@Override
public boolean isConnecting() {
return false;
}
@Override
public String getDisplayName() {
return Messages.Hudson_Computer_DisplayName();
......@@ -2611,6 +2825,11 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
return RetentionStrategy.NOOP;
}
public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
// the master node isn't in the Hudson.getNodes(), so this method makes no sense.
throw new UnsupportedOperationException();
}
@Override
public VirtualChannel getChannel() {
return localChannel;
......@@ -2631,8 +2850,8 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
rsp.sendError(SC_NOT_FOUND);
}
public void launch() {
// noop
public Future<?> connect(boolean forceReconnect) {
return Futures.precomputed(null);
}
/**
......@@ -2779,7 +2998,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup<TopLe
static {
XSTREAM.alias("hudson",Hudson.class);
XSTREAM.alias("slave",Slave.class);
XSTREAM.alias("slave", DumbSlave.class);
XSTREAM.alias("jdk",JDK.class);
// for backward compatibility with <1.75, recognize the tag name "view" as well.
XSTREAM.alias("view", ListView.class);
......
......@@ -33,12 +33,8 @@ public class Items {
ExternalJob.DESCRIPTOR
);
public static TopLevelItemDescriptor getDescriptor(String displayName) {
for (TopLevelItemDescriptor job : LIST) {
if(job.getDisplayName().equals(displayName))
return job;
}
return null;
public static TopLevelItemDescriptor getDescriptor(String fqcn) {
return Descriptor.find(LIST,fqcn);
}
/**
......
package hudson.model;
import hudson.Util;
import hudson.slaves.NodeProvisioner;
import hudson.slaves.Cloud;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import java.util.Set;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
/**
* Group of {@link Node}s.
......@@ -15,14 +25,38 @@ import java.util.ArrayList;
* @see Hudson#getLabels()
* @see Hudson#getLabel(String)
*/
@ExportedBean
public class Label implements Comparable<Label>, ModelObject {
private final String name;
private volatile Set<Node> nodes;
private volatile Set<Cloud> clouds;
public final LoadStatistics loadStatistics;
public final NodeProvisioner nodeProvisioner;
public Label(String name) {
this.name = name;
// passing these causes an infinite loop - getTotalExecutors(),getBusyExecutors());
this.loadStatistics = new LoadStatistics(0,0) {
@Override
public int computeIdleExecutors() {
return Label.this.getIdleExecutors();
}
@Override
public int computeTotalExecutors() {
return Label.this.getTotalExecutors();
}
@Override
public int computeQueueLength() {
return Hudson.getInstance().getQueue().countBuildableItemsFor(Label.this);
}
};
this.nodeProvisioner = new NodeProvisioner(this, loadStatistics);
}
@Exported
public String getName() {
return name;
}
......@@ -38,30 +72,92 @@ public class Label implements Comparable<Label>, ModelObject {
public boolean isSelfLabel() {
Set<Node> nodes = getNodes();
return nodes.size() == 1 && nodes.iterator().next().getSelfLabel() == this;
}
/**
* Gets all {@link Node}s that belong to this label.
*/
@Exported
public Set<Node> getNodes() {
if(nodes==null) {
Set<Node> r = new HashSet<Node>();
Hudson h = Hudson.getInstance();
if(h.getAssignedLabels().contains(this))
r.add(h);
for (Slave s : h.getSlaves()) {
if(s.getAssignedLabels().contains(this))
r.add(s);
for (Node n : h.getNodes()) {
if(n.getAssignedLabels().contains(this))
r.add(n);
}
nodes = Collections.unmodifiableSet(r);
}
return nodes;
}
/**
* Gets all {@link Cloud}s that can launch for this label.
*/
@Exported
public Set<Cloud> getClouds() {
if(clouds==null) {
Set<Cloud> r = new HashSet<Cloud>();
Hudson h = Hudson.getInstance();
for (Cloud c : h.clouds) {
if(c.canProvision(this))
r.add(c);
}
clouds = Collections.unmodifiableSet(r);
}
return clouds;
}
/**
* Number of total {@link Executor}s that belong to this label that are functioning.
* <p>
* This excludes executors that belong to offline nodes.
*/
@Exported
public int getTotalExecutors() {
int r=0;
for (Node n : getNodes()) {
Computer c = n.toComputer();
if(c!=null && c.isOnline())
r += c.countExecutors();
}
return r;
}
/**
* Number of busy {@link Executor}s that are carrying out some work right now.
*/
@Exported
public int getBusyExecutors() {
int r=0;
for (Node n : getNodes()) {
Computer c = n.toComputer();
if(c!=null && c.isOnline())
r += c.countBusy();
}
return r;
}
/**
* Number of idle {@link Executor}s that can start working immediately.
*/
@Exported
public int getIdleExecutors() {
int r=0;
for (Node n : getNodes()) {
Computer c = n.toComputer();
if(c!=null && (c.isOnline() || c.isConnecting()))
r += c.countIdle();
}
return r;
}
/**
* Returns true if all the nodes of this label is offline.
*/
@Exported
public boolean isOffline() {
for (Node n : getNodes()) {
if(n.toComputer() != null && !n.toComputer().isOffline())
......@@ -73,6 +169,7 @@ public class Label implements Comparable<Label>, ModelObject {
/**
* Returns a human readable text that explains this label.
*/
@Exported
public String getDescription() {
Set<Node> nodes = getNodes();
if(nodes.isEmpty())
......@@ -98,6 +195,7 @@ public class Label implements Comparable<Label>, ModelObject {
/**
* Returns projects that are tied on this node.
*/
@Exported
public List<AbstractProject> getTiedJobs() {
List<AbstractProject> r = new ArrayList<AbstractProject>();
for( AbstractProject p : Util.filter(Hudson.getInstance().getItems(),AbstractProject.class) ) {
......@@ -111,14 +209,25 @@ public class Label implements Comparable<Label>, ModelObject {
return getNodes().contains(node);
}
/**
* If there's no such label defined in {@link Node} or {@link Cloud}.
* This is usually used as a signal that this label is invalid.
*/
public boolean isEmpty() {
return getNodes().isEmpty();
return getNodes().isEmpty() && getClouds().isEmpty();
}
/*package*/ void reset() {
nodes = null;
clouds = null;
}
/**
* Expose this object to the remote API.
*/
public Api getApi() {
return new Api(this);
}
public boolean equals(Object that) {
if (this == that) return true;
......@@ -132,8 +241,30 @@ public class Label implements Comparable<Label>, ModelObject {
return name.hashCode();
}
public int compareTo(Label that) {
return this.name.compareTo(that.name);
}
@Override
public String toString() {
return name;
}
public static final class ConverterImpl implements Converter {
public ConverterImpl() {
}
public boolean canConvert(Class type) {
return type==Label.class;
}
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
Label src = (Label) source;
writer.setValue(src.getName());
}
public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) {
return Hudson.getInstance().getLabel(reader.getValue());
}
}
}
......@@ -50,6 +50,15 @@ public class ListView extends View {
return this;
}
/**
* Returns the transient {@link Action}s associated with the top page.
*
* @see Hudson#getActions()
*/
public List<Action> getActions() {
return Hudson.getInstance().getActions();
}
/**
* Returns a read-only view of all {@link Job}s in this view.
*
......
package hudson.model;
import hudson.model.MultiStageTimeSeries.TimeScale;
import hudson.triggers.SafeTimerTask;
import hudson.triggers.Trigger;
import hudson.util.ChartUtil;
import hudson.util.ColorPalette;
import hudson.util.NoOverlapCategoryAxis;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.category.CategoryDataset;
import org.jfree.ui.RectangleInsets;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import java.awt.*;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
/**
* Utilization statistics for a node or a set of nodes.
*
* <h2>Implementation Note</h2>
* <p>
* Instances of this class is not capable of updating the statistics itself
* &mdash; instead, it's done by the single {@link #register()} method.
* This is more efficient (as it allows us a single pass to update all stats),
* but it's not clear to me if the loss of autonomy is worth it.
*
* @author Kohsuke Kawaguchi
* @see Label#loadStatistics
* @see Hudson#overallLoad
*/
public abstract class LoadStatistics {
/**
* Number of busy executors and how it changes over time.
*/
public final MultiStageTimeSeries busyExecutors;
/**
* Number of total executors and how it changes over time.
*/
public final MultiStageTimeSeries totalExecutors;
/**
* Number of {@link Queue.BuildableItem}s that can run on any node in this node set but blocked.
*/
public final MultiStageTimeSeries queueLength;
protected LoadStatistics(int initialTotalExecutors, int initialBusyExecutors) {
this.totalExecutors = new MultiStageTimeSeries(initialTotalExecutors,DECAY);
this.busyExecutors = new MultiStageTimeSeries(initialBusyExecutors,DECAY);
this.queueLength = new MultiStageTimeSeries(0,DECAY);
}
public float getLatestIdleExecutors(TimeScale timeScale) {
return totalExecutors.pick(timeScale).getLatest() - busyExecutors.pick(timeScale).getLatest();
}
/**
* Computes the # of idle executors right now and obtains the snapshot value.
*/
public abstract int computeIdleExecutors();
/**
* Computes the # of total executors right now and obtains the snapshot value.
*/
public abstract int computeTotalExecutors();
/**
* Computes the # of queue length right now and obtains the snapshot value.
*/
public abstract int computeQueueLength();
/**
* Creates a trend chart.
*/
public JFreeChart createChart(CategoryDataset ds) {
final JFreeChart chart = ChartFactory.createLineChart(null, // chart title
null, // unused
null, // range axis label
ds, // data
PlotOrientation.VERTICAL, // orientation
true, // include legend
true, // tooltips
false // urls
);
chart.setBackgroundPaint(Color.white);
final CategoryPlot plot = chart.getCategoryPlot();
plot.setBackgroundPaint(Color.WHITE);
plot.setOutlinePaint(null);
plot.setRangeGridlinesVisible(true);
plot.setRangeGridlinePaint(Color.black);
final LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
renderer.setBaseStroke(new BasicStroke(3));
configureRenderer(renderer);
final CategoryAxis domainAxis = new NoOverlapCategoryAxis(null);
plot.setDomainAxis(domainAxis);
domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90);
domainAxis.setLowerMargin(0.0);
domainAxis.setUpperMargin(0.0);
domainAxis.setCategoryMargin(0.0);
final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
// crop extra space around the graph
plot.setInsets(new RectangleInsets(0, 0, 0, 5.0));
return chart;
}
protected void configureRenderer(LineAndShapeRenderer renderer) {
renderer.setSeriesPaint(0, ColorPalette.BLUE); // total
renderer.setSeriesPaint(1, ColorPalette.RED); // busy
renderer.setSeriesPaint(2, ColorPalette.GREY); // queue
}
/**
* Creates {@link CategoryDataset} which then becomes the basis
* of the load statistics graph.
*/
public CategoryDataset createDataset(TimeScale timeScale) {
return createDataset(timeScale,
new float[][]{
busyExecutors.pick(timeScale).getHistory(),
totalExecutors.pick(timeScale).getHistory(),
queueLength.pick(timeScale).getHistory()
},
new String[]{"Total executors","Busy executors","Queue length"});
}
protected final DefaultCategoryDataset createDataset(TimeScale timeScale, float[][] dataPoints, String[] legends) {
assert dataPoints.length==legends.length;
int dataLength = dataPoints[0].length;
for (float[] dataPoint : dataPoints)
assert dataLength ==dataPoint.length;
DefaultCategoryDataset ds = new DefaultCategoryDataset();
DateFormat format = timeScale.createDateFormat();
Date dt = new Date(System.currentTimeMillis()-timeScale.tick*dataLength);
for (int i = dataLength-1; i>=0; i--) {
dt = new Date(dt.getTime()+timeScale.tick);
String l = format.format(dt);
for(int j=0; j<dataPoints.length; j++)
ds.addValue(dataPoints[j][i],legends[j],l);
}
return ds;
}
/**
* Generates the load statistics graph.
*/
public void doGraph(StaplerRequest req, StaplerResponse rsp, @QueryParameter String type) throws IOException {
if(type==null) type=TimeScale.MIN.name();
TimeScale scale = Enum.valueOf(TimeScale.class, type.toUpperCase());
ChartUtil.generateGraph(req, rsp, createChart(createDataset(scale)), 500, 400);
}
/**
* Start updating the load average.
*/
/*package*/ static void register() {
Trigger.timer.scheduleAtFixedRate(
new SafeTimerTask() {
protected void doRun() {
Hudson h = Hudson.getInstance();
List<Queue.BuildableItem> bis = h.getQueue().getBuildableItems();
// update statistics on slaves
for( Label l : h.getLabels() ) {
l.loadStatistics.totalExecutors.update(l.getTotalExecutors());
l.loadStatistics.busyExecutors .update(l.getBusyExecutors());
int q=0;
for (Queue.BuildableItem bi : bis) {
if(bi.task.getAssignedLabel()==l)
q++;
}
l.loadStatistics.queueLength.update(q);
}
// update statistics of the entire system
ComputerSet cs = h.getComputer();
h.overallLoad.totalExecutors.update(cs.getTotalExecutors());
h.overallLoad.busyExecutors .update(cs.getBusyExecutors());
int q=0;
for (Queue.BuildableItem bi : bis) {
if(bi.task.getAssignedLabel()==null)
q++;
}
h.overallLoad.queueLength.update(q);
h.overallLoad.totalQueueLength.update(bis.size());
}
}, CLOCK, CLOCK
);
}
/**
* With 0.90 decay ratio for every 10sec, half reduction is about 1 min.
*/
public static final float DECAY = Float.parseFloat(System.getProperty(LoadStatistics.class.getName()+".decay","0.9"));
/**
* Load statistics clock cycle in milliseconds. Specify a small value for quickly debugging this feature and node provisioning through cloud.
*/
public static int CLOCK = Integer.getInteger(LoadStatistics.class.getName()+".clock",10*1000);
}
package hudson.model;
import hudson.util.TimeUnit2;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
/**
* Maintains several {@link TimeSeries} with different update frequencies to satisfy three goals;
* (1) retain data over long timespan, (2) save memory, and (3) retain accurate data for the recent past.
*
* All in all, one instance uses about 8KB space.
*
* @author Kohsuke Kawaguchi
*/
public class MultiStageTimeSeries {
/**
* Updated every 10 seconds. Keep data up to 1 hour.
*/
public final TimeSeries sec10;
/**
* Updated every 1 min. Keep data up to 1 day.
*/
public final TimeSeries min;
/**
* Updated every 1 hour. Keep data up to 4 weeks.
*/
public final TimeSeries hour;
private int counter;
public MultiStageTimeSeries(float initialValue, float decay) {
this.sec10 = new TimeSeries(initialValue,decay,6*60);
this.min = new TimeSeries(initialValue,decay,60*24);
this.hour = new TimeSeries(initialValue,decay,28*24);
}
/**
* Call this method every 10 sec and supply a new data point.
*/
public void update(float f) {
counter = (counter+1)%360; // 1hour/10sec = 60mins/10sec=3600secs/10sec = 360
sec10.update(f);
if(counter%6==0) min.update(f);
if(counter==0) hour.update(f);
}
/**
* Selects a {@link TimeSeries}.
*/
public TimeSeries pick(TimeScale timeScale) {
switch (timeScale) {
case HOUR: return hour;
case MIN: return min;
case SEC10: return sec10;
default: throw new AssertionError();
}
}
/**
* Gets the most up-to-date data point value.
*/
public float getLatest(TimeScale timeScale) {
return pick(timeScale).getLatest();
}
/**
* Choose which datapoint to use.
*/
public enum TimeScale {
SEC10(TimeUnit2.SECONDS.toMillis(10)),
MIN(TimeUnit2.MINUTES.toMillis(1)),
HOUR(TimeUnit2.HOURS.toMillis(1));
/**
* Number of milliseconds (10 secs, 1 min, and 1 hour)
* that this constant represents.
*/
public final long tick;
TimeScale(long tick) {
this.tick = tick;
}
/**
* Creates a new {@link DateFormat} suitable for processing
* this {@link TimeScale}.
*/
public DateFormat createDateFormat() {
switch (this) {
case HOUR: return new SimpleDateFormat("MMM/dd HH");
case MIN: return new SimpleDateFormat("HH:mm");
case SEC10: return new SimpleDateFormat("HH:mm:ss");
default: throw new AssertionError();
}
}
}
}
......@@ -2,6 +2,9 @@ package hudson.model;
import hudson.FilePath;
import hudson.Launcher;
import hudson.ExtensionPoint;
import hudson.security.AccessControlled;
import hudson.slaves.NodeDescriptor;
import hudson.node_monitors.NodeMonitor;
import hudson.util.ClockDifference;
import hudson.util.EnumConverter;
......@@ -11,12 +14,16 @@ import java.io.IOException;
import java.util.Set;
/**
* Commonality between {@link Slave} and master {@link Hudson}.
* Base type of Hudson slaves (although in practice, you probably extend {@link Slave} to define a new slave type.)
*
* <p>
* As a special case, {@link Hudson} extends from here.
*
* @author Kohsuke Kawaguchi
* @see NodeMonitor
* @see NodeDescriptor
*/
public interface Node {
public interface Node extends Describable<Node>, ExtensionPoint, AccessControlled {
/**
* Name of this node.
*
......@@ -25,6 +32,18 @@ public interface Node {
*/
String getNodeName();
/**
* When the user clones a {@link Node}, Hudson uses this method to change the node name right after
* the cloned {@link Node} object is instantiated.
*
* <p>
* This method is never used for any other purpose, and as such for all practical intents and purposes,
* the node name should be treated like immutable.
*
* @deprecated to indicate that this method isn't really meant to be called by random code.
*/
void setNodeName(String name);
/**
* Human-readable description of this node.
*/
......@@ -54,10 +73,15 @@ public interface Node {
* Gets the corresponding {@link Computer} object.
*
* @return
* never null.
* this method can return null if there's no {@link Computer} object for this node,
* such as when this node has no executors at all.
*/
Computer toComputer();
/**
* Creates a new {@link Computer} object that acts as the UI peer of this {@link Node}.
* Nobody but {@link Hudson#updateComputerList()} should call this method.
*/
Computer createComputer();
/**
......@@ -107,6 +131,8 @@ public interface Node {
*/
FilePath createPath(String absolutePath);
NodeDescriptor getDescriptor();
/**
* Estimates the clock difference with this slave.
*
......
package hudson.model;
import hudson.model.MultiStageTimeSeries.TimeScale;
import org.jfree.data.category.DefaultCategoryDataset;
/**
* {@link LoadStatistics} for the entire system (the master and all the slaves combined.)
*
* <p>
* {@link #computeQueueLength()} and {@link #queueLength} counts those tasks
* that are unassigned to any node, whereas {@link #totalQueueLength}
* tracks the queue length including tasks that are assigned to a specific node.
*
* @author Kohsuke Kawaguchi
* @see Hudson#overallLoad
*/
public class OverallLoadStatistics extends LoadStatistics {
/**
* Number of total {@link Queue.BuildableItem}s that represents blocked builds.
*/
public final MultiStageTimeSeries totalQueueLength = new MultiStageTimeSeries(0,DECAY);
/*package*/ OverallLoadStatistics() {
super(0,0);
}
@Override
public int computeIdleExecutors() {
return Hudson.getInstance().getComputer().getIdleExecutors();
}
@Override
public int computeTotalExecutors() {
return Hudson.getInstance().getComputer().getTotalExecutors();
}
@Override
public int computeQueueLength() {
return Hudson.getInstance().getQueue().countBuildableItemsFor(null);
}
/**
* When drawing the overall load statistics, use the total queue length,
* not {@link #queueLength}, which just shows jobs that are to be run on the master.
*/
protected DefaultCategoryDataset createOverallDataset(TimeScale timeScale) {
return createDataset(timeScale,
new float[][]{
busyExecutors.pick(timeScale).getHistory(),
totalExecutors.pick(timeScale).getHistory(),
totalQueueLength.pick(timeScale).getHistory()
},
new String[]{"Total executors","Busy executors","Queue length"});
}
}
......@@ -150,7 +150,7 @@ public abstract class Project<P extends Project<P,B>,B extends Build<P,B>>
return null;
}
//
//
//
// actions
//
......
......@@ -345,6 +345,9 @@ public class Queue extends ResourceController implements Saveable {
return r;
}
/**
* Gets all the {@link BuildableItem}s that are waiting for an executor in the given {@link Computer}.
*/
public synchronized List<BuildableItem> getBuildableItems(Computer c) {
List<BuildableItem> result = new ArrayList<BuildableItem>();
for (BuildableItem p : buildables.values()) {
......@@ -359,6 +362,24 @@ public class Queue extends ResourceController implements Saveable {
return result;
}
/**
* Gets the snapshot of {@link #buildables}.
*/
public synchronized List<BuildableItem> getBuildableItems() {
return new ArrayList<BuildableItem>(buildables.values());
}
/**
* How many {@link BuildableItem}s are assigned for the given label?
*/
public synchronized int countBuildableItemsFor(Label l) {
int r = 0;
for (BuildableItem bi : buildables.values())
if(bi.task.getAssignedLabel()==l)
r++;
return r;
}
/**
* Gets the information about the queue item for the given project.
*
......
......@@ -4,11 +4,14 @@ import hudson.FilePath;
import hudson.Launcher;
import hudson.Launcher.RemoteLauncher;
import hudson.Util;
import hudson.security.ACL;
import hudson.security.Permission;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.RetentionStrategy;
import hudson.slaves.CommandLauncher;
import hudson.slaves.JNLPLauncher;
import hudson.slaves.SlaveComputer;
import hudson.slaves.DumbSlave;
import hudson.model.Descriptor.FormException;
import hudson.remoting.Callable;
import hudson.remoting.VirtualChannel;
......@@ -18,6 +21,7 @@ import hudson.util.ClockDifference;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.apache.commons.io.IOUtils;
import javax.servlet.ServletException;
import java.io.File;
......@@ -26,6 +30,7 @@ import java.io.InputStream;
import java.io.Serializable;
import java.net.URL;
import java.net.URLConnection;
import java.net.MalformedURLException;
import java.util.*;
/**
......@@ -35,13 +40,16 @@ import java.util.*;
* Ideally this would have been in the <tt>hudson.slaves</tt> package,
* but for compatibility reasons, it can't.
*
* <p>
* TODO: move out more stuff to {@link DumbSlave}.
*
* @author Kohsuke Kawaguchi
*/
public final class Slave implements Node, Serializable {
public abstract class Slave implements Node, Serializable {
/**
* Name of this slave node.
*/
protected final String name;
protected String name;
/**
* Description of this node.
......@@ -90,9 +98,14 @@ public final class Slave implements Node, Serializable {
@DataBoundConstructor
public Slave(String name, String description, String remoteFS, String numExecutors,
Mode mode, String label, ComputerLauncher launcher, RetentionStrategy retentionStrategy) throws FormException {
this(name,description,remoteFS,Util.tryParseNumber(numExecutors, 1).intValue(),mode,label,launcher,retentionStrategy);
}
public Slave(String name, String description, String remoteFS, int numExecutors,
Mode mode, String label, ComputerLauncher launcher, RetentionStrategy retentionStrategy) throws FormException {
this.name = name;
this.description = description;
this.numExecutors = Util.tryParseNumber(numExecutors, 1).intValue();
this.numExecutors = numExecutors;
this.mode = mode;
this.remoteFS = remoteFS;
this.label = Util.fixNull(label).trim();
......@@ -108,8 +121,9 @@ public final class Slave implements Node, Serializable {
// so this check is harmful.
//if (!localFS.exists())
// throw new FormException("Invalid slave configuration for " + name + ". No such directory exists: " + localFS, null);
if (remoteFS.equals(""))
throw new FormException(Messages.Slave_InvalidConfig_NoRemoteDir(name), null);
// if (remoteFS.equals(""))
// throw new FormException(Messages.Slave_InvalidConfig_NoRemoteDir(name), null);
if (this.numExecutors<=0)
throw new FormException(Messages.Slave_InvalidConfig_Executors(name), null);
......@@ -131,6 +145,10 @@ public final class Slave implements Node, Serializable {
return name;
}
public void setNodeName(String name) {
this.name = name;
}
public String getNodeDescription() {
return description;
}
......@@ -243,6 +261,18 @@ public final class Slave implements Node, Serializable {
return new ClockDifference((startTime+endTime)/2 - slaveTime);
}
public ACL getACL() {
return Hudson.getInstance().getAuthorizationStrategy().getACL(this);
}
public final void checkPermission(Permission permission) {
getACL().checkPermission(permission);
}
public final boolean hasPermission(Permission permission) {
return getACL().hasPermission(permission);
}
public Computer createComputer() {
return new SlaveComputer(this);
}
......@@ -285,16 +315,33 @@ public final class Slave implements Node, Serializable {
}
public void doIndex( StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
URL res = req.getServletContext().getResource("/WEB-INF/" + fileName);
URLConnection con = connect();
InputStream in = con.getInputStream();
rsp.serveFile(req, in, con.getLastModified(), con.getContentLength(), "*.jar" );
in.close();
}
private URLConnection connect() throws IOException {
URL res = getURL();
return res.openConnection();
}
public URL getURL() throws MalformedURLException {
URL res = Hudson.getInstance().servletContext.getResource("/WEB-INF/" + fileName);
if(res==null) {
// during the development this path doesn't have the files.
res = new URL(new File(".").getAbsoluteFile().toURL(),"target/generated-resources/WEB-INF/"+fileName);
}
return res;
}
URLConnection con = res.openConnection();
InputStream in = con.getInputStream();
rsp.serveFile(req, in, con.getLastModified(), con.getContentLength(), "*.jar" );
in.close();
public byte[] readFully() throws IOException {
InputStream in = connect().getInputStream();
try {
return IOUtils.toByteArray(in);
} finally {
in.close();
}
}
}
......
......@@ -8,14 +8,21 @@ import java.io.PrintWriter;
import java.util.Formatter;
/**
* Receives events that happen during some task execution,
* such as a build or SCM change polling.
* Receives events that happen during some lengthy operation
* that has some chance of failures, such as a build, SCM change polling,
* slave launch, and so on.
*
* <p>
* This interface is implemented by Hudson core and passed to extension points so that
* they can record the progress of the build without really knowing how those information
* they can record the progress of the operation without really knowing how those information
* and handled/stored by Hudson.
*
* <p>
* The information is one way or the other made available to users, and
* so the expectation is that when something goes wrong, enough information
* shall be written to a {@link TaskListener} so that the user can diagnose
* what's going wrong.
*
* @author Kohsuke Kawaguchi
*/
public interface TaskListener {
......
package hudson.model;
import hudson.CopyOnWrite;
/**
* Scalar value that changes over the time (such as load average, Q length, # of executors, etc.)
*
* <p>
* This class computes <a href="http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average">
* the exponential moving average</a> from the raw data (to be supplied by {@link #update(float)}).
*
* @author Kohsuke Kawaguchi
*/
public final class TimeSeries {
/**
* Decay ratio. Normally 1-e for some small e.
*/
private final float decay;
/**
* Historical exponential moving average data. Newer ones first.
*/
@CopyOnWrite
private volatile float[] history;
/**
* Maximum history size.
*/
private final int historySize;
public TimeSeries(float initialValue, float decay, int historySize) {
this.history = new float[]{initialValue};
this.decay = decay;
this.historySize = historySize;
}
/**
* Pushes a new data point.
*
* <p>
* Exponential moving average is calculated, and the {@link #history} is updated.
* This method needs to be called periodically and regularly, and it represents
* the raw data stream.
*/
public void update(float newData) {
float data = history[0]*decay + newData*(1-decay);
float[] r = new float[Math.min(history.length+1,historySize)];
System.arraycopy(history,0,r,1,Math.min(history.length,r.length-1));
r[0] = data;
history = r;
}
/**
* Gets the history data of the exponential moving average. The returned array should be treated
* as read-only and immutable.
*
* @return
* Always non-null, contains at least one entry.
*/
public float[] getHistory() {
return history;
}
/**
* Gets the most up-to-date data point value. {@code getHistory[0]}.
*/
public float getLatest() {
return history[0];
}
public String toString() {
return Float.toString(history[0]);
}
}
......@@ -26,7 +26,7 @@ public abstract class TopLevelItemDescriptor extends Descriptor<TopLevelItem> {
*/
public abstract String getDisplayName();
public final String getNewJobDetailPage() {
public final String newInstanceDetailPage() {
return '/'+clazz.getName().replace('.','/').replace('$','/')+"/newJobDetail.jelly";
}
......
package hudson.security;
import hudson.ExtensionPoint;
import hudson.slaves.Cloud;
import hudson.model.AbstractItem;
import hudson.model.AbstractProject;
import hudson.model.Computer;
......@@ -9,6 +10,7 @@ import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.model.User;
import hudson.model.View;
import hudson.model.Node;
import hudson.util.DescriptorList;
import java.io.Serializable;
......@@ -97,11 +99,28 @@ public abstract class AuthorizationStrategy implements Describable<Authorization
* This can be used as a basis for more fine-grained access control.
*
* <p>
* The default implementation returns {@link #getRootACL()}.
* The default implementation delegates to {@link #getACL(Node)}
*
* @since 1.220
*/
public ACL getACL(Computer computer) {
return getACL(computer.getNode());
}
/**
* Implementation can choose to provide different ACL for different {@link Cloud}s.
* This can be used as a basis for more fine-grained access control.
*
* <p>
* The default implementation returns {@link #getRootACL()}.
*
* @since 1.252
*/
public ACL getACL(Cloud cloud) {
return getRootACL();
}
public ACL getACL(Node node) {
return getRootACL();
}
......
package hudson.slaves;
import hudson.ExtensionPoint;
import hudson.slaves.NodeProvisioner.PlannedNode;
import hudson.model.Describable;
import hudson.model.Hudson;
import hudson.model.Node;
import hudson.model.AbstractModelObject;
import hudson.model.Label;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.security.Permission;
import hudson.util.DescriptorList;
import java.util.Collection;
/**
* Creates {@link Node}s to dynamically expand/shrink the slaves attached to Hudson.
*
* <p>
* Put another way, this class encapsulates different communication protocols
* needed to start a new slave programmatically.
*
* @author Kohsuke Kawaguchi
* @see NodeProvisioner
*/
public abstract class Cloud extends AbstractModelObject implements ExtensionPoint, Describable<Cloud>, AccessControlled {
/**
* Uniquely identifies this {@link Cloud} instance among other instances in {@link Hudson#clouds}.
*/
public final String name;
protected Cloud(String name) {
this.name = name;
}
public String getDisplayName() {
return name;
}
public String getSearchUrl() {
return "cloud/"+name;
}
public ACL getACL() {
return Hudson.getInstance().getAuthorizationStrategy().getACL(this);
}
public final void checkPermission(Permission permission) {
getACL().checkPermission(permission);
}
public final boolean hasPermission(Permission permission) {
return getACL().hasPermission(permission);
}
/**
* Provisions new {@link Node}s from this cloud.
*
* <p>
* {@link NodeProvisioner} performs a trend analysis on the load,
* and when it determines that it <b>really</b> needs to bring up
* additional nodes, this method is invoked.
*
* <p>
* The implementation of this method asynchronously starts
* node provisioning.
*
* @param label
* The label that indicates what kind of nodes are needed now.
* Newly launched node needs to have this label.
* Only those {@link Label}s that this instance returned true
* from the {@link #canProvision(Label)} method will be passed here.
* @param excessWorkload
* Number of total executors needed to meet the current demand.
* Always >= 1. For example, if this is 3, the implementation
* should launch 3 slaves with 1 executor each, or 1 slave with
* 3 executors, etc.
*
* @return
* {@link PlannedNode}s that represent asynchronous {@link Node}
* launch operations. Can be empty but must not be null.
*/
public abstract Collection<PlannedNode> provision(Label label, int excessWorkload);
/**
* Returns true if this cloud is capable of provisioning new nodes for the given label.
*/
public abstract boolean canProvision(Label label);
/**
* All registered {@link Cloud} implementations.
*/
public static final DescriptorList<Cloud> ALL = new DescriptorList<Cloud>();
/**
* Permission constant to control mutation operations on {@link Cloud}.
*
* This includes provisioning a new node, as well as removing it.
*/
public static final Permission PROVISION = Hudson.ADMINISTER;
}
......@@ -9,11 +9,20 @@ import hudson.util.StreamTaskListener;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
/**
* Extension point to allow control over how {@link Computer}s are "launched",
* meaning how they get connected to their slave agent program.
*
* <h2>Associated View</h2>
* <dl>
* <dt>main.jelly</dt>
* <dd>
* This page will be rendered into the top page of the computer (/computer/NAME/)
* Useful for showing launch related commands and status reports.
* </dl>
*
* <p>
* <b>EXPERIMENTAL: SIGNATURE MAY CHANGE IN FUTURE RELEASES</b>
*
......@@ -40,8 +49,13 @@ public abstract class ComputerLauncher implements Describable<ComputerLauncher>,
*
* @param listener
* The progress of the launch, as well as any error, should be sent to this listener.
*
* @throws IOException
* if the method throws an {@link IOException} or {@link InterruptedException}, the launch was considered
* a failure and the stack trace is reported into the listener. This handling is just so that the implementation
* of this method doesn't have to dilligently catch those exceptions.
*/
public abstract void launch(SlaveComputer computer, StreamTaskListener listener);
public abstract void launch(SlaveComputer computer, StreamTaskListener listener) throws IOException , InterruptedException;
/**
* Allows the {@link ComputerLauncher} to tidy-up after a disconnect.
......
package hudson.slaves;
import hudson.util.StreamTaskListener;
import hudson.model.Descriptor;
import hudson.model.Node;
import java.io.IOException;
/**
* {@link ComputerLauncher} filter that can be used as a base class for decorators.
*
* <p>
* Using this class also protects you from method additions in {@link ComputerLauncher},
* since these two classes are updated in sync.
*
* @author Kohsuke Kawaguchi
* @see SlaveComputer#grabLauncher(Node)
*/
public abstract class ComputerLauncherFilter extends ComputerLauncher {
protected volatile ComputerLauncher core;
public ComputerLauncherFilter(ComputerLauncher core) {
this.core = core;
}
/**
* Returns the delegation target.
*/
public ComputerLauncher getCore() {
return core;
}
public boolean isLaunchSupported() {
return core.isLaunchSupported();
}
public void launch(SlaveComputer computer, StreamTaskListener listener) throws IOException, InterruptedException {
core.launch(computer, listener);
}
public void afterDisconnect(SlaveComputer computer, StreamTaskListener listener) {
core.afterDisconnect(computer, listener);
}
public void beforeDisconnect(SlaveComputer computer, StreamTaskListener listener) {
core.beforeDisconnect(computer, listener);
}
public Descriptor<ComputerLauncher> getDescriptor() {
throw new UnsupportedOperationException();
}
}
package hudson.slaves;
import hudson.model.Slave;
import hudson.model.Descriptor.FormException;
import org.kohsuke.stapler.DataBoundConstructor;
/**
* Default {@link Slave} implementation for computers that do not belong to a higher level structure,
* like grid or cloud.
*
* @author Kohsuke Kawaguchi
*/
public final class DumbSlave extends Slave {
@DataBoundConstructor
public DumbSlave(String name, String description, String remoteFS, String numExecutors, Mode mode, String label, ComputerLauncher launcher, RetentionStrategy retentionStrategy) throws FormException {
super(name, description, remoteFS, numExecutors, mode, label, launcher, retentionStrategy);
}
public DescriptorImpl getDescriptor() {
return DescriptorImpl.INSTANCE;
}
public static final class DescriptorImpl extends NodeDescriptor {
public static final DescriptorImpl INSTANCE = new DescriptorImpl();
private DescriptorImpl() {
super(DumbSlave.class);
}
public String getDisplayName() {
return "Dumb Slave";
}
}
static {
NodeDescriptor.ALL.add(DescriptorImpl.INSTANCE);
}
}
package hudson.slaves;
import hudson.model.Node;
/**
* {@link Node}s that are created by {@link Cloud} and hence not persisted as configuration by itself.
*
* @author Kohsuke Kawaguchi
*/
public interface EphemeralNode extends Node {
/**
* Gets the {@link Cloud} that created this {@link EphemeralNode}.
*
* @return
* never null.
*/
public Cloud getCloud();
}
package hudson.slaves;
import hudson.model.Descriptor;
import hudson.model.Slave;
import hudson.model.Node;
import hudson.util.DescriptorList;
/**
* {@link Descriptor} for {@link Slave}.
*
* <h2>Views</h2>
* <p>
* This object needs to have <tt>newInstanceDetail.jelly</tt> view, which shows up in
* <tt>http://server/hudson/computers/new</tt> page as an explanation of this job type.
*
* <h2>Other Implementation Notes</h2>
*
* @author Kohsuke Kawaguchi
*/
public abstract class NodeDescriptor extends Descriptor<Node> {
protected NodeDescriptor(Class<? extends Node> clazz) {
super(clazz);
}
public final String newInstanceDetailPage() {
return '/'+clazz.getName().replace('.','/').replace('$','/')+"/newInstanceDetail.jelly";
}
@Override
public String getConfigPage() {
return getViewPage(clazz, "configure-entries.jelly");
}
/**
* All the registered instances.
*/
public static final DescriptorList<Node> ALL = new DescriptorList<Node>();
static {
ALL.load(DumbSlave.class);
}
}
package hudson.slaves;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import hudson.model.Node;
import hudson.util.RobustCollectionConverter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* {@link CopyOnWriteArrayList} for {@link Node} that has special serialization semantics
* of not serializing {@link EphemeralNode}s.
*
* @author Kohsuke Kawaguchi
*/
public final class NodeList extends CopyOnWriteArrayList<Node> {
public NodeList() {
}
public NodeList(Collection<? extends Node> c) {
super(c);
}
public NodeList(Node[] toCopyIn) {
super(toCopyIn);
}
/**
* {@link Converter} implementation for XStream.
*
* Serializaion form is compatible with plain {@link List}.
*/
public static final class ConverterImpl extends RobustCollectionConverter {
public ConverterImpl(XStream xstream) {
super(xstream);
}
public boolean canConvert(Class type) {
return type==NodeList.class;
}
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
for (Node o : (NodeList) source) {
if(o instanceof EphemeralNode)
continue; // skip
writeItem(o, context, writer);
}
}
protected Object createCollection(Class type) {
return new ArrayList();
}
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
return new NodeList((List<Node>)super.unmarshal(reader, context));
}
}
}
package hudson.slaves;
import hudson.model.LoadStatistics;
import hudson.model.Node;
import hudson.model.Hudson;
import hudson.model.MultiStageTimeSeries;
import hudson.model.Label;
import static hudson.model.LoadStatistics.DECAY;
import hudson.model.MultiStageTimeSeries.TimeScale;
import hudson.triggers.SafeTimerTask;
import hudson.triggers.Trigger;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutionException;
import java.util.List;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.logging.Logger;
import java.util.logging.Level;
/**
* Uses the {@link LoadStatistics} and determines when we need to allocate
* new {@link Node}s through {@link Cloud}.
*
* @author Kohsuke Kawaguchi
*/
public class NodeProvisioner {
/**
* The node addition activity in progress.
*/
public static final class PlannedNode {
public final String displayName;
public final Future<Node> future;
public final int numExecutors;
public PlannedNode(String displayName, Future<Node> future, int numExecutors) {
if(displayName==null || future==null || numExecutors<1) throw new IllegalArgumentException();
this.displayName = displayName;
this.future = future;
this.numExecutors = numExecutors;
}
}
/**
* Load for the label.
*/
private final LoadStatistics stat;
/**
* For which label are we working?
* Null if this {@link NodeProvisioner} is working for the entire Hudson,
* for jobs that are unassigned to any particular node.
*/
private final Label label;
private List<PlannedNode> pendingLaunches = new ArrayList<PlannedNode>();
/**
* Exponential moving average of the "planned capacity" over time, which is the number of
* additional executors being brought up.
*
* This is used to filter out high-frequency components from the planned capacity, so that
* the comparison with other low-frequency only variables won't leave spikes.
*/
private final MultiStageTimeSeries plannedCapacitiesEMA = new MultiStageTimeSeries(0,DECAY);
public NodeProvisioner(Label label, LoadStatistics loadStatistics) {
this.label = label;
this.stat = loadStatistics;
}
/**
* Periodically invoked to keep track of the load.
* Launches additional nodes if necessary.
*/
private void update() {
Hudson hudson = Hudson.getInstance();
// clean up the cancelled launch activity, then count the # of executors that we are about to bring up.
float plannedCapacity = 0;
for (Iterator<PlannedNode> itr = pendingLaunches.iterator(); itr.hasNext();) {
PlannedNode f = itr.next();
if(f.future.isDone()) {
LOGGER.info(f.displayName+" provisioning completed. We have now "+hudson.getComputers().length+" computer(s)");
try {
f.future.get();
} catch (InterruptedException e) {
throw new AssertionError(e); // since we confirmed that the future is already done
} catch (ExecutionException e) {
LOGGER.log(Level.WARNING, "Provisioned slave failed to launch",e.getCause());
}
itr.remove();
} else
plannedCapacity += f.numExecutors;
}
plannedCapacitiesEMA.update(plannedCapacity);
/*
Here we determine how many additional slaves we need to keep up with the load (if at all),
which involves a simple math.
Broadly speaking, first we check that all the executors are fully utilized before attempting
to start any new slave (this also helps to ignore the temporary gap between different numbers,
as changes in them are not necessarily synchronized --- for example, there's a time lag between
when a slave launches (thus bringing the planned capacity down) and the time when its executors
pick up builds (thus bringing the queue length down.)
Once we confirm that, we compare the # of buildable items against the additional slaves
that are being brought online. If we have more jobs than our executors can handle, we'll launch a new slave.
So this computation involves three stats:
1. # of idle executors
2. # of jobs that are starving for executors
3. # of additional slaves being provisioned (planned capacities.)
To ignore a temporary surge/drop, we make conservative estimates on each one of them. That is,
we take the current snapshot value, and we take the current exponential moving average (EMA) value,
and use the max/min.
This is another measure to be robust against temporary surge/drop in those indicators, and helps
us avoid over-reacting to stats.
If we only use the snapshot value or EMA value, tests confirmed that the gap creates phantom
excessive loads and Hudson ends up firing excessive capacities. In a static system, over the time
EMA and the snapshot value becomes the same, so this makes sure that in a long run this conservative
estimate won't create a starvation.
*/
int idleSnapshot = stat.computeIdleExecutors();
int totalSnapshot = stat.computeTotalExecutors();
float idle = Math.max(stat.getLatestIdleExecutors(TIME_SCALE), idleSnapshot);
if(idle<MARGIN) {
// make sure the system is fully utilized before attempting any new launch.
// this is the amount of work left to be done
float qlen = Math.min(stat.queueLength.getLatest(TIME_SCALE), stat.computeQueueLength());
// ... and this is the additional executors we've already provisioned.
plannedCapacity = Math.max(plannedCapacitiesEMA.getLatest(TIME_SCALE),plannedCapacity);
float excessWorkload = qlen - plannedCapacity;
if(excessWorkload>1-MARGIN) {// and there's more work to do...
LOGGER.info("Excess workload "+excessWorkload+" detected. (planned capacity="+plannedCapacity+",Qlen="+qlen+",idle="+idle+"&"+idleSnapshot+",total="+totalSnapshot+")");
for( Cloud c : hudson.clouds ) {
if(excessWorkload<0) break; // enough slaves allocated
// provisioning a new node should be conservative --- for example if exceeWorkload is 1.4,
// we don't want to allocate two nodes but just one.
// OTOH, because of the exponential decay, even when we need one slave, excess workload is always
// something like 0.95, in which case we want to allocate one node.
// so the threshold here is 1-MARGIN, and hence floor(excessWorkload+MARGIN) is needed to handle this.
Collection<PlannedNode> additionalCapacities = c.provision(label, (int)Math.round(Math.floor(excessWorkload+MARGIN)));
for (PlannedNode ac : additionalCapacities) {
excessWorkload -= ac.numExecutors;
LOGGER.info("Started provisioning "+ac.displayName+" from "+c.name+" with "+ac.numExecutors+" executors. Remaining excess workload:"+excessWorkload);
}
pendingLaunches.addAll(additionalCapacities);
}
}
}
}
public static void launch() {
// periodically invoke NodeProvisioners
Trigger.timer.scheduleAtFixedRate(new SafeTimerTask() {
@Override
protected void doRun() {
Hudson h = Hudson.getInstance();
h.overallNodeProvisioner.update();
for( Label l : h.getLabels() )
l.nodeProvisioner.update();
}
},
// give some initial warm up time so that statically connected slaves
// can be brought online before we start allocating more.
LoadStatistics.CLOCK*10,
LoadStatistics.CLOCK);
}
private static final float MARGIN = 0.1f;
private static final Logger LOGGER = Logger.getLogger(NodeProvisioner.class.getName());
// TODO: picker should be selectable
private static final TimeScale TIME_SCALE = TimeScale.SEC10;
}
......@@ -65,18 +65,12 @@ public abstract class RetentionStrategy<T extends Computer> implements Describab
public Always() {
}
/**
* {@inheritDoc}
*/
public long check(SlaveComputer c) {
if (c.isOffline() && c.isLaunchSupported())
c.tryReconnect();
return 1;
}
/**
* {@inheritDoc}
*/
public DescriptorImpl getDescriptor() {
return DESCRIPTOR;
}
......@@ -91,9 +85,6 @@ public abstract class RetentionStrategy<T extends Computer> implements Describab
super(Always.class);
}
/**
* {@inheritDoc}
*/
public String getDisplayName() {
return Messages.RetentionStrategy_Always_displayName();
}
......@@ -147,34 +138,28 @@ public abstract class RetentionStrategy<T extends Computer> implements Describab
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
/**
* {@inheritDoc}
*/
public synchronized long check(SlaveComputer c) {
if (c.isOffline()) {
final long demandMilliseconds = System.currentTimeMillis() - c.getDemandStartMilliseconds();
if (demandMilliseconds > inDemandDelay * 1000 * 60 /*MINS->MILLIS*/) {
// we've been in demand for long enough
logger.log(Level.INFO, "Launching computer {0} as it has been in demand for {1}",
new Object[]{c.getNode().getNodeName(), Util.getTimeSpanString(demandMilliseconds)});
new Object[]{c.getName(), Util.getTimeSpanString(demandMilliseconds)});
if (c.isLaunchSupported())
c.launch();
c.connect(true);
}
} else if (c.isIdle()) {
final long idleMilliseconds = System.currentTimeMillis() - c.getIdleStartMilliseconds();
if (idleMilliseconds > idleDelay * 1000 * 60 /*MINS->MILLIS*/) {
// we've been idle for long enough
logger.log(Level.INFO, "Disconnecting computer {0} as it has been idle for {1}",
new Object[]{c.getNode().getNodeName(), Util.getTimeSpanString(idleMilliseconds)});
new Object[]{c.getName(), Util.getTimeSpanString(idleMilliseconds)});
c.disconnect();
}
}
return 1;
}
/**
* {@inheritDoc}
*/
public Descriptor<RetentionStrategy<?>> getDescriptor() {
return DESCRIPTOR;
}
......@@ -187,9 +172,6 @@ public abstract class RetentionStrategy<T extends Computer> implements Describab
super(Demand.class);
}
/**
* {@inheritDoc}
*/
public String getDisplayName() {
return Messages.RetentionStrategy_Demand_displayName();
}
......
......@@ -8,8 +8,10 @@ import hudson.remoting.Callable;
import hudson.util.StreamTaskListener;
import hudson.util.NullStream;
import hudson.util.RingBufferLogHandler;
import hudson.util.Futures;
import hudson.FilePath;
import hudson.lifecycle.WindowsSlaveInstaller;
import hudson.Util;
import hudson.maven.agent.Main;
import hudson.maven.agent.PluginManagerInterceptor;
......@@ -27,6 +29,8 @@ import java.util.List;
import java.util.Collections;
import java.util.ArrayList;
import java.nio.charset.Charset;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.Future;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
......@@ -39,11 +43,19 @@ import javax.servlet.http.HttpServletResponse;
*
* @author Kohsuke Kawaguchi
*/
public final class SlaveComputer extends Computer {
public class SlaveComputer extends Computer {
private volatile Channel channel;
private volatile transient boolean acceptingTasks = true;
private Charset defaultCharset;
private Boolean isUnix;
/**
* Effective {@link ComputerLauncher} that hides the details of
* how we launch a slave agent on this computer.
*
* <p>
* This is normally the same as {@link Slave#getLauncher()} but
* can be different. See {@link #grabLauncher(Node)}.
*/
private ComputerLauncher launcher;
/**
......@@ -53,6 +65,17 @@ public final class SlaveComputer extends Computer {
*/
private transient int numRetryAttempt;
/**
* Tracks the status of the last launch operation, which is always asynchronous.
* This can be used to wait for the completion, or cancel the launch activity.
*/
private volatile Future<?> lastConnectActivity = null;
private Object constructed = new Object();
public SlaveComputer(Slave slave) {
super(slave);
}
/**
* {@inheritDoc}
......@@ -72,10 +95,6 @@ public final class SlaveComputer extends Computer {
this.acceptingTasks = acceptingTasks;
}
public SlaveComputer(Slave slave) {
super(slave);
}
/**
* True if this computer is a Unix machine (as opposed to Windows machine).
*
......@@ -90,6 +109,14 @@ public final class SlaveComputer extends Computer {
return (Slave)super.getNode();
}
@Override
public String getIcon() {
Future<?> l = lastConnectActivity;
if(l!=null && !l.isDone())
return "computer-flash.gif";
return super.getIcon();
}
@Override
@Deprecated
public boolean isJnlpAgent() {
......@@ -105,15 +132,30 @@ public final class SlaveComputer extends Computer {
return launcher;
}
public void launch() {
if(channel!=null) return;
public Future<?> connect(boolean forceReconnect) {
if(channel!=null) return Futures.precomputed(null);
if(!forceReconnect && lastConnectActivity!=null)
return lastConnectActivity;
if(forceReconnect && lastConnectActivity!=null)
logger.fine("Forcing a reconnect");
closeChannel();
Computer.threadPoolForRemoting.execute(new Runnable() {
public void run() {
return lastConnectActivity = Computer.threadPoolForRemoting.submit(new java.util.concurrent.Callable<Object>() {
public Object call() throws Exception {
// do this on another thread so that the lengthy launch operation
// (which is typical) won't block UI thread.
launcher.launch(SlaveComputer.this, new StreamTaskListener(openLogFile()));
StreamTaskListener listener = new StreamTaskListener(openLogFile());
try {
launcher.launch(SlaveComputer.this, listener);
return null;
} catch (IOException e) {
Util.displayIOException(e,listener);
e.printStackTrace(listener.error(Messages.ComputerLauncher_unexpectedError()));
throw e;
} catch (InterruptedException e) {
e.printStackTrace(listener.error(Messages.ComputerLauncher_abortedLaunch()));
throw e;
}
}
});
}
......@@ -161,6 +203,12 @@ public final class SlaveComputer extends Computer {
}
}
@Override
public boolean isConnecting() {
Future<?> l = lastConnectActivity;
return isOffline() && l!=null && !l.isDone();
}
public OutputStream openLogFile() {
OutputStream os;
try {
......@@ -174,8 +222,26 @@ public final class SlaveComputer extends Computer {
private final Object channelLock = new Object();
public void setChannel(InputStream in, OutputStream out, TaskListener taskListener, Channel.Listener listener) throws IOException, InterruptedException {
setChannel(in,out,taskListener.getLogger(),listener);
}
/**
* Creates a {@link Channel} from the given stream and sets that to this slave.
*
* @param in
* Stream connected to the remote "slave.jar". It's the caller's responsibility to do
* buffering on this stream, if that's necessary.
* @param out
* Stream connected to the remote peer. It's the caller's responsibility to do
* buffering on this stream, if that's necessary.
* @param launchLog
* If non-null, receive the portion of data in <tt>is</tt> before
* the data goes into the "binary mode". This is useful
* when the established communication channel might include some data that might
* be useful for debugging/trouble-shooting.
* @param listener
* Gets a notification when the channel closes, to perform clean up.
*/
public void setChannel(InputStream in, OutputStream out, OutputStream launchLog, Channel.Listener listener) throws IOException, InterruptedException {
if(this.channel!=null)
......@@ -204,10 +270,8 @@ public final class SlaveComputer extends Computer {
{// send jars that we need for our operations
// TODO: maybe I should generalize this kind of "post initialization" processing
FilePath dst = new FilePath(channel, remoteFs);
new FilePath(Which.jarFile(Main.class)).copyTo(dst.child("maven-agent.jar"));
log.println("Copied maven-agent.jar");
new FilePath(Which.jarFile(PluginManagerInterceptor.class)).copyTo(dst.child("maven-interceptor.jar"));
log.println("Copied maven-interceptor.jar");
copyJar(log, dst, Main.class, "maven-agent");
copyJar(log, dst, PluginManagerInterceptor.class, "maven-interceptor");
}
channel.call(new LogInstaller());
......@@ -235,6 +299,26 @@ public final class SlaveComputer extends Computer {
Hudson.getInstance().getQueue().scheduleMaintenance();
}
/**
* Copies a jar file from the master to slave.
*/
private void copyJar(PrintWriter log, FilePath dst, Class<?> representative, String seedName) throws IOException, InterruptedException {
// in normal execution environment, the master should be loading 'representative' from this jar, so
// in that way we can find it.
File jar = Which.jarFile(representative);
if(jar.isDirectory()) {
// but during the development and unit test environment, we may be picking the class up from the classes dir,
// in which case we need to find this in a tricker way.
String dir = Hudson.getInstance().servletContext.getRealPath("/WEB-INF/lib");
FilePath[] paths = new FilePath(new File(dir)).list(seedName + "-*.jar");
jar = new File(paths[0].getRemote());
}
new FilePath(jar).copyTo(dst.child(seedName +".jar"));
log.println("Copied "+seedName+".jar");
}
@Override
public VirtualChannel getChannel() {
return channel;
......@@ -262,8 +346,8 @@ public final class SlaveComputer extends Computer {
}
@Override
public void disconnect() {
Computer.threadPoolForRemoting.execute(new Runnable() {
public Future<?> disconnect() {
return Computer.threadPoolForRemoting.submit(new Runnable() {
public void run() {
// do this on another thread so that any lengthy disconnect operation
// (which could be typical) won't block UI thread.
......@@ -281,7 +365,7 @@ public final class SlaveComputer extends Computer {
return;
}
launch();
connect(true);
// TODO: would be nice to redirect the user to "launching..." wait page,
// then spend a few seconds there and poll for the completion periodically.
......@@ -293,7 +377,7 @@ public final class SlaveComputer extends Computer {
if(numRetryAttempt<6 || (numRetryAttempt%12)==0) {
// initially retry several times quickly, and after that, do it infrequently.
logger.info("Attempting to reconnect "+nodeName);
launch();
connect(true);
}
}
......@@ -340,10 +424,29 @@ public final class SlaveComputer extends Computer {
@Override
protected void setNode(Node node) {
super.setNode(node);
launcher = ((Slave)node).getLauncher();
launcher = grabLauncher(node);
// maybe the configuration was changed to relaunch the slave, so try to re-launch now.
launch();
// "constructed==null" test is an ugly hack to avoid launching before the object is fully
// constructed.
if(constructed!=null)
connect(false);
}
/**
* Grabs a {@link ComputerLauncher} out of {@link Node} to keep it in this {@link Computer}.
* The returned launcher will be set to {@link #launcher} and used to carry out the actual launch operation.
*
* <p>
* Subtypes that needs to decorate {@link ComputerLauncher} can do so by overriding this method.
* This is useful for {@link SlaveComputer}s for clouds for example, where one normally needs
* additional pre-launch step (such as waiting for the provisioned node to become available)
* before the user specified launch step (like SSH connection) kicks in.
*
* @see ComputerLauncherFilter
*/
protected ComputerLauncher grabLauncher(Node node) {
return ((Slave)node).getLauncher();
}
private static final Logger logger = Logger.getLogger(SlaveComputer.class.getName());
......
......@@ -8,11 +8,17 @@ import java.util.StringTokenizer;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import java.util.BitSet;
import java.util.Properties;
import java.util.Map.Entry;
import java.io.Serializable;
import java.io.StringReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.File;
import java.io.StringReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.jvnet.animal_sniffer.IgnoreJRERequirement;
......@@ -23,11 +29,16 @@ import org.jvnet.animal_sniffer.IgnoreJRERequirement;
*/
public class ArgumentListBuilder implements Serializable {
private final List<String> args = new ArrayList<String>();
private BitSet mask = new BitSet();
public ArgumentListBuilder add(Object a) {
return add(a.toString());
}
public ArgumentListBuilder add(File f) {
return add(f.getAbsolutePath());
}
public ArgumentListBuilder add(String a) {
if(a!=null)
args.add(a);
......@@ -35,6 +46,12 @@ public class ArgumentListBuilder implements Serializable {
}
public ArgumentListBuilder prepend(String... args) {
// left-shift the mask
BitSet nm = new BitSet(this.args.size()+args.length);
for(int i=0; i<this.args.size(); i++)
nm.set(i+args.length, mask.get(i));
mask = nm;
this.args.addAll(0, Arrays.asList(args));
return this;
}
......@@ -124,6 +141,13 @@ public class ArgumentListBuilder implements Serializable {
return r;
}
/**
* Re-initializes the arguments list.
*/
public void clear() {
args.clear();
}
public List<String> toList() {
return args;
}
......@@ -141,5 +165,33 @@ public class ArgumentListBuilder implements Serializable {
return buf.toString();
}
/**
* Returns true if there are any masked arguments.
* @return true if there are any masked arguments; false otherwise
*/
public boolean hasMaskedArguments() {
return mask.length()>0;
}
/**
* Returns an array of booleans where the masked arguments are marked as true
* @return an array of booleans.
*/
public boolean[] toMaskArray() {
boolean[] mask = new boolean[args.size()];
for( int i=0; i<mask.length; i++)
mask[i] = this.mask.get(i);
return mask;
}
/**
* Add a masked argument
* @param string the argument
*/
public void addMasked(String string) {
mask.set(args.size());
add(string);
}
private static final long serialVersionUID = 1L;
}
......@@ -12,7 +12,12 @@ import java.util.TreeSet;
*
* <p>
* This code works around an issue in {@link DefaultCategoryDataset} where
* order of addition changes the way they are drawn.
* order of addition changes the way they are drawn.
*
* @param <Row>
* Names that identify different graphs drawn in the same chart.
* @param <Column>
* X-axis.
*/
public final class DataSetBuilder<Row extends Comparable,Column extends Comparable> {
......
......@@ -71,7 +71,7 @@ public class DescribableList<T extends Describable<T>, D extends Descriptor<T>>
public void add(T item) throws IOException {
data.add(item);
owner.save();
onModified();
}
public T get(D descriptor) {
......@@ -89,7 +89,7 @@ public class DescribableList<T extends Describable<T>, D extends Descriptor<T>>
for (T t : data) {
if(t.getDescriptor()==descriptor) {
data.remove(t);
owner.save();
onModified();
return;
}
}
......@@ -99,6 +99,13 @@ public class DescribableList<T extends Describable<T>, D extends Descriptor<T>>
return data.iterator();
}
/**
* Called when a list is mutated.
*/
protected void onModified() throws IOException {
owner.save();
}
@SuppressWarnings("unchecked")
public Map<D,T> toMap() {
return (Map)Descriptor.toMap(data);
......@@ -182,7 +189,7 @@ public class DescribableList<T extends Describable<T>, D extends Descriptor<T>>
*
* Serializaion form is compatible with plain {@link List}.
*/
public static final class ConverterImpl extends AbstractCollectionConverter {
public static class ConverterImpl extends AbstractCollectionConverter {
CopyOnWriteList.ConverterImpl copyOnWriteListConverter;
public ConverterImpl(Mapper mapper) {
......@@ -191,7 +198,8 @@ public class DescribableList<T extends Describable<T>, D extends Descriptor<T>>
}
public boolean canConvert(Class type) {
return type==DescribableList.class;
// handle subtypes in case the onModified method is overridden.
return DescribableList.class.isAssignableFrom(type);
}
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
......
......@@ -72,4 +72,11 @@ public final class DescriptorList<T extends Describable<T>> extends CopyOnWriteA
throw new AssertionError(e); // Can't happen
}
}
/**
* Finds the descriptor that has the matching fully-qualified class name.
*/
public Descriptor<T> find(String fqcn) {
return Descriptor.find(this,fqcn);
}
}
......@@ -7,6 +7,8 @@ import hudson.Util;
import hudson.model.AbstractProject;
import hudson.model.Hudson;
import hudson.model.Item;
import hudson.model.Item;
import hudson.model.Job;
import hudson.security.Permission;
import hudson.security.AccessControlled;
......@@ -23,6 +25,8 @@ import javax.servlet.ServletException;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.acegisecurity.AccessDeniedException;
import org.kohsuke.stapler.Stapler;
import org.acegisecurity.AccessDeniedException;
/**
* Base class that provides the framework for doing on-the-fly form field validation.
......@@ -59,10 +63,28 @@ public abstract class FormFieldValidator {
this(request, response, adminOnly?Hudson.getInstance():null, adminOnly?CHECK:null);
}
/**
* @deprecated
* Use {@link #FormFieldValidator(Permission)} and remove {@link StaplerRequest} and {@link StaplerResponse}
* from your "doCheck..." method parameter
*/
protected FormFieldValidator(StaplerRequest request, StaplerResponse response, Permission permission) {
this(request,response,Hudson.getInstance(),permission);
}
/**
* @param permission
* Permission needed to perform this validation, or null if no permission is necessary.
*/
protected FormFieldValidator(Permission permission) {
this(Stapler.getCurrentRequest(),Stapler.getCurrentResponse(),permission);
}
/**
* @deprecated
* Use {@link #FormFieldValidator(AccessControlled,Permission)} and remove {@link StaplerRequest} and {@link StaplerResponse}
* from your "doCheck..." method parameter
*/
protected FormFieldValidator(StaplerRequest request, StaplerResponse response, AccessControlled subject, Permission permission) {
this.request = request;
this.response = response;
......@@ -70,6 +92,10 @@ public abstract class FormFieldValidator {
this.permission = permission;
}
protected FormFieldValidator(AccessControlled subject, Permission permission) {
this(Stapler.getCurrentRequest(),Stapler.getCurrentResponse(),subject,permission);
}
/**
* Runs the validation code.
*/
......@@ -120,7 +146,11 @@ public abstract class FormFieldValidator {
public void warning(String message) throws IOException, ServletException {
warningWithMarkup(message==null?null:Util.escape(message));
}
public void ok(String message) throws IOException, ServletException {
okWithMarkup(message==null?null:Util.escape(message));
}
/**
* Sends out a string error message that indicates an error,
* by formatting it with {@link String#format(String, Object[])}
......@@ -133,6 +163,10 @@ public abstract class FormFieldValidator {
warning(String.format(format,args));
}
public void ok(String format, Object... args) throws IOException, ServletException {
ok(String.format(format,args));
}
/**
* Sends out an HTML fragment that indicates an error.
*
......@@ -152,6 +186,10 @@ public abstract class FormFieldValidator {
_errorWithMarkup(message,"warning");
}
public void okWithMarkup(String message) throws IOException, ServletException {
_errorWithMarkup(message,"ok");
}
private void _errorWithMarkup(String message, String cssClass) throws IOException, ServletException {
if(message==null) {
ok();
......@@ -501,4 +539,48 @@ public abstract class FormFieldValidator {
ok();
}
}
/**
* Verifies that the 'value' parameter is correct base64 encoded text.
*
* @since 1.257
*/
public static class Base64 extends FormFieldValidator {
private final boolean allowWhitespace;
private final boolean allowEmpty;
private final String errorMessage;
public Base64(StaplerRequest request, StaplerResponse response, boolean allowWhitespace, boolean allowEmpty, String errorMessage) {
super(request, response, false);
this.allowWhitespace = allowWhitespace;
this.allowEmpty = allowEmpty;
this.errorMessage = errorMessage;
}
protected void check() throws IOException, ServletException {
try {
String v = request.getParameter("value");
if(!allowWhitespace) {
if(v.indexOf(' ')>=0 || v.indexOf('\n')>=0) {
fail();
return;
}
}
v=v.trim();
if(!allowEmpty && v.length()==0) {
fail();
return;
}
com.trilead.ssh2.crypto.Base64.decode(v.toCharArray());
ok();
} catch (IOException e) {
fail();
}
}
protected void fail() throws IOException, ServletException {
error(errorMessage);
}
}
}
package hudson.util;
import hudson.remoting.Future;
import java.util.concurrent.TimeUnit;
/**
* Various {@link Future} implementations.
*
* @author Kohsuke Kawaguchi
*/
public class Futures {
/**
* Creates a {@link Future} instance that already has its value pre-computed.
*/
public static <T> Future<T> precomputed(final T value) {
return new Future<T>() {
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
public boolean isCancelled() {
return false;
}
public boolean isDone() {
return true;
}
public T get() {
return value;
}
public T get(long timeout, TimeUnit unit) {
return value;
}
};
}
}
package hudson.util;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.AxisState;
import org.jfree.chart.axis.CategoryTick;
import org.jfree.chart.axis.CategoryLabelPosition;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.entity.CategoryLabelEntity;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.RectangleAnchor;
import org.jfree.text.TextBlock;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Point2D;
import java.util.*;
/**
* This class implements X-axis label skipping algorithm to
* avoid drawing overlapping labels.
*
* @author Kohsuke Kawaguchi
*/
public class NoOverlapCategoryAxis extends CategoryAxis {
public NoOverlapCategoryAxis(String label) {
super(label);
}
@Override
protected AxisState drawCategoryLabels(Graphics2D g2,
Rectangle2D plotArea,
Rectangle2D dataArea,
RectangleEdge edge,
AxisState state,
PlotRenderingInfo plotState) {
if (state == null) {
throw new IllegalArgumentException("Null 'state' argument.");
}
if (isTickLabelsVisible()) {
java.util.List ticks = refreshTicks(g2, state, plotArea, edge);
state.setTicks(ticks);
// remember the last drawn label so that we can avoid drawing overlapping labels.
Rectangle2D r = null;
int categoryIndex = 0;
Iterator iterator = ticks.iterator();
while (iterator.hasNext()) {
CategoryTick tick = (CategoryTick) iterator.next();
g2.setFont(getTickLabelFont(tick.getCategory()));
g2.setPaint(getTickLabelPaint(tick.getCategory()));
CategoryLabelPosition position
= this.getCategoryLabelPositions().getLabelPosition(edge);
double x0 = 0.0;
double x1 = 0.0;
double y0 = 0.0;
double y1 = 0.0;
if (edge == RectangleEdge.TOP) {
x0 = getCategoryStart(categoryIndex, ticks.size(),
dataArea, edge);
x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
edge);
y1 = state.getCursor() - this.getCategoryLabelPositionOffset();
y0 = y1 - state.getMax();
}
else if (edge == RectangleEdge.BOTTOM) {
x0 = getCategoryStart(categoryIndex, ticks.size(),
dataArea, edge);
x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
edge);
y0 = state.getCursor() + this.getCategoryLabelPositionOffset();
y1 = y0 + state.getMax();
}
else if (edge == RectangleEdge.LEFT) {
y0 = getCategoryStart(categoryIndex, ticks.size(),
dataArea, edge);
y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
edge);
x1 = state.getCursor() - this.getCategoryLabelPositionOffset();
x0 = x1 - state.getMax();
}
else if (edge == RectangleEdge.RIGHT) {
y0 = getCategoryStart(categoryIndex, ticks.size(),
dataArea, edge);
y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
edge);
x0 = state.getCursor() + this.getCategoryLabelPositionOffset();
x1 = x0 - state.getMax();
}
Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0),
(y1 - y0));
if(r==null || !r.intersects(area)) {
Point2D anchorPoint = RectangleAnchor.coordinates(area,
position.getCategoryAnchor());
TextBlock block = tick.getLabel();
block.draw(g2, (float) anchorPoint.getX(),
(float) anchorPoint.getY(), position.getLabelAnchor(),
(float) anchorPoint.getX(), (float) anchorPoint.getY(),
position.getAngle());
Shape bounds = block.calculateBounds(g2,
(float) anchorPoint.getX(), (float) anchorPoint.getY(),
position.getLabelAnchor(), (float) anchorPoint.getX(),
(float) anchorPoint.getY(), position.getAngle());
if (plotState != null && plotState.getOwner() != null) {
EntityCollection entities
= plotState.getOwner().getEntityCollection();
if (entities != null) {
String tooltip = getCategoryLabelToolTip(
tick.getCategory());
entities.add(new CategoryLabelEntity(tick.getCategory(),
bounds, tooltip, null));
}
}
r = bounds.getBounds2D();
}
categoryIndex++;
}
if (edge.equals(RectangleEdge.TOP)) {
double h = state.getMax();
state.cursorUp(h);
}
else if (edge.equals(RectangleEdge.BOTTOM)) {
double h = state.getMax();
state.cursorDown(h);
}
else if (edge == RectangleEdge.LEFT) {
double w = state.getMax();
state.cursorLeft(w);
}
else if (edge == RectangleEdge.RIGHT) {
double w = state.getMax();
state.cursorRight(w);
}
}
return state;
}
}
package hudson.util;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.trilead.ssh2.crypto.Base64;
import hudson.model.Hudson;
import javax.crypto.SecretKey;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
/**
* Glorified {@link String} that uses encryption in the persisted form, to avoid accidental exposure of a secret.
*
* <p>
* Note that since the cryptography relies on {@link Hudson#getSecretKey()}, this is not meant as a protection
* against code running in the same VM, nor against an attacker who has local file system access.
*
* @author Kohsuke Kawaguchi
*/
public final class Secret {
/**
* Unencrypted secret text.
*/
private final String value;
private Secret(String value) {
this.value = value;
}
/**
* Obtains the secret text.
*/
public String toString() {
return value;
}
public boolean equals(Object that) {
return that instanceof Secret && value.equals(((Secret)that).value);
}
public int hashCode() {
return value.hashCode();
}
/**
* Turns {@link Hudson#getSecretKey()} into an AES key.
*/
private static SecretKey getKey() throws UnsupportedEncodingException, GeneralSecurityException {
String secret = SECRET;
if(secret==null) secret = Hudson.getInstance().getSecretKey();
// turn secretKey into 256 bit hash
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.reset();
digest.update(secret.getBytes("UTF-8"));
// Due to the stupid US export restriction JDK only ships 128bit version.
return new SecretKeySpec(digest.digest(),0,128/8, "AES");
}
/**
* Encrypts {@link #value} and returns it in an encoded printable form.
*/
public String getEncryptedValue() {
try {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, getKey());
// add the magic suffix which works like a check sum.
return new String(Base64.encode(cipher.doFinal((value+MAGIC).getBytes("UTF-8"))));
} catch (GeneralSecurityException e) {
throw new Error(e); // impossible
} catch (UnsupportedEncodingException e) {
throw new Error(e); // impossible
}
}
/**
* Reverse operation of {@link #getEncryptedValue()}. Returns null
* if the given cipher text was invalid.
*/
public static Secret decrypt(String data) {
if(data==null) return null;
try {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, getKey());
String plainText = new String(cipher.doFinal(Base64.decode(data.toCharArray())), "UTF-8");
if(plainText.endsWith(MAGIC))
return new Secret(plainText.substring(0,plainText.length()-MAGIC.length()));
return null;
} catch (GeneralSecurityException e) {
return null;
} catch (UnsupportedEncodingException e) {
throw new Error(e); // impossible
} catch (IOException e) {
return null;
}
}
/**
* Attempts to treat the given string first as a cipher text, and if it doesn't work,
* treat the given string as the unencrypted secret value.
*
* <p>
* Useful for recovering a value from a form field.
*
* @return never null
*/
public static Secret fromString(String data) {
Secret s = decrypt(data);
if(s==null) s=new Secret(data);
return s;
}
public static final class ConverterImpl implements Converter {
public ConverterImpl() {
}
public boolean canConvert(Class type) {
return type==Secret.class;
}
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
Secret src = (Secret) source;
writer.setValue(src.getEncryptedValue());
}
public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) {
return Secret.decrypt(reader.getValue());
}
}
private static final String MAGIC = "::::MAGIC::::";
/**
* For testing only. Override the secret key so that we can test this class without {@link Hudson}.
*/
/*package*/ static String SECRET = null;
}
......@@ -21,12 +21,8 @@ import java.util.Iterator;
/**
* {@link CategoryAxis} shifted to left to eliminate redundant space
* between area and the Y-axis.
*
* <p>
* This class also implements X-axis label skipping algorithm to
* avoid drawing overlapping labels.
*/
public final class ShiftedCategoryAxis extends CategoryAxis {
public final class ShiftedCategoryAxis extends NoOverlapCategoryAxis {
public ShiftedCategoryAxis(String label) {
super(label);
}
......@@ -51,118 +47,4 @@ public final class ShiftedCategoryAxis extends CategoryAxis {
- calculateCategorySize(categoryCount, area, edge) / 2;
}
@Override
protected AxisState drawCategoryLabels(Graphics2D g2,
Rectangle2D plotArea,
Rectangle2D dataArea,
RectangleEdge edge,
AxisState state,
PlotRenderingInfo plotState) {
if (state == null) {
throw new IllegalArgumentException("Null 'state' argument.");
}
if (isTickLabelsVisible()) {
List ticks = refreshTicks(g2, state, plotArea, edge);
state.setTicks(ticks);
// remember the last drawn label so that we can avoid drawing overlapping labels.
Rectangle2D r = null;
int categoryIndex = 0;
Iterator iterator = ticks.iterator();
while (iterator.hasNext()) {
CategoryTick tick = (CategoryTick) iterator.next();
g2.setFont(getTickLabelFont(tick.getCategory()));
g2.setPaint(getTickLabelPaint(tick.getCategory()));
CategoryLabelPosition position
= this.getCategoryLabelPositions().getLabelPosition(edge);
double x0 = 0.0;
double x1 = 0.0;
double y0 = 0.0;
double y1 = 0.0;
if (edge == RectangleEdge.TOP) {
x0 = getCategoryStart(categoryIndex, ticks.size(),
dataArea, edge);
x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
edge);
y1 = state.getCursor() - this.getCategoryLabelPositionOffset();
y0 = y1 - state.getMax();
}
else if (edge == RectangleEdge.BOTTOM) {
x0 = getCategoryStart(categoryIndex, ticks.size(),
dataArea, edge);
x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
edge);
y0 = state.getCursor() + this.getCategoryLabelPositionOffset();
y1 = y0 + state.getMax();
}
else if (edge == RectangleEdge.LEFT) {
y0 = getCategoryStart(categoryIndex, ticks.size(),
dataArea, edge);
y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
edge);
x1 = state.getCursor() - this.getCategoryLabelPositionOffset();
x0 = x1 - state.getMax();
}
else if (edge == RectangleEdge.RIGHT) {
y0 = getCategoryStart(categoryIndex, ticks.size(),
dataArea, edge);
y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
edge);
x0 = state.getCursor() + this.getCategoryLabelPositionOffset();
x1 = x0 - state.getMax();
}
Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0),
(y1 - y0));
if(r==null || !r.intersects(area)) {
Point2D anchorPoint = RectangleAnchor.coordinates(area,
position.getCategoryAnchor());
TextBlock block = tick.getLabel();
block.draw(g2, (float) anchorPoint.getX(),
(float) anchorPoint.getY(), position.getLabelAnchor(),
(float) anchorPoint.getX(), (float) anchorPoint.getY(),
position.getAngle());
Shape bounds = block.calculateBounds(g2,
(float) anchorPoint.getX(), (float) anchorPoint.getY(),
position.getLabelAnchor(), (float) anchorPoint.getX(),
(float) anchorPoint.getY(), position.getAngle());
if (plotState != null && plotState.getOwner() != null) {
EntityCollection entities
= plotState.getOwner().getEntityCollection();
if (entities != null) {
String tooltip = getCategoryLabelToolTip(
tick.getCategory());
entities.add(new CategoryLabelEntity(tick.getCategory(),
bounds, tooltip, null));
}
}
r = bounds.getBounds2D();
}
categoryIndex++;
}
if (edge.equals(RectangleEdge.TOP)) {
double h = state.getMax();
state.cursorUp(h);
}
else if (edge.equals(RectangleEdge.BOTTOM)) {
double h = state.getMax();
state.cursorDown(h);
}
else if (edge == RectangleEdge.LEFT) {
double w = state.getMax();
state.cursorLeft(w);
}
else if (edge == RectangleEdge.RIGHT) {
double w = state.getMax();
state.cursorRight(w);
}
}
return state;
}
}
......@@ -50,6 +50,7 @@ public class XStream2 extends XStream {
private void init() {
registerConverter(new RobustCollectionConverter(getMapper(),getReflectionProvider()),10);
registerConverter(new CopyOnWriteMap.Tree.ConverterImpl(getMapper()),10); // needs to override MapConverter
registerConverter(new DescribableList.ConverterImpl(getMapper()),10); // explicitly added to handle subtypes
// this should come after all the XStream's default simpler converters,
// but before reflection-based one kicks in.
......
<!--
Config page
-->
<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">
<l:layout norefresh="true" permission="${app.ADMINISTER}">
<st:include page="sidepanel.jelly"/>
<l:main-panel>
<f:form method="post" action="configSubmit">
<f:entry title="${%Name}" help="/help/system-config/master-slave/name.html">
<f:textbox name="name" value="${it.name}"
checkUrl="'${rootURL}/fieldCheck?errorText='+escape('${%Name is mandatory}')+'&amp;value='+escape(this.value)"/>
</f:entry>
<!-- main body of the configuration -->
<st:include it="${it.node}" page="configure-entries.jelly" />
<f:block>
<f:submit value="${%Save}"/>
</f:block>
</f:form>
</l:main-panel>
</l:layout>
</j:jelly>
<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:layout>
<st:include page="sidepanel.jelly" />
<l:main-panel>
<form method="post" action="doDelete">
${%Are you sure about deleting the slave?}
<f:submit value="${%Yes}" />
</form>
</l:main-panel>
</l:layout>
</j:jelly>
\ No newline at end of file
......@@ -18,7 +18,10 @@
<h1>
<img src="${imagesURL}/48x48/${it.icon}" width="48" height="48" alt=""/>
${it.caption} <span style="font-size:smaller">(${it.node.nodeDescription})</span>
${it.caption}
<j:if test="${!empty(it.node.nodeDescription)}">
<span style="font-size:smaller">(${it.node.nodeDescription})</span>
</j:if>
</h1>
<st:include from="${it.launcher}" page="main.jelly" optional="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">
<l:layout title="${it.displayName} Load Statistics">
<st:include page="sidepanel.jelly" />
<l:main-panel>
<st:include page="main.jelly" from="${it.loadStatistics}" />
</l:main-panel>
</l:layout>
</j:jelly>
......@@ -6,10 +6,13 @@
<l:side-panel>
<l:tasks>
<l:task icon="images/24x24/up.gif" href=".." title="${%Back to List}" />
<l:task icon="images/24x24/notepad.gif" href="${rootURL}/${it.url}builds" title="${%Build History}" />
<l:isAdmin>
<l:task icon="images/24x24/notepad.gif" href="script" title="${%Script Console}" />
</l:isAdmin>
<l:task icon="images/24x24/search.gif" href="${rootURL}/computer/${it.displayName}/" title="${%Status}" />
<l:task icon="images/24x24/edit-delete.gif" href="${url}/delete" title="${%Delete Slave}" permission="${it.DELETE}" />
<l:task icon="images/24x24/setting.gif" href="${url}/configure" title="${%Configure}" permission="${it.CONFIGURE}" />
<l:task icon="images/24x24/notepad.gif" href="${url}/builds" title="${%Build History}" />
<l:task icon="images/24x24/monitor.gif" href="load-statistics" title="${%Load Statistics}" />
<l:task icon="images/24x24/terminal.gif" href="script" title="${%Script Console}" permission="${app.ADMINISTER}" />
<st:include page="sidepanel2.jelly" optional="true" /><!-- hook for derived class to add more items -->
</l:tasks>
</l:side-panel>
</j:jelly>
<!--
2nd page in the "new slave" page for displaying the entire configuration entries for the selected job type.
-->
<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">
<l:layout norefresh="true" permission="${app.ADMINISTER}">
<st:include page="sidepanel.jelly"/>
<l:main-panel>
<f:form method="post" action="doCreateItem">
<f:entry title="${%Name}" help="/help/system-config/master-slave/name.html">
<f:textbox name="name" value="${request.getParameter('name')}"
checkUrl="'../fieldCheck?errorText='+escape('${%Name is mandatory}')+'&amp;value='+escape(this.value)"/>
</f:entry>
<!-- main body of the configuration -->
<j:set var="it" value="${null}" />
<st:include class="${requestScope.descriptor.clazz}" page="configure-entries.jelly" />
<f:block>
<input type="hidden" name="type" value="${request.getParameter('mode')}"/>
<f:submit value="${%Save}"/>
</f:block>
</f:form>
</l:main-panel>
</l:layout>
</j:jelly>
......@@ -37,6 +37,11 @@
</j:forEach>
</tr>
</j:forEach>
<!-- let clouds contribute fragments here -->
<j:forEach var="cloud" items="${app.clouds}">
<st:include it="${cloud}" page="computerSet.jelly" optional="true" />
</j:forEach>
</table>
<j:if test="${app.hasPermission(app.ADMINISTER)}">
<div align="right" style="margin-top:0.5em">
......
<!--
"New slave" page.
The flow of this is:
new.jelly -> ComputerSet.doCreateItem -> _new2.jelly -> ComputerSet.doDoCreateItem
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:s="/lib/form">
<l:layout norefresh="true">
<st:include page="sidepanel.jelly" />
<l:main-panel>
<j:getStatic var="slaves" className="hudson.slaves.NodeDescriptor" field="ALL" />
<n:form nameTitle="${%Node name}" copyTitle="${%Copy Existing Node}" copyNames="${it._slaveNames}"
descriptors="${slaves}" xmlns:n="/lib/hudson/newFromList" />
</l:main-panel>
</l:layout>
</j:jelly>
......@@ -6,6 +6,8 @@
<l:side-panel>
<l:tasks>
<l:task icon="images/24x24/up.gif" href="${rootURL}/" title="${%Back to Dashboard}" />
<l:task icon="images/24x24/new-computer.gif" href="new" title="${%New Node}" />
<l:task icon="images/24x24/setting.gif" href="${rootURL}/configureExecutors" title="${%Configure Executors}" />
</l:tasks>
<t:queue items="${app.queue.items}" />
<t:executors />
......
......@@ -12,6 +12,13 @@
<f:entry title="${%System Message}" help="/help/system-config/systemMessage.html">
<f:textarea name="system_message" value="${it.systemMessage}" />
</f:entry>
<f:entry title="${%# of executors}" help="/help/system-config/numExecutors.html">
<input type="text" name="numExecutors" class="setting-input number"
value="${it.numExecutors}"/>
</f:entry>
<j:if test="${!empty(it.slaves)}">
<f:slave-mode name="master.mode" node="${it}" />
</j:if>
<f:entry title="${%Quiet period}" help="/help/project-config/quietPeriod.html">
<input class="setting-input number" name="quiet_period"
type="text" value="${it.quietPeriod}"/>
......@@ -119,6 +126,17 @@
<local:globalConfig className="hudson.model.Jobs" field="PROPERTIES" />
<local:globalConfig className="hudson.model.PageDecorator" field="ALL" />
<j:if test="${!empty(h.getCloudDescriptors())}">
<f:section title="${%Cloud}">
<f:block>
<f:hetero-list name="cloud" hasHeader="true"
descriptors="${h.getCloudDescriptors()}"
items="${it.clouds}"
addCaption="${%Add a new cloud}"/>
</f:block>
</f:section>
</j:if>
<f:block>
<f:submit value="${%Save}" />
</f:block>
......
<!--
Config page
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
xmlns:t="/lib/hudson" xmlns:s="/lib/form">
<d:taglib uri="local">
<d:tag name="mode">
<s:entry title="${%Usage}" help="/help/system-config/master-slave/usage.html">
<select class="setting-input" name="${name}">
<j:forEach var="m" items="${h.getNodeModes()}">
<s:option value="${m.name}" selected="${m==node.mode}">${m.description}</s:option>
</j:forEach>
</select>
</s:entry>
</d:tag>
</d:taglib>
<l:layout norefresh="true" permission="${app.ADMINISTER}" xmlns:local="local">
<st:include page="sidepanel.jelly"/>
<l:main-panel>
<s:form method="post" action="configExecutorsSubmit">
<s:section title="${%Master/Slave Support}">
<s:entry title="${%Master}">
<table width="100%">
<s:entry title="${%Name}">
<b>${%Master}</b>
</s:entry>
<s:entry title="${%Description}">
<b>${%This Hudson server}</b>
</s:entry>
<s:entry title="${%# of executors}" help="/help/system-config/numExecutors.html">
<input type="text" name="numExecutors" class="setting-input number"
value="${it.numExecutors}"/>
</s:entry>
<s:entry title="${%Local FS root}">
<b>${it.rootDir}</b>
</s:entry>
<j:if test="${!empty(it.slaves)}">
<local:mode name="master.mode" node="${it}" />
</j:if>
</table>
</s:entry>
<s:entry title="${%Slaves}">
<s:repeatable var="s" items="${it.slaves}" name="slaves">
<table width="100%">
<s:entry title="${%Name}" help="/help/system-config/master-slave/name.html">
<s:textbox name="slave.name" value="${s.nodeName}"
checkUrl="'fieldCheck?errorText='+escape('${%Name is mandatory}')+'&amp;value='+escape(this.value)"/>
</s:entry>
<s:entry title="${%Description}" help="/help/system-config/master-slave/description.html">
<s:textbox name="slave.description" value="${s.nodeDescription}"/>
</s:entry>
<s:entry title="${%# of executors}"
help="/help/system-config/master-slave/numExecutors.html">
<s:textbox name="slave.numExecutors" value="${s.numExecutors}"
checkUrl="'fieldCheck?errorText='+escape('${%Number of executors is mandatory.}')+'&amp;type=number-positive&amp;value='+escape(this.value)"/>
</s:entry>
<s:entry title="${%Remote FS root}" help="/help/system-config/master-slave/remoteFS.html">
<s:textbox name="slave.remoteFS" value="${s.remoteFS}"
checkUrl="'remoteFSCheck?value='+escape(this.value)"/>
</s:entry>
<s:entry title="${%Labels}" help="/help/system-config/master-slave/label.html">
<s:textbox name="slave.label" value="${s.labelString}"/>
</s:entry>
<local:mode name="slave.mode" node="${s}" />
<s:dropdownList name="slave.launcher" title="${%Launch method}"
help="/help/system-config/master-slave/launcher.html">
<j:forEach var="d" items="${h.getComputerLauncherDescriptors()}" varStatus="loop">
<s:dropdownListBlock value="${d.clazz.name}" name="${d.displayName}"
selected="${s.launcher.descriptor==d}"
title="${d.displayName}">
<j:set var="descriptor" value="${d}"/>
<j:set var="instance"
value="${h.ifThenElse(s.launcher.descriptor==d,s.launcher,null)}"/>
<tr><td>
<input type="hidden" name="stapler-class" value="${d.clazz.name}" />
</td></tr>
<st:include from="${d}" page="${d.configPage}" optional="true"/>
</s:dropdownListBlock>
</j:forEach>
</s:dropdownList>
<!-- pointless to show this if there's only one option, which is the default -->
<j:if test="${h.getRetentionStrategyDescriptors().size() gt 1}">
<s:dropdownList name="slave.retentionStrategy" title="${%Availability}"
help="/help/system-config/master-slave/availability.html">
<j:forEach var="d" items="${h.getRetentionStrategyDescriptors()}">
<j:if test="${d != null}">
<s:dropdownListBlock value="${d.clazz.name}" name="${d.displayName}"
selected="${s.retentionStrategy.descriptor==d}"
title="${d.displayName}">
<j:set var="descriptor" value="${d}"/>
<j:set var="instance"
value="${h.ifThenElse(s.retentionStrategy.descriptor==d,s.retentionStrategy,null)}"/>
<tr><td>
<input type="hidden" name="stapler-class" value="${d.clazz.name}" />
</td></tr>
<st:include from="${d}" page="${d.configPage}" optional="true"/>
</s:dropdownListBlock>
</j:if>
</j:forEach>
</s:dropdownList>
</j:if>
<s:entry title="">
<div align="right">
<s:repeatableDeleteButton/>
</div>
</s:entry>
</table>
</s:repeatable>
</s:entry>
</s:section>
<s:block>
<s:submit value="${%Save}"/>
</s:block>
</s:form>
</l:main-panel>
</l:layout>
</j:jelly>
<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:layout title="${it.displayName} Load Statistics">
<st:include page="sidepanel.jelly" />
<l:main-panel>
<j:set var="prefix" value="overallLoad" />
<st:include page="main.jelly" from="${it.overallLoad}" />
</l:main-panel>
</l:layout>
</j:jelly>
......@@ -47,9 +47,6 @@
<local:feature icon="setting.gif" href="configure" title="${%Configure System}">
${%Configure global settings and paths.}
</local:feature>
<local:feature icon="setting.gif" href="configureExecutors" title="${%Configure Executors}">
${%Configure resources available for executing jobs.}
</local:feature>
<local:feature icon="refresh.gif" href="reload" title="${%Reload Configuration from Disk}">
${%Discard all the loaded data in memory and reload everything from file system.}
${%Useful when you modified config files directly on disk.}
......@@ -66,10 +63,13 @@
<local:feature icon="clipboard.gif" href="log" title="${%System Log}">
${%SystemLogText}
</local:feature>
<local:feature icon="monitor.gif" href="load-statistics" title="${%Load Statistics}">
${%LoadStatisticsText}
</local:feature>
<local:feature icon="notepad.gif" href="script" title="${%Script Console}">
${%Executes arbitrary script for administration/trouble-shooting/diagnostics.}
</local:feature>
<j:if test="${!empty(it.slaves)}">
<j:if test="${!empty(it.nodes) or !empty(h.getNodeFactoryDescriptors())}">
<local:feature icon="network.gif" href="computer/" title="${%Manage Slaves}">
${%Check the health of slaves and controls them.}
</local:feature>
......
SystemLogText=\
System log captures output from <tt>java.util.logging</tt> output related to Hudson.
NewVersionAvailable=New version of Hudson ({0}) is available for <a href="{1}">download</a>.
\ No newline at end of file
NewVersionAvailable=New version of Hudson ({0}) is available for <a href="{1}">download</a>.
LoadStatisticsText=Check your resource utilization and see if you need more computers for your builds.
\ No newline at end of file
<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:layout title="${it.displayName} Load Statistics">
<st:include page="sidepanel.jelly" />
<l:main-panel>
<st:include page="main.jelly" from="${it.loadStatistics}" />
</l:main-panel>
</l:layout>
</j:jelly>
......@@ -6,6 +6,8 @@
<l:side-panel>
<l:tasks>
<l:task icon="images/24x24/up.gif" href="${rootURL}/" title="${%Back to Dashboard}" />
<l:task icon="images/24x24/computer.gif" href="." title="${%Overview}" />
<l:task icon="images/24x24/monitor.gif" href="load-statistics" title="${%Load Statistics}" />
</l:tasks>
</l:side-panel>
</j:jelly>
\ No newline at end of file
</j:jelly>
<!-- renders an HTML fragment that shows trend graph -->
<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">
<h1>
<img src="${imagesURL}/48x48/monitor.gif" alt=""/>
${%title(it.displayName)}
</h1>
<div>
${%Timespan}:
<a href="?type=sec10">${%Short}</a>
<st:nbsp />
<a href="?type=min">${%Medium}</a>
<st:nbsp />
<a href="?type=hour">${%Long}</a>
</div>
<j:set var="type" value="${h.defaulted(request.getParameter('type'),'min')}" />
<img src="${h.defaulted(prefix,'loadStatistics')}/graph?type=${type}&amp;width=500&amp;height=300" />
<div style="margin-top: 2em;">
${%blurb}
</div>
</j:jelly>
title=Load statistics: {0}
blurb=\
Load statistics keep track of three key metrics of resource utilization: \
<dl> \
<dt>Total number of executors</dt> \
<dd>\
For a computer, this is the number of executors that the computer has. \
For a label, this is the sum of all executors across all computers in this label. \
For the entire Hudson, this is the sum of all executors across all computers in this Hudson installation. \
Other than configuration changes, this value can also change when slaves go offline. \
</dd> \
<dt>Number of busy executors</dt> \
<dd>\
This line tracks the number of executors (among the executors counted above) \
that are carrying out builds. The ratio of this to the total number of executors \
gives you the resource utilization. If all your executors are busy for \
a prolonged period of time, consider adding more computers to your Hudson cluster.\
</dd>\
<dt>Queue length</dt>\
<dd>\
This is the number of jobs that are in the build queue, waiting for an \
available executor (of this computer, of this label, or in this Hudson, respectively.) \
This doesn't include jobs that are in the quiet period, nor does it include \
jobs that are in the queue because earlier builds are still in progress. \
If this line ever goes above 0, that means your Hudson will run more builds by \
adding more computers.\
</dd>\
</dl>\
The graph is exponential moving average of periodically collected data values. \
3 timespans are updated every 10 seconds, 1 minute, and 1 hour respectively.
\ No newline at end of file
......@@ -33,6 +33,9 @@ BallColor.Unstable=Unstable
Computer.Caption=Slave {0}
ComputerSet.NoSuchSlave=No such slave: {0}
ComputerSet.SlaveAlreadyExists=Slave called ''{0}'' already exists
ComputerSet.SpecifySlaveToCopy=Specify which slave to copy
Executor.NotAvailable=N/A
ExternalJob.DisplayName=Monitor an external job
......
......@@ -6,57 +6,9 @@
<l:layout norefresh="true" permission="${permission}">
<st:include page="sidepanel.jelly" />
<l:main-panel>
<s:form method="post" action="createItem">
<s:entry title="${%Job name}">
<s:textbox id="name" name="name" checkUrl="'${rootURL}/itemExistsCheck?value='+escape(this.value)"
onchange="updateOk(this.form)" onkeyup="updateOk(this.form)" />
<script>$('name').focus();</script>
</s:entry>
<j:getStatic var="jobs" className="hudson.model.Items" field="LIST" />
<j:forEach var="d" items="${jobs}">
<s:block>
<j:set var="id" value="${h.generateId()}"/>
<input type="radio" name="mode" value="${d.displayName}" onchange="updateOk(this.form)" onclick="updateOk(this.form)" id="${id}" />
<st:nbsp/>
<label for="${id}"><b>${d.displayName}</b></label>
</s:block>
<s:entry>
<st:include page="${d.newJobDetailPage}" from="${d}" optional="true"/>
</s:entry>
</j:forEach>
<s:block>
<j:set var="id" value="${h.generateId()}"/>
<input type="radio" name="mode" value="copyJob" onchange="updateOk(this.form)" onclick="updateOk(this.form)" id="${id}"/>
<st:nbsp/>
<label for="${id}"><b>${%Copy existing job}</b></label>
</s:block>
<s:entry>
${%Copy from}
<s:editableComboBox id="from" name="from" items="${app.topLevelItemNames}" />
</s:entry>
<s:block>
<input type="submit" name="Submit" value="OK" id="ok" style="margin-left:5em" />
</s:block>
</s:form>
<script><![CDATA[
var okButton = makeButton($('ok'),null);
function updateOk(form) {
function state() {
if($('name').value.length==0) return true;
var radio = form.elements['mode'];
for(i=0;i<radio.length;i++)
if(radio[i].checked)
return false;
return true;
}
okButton.set('disabled',state(),false);
}
updateOk(okButton.getForm());
]]></script>
<j:getStatic var="jobs" className="hudson.model.Items" field="LIST" />
<n:form nameTitle="${%Job name}" copyTitle="${%Copy existing job}" copyNames="${app.topLevelItemNames}"
descriptors="${jobs}" xmlns:n="/lib/hudson/newFromList" />
</l:main-panel>
</l:layout>
</j:jelly>
<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">
<st:include it="${it.core}" page="main.jelly" />
</j:jelly>
\ No newline at end of file
<!--
Config page
-->
<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">
<f:entry title="${%Description}" help="/help/system-config/master-slave/description.html">
<f:textbox name="description" value="${it.nodeDescription}"/>
</f:entry>
<f:entry title="${%# of executors}"
help="/help/system-config/master-slave/numExecutors.html">
<f:textbox name="numExecutors" value="${it.numExecutors}"
checkUrl="'/fieldCheck?errorText='+escape('${%Number of executors is mandatory.}')+'&amp;type=number-positive&amp;value='+escape(this.value)"/>
</f:entry>
<f:entry title="${%Remote FS root}" help="/help/system-config/master-slave/remoteFS.html">
<f:textbox name="remoteFS" value="${it.remoteFS}"
checkUrl="'/remoteFSCheck?value='+escape(this.value)"/>
</f:entry>
<f:entry title="${%Labels}" help="/help/system-config/master-slave/label.html">
<f:textbox name="label" value="${it.labelString}"/>
</f:entry>
<f:slave-mode name="mode" node="${s}" />
<f:dropdownList name="slave.launcher" title="${%Launch method}"
help="/help/system-config/master-slave/launcher.html">
<j:forEach var="d" items="${h.getComputerLauncherDescriptors()}" varStatus="loop">
<f:dropdownListBlock value="${d.clazz.name}" name="${d.displayName}"
selected="${it.launcher.descriptor==d}"
title="${d.displayName}">
<j:set var="descriptor" value="${d}"/>
<j:set var="instance"
value="${h.ifThenElse(it.launcher.descriptor==d,it.launcher,null)}"/>
<tr><td>
<input type="hidden" name="stapler-class" value="${d.clazz.name}" />
</td></tr>
<st:include from="${d}" page="${d.configPage}" optional="true"/>
</f:dropdownListBlock>
</j:forEach>
</f:dropdownList>
<!-- pointless to show this if there's only one option, which is the default -->
<j:if test="${h.getRetentionStrategyDescriptors().size() gt 1}">
<f:dropdownList name="slave.retentionStrategy" title="${%Availability}"
help="/help/system-config/master-slave/availability.html">
<j:forEach var="d" items="${h.getRetentionStrategyDescriptors()}">
<j:if test="${d != null}">
<f:dropdownListBlock value="${d.clazz.name}" name="${d.displayName}"
selected="${it.retentionStrategy.descriptor==d}"
title="${d.displayName}">
<j:set var="descriptor" value="${d}"/>
<j:set var="instance"
value="${h.ifThenElse(it.retentionStrategy.descriptor==d,it.retentionStrategy,null)}"/>
<tr><td>
<input type="hidden" name="stapler-class" value="${d.clazz.name}" />
</td></tr>
<st:include from="${d}" page="${d.configPage}" optional="true"/>
</f:dropdownListBlock>
</j:if>
</j:forEach>
</f:dropdownList>
</j:if>
</j:jelly>
<div>
${%detail}
</div>
\ No newline at end of file
detail=\
Adds a plain, dumb slave to Hudson. This is called "dumb" because Hudson doesn''t provide \
higher level of integration with these slaves, such as dynamic provisioning. \
Select this type if no other slave types apply &mdash; for example such as when you are adding \
a physical computer, virtual machines managed outside Hudson, etc.
\ No newline at end of file
......@@ -3,11 +3,11 @@
<f:entry title="${%In demand delay}" help="/help/system-config/master-slave/demand/inDemandDelay.html">
<input class="setting-input number validated" name="retentionStrategy.inDemandDelay"
type="text" value="${instance.inDemandDelay}"
checkUrl="'fieldCheck?errorText='+escape('${%In demand delay is mandatory.}')+'&amp;value='+escape(this.value)"/>
checkUrl="'${rootURL}/fieldCheck?errorText='+escape('${%In demand delay is mandatory.}')+'&amp;value='+escape(this.value)"/>
</f:entry>
<f:entry title="${%Idle delay}" help="/help/system-config/master-slave/demand/idleDelay.html">
<input class="setting-input number validated" name="retentionStrategy.idleDelay"
type="text" value="${instance.idleDelay}"
checkUrl="'fieldCheck?errorText='+escape('${%Idle delay is mandatory.}')+'&amp;value='+escape(this.value)"/>
checkUrl="'${rootURL}/fieldCheck?errorText='+escape('${%Idle delay is mandatory.}')+'&amp;value='+escape(this.value)"/>
</f:entry>
</j:jelly>
\ No newline at end of file
<!--
Side panel for a slave.
-->
<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 title="${it.displayName}" />
<l:side-panel>
<l:tasks>
<l:task icon="images/24x24/up.gif" href=".." title="${%Back to List}" />
<l:task icon="images/24x24/search.gif" href="${rootURL}/computer/${it.displayName}/" title="${%Status}" />
<l:task icon="images/24x24/notepad.gif" href="${rootURL}/${it.url}builds" title="${%Build History}" />
<l:isAdmin>
<l:task icon="images/24x24/clipboard.gif" href="log" title="${%Log}" />
<l:task icon="images/24x24/computer.gif" href="systemInfo" title="${%System Information}" />
<j:if test="${it.channel!=null}">
<l:task icon="images/24x24/edit-delete.gif" href="disconnect" title="${%Disconnect}" />
</j:if>
</l:isAdmin>
</l:tasks>
</l:side-panel>
</j:jelly>
\ No newline at end of file
<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:isAdmin>
<l:task icon="images/24x24/clipboard.gif" href="log" title="${%Log}" />
<l:task icon="images/24x24/computer.gif" href="systemInfo" title="${%System Information}" />
<j:if test="${it.channel!=null}">
<l:task icon="images/24x24/edit-delete.gif" href="disconnect" title="${%Disconnect}" />
</j:if>
</l:isAdmin>
</j:jelly>
\ No newline at end of file
<!--
Binds an enum field to a <select> element.
Attributes:
field: specifies the field of the bean to be bound
Body:
body should render the text of the enum item (which is passed as 'it')
-->
<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">
<select class="setting-input" name="${field}">
<j:forEach var="it" items="${descriptor.getPropertyType(field).enumConstants}">
<f:option value="${it.name()}" selected="${it==instance[field]}">
<d:invokeBody />
</f:option>
</j:forEach>
</select>
</j:jelly>
......@@ -2,7 +2,12 @@
Glorified <input type="password">
-->
<j:jelly xmlns:j="jelly:core" xmlns:d="jelly:define">
<input class="setting-input ${h.ifThenElse(attrs.checkUrl!=null,'validated','')}" name="${attrs.name}" id="${attrs.id}"
type="password" value="${attrs.value}" checkUrl="${attrs.checkUrl}" checkMethod="${attrs.checkMethod}"
<j:set var="checkUrl" value="${h.getCheckUrl(attrs.checkUrl,descriptor,attrs.field)}" />
<input class="setting-input ${h.ifThenElse(checkUrl!=null,'validated','')}"
name ="${h.defaulted(attrs.name,'_.'+attrs.field)}"
value="${h.defaulted(attrs.value,instance[attrs.field])}"
id="${attrs.id}"
type="password"
checkUrl="${checkUrl}" checkMethod="${attrs.checkMethod}"
onchange="${attrs.onchange}" onkeyup="${attrs.onkeyup}"/>
</j:jelly>
\ No newline at end of file
......@@ -64,7 +64,23 @@
</st:documentation>
<j:set var="name" value="${h.ifThenElse(attrs.name!=null,attrs.name,attrs.var)}"/>
<!--
If bi-directional binding, fill in the rest of attributes automatically
-->
<j:choose>
<j:when test="${attrs.field!=null}">
<j:set var="name" value="${field}"/>
<j:set var="var" value="instance"/>
<j:set var="items" value="${instance[name]}"/>
<!-- and expose update descriptor to the body of this tag -->
<j:set var="descriptor" value="${descriptor.getPropertyType(field).itemTypeDescriptor}" />
</j:when>
<j:otherwise>
<j:set var="name" value="${h.ifThenElse(attrs.name!=null,attrs.name,attrs.var)}"/>
</j:otherwise>
</j:choose>
<div class="repeated-container">
<!-- The first DIV is the master copy. -->
<div class="repeated-chunk" style="display:none" name="${name}">
......
<!--
listbox for choosing the slave's usage.
@name : name of the <select> element
@node : Node object
-->
<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">
<f:entry title="${%Usage}" help="/help/system-config/master-slave/usage.html">
<select class="setting-input" name="${name}">
<j:forEach var="m" items="${h.getNodeModes()}">
<f:option value="${m.name}" selected="${m==node.mode}">${m.description}</f:option>
</j:forEach>
</select>
</f:entry>
</j:jelly>
\ No newline at end of file
<!--
See http://hudson.gotdns.com/wiki/display/HUDSON/Jelly+form+controls for the reference.
-->
<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">
<f:nested>
<div style="float:right">
<input type="button" value="${title}" class="yui-button" onclick="validateButton('${rootURL}/descriptor/${descriptor.clazz.name}/${method}','${with}',this)" />
</div>
<div style="display:none;">
<img src="${imagesURL}/spinner.gif" /> ${progress}
</div>
<div><!-- this is where the error message goes --></div>
</f:nested>
</j:jelly>
\ No newline at end of file
......@@ -2,7 +2,7 @@
displays the status of executors.
-->
<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">
<l:pane title="${%Build Executor Status}" width="3" id="executors">
<l:pane title="&lt;a href='${rootURL}/computer/'>${%Build Executor Status}&lt;/a>" width="3" id="executors">
<j:forEach var="c" items="${app.computers}" varStatus="cloop">
<j:choose>
<j:when test="${c.node==app}">
......
<!--
Overall form that should be placed inside <l:main-panel>
Attributes:
@descriptors : collection of Descriptors
@nameTitle : title of the name text field
@copyTitle : title of the copy option
@copyNames : options of names to choose from as the copy source. null to hide "copy" option
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:s="/lib/form">
<s:form method="post" action="createItem">
<s:entry title="${nameTitle}">
<s:textbox id="name" name="name" checkUrl="'${rootURL}/itemExistsCheck?value='+escape(this.value)"
onchange="updateOk(this.form)" onkeyup="updateOk(this.form)" />
<script>$('name').focus();</script>
</s:entry>
<j:forEach var="d" items="${descriptors}">
<s:block>
<input type="radio" name="mode" value="${d.class.name}" onchange="updateOk(this.form)" onclick="updateOk(this.form)" />
<st:nbsp/>
<label class="attach-previous"><b>${d.displayName}</b></label>
</s:block>
<s:entry>
<st:include page="${d.newInstanceDetailPage()}" from="${d}" optional="true"/>
</s:entry>
</j:forEach>
<j:if test="${!empty(copyNames)}">
<s:block>
<input type="radio" name="mode" value="copy" onchange="updateOk(this.form)" onclick="updateOk(this.form)" />
<st:nbsp/>
<label class="attach-previous"><b>${copyTitle}</b></label>
</s:block>
<s:entry>
${%Copy from}
<s:editableComboBox id="from" name="from" items="${copyNames}" />
</s:entry>
</j:if>
<s:block>
<!--
when there's only one radio above, form.elements['mode]' won't return an array, which makes the script complex.
So always force non-empty array
-->
<input type="radio" name="mode" value="dummy1" style="display:none" />
<input type="radio" name="mode" value="dummy2" style="display:none" />
<input type="submit" name="Submit" value="OK" id="ok" style="margin-left:5em" />
</s:block>
</s:form>
<script><![CDATA[
var okButton = makeButton($('ok'),null);
function updateOk(form) {
function state() {
if($('name').value.length==0) return true;
var radio = form.elements['mode'];
for(i=0;i<radio.length;i++)
if(radio[i].checked)
return false;
return true;
}
okButton.set('disabled',state(),false);
}
updateOk(okButton.getForm());
]]></script>
</j:jelly>
These tags provide a higher level primitive for building a form page for creating a new item from a list of descriptors.
Used in "create new job" page, for an example.
\ No newline at end of file
package hudson.model;
import hudson.model.MultiStageTimeSeries.TimeScale;
import junit.framework.TestCase;
import org.jfree.chart.JFreeChart;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* @author Kohsuke Kawaguchi
*/
public class LoadStatisticsTest extends TestCase {
public void testGraph() throws IOException {
LoadStatistics ls = new LoadStatistics(0, 0) {
public int computeIdleExecutors() {
throw new UnsupportedOperationException();
}
public int computeTotalExecutors() {
throw new UnsupportedOperationException();
}
public int computeQueueLength() {
throw new UnsupportedOperationException();
}
};
for(int i=0;i<50;i++) {
ls.totalExecutors.update(4);
ls.busyExecutors.update(3);
ls.queueLength.update(3);
}
for(int i=0;i<50;i++) {
ls.totalExecutors.update(0);
ls.busyExecutors.update(0);
ls.queueLength.update(1);
}
JFreeChart chart = ls.createChart(ls.createDataset(TimeScale.SEC10));
BufferedImage image = chart.createBufferedImage(400,200);
ImageIO.write(image, "PNG", new FileOutputStream("chart.png"));
}
}
package hudson.model;
import junit.framework.TestCase;
/**
* @author Kohsuke Kawaguchi
*/
public class TimeSeriesTest extends TestCase {
public void test1() {
TimeSeries ts = new TimeSeries(0,1-0.1f,100);
float last;
assertEquals(0f,last=ts.getLatest());
for( int i=0; i<100; i++ ) {
assertEquals(ts.getHistory().length,i+1);
ts.update(1);
assertTrue(last<=ts.getLatest());
assertTrue(ts.getLatest()<=1);
last = ts.getLatest();
}
for( int i=0; i<100; i++ )
ts.update(1);
}
}
package hudson.slaves;
import junit.framework.TestCase;
import hudson.model.Hudson;
import hudson.model.Node;
import hudson.model.TaskListener;
import hudson.model.Computer;
import hudson.model.Label;
import hudson.model.TopLevelItem;
import hudson.XmlFile;
import hudson.Launcher;
import hudson.FilePath;
import hudson.security.ACL;
import hudson.security.Permission;
import hudson.util.ClockDifference;
import java.io.File;
import java.io.IOException;
import java.util.Set;
import org.apache.commons.io.FileUtils;
/**
* @author Kohsuke Kawaguchi
*/
public class NodeListTest extends TestCase {
static class DummyNode implements Node {
public String getNodeName() {
throw new UnsupportedOperationException();
}
public void setNodeName(String name) {
throw new UnsupportedOperationException();
}
public String getNodeDescription() {
throw new UnsupportedOperationException();
}
public Launcher createLauncher(TaskListener listener) {
throw new UnsupportedOperationException();
}
public int getNumExecutors() {
throw new UnsupportedOperationException();
}
public Mode getMode() {
throw new UnsupportedOperationException();
}
public Computer toComputer() {
throw new UnsupportedOperationException();
}
public Computer createComputer() {
throw new UnsupportedOperationException();
}
public Set<Label> getAssignedLabels() {
throw new UnsupportedOperationException();
}
public Set<Label> getDynamicLabels() {
throw new UnsupportedOperationException();
}
public Label getSelfLabel() {
throw new UnsupportedOperationException();
}
public FilePath getWorkspaceFor(TopLevelItem item) {
throw new UnsupportedOperationException();
}
public FilePath getRootPath() {
throw new UnsupportedOperationException();
}
public FilePath createPath(String absolutePath) {
throw new UnsupportedOperationException();
}
public ClockDifference getClockDifference() throws IOException, InterruptedException {
throw new UnsupportedOperationException();
}
public NodeDescriptor getDescriptor() {
throw new UnsupportedOperationException();
}
public ACL getACL() {
throw new UnsupportedOperationException();
}
public void checkPermission(Permission permission) {
throw new UnsupportedOperationException();
}
public boolean hasPermission(Permission permission) {
throw new UnsupportedOperationException();
}
}
static class EphemeralNode extends DummyNode implements hudson.slaves.EphemeralNode {
public Cloud getCloud() {
throw new UnsupportedOperationException();
}
}
public void testSerialization() throws Exception {
NodeList nl = new NodeList();
nl.add(new DummyNode());
nl.add(new EphemeralNode());
File tmp = File.createTempFile("test","test");
try {
XmlFile x = new XmlFile(Hudson.XSTREAM, tmp);
x.write(nl);
String xml = FileUtils.readFileToString(tmp);
System.out.println(xml);
assertEquals(4,xml.split("\n").length);
NodeList back = (NodeList)x.read();
assertEquals(1,back.size());
assertEquals(DummyNode.class,back.get(0).getClass());
} finally {
tmp.delete();
}
}
}
package hudson.util;
import org.junit.Assert;
import org.junit.Test;
public class ArgumentListBuilderTest extends Assert {
public static void assertArrayEquals(String msg, boolean[] expected, boolean[] actual) {
assertArrayEquals(msg,box(expected),box(actual));
}
private static Boolean[] box(boolean[] a) {
if(a==null) return null;
Boolean[] r = new Boolean[a.length];
for (int i = 0; i < a.length; i++)
r[i] = a[i];
return r;
}
@Test
public void assertEmptyMask() {
ArgumentListBuilder builder = new ArgumentListBuilder();
builder.add("arg");
builder.add("other", "arguments");
assertFalse("There shouldnt be any masked arguments", builder.hasMaskedArguments());
boolean[] array = builder.toMaskArray();
assertNotNull("The mask array should not be null", array);
assertArrayEquals("The mask array was incorrect", new boolean[]{false,false,false}, array);
}
@Test
public void assertLastArgumentIsMasked() {
ArgumentListBuilder builder = new ArgumentListBuilder();
builder.add("arg");
builder.addMasked("ismasked");
assertTrue("There should be masked arguments", builder.hasMaskedArguments());
boolean[] array = builder.toMaskArray();
assertNotNull("The mask array should not be null", array);
assertArrayEquals("The mask array was incorrect", new boolean[]{false,true}, array);
}
@Test
public void assertSeveralMaskedArguments() {
ArgumentListBuilder builder = new ArgumentListBuilder();
builder.add("arg");
builder.addMasked("ismasked");
builder.add("non masked arg");
builder.addMasked("ismasked2");
assertTrue("There should be masked arguments", builder.hasMaskedArguments());
boolean[] array = builder.toMaskArray();
assertNotNull("The mask array should not be null", array);
assertArrayEquals("The mask array was incorrect", new boolean[]{false,true, false, true}, array);
}
@Test
public void assertPrependAfterAddingMasked() {
ArgumentListBuilder builder = new ArgumentListBuilder();
builder.addMasked("ismasked");
builder.add("arg");
builder.prepend("first", "second");
assertTrue("There should be masked arguments", builder.hasMaskedArguments());
boolean[] array = builder.toMaskArray();
assertNotNull("The mask array should not be null", array);
assertArrayEquals("The mask array was incorrect", new boolean[]{false,false,true,false}, array);
}
@Test
public void assertPrependBeforeAddingMasked() {
ArgumentListBuilder builder = new ArgumentListBuilder();
builder.prepend("first", "second");
builder.addMasked("ismasked");
builder.add("arg");
assertTrue("There should be masked arguments", builder.hasMaskedArguments());
boolean[] array = builder.toMaskArray();
assertNotNull("The mask array should not be null", array);
assertArrayEquals("The mask array was incorrect", new boolean[]{false,false,true,false}, array);
}
}
package hudson.util;
import junit.framework.TestCase;
import java.security.SecureRandom;
import hudson.Util;
import hudson.model.Hudson;
/**
* @author Kohsuke Kawaguchi
*/
public class SecretTest extends TestCase {
protected void setUp() throws Exception {
SecureRandom sr = new SecureRandom();
byte[] random = new byte[32];
sr.nextBytes(random);
Secret.SECRET = Util.toHexString(random);
}
protected void tearDown() throws Exception {
Secret.SECRET = null;
}
public void testEncrypt() {
Secret secret = Secret.fromString("abc");
assertEquals("abc",secret.toString());
// make sure we got some encryption going
System.out.println(secret.getEncryptedValue());
assertTrue(!"abc".equals(secret.getEncryptedValue()));
// can we round trip?
assertEquals(secret,Secret.fromString(secret.getEncryptedValue()));
}
public void testDecrypt() {
assertEquals("abc",Secret.fromString("abc").toString());
}
public void testSerialization() {
Secret s = Secret.fromString("Mr.Hudson");
String xml = Hudson.XSTREAM.toXML(s);
System.out.println(xml);
assertTrue(!xml.contains(s.toString()));
assertTrue(xml.contains(s.getEncryptedValue()));
Object o = Hudson.XSTREAM.fromXML(xml);
assertEquals(o,s);
}
}
......@@ -18,4 +18,4 @@
<version>${maven.version}</version>
</dependency>
</dependencies>
</project>
\ No newline at end of file
</project>
......@@ -77,6 +77,13 @@
<artifactId>htmlunit</artifactId>
<version>2.2-hudson-9</version>
</dependency>
<dependency><!-- temporary, until we bump to new htmlunit -->
<groupId>org.jvnet.hudson</groupId>
<artifactId>htmlunit-core-js</artifactId>
<version>2.2-hudson-2</version>
</dependency>
<dependency>
<!-- for testing JNLP launch. -->
<groupId>org.jvnet.hudson</groupId>
......
<j:jelly xmlns:j="jelly:core" />
\ No newline at end of file
此差异已折叠。
......@@ -99,7 +99,7 @@ public class JNLPLauncherTest extends HudsonTestCase {
private Computer addTestSlave() throws Exception {
List<Slave> slaves = new ArrayList<Slave>(hudson.getSlaves());
File dir = Util.createTempDir();
slaves.add(new Slave("test","dummy",dir.getAbsolutePath(),"1", Mode.NORMAL, "",
slaves.add(new DumbSlave("test","dummy",dir.getAbsolutePath(),"1", Mode.NORMAL, "",
new JNLPLauncher(), RetentionStrategy.INSTANCE));
hudson.setSlaves(slaves);
Computer c = hudson.getComputer("test");
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册