commit 8a0dc230f44e84e5a7f7920cf9a31f09a54999ac Author: kohsuke Date: Sun Nov 5 21:16:01 2006 +0000 initial commit for a new layout. git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@969 71c3de6d-444a-0410-be80-ed276b4c234a diff --git a/core/bin/hudson b/core/bin/hudson new file mode 100644 index 0000000000..c43298b3c4 --- /dev/null +++ b/core/bin/hudson @@ -0,0 +1,32 @@ +#!/bin/sh +if [ "$HUDSON_HOME" = "" ]; then + echo HUDSON_HOME is not set + exit 1 +fi + + +# search the installation directory + +PRG=$0 +progname=`basename $0` +saveddir=`pwd` + +cd "`dirname $PRG`" + +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '.*/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname $PRG`/$link" + fi +done + +BINDIR=`dirname "$PRG"`/.. + +# make it fully qualified +cd "$saveddir" +BINDIR="`cd "$BINDIR" && pwd`" + +exec java -jar "$BINDIR/lib/hudson.jar" "$@" diff --git a/core/bin/slave b/core/bin/slave new file mode 100644 index 0000000000..a40cfd5e58 --- /dev/null +++ b/core/bin/slave @@ -0,0 +1,51 @@ +#!/bin/sh +# Usage: slave ... -- ... +# +# This wrapper is used to launch a process remotely with some environment variables +# set. + +# if there's any environment entries read them +if [ -f $HOME/.hudson_slave_profile ]; +then + . $HOME/.hudson_slave_profile +fi + +# set the current directory +cd "$1" +shift + +# fill in environment variables +while [ $# -gt 1 -a "$1" != "--" ]; +do + eval $1="$2" + export $1 + shift 2 +done + +if [ "$1" != "--" ]; +then + echo Error: no command given + exit -1 +fi + +shift + +# execute. use eval so that variables can be expanded now +# this allows users to specify $HOME or $DISPLAY or anything else, +# and it works as expected. +# +# but since eval mess up with whitespace ('eval ls "a b"' means the same as 'eval ls a b') +# we need to re-escape arguments (we need to say 'eval ls \"a b\"' to achieve the desired effect) +list="" +for a in "$@" +do + list="$list \"$a\"" +done +eval "$list" +ret=$? +# these additional hooks seem to prevent "select: bad filer number" error +# on some systems, so use this as a precaution. We can afford to waste +# one second, can't we? +sleep 1 +echo +exit $ret diff --git a/core/src/main/grammar/crontab.g b/core/src/main/grammar/crontab.g new file mode 100644 index 0000000000..a972058d36 --- /dev/null +++ b/core/src/main/grammar/crontab.g @@ -0,0 +1,88 @@ +header { + package hudson.scheduler; +} + +class CrontabParser extends Parser("BaseParser"); +options { + defaultErrorHandler=false; +} + +startRule [CronTab table] +throws ANTLRException +{ + long m,h,d,mnth,dow; +} + : m=expr[0] WS h=expr[1] WS d=expr[2] WS mnth=expr[3] WS dow=expr[4] EOF + { + table.bits[0]=m; + table.bits[1]=h; + table.bits[2]=d; + table.bits[3]=mnth; + table.dayOfWeek=(int)dow; + } + ; + +expr [int field] +returns [long bits=0] +throws ANTLRException +{ + long lhs,rhs=0; +} + : lhs=term[field] ("," rhs=expr[field])? + { + bits = lhs|rhs; + } + ; + +term [int field] +returns [long bits=0] +throws ANTLRException +{ + int d=1,s,e,t; +} + : (token "-")=> s=token "-" e=token ( "/" d=token )? + { + bits = doRange(s,e,d,field); + } + | t=token + { + rangeCheck(t,field); + bits = 1L< masterEnvVars; + + static { + Vector envs = Execute.getProcEnvironment(); + Map m = new HashMap(); + for (String e : envs) { + int idx = e.indexOf('='); + m.put(e.substring(0, idx), e.substring(idx + 1)); + } + masterEnvVars = Collections.unmodifiableMap(m); + } + +} diff --git a/core/src/main/java/hudson/ExtensionPoint.java b/core/src/main/java/hudson/ExtensionPoint.java new file mode 100644 index 0000000000..45e5af2a21 --- /dev/null +++ b/core/src/main/java/hudson/ExtensionPoint.java @@ -0,0 +1,16 @@ +package hudson; + +/** + * Marker interface that designates extensible components + * in Hudson that can be implemented by {@link Plugin}s. + * + *

+ * Interfaces/classes that implement this interface can be extended by plugins. + * See respective interfaces/classes for more about how to register custom + * implementations to Hudson. + * + * @author Kohsuke Kawaguchi + * @see Plugin + */ +public interface ExtensionPoint { +} diff --git a/core/src/main/java/hudson/FeedAdapter.java b/core/src/main/java/hudson/FeedAdapter.java new file mode 100644 index 0000000000..49baf94320 --- /dev/null +++ b/core/src/main/java/hudson/FeedAdapter.java @@ -0,0 +1,15 @@ +package hudson; + +import java.util.Calendar; + +/** + * Provides a RSS feed view of the data. + * + * @author Kohsuke Kawaguchi + */ +public interface FeedAdapter { + String getEntryTitle(E entry); + String getEntryUrl(E entry); + String getEntryID(E entry); + Calendar getEntryTimestamp(E entry); +} diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java new file mode 100644 index 0000000000..6e80d920eb --- /dev/null +++ b/core/src/main/java/hudson/FilePath.java @@ -0,0 +1,143 @@ +package hudson; + +import java.io.File; +import java.io.IOException; + +/** + * {@link File} like path-manipulation object. + * + *

+ * In general, because programs could be executed remotely, + * we need two path strings to identify the same directory. + * One from a point of view of the master (local), the other + * from a point of view of the slave (remote). + * + *

+ * This class allows path manipulation to be done + * and allow the local/remote versions to be obtained + * after the computation. + * + * @author Kohsuke Kawaguchi + */ +public final class FilePath { + private final File local; + private final String remote; + + public FilePath(File local, String remote) { + this.local = local; + this.remote = remote; + } + + /** + * Useful when there's no remote path. + */ + public FilePath(File local) { + this(local,local.getPath()); + } + + public FilePath(FilePath base, String rel) { + this.local = new File(base.local,rel); + if(base.isUnix()) { + this.remote = base.remote+'/'+rel; + } else { + this.remote = base.remote+'\\'+rel; + } + } + + /** + * Checks if the remote path is Unix. + */ + private boolean isUnix() { + // Windows can handle '/' as a path separator but Unix can't, + // so err on Unix side + return remote.indexOf("\\")==-1; + } + + public File getLocal() { + return local; + } + + public String getRemote() { + return remote; + } + + /** + * Creates this directory. + */ + public void mkdirs() throws IOException { + if(!local.mkdirs() && !local.exists()) + throw new IOException("Failed to mkdirs: "+local); + } + + /** + * Deletes all the contents of this directory, but not the directory itself + */ + public void deleteContents() throws IOException { + // TODO: consider doing this remotely if possible + Util.deleteContentsRecursive(getLocal()); + } + + /** + * Gets just the file name portion. + * + * This method assumes that the file name is the same between local and remote. + */ + public String getName() { + return local.getName(); + } + + /** + * The same as {@code new FilePath(this,rel)} but more OO. + */ + public FilePath child(String rel) { + return new FilePath(this,rel); + } + + /** + * Gets the parent file. + */ + public FilePath getParent() { + int len = remote.length()-1; + while(len>=0) { + char ch = remote.charAt(len); + if(ch=='\\' || ch=='/') + break; + len--; + } + + return new FilePath( local.getParentFile(), remote.substring(0,len) ); + } + + /** + * Creates a temporary file. + */ + public FilePath createTempFile(String prefix, String suffix) throws IOException { + File f = File.createTempFile(prefix, suffix, getLocal()); + return new FilePath(this,f.getName()); + } + + /** + * Deletes this file. + */ + public boolean delete() { + return local.delete(); + } + + public boolean exists() { + return local.exists(); + } + + /** + * Always use {@link #getLocal()} or {@link #getRemote()} + */ + @Deprecated + public String toString() { + // to make writing JSPs easily, return local + return local.toString(); + } + + /** + * {@link FilePath} constant that can be used if the directory is not importatn. + */ + public static final FilePath RANDOM = new FilePath(new File(".")); +} diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java new file mode 100644 index 0000000000..73d60c9c8f --- /dev/null +++ b/core/src/main/java/hudson/Functions.java @@ -0,0 +1,187 @@ +package hudson; + +import hudson.model.ModelObject; +import hudson.model.Node; +import hudson.model.Project; +import hudson.model.Run; +import hudson.model.Hudson; +import org.kohsuke.stapler.Ancestor; +import org.kohsuke.stapler.StaplerRequest; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.Calendar; +import java.util.SortedMap; +import java.util.logging.LogRecord; +import java.util.logging.SimpleFormatter; +import java.io.File; + +/** + * @author Kohsuke Kawaguchi + */ +public class Functions { + public static boolean isModel(Object o) { + return o instanceof ModelObject; + } + + public static String xsDate(Calendar cal) { + return Util.XS_DATETIME_FORMATTER.format(cal.getTime()); + } + + public static String getDiffString(int i) { + if(i==0) return "\u00B10"; // +/-0 + String s = Integer.toString(i); + if(i>0) return "+"+s; + else return s; + } + + /** + * {@link #getDiffString2(int)} that doesn't show anything for +/-0 + */ + public static String getDiffString2(int i) { + if(i==0) return ""; + String s = Integer.toString(i); + if(i>0) return "+"+s; + else return s; + } + + /** + * Adds the proper suffix. + */ + public static String addSuffix(int n, String singular, String plural) { + StringBuffer buf = new StringBuffer(); + buf.append(n).append(' '); + if(n==1) + buf.append(singular); + else + buf.append(plural); + return buf.toString(); + } + + public static RunUrl decompose(StaplerRequest req) { + List ancestors = (List) req.getAncestors(); + for (Ancestor anc : ancestors) { + if(anc.getObject() instanceof Run) { + // bingo + String ancUrl = anc.getUrl(); + + String reqUri = req.getOriginalRequestURI(); + + return new RunUrl( + (Run) anc.getObject(), ancUrl, + reqUri.substring(ancUrl.length()), + req.getContextPath() ); + } + } + return null; + } + + public static final class RunUrl { + private final String contextPath; + private final String basePortion; + private final String rest; + private final Run run; + + public RunUrl(Run run, String basePortion, String rest, String contextPath) { + this.run = run; + this.basePortion = basePortion; + this.rest = rest; + this.contextPath = contextPath; + } + + public String getBaseUrl() { + return basePortion; + } + + /** + * Returns the same page in the next build. + */ + public String getNextBuildUrl() { + return getUrl(run.getNextBuild()); + } + + /** + * Returns the same page in the previous build. + */ + public String getPreviousBuildUrl() { + return getUrl(run.getPreviousBuild()); + } + + private String getUrl(Run n) { + if(n ==null) + return null; + else { + String url = contextPath + '/' + n.getUrl(); + assert url.endsWith("/"); + url = url.substring(0,url.length()-1); // cut off the trailing '/' + return url+rest; + } + } + } + + public static Node.Mode[] getNodeModes() { + return Node.Mode.values(); + } + + public static String getProjectListString(List projects) { + return Project.toNameList(projects); + } + + public static Object ifThenElse(boolean cond, Object thenValue, Object elseValue) { + return cond ? thenValue : elseValue; + } + + public static String appendIfNotNull(String text, String suffix, String nullText) { + return text == null ? nullText : text + suffix; + } + + public static Map getSystemProperties() { + return new TreeMap(System.getProperties()); + } + + public static Map getEnvVars() { + return new TreeMap(EnvVars.masterEnvVars); + } + + public static boolean isWindows() { + return File.pathSeparatorChar==';'; + } + + public static List getLogRecords() { + return Hudson.logRecords; + } + + public static String printLogRecord(LogRecord r) { + return formatter.format(r); + } + + public static Cookie getCookie(HttpServletRequest req,String name) { + Cookie[] cookies = req.getCookies(); + if(cookies!=null) { + for (Cookie cookie : cookies) { + if(cookie.getName().equals(name)) { + return cookie; + } + } + } + return null; + } + + /** + * Creates a sub map by using the given range (both ends inclusive). + */ + public static SortedMap filter(SortedMap map, String from, String to) { + if(from==null && to==null) return map; + if(to==null) + return map.headMap(Integer.parseInt(from)-1); + if(from==null) + return map.tailMap(Integer.parseInt(to)); + + return map.subMap(Integer.parseInt(to),Integer.parseInt(from)-1); + } + + private static final SimpleFormatter formatter = new SimpleFormatter(); +} diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java new file mode 100644 index 0000000000..de07eb6871 --- /dev/null +++ b/core/src/main/java/hudson/Launcher.java @@ -0,0 +1,102 @@ +package hudson; + +import hudson.model.TaskListener; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * Starts a process. + * + *

+ * This hides the difference between running programs locally vs remotely. + * + * + *

'env' parameter

+ *

+ * To allow important environment variables to be copied over to the remote machine, + * the 'env' parameter shouldn't contain default inherited environment varialbles + * (which often contains machine-specific information, like PATH, TIMEZONE, etc.) + * + *

+ * {@link Launcher} is responsible for inheriting environment variables. + * + * + * @author Kohsuke Kawaguchi + */ +public class Launcher { + + protected final TaskListener listener; + + public Launcher(TaskListener listener) { + this.listener = listener; + } + + public final Proc launch(String cmd, Map env, OutputStream out, FilePath workDir) throws IOException { + return launch(cmd,Util.mapToEnv(env),out,workDir); + } + + public final Proc launch(String[] cmd, Map env, OutputStream out, FilePath workDir) throws IOException { + return launch(cmd,Util.mapToEnv(env),out,workDir); + } + + public final Proc launch(String[] cmd,Map env, InputStream in, OutputStream out) throws IOException { + return launch(cmd,Util.mapToEnv(env),in,out); + } + + public final Proc launch(String cmd,String[] env,OutputStream out, FilePath workDir) throws IOException { + return launch(Util.tokenize(cmd),env,out,workDir); + } + + public Proc launch(String[] cmd,String[] env,OutputStream out, FilePath workDir) throws IOException { + printCommandLine(cmd, workDir); + return new Proc(cmd,Util.mapToEnv(inherit(env)),out,workDir.getLocal()); + } + + public Proc launch(String[] cmd,String[] env,InputStream in,OutputStream out) throws IOException { + printCommandLine(cmd, null); + return new Proc(cmd,inherit(env),in,out); + } + + /** + * Returns true if this {@link Launcher} is going to launch on Unix. + */ + public boolean isUnix() { + return File.pathSeparatorChar==':'; + } + + /** + * Expands the list of environment variables by inheriting current env variables. + */ + private Map inherit(String[] env) { + Map m = new HashMap(EnvVars.masterEnvVars); + for (String e : env) { + int index = e.indexOf('='); + String key = e.substring(0,index); + String value = e.substring(index+1); + if(value.length()==0) + m.remove(key); + else + m.put(key,value); + } + return m; + } + + private void printCommandLine(String[] cmd, FilePath workDir) { + StringBuffer buf = new StringBuffer(); + if (workDir != null) { + buf.append('['); + buf.append(workDir.getRemote().replaceFirst("^.+[/\\\\]", "")); + buf.append("] "); + } + buf.append('$'); + for (String c : cmd) { + buf.append(' ').append(c); + } + listener.getLogger().println(buf.toString()); + } +} diff --git a/core/src/main/java/hudson/Main.java b/core/src/main/java/hudson/Main.java new file mode 100644 index 0000000000..aaa8cb33b9 --- /dev/null +++ b/core/src/main/java/hudson/Main.java @@ -0,0 +1,138 @@ +package hudson; + +import hudson.model.ExternalJob; +import hudson.model.ExternalRun; +import hudson.model.Hudson; +import hudson.model.Job; +import hudson.model.Result; +import hudson.util.DualOutputStream; +import hudson.util.EncodingStream; + +import java.io.File; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; + +/** + * Entry point to Hudson from command line. + * + * @author Kohsuke Kawaguchi + */ +public class Main { + public static void main(String[] args) { + try { + System.exit(run(args)); + } catch (Exception e) { + e.printStackTrace(); + System.exit(-1); + } + } + + public static int run(String[] args) throws Exception { + String home = getHudsonHome(); + if(home==null) { + System.err.println("HUDSON_HOME is not set."); + return -1; + } + + if(home.startsWith("http")) { + return remotePost(args); + } else { + return localPost(args); + } + } + + private static String getHudsonHome() { + return EnvVars.masterEnvVars.get("HUDSON_HOME"); + } + + /** + * Run command and place the result directly into the local installation of Hudson. + */ + public static int localPost(String[] args) throws Exception { + Hudson app = new Hudson(new File(getHudsonHome()),null); + + Job job = app.getJob(args[0]); + if(!(job instanceof ExternalJob)) { + System.err.println(args[0]+" is not a valid external job name in Hudson"); + return -1; + } + ExternalJob ejob = (ExternalJob) job; + + ExternalRun run = ejob.newBuild(); + + // run the command + List cmd = new ArrayList(); + for( int i=1; i"); + w.write(""); + w.flush(); + + // run the command + long start = System.currentTimeMillis(); + + List cmd = new ArrayList(); + for( int i=1; i"+ret+""+(System.currentTimeMillis()-start)+""); + w.close(); + + if(con.getResponseCode()!=200) { + Util.copyStream(con.getErrorStream(),System.err); + } + + return ret; + } +} diff --git a/core/src/main/java/hudson/Plugin.java b/core/src/main/java/hudson/Plugin.java new file mode 100644 index 0000000000..cafe25995d --- /dev/null +++ b/core/src/main/java/hudson/Plugin.java @@ -0,0 +1,105 @@ +package hudson; + +import hudson.model.Hudson; +import hudson.scm.SCM; +import hudson.tasks.Builder; +import hudson.tasks.Publisher; +import hudson.triggers.Trigger; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URL; + +/** + * Base class of Hudson plugin. + * + *

+ * A plugin needs to derive from this class. + * + * @author Kohsuke Kawaguchi + * @since 1.42 + */ +public abstract class Plugin { + + /** + * Set by the {@link PluginManager}. + */ + /*package*/ PluginWrapper wrapper; + + /** + * Called when a plugin is loaded to make the {@link ServletContext} object available to a plugin. + * This object allows plugins to talk to the surrounding environment. + * + *

+ * The default implementation is no-op. + * + * @param context + * Always non-null. + * + * @since 1.42 + */ + public void setServletContext(ServletContext context) { + } + + /** + * Called to allow plugins to initialize themselves. + * + *

+ * This method is called after {@link #setServletContext(ServletContext)} is invoked. + * You can also use {@link Hudson#getInstance()} to access the singleton hudson instance. + * + *

+ * Plugins should override this method and register custom + * {@link Publisher}, {@link Builder}, {@link SCM}, and {@link Trigger}s to the corresponding list. + * See {@link ExtensionPoint} for the complete list of extension points in Hudson. + * + * + * @throws Exception + * any exception thrown by the plugin during the initialization will disable plugin. + * + * @since 1.42 + * @see ExtensionPoint + */ + public void start() throws Exception { + } + + /** + * Called to orderly shut down Hudson. + * + *

+ * This is a good opportunity to clean up resources that plugin started. + * This method will not be invoked if the {@link #start()} failed abnormally. + * + * @throws Exception + * if any exception is thrown, it is simply recorded and shut-down of other + * plugins continue. This is primarily just a convenience feature, so that + * each plugin author doesn't have to worry about catching an exception and + * recording it. + * + * @since 1.42 + */ + public void stop() throws Exception { + } + + /** + * This method serves static resources in the plugin under hudson/plugin/SHORTNAME. + */ + public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + String path = req.getRestOfPath(); + + if(path.length()==0) + path = "/"; + + if(path.indexOf("..")!=-1 || path.length()<1) { + // don't serve anything other than files in the sub directory. + rsp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + rsp.serveFile(req, new URL(wrapper.baseResourceURL,'.'+path)); + } +} diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java new file mode 100644 index 0000000000..30297df3c0 --- /dev/null +++ b/core/src/main/java/hudson/PluginManager.java @@ -0,0 +1,146 @@ +package hudson; + +import hudson.model.Hudson; +import hudson.util.Service; +import java.util.logging.Level; + +import javax.servlet.ServletContext; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +/** + * Manages {@link PluginWrapper}s. + * + * @author Kohsuke Kawaguchi + */ +public final class PluginManager { + /** + * All discovered plugins. + */ + private final List plugins = new ArrayList(); + + /** + * All active plugins. + */ + private final List activePlugins = new ArrayList(); + + /** + * Plug-in root directory. + */ + public final File rootDir; + + public final ServletContext context; + + /** + * {@link ClassLoader} that can load all the publicly visible classes from plugins + * (and including the classloader that loads Hudson itself.) + * + */ + // implementation is minmal --- just enough to run XStream + // and load plugin-contributed classes. + public final ClassLoader uberClassLoader = new UberClassLoader(); + + public PluginManager(ServletContext context) { + this.context = context; + rootDir = new File(Hudson.getInstance().getRootDir(),"plugins"); + if(!rootDir.exists()) + rootDir.mkdirs(); + + File[] archives = rootDir.listFiles(new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.endsWith(".hpi") // plugin jar file + || name.endsWith(".hpl"); // linked plugin. for debugging. + } + }); + + if(archives==null) { + LOGGER.severe("Hudson is unable to create "+rootDir+"\nPerhaps its security privilege is insufficient"); + return; + } + for( File arc : archives ) { + try { + PluginWrapper p = new PluginWrapper(this, arc); + plugins.add(p); + if(p.isActive()) + activePlugins.add(p); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to load a plug-in " + arc, e); + } + } + } + + public List getPlugins() { + return plugins; + } + + public PluginWrapper getPlugin(String shortName) { + for (PluginWrapper p : plugins) { + if(p.getShortName().equals(shortName)) + return p; + } + return null; + } + + /** + * Discover all the service provider implementations of the given class, + * via META-INF/services. + */ + public Collection> discover( Class spi ) { + Set> result = new HashSet>(); + + for (PluginWrapper p : activePlugins) { + Service.load(spi, p.classLoader, result); + } + + return result; + } + + /** + * Orderly terminates all the plugins. + */ + public void stop() { + for (PluginWrapper p : activePlugins) { + p.stop(); + } + } + + private final class UberClassLoader extends ClassLoader { + public UberClassLoader() { + super(PluginManager.class.getClassLoader()); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + for (PluginWrapper p : activePlugins) { + try { + return p.classLoader.loadClass(name); + } catch (ClassNotFoundException e) { + //not found. try next + } + } + // not found in any of the classloader. delegate. + throw new ClassNotFoundException(name); + } + + @Override + protected URL findResource(String name) { + for (PluginWrapper p : activePlugins) { + URL url = p.classLoader.getResource(name); + if(url!=null) + return url; + } + return null; + } + } + + private static final Logger LOGGER = Logger.getLogger(PluginManager.class.getName()); + +} diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java new file mode 100644 index 0000000000..f2a43f25a0 --- /dev/null +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -0,0 +1,361 @@ +package hudson; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.types.FileSet; +import org.apache.tools.ant.taskdefs.Expand; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.BufferedReader; +import java.io.FileReader; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.Manifest; +import java.util.logging.Logger; + +import hudson.util.IOException2; + +/** + * Represents a Hudson plug-in and associated control information + * for Hudson to control {@link Plugin}. + * + *

+ * A plug-in is packaged into a jar file whose extension is ".hpi", + * A plugin needs to have a special manifest entry to identify what it is. + * + *

+ * At the runtime, a plugin has two distinct state axis. + *

    + *
  1. Enabled/Disabled. If enabled, Hudson is going to use it + * next time Hudson runs. Otherwise the next run will ignore it. + *
  2. Activated/Deactivated. If activated, that means Hudson is using + * the plugin in this session. Otherwise it's not. + *
+ *

+ * For example, an activated but disabled plugin is still running but the next + * time it won't. + * + * @author Kohsuke Kawaguchi + */ +public final class PluginWrapper { + /** + * Plugin manifest. + * Contains description of the plugin. + */ + private final Manifest manifest; + + /** + * Loaded plugin instance. + * Null if disabled. + */ + public final Plugin plugin; + + /** + * {@link ClassLoader} for loading classes from this plugin. + * Null if disabled. + */ + public final ClassLoader classLoader; + + /** + * Base URL for loading static resources from this plugin. + * Null if disabled. The static resources are mapped under + * hudson/plugin/SHORTNAME/. + */ + public final URL baseResourceURL; + + /** + * Used to control enable/disable setting of the plugin. + * If this file exists, plugin will be disabled. + */ + private final File disableFile; + + /** + * Short name of the plugin. The "abc" portion of "abc.hpl". + */ + private final String shortName; + + /** + * @param archive + * A .hpi archive file jar file, or a .hpl linked plugin. + * + * @throws IOException + * if an installation of this plugin failed. The caller should + * proceed to work with other plugins. + */ + public PluginWrapper(PluginManager owner, File archive) throws IOException { + LOGGER.info("Loading plugin: "+archive); + + this.shortName = getShortName(archive); + + boolean isLinked = archive.getName().endsWith(".hpl"); + + File expandDir = null; // if .hpi, this is the directory where war is expanded + + if(isLinked) { + // resolve the .hpl file to the location of the manifest file + archive = resolve(archive,new BufferedReader(new FileReader(archive)).readLine()); + // then parse manifest + FileInputStream in = new FileInputStream(archive); + try { + manifest = new Manifest(in); + } catch(IOException e) { + throw new IOException2("Failed to load "+archive,e); + } finally { + in.close(); + } + } else { + expandDir = new File(archive.getParentFile(), shortName); + explode(archive,expandDir); + + File manifestFile = new File(expandDir,"META-INF/MANIFEST.MF"); + if(!manifestFile.exists()) { + throw new IOException("Plugin installation failed. No manifest at "+manifestFile); + } + FileInputStream fin = new FileInputStream(manifestFile); + try { + manifest = new Manifest(fin); + } finally { + fin.close(); + } + } + + // TODO: define a mechanism to hide classes + // String export = manifest.getMainAttributes().getValue("Export"); + + List paths = new ArrayList(); + if(isLinked) { + String classPath = manifest.getMainAttributes().getValue("Class-Path"); + for (String s : classPath.split(" +")) { + File file = resolve(archive, s); + if(file.getName().contains("*")) { + // handle wildcard + FileSet fs = new FileSet(); + File dir = file.getParentFile(); + fs.setDir(dir); + fs.setIncludes(file.getName()); + for( String included : fs.getDirectoryScanner(new Project()).getIncludedFiles() ) { + paths.add(new File(dir,included).toURL()); + } + } else { + if(!file.exists()) + throw new IOException("No such file: "+file); + paths.add(file.toURL()); + } + } + + this.baseResourceURL = resolve(archive, + manifest.getMainAttributes().getValue("Resource-Path")).toURL(); + } else { + File classes = new File(expandDir,"WEB-INF/classes"); + if(classes.exists()) + paths.add(classes.toURL()); + File lib = new File(expandDir,"WEB-INF/lib"); + File[] libs = lib.listFiles(JAR_FILTER); + if(libs!=null) { + for (File jar : libs) + paths.add(jar.toURL()); + } + + this.baseResourceURL = expandDir.toURL(); + } + this.classLoader = new URLClassLoader(paths.toArray(new URL[0]), getClass().getClassLoader()); + + disableFile = new File(archive.getPath()+".disabled"); + if(disableFile.exists()) { + LOGGER.info("Plugin is disabled"); + this.plugin = null; + return; + } + + String className = manifest.getMainAttributes().getValue("Plugin-Class"); + if(className ==null) { + throw new IOException("Plugin installation failed. No 'Plugin-Class' entry in the manifest of "+archive); + } + + try { + Class clazz = classLoader.loadClass(className); + Object plugin = clazz.newInstance(); + if(!(plugin instanceof Plugin)) { + throw new IOException(className+" doesn't extend from hudson.Plugin"); + } + this.plugin = (Plugin)plugin; + this.plugin.wrapper = this; + } catch (ClassNotFoundException e) { + IOException ioe = new IOException("Unable to load " + className + " from " + archive); + ioe.initCause(e); + throw ioe; + } catch (IllegalAccessException e) { + IOException ioe = new IOException("Unable to create instance of " + className + " from " + archive); + ioe.initCause(e); + throw ioe; + } catch (InstantiationException e) { + IOException ioe = new IOException("Unable to create instance of " + className + " from " + archive); + ioe.initCause(e); + throw ioe; + } + + // initialize plugin + try { + plugin.setServletContext(owner.context); + plugin.start(); + } catch(Throwable t) { + // gracefully handle any error in plugin. + IOException ioe = new IOException("Failed to initialize"); + ioe.initCause(t); + throw ioe; + } + } + + private static File resolve(File base, String relative) { + File rel = new File(relative); + if(rel.isAbsolute()) + return rel; + else + return new File(base.getParentFile(),relative); + } + + /** + * Returns the URL of the index page jelly script. + */ + public URL getIndexPage() { + return classLoader.getResource("index.jelly"); + } + + /** + * Returns the short name suitable for URL. + */ + public String getShortName() { + return shortName; + } + + /** + * Returns a one-line descriptive name of this plugin. + */ + public String getLongName() { + String name = manifest.getMainAttributes().getValue("Long-Name"); + if(name!=null) return name; + return shortName; + } + + /** + * Gets the "abc" portion from "abc.ext". + */ + private static String getShortName(File archive) { + String n = archive.getName(); + int idx = n.lastIndexOf('.'); + if(idx>=0) + n = n.substring(0,idx); + return n; + } + + /** + * Terminates the plugin. + */ + void stop() { + LOGGER.info("Stopping "+shortName); + try { + plugin.stop(); + } catch(Throwable t) { + System.err.println("Failed to shut down "+shortName); + System.err.println(t); + } + } + + /** + * Enables this plugin next time Hudson runs. + */ + public void enable() throws IOException { + if(!disableFile.delete()) + throw new IOException("Failed to delete "+disableFile); + } + + /** + * Disables this plugin next time Hudson runs. + */ + public void disable() throws IOException { + // creates an empty file + OutputStream os = new FileOutputStream(disableFile); + os.close(); + } + + /** + * Returns true if this plugin is enabled for this session. + */ + public boolean isActive() { + return plugin!=null; + } + + /** + * If true, the plugin is going to be activated next time + * Hudson runs. + */ + public boolean isEnabled() { + return !disableFile.exists(); + } + + /** + * Explodes the plugin into a directory, if necessary. + */ + private void explode(File archive, File destDir) throws IOException { + if(!destDir.exists()) + destDir.mkdirs(); + + // timestamp check + File explodeTime = new File(destDir,".timestamp"); + if(explodeTime.exists() && explodeTime.lastModified()>archive.lastModified()) + return; // no need to expand + + LOGGER.info("Extracting "+archive); + + try { + Expand e = new Expand(); + e.setProject(new Project()); + e.setTaskType("unzip"); + e.setSrc(archive); + e.setDest(destDir); + e.execute(); + } catch (BuildException x) { + IOException ioe = new IOException("Failed to expand " + archive); + ioe.initCause(x); + throw ioe; + } + + Util.touch(explodeTime); + } + + +// +// +// Action methods +// +// + public void doMakeEnabled(StaplerRequest req, StaplerResponse rsp) throws IOException { + enable(); + rsp.setStatus(200); + } + public void doMakeDisabled(StaplerRequest req, StaplerResponse rsp) throws IOException { + disable(); + rsp.setStatus(200); + } + + + private static final Logger LOGGER = Logger.getLogger(PluginWrapper.class.getName()); + + /** + * Filter for jar files. + */ + private static final FilenameFilter JAR_FILTER = new FilenameFilter() { + public boolean accept(File dir,String name) { + return name.endsWith(".jar"); + } + }; +} diff --git a/core/src/main/java/hudson/Proc.java b/core/src/main/java/hudson/Proc.java new file mode 100644 index 0000000000..1092839560 --- /dev/null +++ b/core/src/main/java/hudson/Proc.java @@ -0,0 +1,118 @@ +package hudson; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; + +/** + * External process wrapper. + * + *

+ * Used for launching, monitoring, waiting for a process. + * + * @author Kohsuke Kawaguchi + */ +public final class Proc { + private final Process proc; + private final Thread t1,t2; + + public Proc(String cmd,Map env, OutputStream out, File workDir) throws IOException { + this(cmd,Util.mapToEnv(env),out,workDir); + } + + public Proc(String[] cmd,Map env,InputStream in, OutputStream out) throws IOException { + this(cmd,Util.mapToEnv(env),in,out); + } + + public Proc(String cmd,String[] env,OutputStream out, File workDir) throws IOException { + this( Util.tokenize(cmd), env, out, workDir ); + } + + public Proc(String[] cmd,String[] env,OutputStream out, File workDir) throws IOException { + this( calcName(cmd), Runtime.getRuntime().exec(cmd,env,workDir), null, out ); + } + + public Proc(String[] cmd,String[] env,InputStream in,OutputStream out) throws IOException { + this( calcName(cmd), Runtime.getRuntime().exec(cmd,env), in, out ); + } + + private Proc( String name, Process proc, InputStream in, OutputStream out ) throws IOException { + this.proc = proc; + t1 = new Copier(name+": stdout copier", proc.getInputStream(), out); + t1.start(); + t2 = new Copier(name+": stderr copier", proc.getErrorStream(), out); + t2.start(); + if(in!=null) + new ByteCopier(name+": stdin copier",in,proc.getOutputStream()).start(); + else + proc.getOutputStream().close(); + } + + public int join() { + try { + t1.join(); + t2.join(); + return proc.waitFor(); + } catch (InterruptedException e) { + // aborting. kill the process + proc.destroy(); + return -1; + } + } + + private static class Copier extends Thread { + private final InputStream in; + private final OutputStream out; + + public Copier(String threadName, InputStream in, OutputStream out) { + super(threadName); + this.in = in; + this.out = out; + } + + public void run() { + try { + Util.copyStream(in,out); + in.close(); + } catch (IOException e) { + // TODO: what to do? + } + } + } + + private static class ByteCopier extends Thread { + private final InputStream in; + private final OutputStream out; + + public ByteCopier(String threadName, InputStream in, OutputStream out) { + super(threadName); + this.in = in; + this.out = out; + } + + public void run() { + try { + while(true) { + int ch = in.read(); + if(ch==-1) break; + out.write(ch); + } + in.close(); + out.close(); + } catch (IOException e) { + // TODO: what to do? + } + } + } + + private static String calcName(String[] cmd) { + StringBuffer buf = new StringBuffer(); + for (String token : cmd) { + if(buf.length()>0) buf.append(' '); + buf.append(token); + } + return buf.toString(); + } +} diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java new file mode 100644 index 0000000000..20d271891b --- /dev/null +++ b/core/src/main/java/hudson/Util.java @@ -0,0 +1,326 @@ +package hudson; + +import hudson.model.BuildListener; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.StringTokenizer; +import java.util.SimpleTimeZone; +import java.util.logging.Logger; +import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.text.SimpleDateFormat; + +import org.apache.tools.ant.taskdefs.Chmod; +import org.apache.tools.ant.taskdefs.Copy; +import org.apache.tools.ant.BuildException; + +/** + * @author Kohsuke Kawaguchi + */ +public class Util { + /** + * Loads the contents of a file into a string. + */ + public static String loadFile(File logfile) throws IOException { + if(!logfile.exists()) + return ""; + + StringBuffer str = new StringBuffer((int)logfile.length()); + + BufferedReader r = new BufferedReader(new FileReader(logfile)); + char[] buf = new char[1024]; + int len; + while((len=r.read(buf,0,buf.length))>0) + str.append(buf,0,len); + r.close(); + + return str.toString(); + } + + /** + * Deletes the contents of the given directory (but not the directory itself) + * recursively. + * + * @throws IOException + * if the operation fails. + */ + public static void deleteContentsRecursive(File file) throws IOException { + File[] files = file.listFiles(); + if(files==null) + return; // the directory didn't exist in the first place + for (File child : files) { + if (child.isDirectory()) + deleteContentsRecursive(child); + deleteFile(child); + } + } + + private static void deleteFile(File f) throws IOException { + if (!f.delete()) { + if(!f.exists()) + // we are trying to delete a file that no longer exists, so this is not an error + return; + + // perhaps this file is read-only? + // try chmod. this becomes no-op if this is not Unix. + try { + Chmod chmod = new Chmod(); + chmod.setProject(new org.apache.tools.ant.Project()); + chmod.setFile(f); + chmod.setPerm("u+w"); + chmod.execute(); + } catch (BuildException e) { + LOGGER.log(Level.INFO,"Failed to chmod "+f,e); + } + + throw new IOException("Unable to delete " + f.getPath()); + + } + } + + public static void deleteRecursive(File dir) throws IOException { + deleteContentsRecursive(dir); + deleteFile(dir); + } + + /** + * Creates a new temporary directory. + */ + public static File createTempDir() throws IOException { + File tmp = File.createTempFile("hudson", "tmp"); + if(!tmp.delete()) + throw new IOException("Failed to delete "+tmp); + if(!tmp.mkdirs()) + throw new IOException("Failed to create a new directory "+tmp); + return tmp; + } + + private static final Pattern errorCodeParser = Pattern.compile(".*error=([0-9]+).*"); + + /** + * On Windows, error messages for IOException aren't very helpful. + * This method generates additional user-friendly error message to the listener + */ + public static void displayIOException( IOException e, BuildListener listener ) { + if(File.separatorChar!='\\') + return; // not Windows + + Matcher m = errorCodeParser.matcher(e.getMessage()); + if(!m.matches()) + return; // failed to parse + + try { + ResourceBundle rb = ResourceBundle.getBundle("/hudson/win32errors"); + listener.getLogger().println(rb.getString("error"+m.group(1))); + } catch (Exception _) { + // silently recover from resource related failures + } + } + + /** + * Guesses the current host name. + */ + public static String getHostName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + return "localhost"; + } + } + + public static void copyStream(InputStream in,OutputStream out) throws IOException { + byte[] buf = new byte[8192]; + int len; + while((len=in.read(buf))>0) + out.write(buf,0,len); + } + + public static String[] tokenize(String s) { + StringTokenizer st = new StringTokenizer(s); + String[] a = new String[st.countTokens()]; + for (int i = 0; st.hasMoreTokens(); i++) + a[i] = st.nextToken(); + return a; + } + + public static String[] mapToEnv(Map m) { + String[] r = new String[m.size()]; + int idx=0; + + for (final Map.Entry e : m.entrySet()) { + r[idx++] = e.getKey().toString() + '=' + e.getValue().toString(); + } + return r; + } + + public static int min(int x, int... values) { + for (int i : values) { + if(i UTF8 + w.write(c); + w.flush(); + for (byte b : buf.toByteArray()) { + out.append('%'); + out.append(toDigit((b >> 4) & 0xF)); + out.append(toDigit(b & 0xF)); + } + buf.reset(); + escaped = true; + } + } + + return escaped ? out.toString() : s; + } catch (IOException e) { + throw new Error(e); // impossible + } + } + + private static char toDigit(int n) { + char ch = Character.forDigit(n,16); + if(ch>='a') ch = (char)(ch-'a'+'A'); + return ch; + } + + /** + * Creates an empty file. + */ + public static void touch(File file) throws IOException { + new FileOutputStream(file).close(); + } + + /** + * Copies a single file by using Ant. + */ + public static void copyFile(File src, File dst) throws BuildException { + Copy cp = new Copy(); + cp.setProject(new org.apache.tools.ant.Project()); + cp.setTofile(dst); + cp.setFile(src); + cp.setOverwrite(true); + cp.execute(); + } + + /** + * Convert null to "". + */ + public static String fixNull(String s) { + if(s==null) return ""; + else return s; + } + + /** + * Convert empty string to null. + */ + public static String fixEmpty(String s) { + if(s==null || s.length()==0) return null; + return s; + } + + /** + * Cuts all the leading path portion and get just the file name. + */ + public static String getFileName(String filePath) { + int idx = filePath.lastIndexOf('\\'); + if(idx>=0) + return getFileName(filePath.substring(idx+1)); + idx = filePath.lastIndexOf('/'); + if(idx>=0) + return getFileName(filePath.substring(idx+1)); + return filePath; + } + + public static final SimpleDateFormat XS_DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + static { + XS_DATETIME_FORMATTER.setTimeZone(new SimpleTimeZone(0,"GMT")); + } + + + + private static final Logger LOGGER = Logger.getLogger(Util.class.getName()); + +} diff --git a/core/src/main/java/hudson/WebAppMain.java b/core/src/main/java/hudson/WebAppMain.java new file mode 100644 index 0000000000..804d6ee529 --- /dev/null +++ b/core/src/main/java/hudson/WebAppMain.java @@ -0,0 +1,162 @@ +package hudson; + +import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider; +import com.thoughtworks.xstream.core.JVM; +import hudson.model.Hudson; +import hudson.model.User; +import hudson.triggers.Trigger; +import hudson.util.IncompatibleVMDetected; +import hudson.util.RingBufferLogHandler; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.TimerTask; +import java.util.logging.Logger; +import java.util.logging.Level; + +/** + * Entry point when Hudson is used as a webapp. + * + * @author Kohsuke Kawaguchi + */ +public class WebAppMain implements ServletContextListener { + + /** + * Creates the sole instance of {@link Hudson} and register it to the {@link ServletContext}. + */ + public void contextInitialized(ServletContextEvent event) { + installLogger(); + + File home = getHomeDir(event); + home.mkdirs(); + System.out.println("hudson home directory: "+home); + + ServletContext context = event.getServletContext(); + + // make sure that we are using XStream in the "enhenced" (JVM-specific) mode + if(new JVM().bestReflectionProvider().getClass()==PureJavaReflectionProvider.class) { + // nope + context.setAttribute("app",new IncompatibleVMDetected()); + return; + } + + // Tomcat breaks XSLT with JDK 5.0 and onward. Check if that's the case, and if so, + // try to correct it + try { + TransformerFactory.newInstance(); + // if this works we are all happy + } catch (TransformerFactoryConfigurationError x) { + // no it didn't. + Logger logger = Logger.getLogger(WebAppMain.class.getName()); + + logger.log(Level.WARNING, "XSLT not configured correctly. Hudson will try to fix this. See http://issues.apache.org/bugzilla/show_bug.cgi?id=40895 for more details",x); + System.setProperty(TransformerFactory.class.getName(),"com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"); + try { + TransformerFactory.newInstance(); + logger.info("XSLT is set to the JAXP RI in JRE"); + } catch(TransformerFactoryConfigurationError y) { + logger.log(Level.SEVERE, "Failed to correct the problem."); + } + } + + + try { + context.setAttribute("app",new Hudson(home,context)); + } catch( IOException e ) { + throw new Error(e); + } + + // set the version + Properties props = new Properties(); + try { + InputStream is = getClass().getResourceAsStream("hudson-version.properties"); + if(is!=null) + props.load(is); + } catch (IOException e) { + e.printStackTrace(); // if the version properties is missing, that's OK. + } + Object ver = props.get("version"); + if(ver==null) ver="?"; + context.setAttribute("version",ver); + + Trigger.init(); // start running trigger + + // trigger the loading of changelogs in the background, + // but give the system 10 seconds so that the first page + // can be served quickly + Trigger.timer.schedule(new TimerTask() { + public void run() { + User.get("nobody").getBuilds(); + } + }, 1000*10); + + } + + /** + * Installs log handler to monitor all Hudson logs. + */ + private void installLogger() { + RingBufferLogHandler handler = new RingBufferLogHandler(); + Hudson.logRecords = handler.getView(); + Logger.getLogger("hudson").addHandler(handler); + } + + /** + * Determines the home directory for Hudson. + * + * People makes configuration mistakes, so we are trying to be nice + * with those by doing {@link String#trim()}. + */ + private File getHomeDir(ServletContextEvent event) { + // check JNDI for the home directory first + try { + Context env = (Context) new InitialContext().lookup("java:comp/env"); + String value = (String) env.lookup("HUDSON_HOME"); + if(value!=null && value.trim().length()>0) + return new File(value.trim()); + } catch (NamingException e) { + // ignore + } + + // look at the env var next + String env = EnvVars.masterEnvVars.get("HUDSON_HOME"); + if(env!=null) + return new File(env.trim()); + + // finally check the system property + String sysProp = System.getProperty("HUDSON_HOME"); + if(sysProp!=null) + return new File(sysProp.trim()); + + // otherwise pick a place by ourselves + + String root = event.getServletContext().getRealPath("/WEB-INF/workspace"); + if(root!=null) { + File ws = new File(root.trim()); + if(ws.exists()) + // Hudson <1.42 used to prefer this betfore ~/.hudson, so + // check the existence and if it's there, use it. + // otherwise if this is a new installation, prefer ~/.hudson + return ws; + } + + // if for some reason we can't put it within the webapp, use home directory. + return new File(new File(System.getProperty("user.home")),".hudson"); + } + + public void contextDestroyed(ServletContextEvent event) { + Hudson instance = Hudson.getInstance(); + if(instance!=null) + instance.cleanUp(); + } +} diff --git a/core/src/main/java/hudson/XmlFile.java b/core/src/main/java/hudson/XmlFile.java new file mode 100644 index 0000000000..ea228b2301 --- /dev/null +++ b/core/src/main/java/hudson/XmlFile.java @@ -0,0 +1,97 @@ +package hudson; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.converters.ConversionException; +import com.thoughtworks.xstream.io.StreamException; +import com.thoughtworks.xstream.io.xml.XppReader; +import hudson.util.AtomicFileWriter; +import hudson.util.IOException2; +import hudson.util.XStream2; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; + +/** + * Represents an XML data file that Hudson uses as a data file. + * + * @author Kohsuke Kawaguchi + */ +public final class XmlFile { + private final XStream xs; + private final File file; + + public XmlFile(File file) { + this(DEFAULT_XSTREAM,file); + } + + public XmlFile(XStream xs, File file) { + this.xs = xs; + this.file = file; + } + + /** + * Loads the contents of this file into a new object. + */ + public Object read() throws IOException { + Reader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8")); + try { + return xs.fromXML(r); + } catch(StreamException e) { + throw new IOException2("Unable to read "+file,e); + } catch(ConversionException e) { + throw new IOException2("Unable to read "+file,e); + } finally { + r.close(); + } + } + + /** + * Loads the contents of this file into an existing object. + */ + public void unmarshal( Object o ) throws IOException { + Reader r = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF-8")); + try { + xs.unmarshal(new XppReader(r),o); + } catch (StreamException e) { + throw new IOException2(e); + } catch(ConversionException e) { + throw new IOException2("Unable to read "+file,e); + } finally { + r.close(); + } + } + + public void write( Object o ) throws IOException { + AtomicFileWriter w = new AtomicFileWriter(file); + try { + w.write("\n"); + xs.toXML(o,w); + w.commit(); + } catch(StreamException e) { + throw new IOException2(e); + } finally { + w.close(); + } + } + + public boolean exists() { + return file.exists(); + } + + public void mkdirs() { + file.getParentFile().mkdirs(); + } + + public String toString() { + return file.toString(); + } + + /** + * {@link XStream} instance is supposed to be thread-safe. + */ + private static final XStream DEFAULT_XSTREAM = new XStream2(); +} diff --git a/core/src/main/java/hudson/hudson-version.properties b/core/src/main/java/hudson/hudson-version.properties new file mode 100644 index 0000000000..3d8d587dd7 --- /dev/null +++ b/core/src/main/java/hudson/hudson-version.properties @@ -0,0 +1,2 @@ +# overwritten by the Ant task +version=development \ No newline at end of file diff --git a/core/src/main/java/hudson/model/AbstractModelObject.java b/core/src/main/java/hudson/model/AbstractModelObject.java new file mode 100644 index 0000000000..0efc964af5 --- /dev/null +++ b/core/src/main/java/hudson/model/AbstractModelObject.java @@ -0,0 +1,26 @@ +package hudson.model; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import java.io.IOException; + +/** + * {@link ModelObject} with some convenience methods. + * + * @author Kohsuke Kawaguchi + */ +abstract class AbstractModelObject implements ModelObject { + /** + * Displays the error in a page. + */ + protected final void sendError(Exception e, StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException { + sendError(e.getMessage(),req,rsp); + } + + protected final void sendError(String message, StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException { + req.setAttribute("message",message); + rsp.forward(this,"error",req); + } +} diff --git a/core/src/main/java/hudson/model/Action.java b/core/src/main/java/hudson/model/Action.java new file mode 100644 index 0000000000..95c631f4f8 --- /dev/null +++ b/core/src/main/java/hudson/model/Action.java @@ -0,0 +1,28 @@ +package hudson.model; + +import java.io.Serializable; + +/** + * Contributes an item to the task list. + * + * @author Kohsuke Kawaguchi + */ +public interface Action extends Serializable, ModelObject { + /** + * Gets the file name of the icon (relative to /images/24x24) + */ + String getIconFileName(); + + /** + * Gets the string to be displayed. + * + * The convention is to capitalize the first letter of each word, + * such as "Test Result". + */ + String getDisplayName(); + + /** + * Gets the URL path name. + */ + String getUrlName(); +} diff --git a/core/src/main/java/hudson/model/Actionable.java b/core/src/main/java/hudson/model/Actionable.java new file mode 100644 index 0000000000..41e60961cd --- /dev/null +++ b/core/src/main/java/hudson/model/Actionable.java @@ -0,0 +1,52 @@ +package hudson.model; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import java.util.List; +import java.util.Vector; + +/** + * {@link ModelObject} that can have additional {@link Action}s. + * + * @author Kohsuke Kawaguchi + */ +public abstract class Actionable extends AbstractModelObject { + /** + * Actions contributed to this model object. + */ + private List actions; + + /** + * Gets actions contributed to this build. + * + * @return + * may be empty but never null. + */ + public synchronized List getActions() { + if(actions==null) + actions = new Vector(); + return actions; + } + + public Action getAction(int index) { + if(actions==null) return null; + return actions.get(index); + } + + public T getAction(Class type) { + for (Action a : getActions()) { + if(type.isInstance(a)) + return (T)a; // type.cast() not available in JDK 1.4 + } + return null; + } + + public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { + for (Action a : getActions()) { + if(a.getUrlName().equals(token)) + return a; + } + return null; + } +} diff --git a/core/src/main/java/hudson/model/Build.java b/core/src/main/java/hudson/model/Build.java new file mode 100644 index 0000000000..c8aadc0e5b --- /dev/null +++ b/core/src/main/java/hudson/model/Build.java @@ -0,0 +1,428 @@ +package hudson.model; + +import hudson.Launcher; +import hudson.Proc; +import hudson.Util; +import static hudson.model.Hudson.isWindows; +import hudson.model.Fingerprint.RangeSet; +import hudson.model.Fingerprint.BuildPtr; +import hudson.scm.CVSChangeLogParser; +import hudson.scm.ChangeLogParser; +import hudson.scm.ChangeLogSet; +import hudson.scm.SCM; +import hudson.scm.ChangeLogSet.Entry; +import hudson.tasks.BuildStep; +import hudson.tasks.Builder; +import hudson.tasks.Publisher; +import hudson.tasks.Fingerprinter.FingerprintAction; +import hudson.tasks.test.AbstractTestResultAction; +import hudson.triggers.SCMTrigger; +import org.xml.sax.SAXException; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Calendar; +import java.util.Map; +import java.util.HashMap; +import java.util.Collections; + +/** + * @author Kohsuke Kawaguchi + */ +public final class Build extends Run implements Runnable { + + /** + * Name of the slave this project was built on. + * Null if built by the master. + */ + private String builtOn; + + /** + * SCM used for this build. + * Maybe null, for historical reason, in which case CVS is assumed. + */ + private ChangeLogParser scm; + + /** + * Changes in this build. + */ + private volatile transient ChangeLogSet changeSet; + + /** + * Creates a new build. + */ + Build(Project project) throws IOException { + super(project); + } + + public Project getProject() { + return getParent(); + } + + /** + * Loads a build from a log file. + */ + Build(Project project, File buildDir) throws IOException { + super(project,buildDir); + } + + @Override + public boolean isKeepLog() { + // if any of the downstream project is configured with 'keep dependency component', + // we need to keep this log + for (Map.Entry e : getDownstreamBuilds().entrySet()) { + Project p = e.getKey(); + if(!p.isKeepDependencies()) continue; + + // is there any active build that depends on us? + for (Build build : p.getBuilds()) { + if(e.getValue().includes(build.getNumber())) + return true; // yep. an active build depends on us. can't recycle. + } + } + // TODO: report why the log is kept in UI + + return super.isKeepLog(); + } + + /** + * Gets the changes incorporated into this build. + * + * @return never null. + */ + public ChangeLogSet getChangeSet() { + if(scm==null) + scm = new CVSChangeLogParser(); + + if(changeSet==null) // cached value + changeSet = calcChangeSet(); + return changeSet; + } + + private ChangeLogSet calcChangeSet() { + File changelogFile = new File(getRootDir(), "changelog.xml"); + if(!changelogFile.exists()) + return ChangeLogSet.EMPTY; + + try { + return scm.parse(this,changelogFile); + } catch (IOException e) { + e.printStackTrace(); + } catch (SAXException e) { + e.printStackTrace(); + } + return ChangeLogSet.EMPTY; + } + + public Calendar due() { + return timestamp; + } + + /** + * Returns a {@link Slave} on which this build was done. + */ + public Node getBuiltOn() { + if(builtOn==null) + return Hudson.getInstance(); + else + return Hudson.getInstance().getSlave(builtOn); + } + + /** + * Returns the name of the slave it was built on, or null if it was the master. + */ + public String getBuiltOnStr() { + return builtOn; + } + + /** + * Gets {@link AbstractTestResultAction} associated with this build if any. + */ + public AbstractTestResultAction getTestResultAction() { + return getAction(AbstractTestResultAction.class); + } + + /** + * Gets the dependency relationship from this build (as the source) + * and that project (as the sink.) + * + * @return + * range of build numbers that represent which downstream builds are using this build. + * The range will be empty if no build of that project matches this. + */ + public RangeSet getDownstreamRelationship(Project that) { + RangeSet rs = new RangeSet(); + + FingerprintAction f = getAction(FingerprintAction.class); + if(f==null) return rs; + + // look for fingerprints that point to this build as the source, and merge them all + for (Fingerprint e : f.getFingerprints().values()) { + BuildPtr o = e.getOriginal(); + if(o!=null && o.is(this)) + rs.add(e.getRangeSet(that)); + } + + return rs; + } + + /** + * Gets the downstream builds of this build, which are the builds of the + * downstream project sthat use artifacts of this build. + * + * @return + * For each project with fingerprinting enabled, returns the range + * of builds (which can be empty if no build uses the artifact from this build.) + */ + public Map getDownstreamBuilds() { + Map r = new HashMap(); + for (Project p : getParent().getDownstreamProjects()) { + if(p.isFingerprintConfigured()) + r.put(p,getDownstreamRelationship(p)); + } + return r; + } + + /** + * Gets the dependency relationship from this build (as the sink) + * and that project (as the source.) + * + * @return + * Build number of the upstream build that feed into this build, + * or -1 if no record is avilable. + */ + public int getUpstreamRelationship(Project that) { + FingerprintAction f = getAction(FingerprintAction.class); + if(f==null) return -1; + + int n = -1; + + // look for fingerprints that point to the given project as the source, and merge them all + for (Fingerprint e : f.getFingerprints().values()) { + BuildPtr o = e.getOriginal(); + if(o!=null && o.is(that)) + n = Math.max(n,o.getNumber()); + } + + return n; + } + + /** + * Gets the upstream builds of this build, which are the builds of the + * upstream projects whose artifacts feed into this build. + */ + public Map getUpstreamBuilds() { + Map r = new HashMap(); + for (Project p : getParent().getUpstreamProjects()) { + int n = getUpstreamRelationship(p); + if(n>=0) + r.put(p,n); + } + return r; + } + + /** + * Gets the changes in the dependency between the given build and this build. + */ + public Map getDependencyChanges(Build from) { + if(from==null) return Collections.EMPTY_MAP; // make it easy to call this from views + FingerprintAction n = this.getAction(FingerprintAction.class); + FingerprintAction o = from.getAction(FingerprintAction.class); + if(n==null || o==null) return Collections.EMPTY_MAP; + + Map ndep = n.getDependencies(); + Map odep = o.getDependencies(); + + Map r = new HashMap(); + + for (Map.Entry entry : odep.entrySet()) { + Project p = entry.getKey(); + Integer oldNumber = entry.getValue(); + Integer newNumber = ndep.get(p); + if(newNumber!=null && oldNumber.compareTo(newNumber)<0) { + r.put(p,new DependencyChange(p,oldNumber,newNumber)); + } + } + + return r; + } + + /** + * Represents a change in the dependency. + */ + public static final class DependencyChange { + /** + * The dependency project. + */ + public final Project project; + /** + * Version of the dependency project used in the previous build. + */ + public final int fromId; + /** + * {@link Build} object for {@link #fromId}. Can be null if the log is gone. + */ + public final Build from; + /** + * Version of the dependency project used in this build. + */ + public final int toId; + + public final Build to; + + public DependencyChange(Project project, int fromId, int toId) { + this.project = project; + this.fromId = fromId; + this.toId = toId; + this.from = project.getBuildByNumber(fromId); + this.to = project.getBuildByNumber(toId); + } + } + + /** + * Performs a build. + */ + public void run() { + run(new Runner() { + + /** + * Since configuration can be changed while a build is in progress, + * stick to one launcher and use it. + */ + private Launcher launcher; + + public Result run(BuildListener listener) throws IOException { + Node node = Executor.currentExecutor().getOwner().getNode(); + assert builtOn==null; + builtOn = node.getNodeName(); + + launcher = node.createLauncher(listener); + if(node instanceof Slave) + listener.getLogger().println("Building remotely on "+node.getNodeName()); + + + if(!project.checkout(Build.this,launcher,listener,new File(getRootDir(),"changelog.xml"))) + return Result.FAILURE; + + SCM scm = project.getScm(); + + Build.this.scm = scm.createChangeLogParser(); + Build.this.changeSet = Build.this.calcChangeSet(); + + if(!preBuild(listener,project.getBuilders())) + return Result.FAILURE; + if(!preBuild(listener,project.getPublishers())) + return Result.FAILURE; + + if(!build(listener,project.getBuilders())) + return Result.FAILURE; + + if(!isWindows()) { + try { + // ignore a failure. + new Proc(new String[]{"rm","../lastSuccessful"},new String[0],listener.getLogger(),getProject().getBuildDir()).join(); + + int r = new Proc(new String[]{ + "ln","-s","builds/"+getId()/*ugly*/,"../lastSuccessful"}, + new String[0],listener.getLogger(),getProject().getBuildDir()).join(); + if(r!=0) + listener.getLogger().println("ln failed: "+r); + } catch (IOException e) { + PrintStream log = listener.getLogger(); + log.println("ln failed"); + Util.displayIOException(e,listener); + e.printStackTrace( log ); + } + } + + return Result.SUCCESS; + } + + public void post(BuildListener listener) { + // run all of them even if one of them failed + for( Publisher bs : project.getPublishers().values() ) + bs.perform(Build.this, launcher, listener); + } + + private boolean build(BuildListener listener, Map steps) { + for( Builder bs : steps.values() ) + if(!bs.perform(Build.this, launcher, listener)) + return false; + return true; + } + + private boolean preBuild(BuildListener listener,Map steps) { + for( BuildStep bs : steps.values() ) + if(!bs.prebuild(Build.this,listener)) + return false; + return true; + } + }); + } + + @Override + protected void onStartBuilding() { + SCMTrigger t = (SCMTrigger)project.getTriggers().get(SCMTrigger.DESCRIPTOR); + if(t==null) { + super.onStartBuilding(); + } else { + synchronized(t) { + try { + t.abort(); + } catch (InterruptedException e) { + // handle the interrupt later + Thread.currentThread().interrupt(); + } + super.onStartBuilding(); + } + } + } + + @Override + protected void onEndBuilding() { + SCMTrigger t = (SCMTrigger)project.getTriggers().get(SCMTrigger.DESCRIPTOR); + if(t==null) { + super.onEndBuilding(); + } else { + synchronized(t) { + super.onEndBuilding(); + t.startPolling(); + } + } + } + + @Override + public Map getEnvVars() { + Map env = super.getEnvVars(); + + JDK jdk = project.getJDK(); + if(jdk !=null) + jdk.buildEnvVars(env); + project.getScm().buildEnvVars(env); + return env; + } + +// +// +// actions +// +// + /** + * Stops this build if it's still going. + * + * If we use this/executor/stop URL, it causes 404 if the build is already killed, + * as {@link #getExecutor()} returns null. + */ + public synchronized void doStop( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + Executor e = getExecutor(); + if(e!=null) + e.doStop(req,rsp); + else + // nothing is building + rsp.forwardToPreviousPage(req); + } +} diff --git a/core/src/main/java/hudson/model/BuildListener.java b/core/src/main/java/hudson/model/BuildListener.java new file mode 100644 index 0000000000..789eb18edf --- /dev/null +++ b/core/src/main/java/hudson/model/BuildListener.java @@ -0,0 +1,19 @@ +package hudson.model; + +/** + * Receives events that happen during a build. + * + * @author Kohsuke Kawaguchi + */ +public interface BuildListener extends TaskListener { + + /** + * Called when a build is started. + */ + void started(); + + /** + * Called when a build is finished. + */ + void finished(Result result); +} diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java new file mode 100644 index 0000000000..134cf9c1e4 --- /dev/null +++ b/core/src/main/java/hudson/model/Computer.java @@ -0,0 +1,201 @@ +package hudson.model; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import hudson.util.RunList; + +/** + * Represents a set of {@link Executor}s on the same computer. + * + *

+ * {@link Executor}s on one {@link Computer} is transparently interchangeable + * (that is the definition of {@link Computer}.) + * + *

+ * This object is related to {@link Node} but they have some significant difference. + * {@link Computer} primarily works as a holder of {@link Executor}s, so + * if a {@link Node} is configured (probably temporarily) with 0 executors, + * you won't have a {@link Computer} object for it. + * + * 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. + * + *

+ * This object also serves UI (since {@link Node} is an interface and can't have + * related side pages.) + * + * @author Kohsuke Kawaguchi + */ +public class Computer implements ModelObject { + private final List executors = new ArrayList(); + + private int numExecutors; + + /** + * True if Hudson shouldn't start new builds on this node. + */ + private boolean temporarilyOffline; + + /** + * {@link Node} object may be created and deleted independently + * from this object. + */ + private String nodeName; + + public Computer(Node node) { + assert node.getNumExecutors()!=0 : "Computer created with 0 executors"; + setNode(node); + } + + /** + * Number of {@link Executor}s that are configured for this computer. + * + *

+ * When this value is decreased, it is temporarily possible + * for {@link #executors} to have a larger number than this. + */ + // ugly name to let EL access this + public int getNumExecutors() { + return numExecutors; + } + + /** + * Returns the {@link Node} that this computer represents. + */ + public Node getNode() { + if(nodeName==null) + return Hudson.getInstance(); + return Hudson.getInstance().getSlave(nodeName); + } + + public boolean isTemporarilyOffline() { + return temporarilyOffline; + } + + public void setTemporarilyOffline(boolean temporarilyOffline) { + this.temporarilyOffline = temporarilyOffline; + Hudson.getInstance().getQueue().scheduleMaintenance(); + } + + public String getIcon() { + if(temporarilyOffline) + return "computer-x.gif"; + else + return "computer.gif"; + } + + public String getDisplayName() { + return getNode().getNodeName(); + } + + public String getUrl() { + return "computer/"+getDisplayName()+"/"; + } + + /** + * Returns projects that are tied on this node. + */ + public List getTiedJobs() { + List r = new ArrayList(); + for( Project p : Hudson.getInstance().getProjects() ) { + if(p.getAssignedNode()==getNode()) + r.add(p); + } + return r; + } + + /*package*/ void setNode(Node node) { + assert node!=null; + if(node instanceof Slave) + this.nodeName = node.getNodeName(); + else + this.nodeName = null; + + setNumExecutors(node.getNumExecutors()); + } + + /*package*/ void kill() { + setNumExecutors(0); + } + + private synchronized void setNumExecutors(int n) { + this.numExecutors = n; + + // send signal to all idle executors to potentially kill them off + for( Executor e : executors ) + if(e.getCurrentBuild()==null) + e.interrupt(); + + // if the number is increased, add new ones + while(executors.size() getExecutors() { + return new ArrayList(executors); + } + + /** + * Called by {@link Executor} to kill excessive executors from this computer. + */ + /*package*/ synchronized void removeExecutor(Executor e) { + executors.remove(e); + if(executors.isEmpty()) + Hudson.getInstance().removeComputer(this); + } + + /** + * Interrupt all {@link Executor}s. + */ + public synchronized void interrupt() { + for (Executor e : executors) { + e.interrupt(); + } + } + +// +// +// UI +// +// + public void doRssAll( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + rss(req, rsp, " all builds", new RunList(getTiedJobs())); + } + public void doRssFailed( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + rss(req, rsp, " failed builds", new RunList(getTiedJobs()).failureOnly()); + } + private void rss(StaplerRequest req, StaplerResponse rsp, String suffix, RunList runs) throws IOException, ServletException { + RSS.forwardToRss(getDisplayName()+ suffix, getUrl(), + runs.newBuilds(), Run.FEED_ADAPTER, req, rsp ); + } + + public void doToggleOffline( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + + setTemporarilyOffline(!temporarilyOffline); + rsp.forwardToPreviousPage(req); + } +} diff --git a/core/src/main/java/hudson/model/Describable.java b/core/src/main/java/hudson/model/Describable.java new file mode 100644 index 0000000000..8f89adfc9d --- /dev/null +++ b/core/src/main/java/hudson/model/Describable.java @@ -0,0 +1,13 @@ +package hudson.model; + +/** + * Classes that are described by {@link Descriptor}. + * + * @author Kohsuke Kawaguchi + */ +public interface Describable> { + /** + * Gets the descriptor for this instance. + */ + Descriptor getDescriptor(); +} diff --git a/core/src/main/java/hudson/model/Descriptor.java b/core/src/main/java/hudson/model/Descriptor.java new file mode 100644 index 0000000000..2c87527a29 --- /dev/null +++ b/core/src/main/java/hudson/model/Descriptor.java @@ -0,0 +1,206 @@ +package hudson.model; + +import hudson.XmlFile; +import hudson.scm.CVSSCM; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; +import java.util.LinkedHashMap; + +import org.kohsuke.stapler.StaplerRequest; + +/** + * Metadata about a configurable instance. + * + *

+ * {@link Descriptor} is an object that has metadata about a {@link Describable} + * object, and also serves as a factory. A {@link Descriptor}/{@link Describable} + * combination is used throghout in Hudson to implement a + * configuration/extensibility mechanism. + * + *

+ * For example, Take the CVS support as an example, which is implemented + * in {@link CVSSCM} class. Whenever a job is configured with CVS, a new + * {@link CVSSCM} instance is created with the per-job configuration + * information. This instance gets serialized to XML, and this instance + * will be called to perform CVS operations for that job. This is the job + * of {@link Describable} — each instance represents a specific + * configuration of the CVS support (branch, CVSROOT, etc.) + * + *

+ * For Hudson to create such configured {@link CVSSCM} instance, Hudson + * needs another object that captures the metadata of {@link CVSSCM}, + * and that is what a {@link Descriptor} is for. {@link CVSSCM} class + * has a singleton descriptor, and this descriptor helps render + * the configuration form, remember system-wide configuration (such as + * where cvs.exe is), and works as a factory. + * + *

+ * {@link Descriptor} also usually have its associated views. + * + * @author Kohsuke Kawaguchi + * @see Describable + */ +public abstract class Descriptor> { + private Map properties; + + /** + * The class being described by this descriptor. + */ + public final Class clazz; + + protected Descriptor(Class clazz) { + this.clazz = clazz; + } + + /** + * Human readable name of this kind of configurable object. + */ + public abstract String getDisplayName(); + + /** + * Creates a configured instance from the submitted form. + * + *

+ * Hudson only invokes this method when the user wants an instance of T. + * So there's no need to check that in the implementation. + * + * @param req + * Always non-null. This object includes all the submitted form values. + * + * @throws FormException + * Signals a problem in the submitted form. + */ + public abstract T newInstance(StaplerRequest req) throws FormException; + + /** + * Returns the resource path to the help screen HTML, if any. + */ + public String getHelpFile() { + return ""; + } + + /** + * Checks if the given object is created from this {@link Descriptor}. + */ + public final boolean isInstance( T instance ) { + return clazz.isInstance(instance); + } + + /** + * Returns the data store that can be used to store configuration info. + * + *

+ * The data store is local to each {@link Descriptor}. + * + * @return + * never return null. + */ + protected synchronized Map getProperties() { + if(properties==null) + properties = load(); + return properties; + } + + /** + * Invoked when the global configuration page is submitted. + * + * Can be overrided to store descriptor-specific information. + * + * @return false + * to keep the client in the same config page. + */ + public boolean configure( HttpServletRequest req ) throws FormException { + return true; + } + + public final String getConfigPage() { + return '/'+clazz.getName().replace('.','/').replace('$','/')+"/config.jelly"; + } + + public final String getGlobalConfigPage() { + return '/'+clazz.getName().replace('.','/').replace('$','/')+"/global.jelly"; + } + + + /** + * Saves the configuration info to the disk. + */ + protected synchronized void save() { + if(properties!=null) + try { + getConfigFile().write(properties); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private Map load() { + // load + XmlFile file = getConfigFile(); + if(!file.exists()) + return new HashMap(); + + try { + return (Map)file.read(); + } catch (IOException e) { + return new HashMap(); + } + } + + private XmlFile getConfigFile() { + return new XmlFile(new File(Hudson.getInstance().getRootDir(),clazz.getName()+".xml")); + } + + // to work around warning when creating a generic array type + public static T[] toArray( T... values ) { + return values; + } + + public static List toList( T... values ) { + final ArrayList r = new ArrayList(); + for (T v : values) + r.add(v); + return r; + } + + public static > + Map,T> toMap(List describables) { + Map,T> m = new LinkedHashMap,T>(); + for (T d : describables) { + m.put(d.getDescriptor(),d); + } + return m; + } + + public static final class FormException extends Exception { + private final String formField; + + public FormException(String message, String formField) { + super(message); + this.formField = formField; + } + + public FormException(String message, Throwable cause, String formField) { + super(message, cause); + this.formField = formField; + } + + public FormException(Throwable cause, String formField) { + super(cause); + this.formField = formField; + } + + /** + * Which form field contained an error? + */ + public String getFormField() { + return formField; + } + } +} diff --git a/core/src/main/java/hudson/model/DirectoryHolder.java b/core/src/main/java/hudson/model/DirectoryHolder.java new file mode 100644 index 0000000000..f9d8aaa922 --- /dev/null +++ b/core/src/main/java/hudson/model/DirectoryHolder.java @@ -0,0 +1,234 @@ +package hudson.model; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.StringTokenizer; + +/** + * Has convenience methods to serve file system. + * + * @author Kohsuke Kawaguchi + */ +public abstract class DirectoryHolder extends Actionable { + + /** + * Serves a file from the file system (Maps the URL to a directory in a file system.) + * + * @param icon + * The icon file name, like "folder-open.gif" + * @param serveDirIndex + * True to generate the directory index. + * False to serve "index.html" + */ + protected final void serveFile(StaplerRequest req, StaplerResponse rsp, File root, String icon, boolean serveDirIndex) throws IOException, ServletException { + if(req.getQueryString()!=null) { + req.setCharacterEncoding("UTF-8"); + String path = req.getParameter("path"); + if(path!=null) { + rsp.sendRedirect(URLEncoder.encode(path,"UTF-8")); + return; + } + } + + String path = req.getRestOfPath(); + + if(path.length()==0) + path = "/"; + + if(path.indexOf("..")!=-1 || path.length()<1) { + // don't serve anything other than files in the artifacts dir + rsp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + File f = new File(root,path.substring(1)); + + boolean isFingerprint=false; + if(f.getName().equals("*fingerprint*")) { + f = f.getParentFile(); + isFingerprint = true; + } + + if(!f.exists()) { + rsp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + if(f.isDirectory()) { + if(!req.getRequestURL().toString().endsWith("/")) { + rsp.sendRedirect2(req.getRequestURL().append('/').toString()); + return; + } + + if(serveDirIndex) { + req.setAttribute("it",this); + List parentPaths = buildParentPath(path); + req.setAttribute("parentPath",parentPaths); + req.setAttribute("topPath", + parentPaths.isEmpty() ? "." : repeat("../",parentPaths.size())); + req.setAttribute("files",buildChildPathList(f)); + req.setAttribute("icon",icon); + req.setAttribute("path",path); + req.getView(this,"dir.jelly").forward(req,rsp); + return; + } else { + f = new File(f,"index.html"); + } + } + + + if(isFingerprint) { + FileInputStream in = new FileInputStream(f); + try { + Hudson hudson = Hudson.getInstance(); + rsp.forward(hudson.getFingerprint(hudson.getDigestOf(in)),"/",req); + } finally { + in.close(); + } + } else { + rsp.serveFile(req,f.toURL()); + } + } + + /** + * Builds a list of {@link Path} that represents ancestors + * from a string like "/foo/bar/zot". + */ + private List buildParentPath(String pathList) { + List r = new ArrayList(); + StringTokenizer tokens = new StringTokenizer(pathList, "/"); + int total = tokens.countTokens(); + int current=1; + while(tokens.hasMoreTokens()) { + String token = tokens.nextToken(); + r.add(new Path(repeat("../",total-current),token,true,0)); + current++; + } + return r; + } + + /** + * Builds a list of list of {@link Path}. The inner + * list of {@link Path} represents one child item to be shown + * (this mechanism is used to skip empty intermediate directory.) + */ + private List> buildChildPathList(File cur) { + List> r = new ArrayList>(); + + File[] files = cur.listFiles(); + Arrays.sort(files,FILE_SORTER); + + for( File f : files ) { + Path p = new Path(f.getName(),f.getName(),f.isDirectory(),f.length()); + if(!f.isDirectory()) { + r.add(Collections.singletonList(p)); + } else { + // find all empty intermediate directory + List l = new ArrayList(); + l.add(p); + String relPath = f.getName(); + while(true) { + // files that don't start with '.' qualify for 'meaningful files', nor SCM related files + File[] sub = f.listFiles(new FilenameFilter() { + public boolean accept(File dir, String name) { + return !name.startsWith(".") && !name.equals("CVS") && !name.equals(".svn"); + } + }); + if(sub.length!=1 || !sub[0].isDirectory()) + break; + f = sub[0]; + relPath += '/'+f.getName(); + l.add(new Path(relPath,f.getName(),true,0)); + } + r.add(l); + } + } + + return r; + } + + private static String repeat(String s,int times) { + StringBuffer buf = new StringBuffer(s.length()*times); + for(int i=0; i FILE_SORTER = new Comparator() { + public int compare(File lhs, File rhs) { + // directories first, files next + int r = dirRank(lhs)-dirRank(rhs); + if(r!=0) return r; + // otherwise alphabetical + return lhs.getName().compareTo(rhs.getName()); + } + + private int dirRank(File f) { + if(f.isDirectory()) return 0; + else return 1; + } + }; +} diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java new file mode 100644 index 0000000000..313cb86498 --- /dev/null +++ b/core/src/main/java/hudson/model/Executor.java @@ -0,0 +1,121 @@ +package hudson.model; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import java.io.IOException; + + +/** + * Thread that executes builds. + * + * @author Kohsuke Kawaguchi + */ +public class Executor extends Thread { + private final Computer owner; + private final Queue queue; + + private Build build; + + private long startTime; + + public Executor(Computer owner) { + super("Executor #"+owner.getExecutors().size()+" for "+owner.getDisplayName()); + this.owner = owner; + this.queue = Hudson.getInstance().getQueue(); + start(); + } + + public void run() { + while(true) { + if(Hudson.getInstance().isTerminating()) + return; + + synchronized(owner) { + if(owner.getNumExecutors()=100) num=99; + return num; + } + + /** + * Stops the current build. + */ + public void doStop( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + + interrupt(); + rsp.forwardToPreviousPage(req); + } + + public Computer getOwner() { + return owner; + } + + /** + * Returns the executor of the current thread. + */ + public static Executor currentExecutor() { + return (Executor)Thread.currentThread(); + } +} diff --git a/core/src/main/java/hudson/model/ExternalJob.java b/core/src/main/java/hudson/model/ExternalJob.java new file mode 100644 index 0000000000..532829cc2f --- /dev/null +++ b/core/src/main/java/hudson/model/ExternalJob.java @@ -0,0 +1,78 @@ +package hudson.model; + +import hudson.model.RunMap.Constructor; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + +/** + * Job that runs outside Hudson whose result is submitted to Hudson + * (either via web interface, or simply by placing files on the file system, + * for compatibility.) + * + * @author Kohsuke Kawaguchi + */ +public class ExternalJob extends ViewJob { + public ExternalJob(Hudson parent,String name) { + super(parent,name); + } + + @Override + protected void reload() { + this.runs.load(this,new Constructor() { + public ExternalRun create(File dir) throws IOException { + return new ExternalRun(ExternalJob.this,dir); + } + }); + } + + + /** + * Creates a new build of this project for immediate execution. + * + * Needs to be synchronized so that two {@link #newBuild()} invocations serialize each other. + */ + public ExternalRun newBuild() throws IOException { + ExternalRun run = new ExternalRun(this); + runs.put(run); + return run; + } + + /** + * Used to check if this is an external job and ready to accept a build result. + */ + public void doAcceptBuildResult( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + rsp.setStatus(HttpServletResponse.SC_OK); + } + + /** + * Used to post the build result from a remote machine. + */ + public void doPostBuildResult( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + ExternalRun run = newBuild(); + run.acceptRemoteSubmission(req.getReader()); + rsp.setStatus(HttpServletResponse.SC_OK); + } + + + private static final Logger logger = Logger.getLogger(ExternalJob.class.getName()); + + public JobDescriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final JobDescriptor DESCRIPTOR = new JobDescriptor(ExternalJob.class) { + public String getDisplayName() { + return "Monitoring an external job"; + } + + public ExternalJob newInstance(String name) { + return new ExternalJob(Hudson.getInstance(),name); + } + }; +} diff --git a/core/src/main/java/hudson/model/ExternalRun.java b/core/src/main/java/hudson/model/ExternalRun.java new file mode 100644 index 0000000000..14485acc07 --- /dev/null +++ b/core/src/main/java/hudson/model/ExternalRun.java @@ -0,0 +1,100 @@ +package hudson.model; + +import hudson.Proc; +import hudson.util.DecodingStream; +import hudson.util.DualOutputStream; +import org.xmlpull.mxp1.MXParser; +import org.xmlpull.v1.XmlPullParser; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.Reader; + +/** + * {@link Run} for {@link ExternalJob}. + * + * @author Kohsuke Kawaguchi + */ +public class ExternalRun extends Run { + /** + * Loads a run from a log file. + */ + ExternalRun(ExternalJob owner, File runDir) throws IOException { + super(owner,runDir); + } + + /** + * Creates a new run. + */ + ExternalRun(ExternalJob project) throws IOException { + super(project); + } + + /** + * Instead of performing a build, run the specified command, + * record the log and its exit code, then call it a build. + */ + public void run(final String[] cmd) { + run(new Runner() { + public Result run(BuildListener listener) throws Exception { + Proc proc = new Proc(cmd,getEnvVars(),System.in,new DualOutputStream(System.out,listener.getLogger())); + return proc.join()==0?Result.SUCCESS:Result.FAILURE; + } + + public void post(BuildListener listener) { + // do nothing + } + }); + } + + /** + * Instead of performing a build, accept the log and the return code + * from a remote machine in an XML format of: + * + *


+     * <run>
+     *  <log>...console output...</log>
+     *  <result>exit code</result>
+     * </run>
+     * 
+ */ + public void acceptRemoteSubmission(final Reader in) { + final long[] duration = new long[1]; + run(new Runner() { + public Result run(BuildListener listener) throws Exception { + PrintStream logger = new PrintStream(new DecodingStream(listener.getLogger())); + + XmlPullParser xpp = new MXParser(); + xpp.setInput(in); + xpp.nextTag(); // get to the + xpp.nextTag(); // get to the + while(xpp.nextToken()!=XmlPullParser.END_TAG) { + int type = xpp.getEventType(); + if(type==XmlPullParser.TEXT + || type==XmlPullParser.CDSECT) + logger.print(xpp.getText()); + } + xpp.nextTag(); // get to + + Result r = Integer.parseInt(xpp.nextText())==0?Result.SUCCESS:Result.FAILURE; + + xpp.nextTag(); // get to (optional) + if(xpp.getEventType()==XmlPullParser.START_TAG + && xpp.getName().equals("duration")) { + duration[0] = Long.parseLong(xpp.nextText()); + } + + return r; + } + + public void post(BuildListener listener) { + // do nothing + } + }); + + if(duration[0]!=0) + super.duration = duration[0]; + } + +} diff --git a/core/src/main/java/hudson/model/Fingerprint.java b/core/src/main/java/hudson/model/Fingerprint.java new file mode 100644 index 0000000000..1ccd6e491c --- /dev/null +++ b/core/src/main/java/hudson/model/Fingerprint.java @@ -0,0 +1,534 @@ +package hudson.model; + +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.converters.collections.CollectionConverter; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import hudson.Util; +import hudson.XmlFile; +import hudson.util.HexBinaryConverter; +import hudson.util.XStream2; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Hashtable; +import java.util.List; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A file being tracked by Hudson. + * + * @author Kohsuke Kawaguchi + */ +public class Fingerprint implements ModelObject { + /** + * Pointer to a {@link Build}. + */ + public static class BuildPtr { + final String name; + final int number; + + public BuildPtr(String name, int number) { + this.name = name; + this.number = number; + } + + public BuildPtr(Run run) { + this( run.getParent().getName(), run.getNumber() ); + } + + /** + * Gets the name of the job. + *

+ * Such job could be since then removed, + * so there might not be a corresponding + * {@link Job}. + */ + public String getName() { + return name; + } + + /** + * Gets the {@link Job} that this pointer points to, + * or null if such a job no longer exists. + */ + public Job getJob() { + return Hudson.getInstance().getJob(name); + } + + /** + * Gets the project build number. + *

+ * Such {@link Run} could be since then + * discarded. + */ + public int getNumber() { + return number; + } + + /** + * Gets the {@link Job} that this pointer points to, + * or null if such a job no longer exists. + */ + public Run getRun() { + Job j = getJob(); + if(j==null) return null; + return j.getBuildByNumber(number); + } + + private boolean isAlive() { + return getRun()!=null; + } + + /** + * Returns true if {@link BuildPtr} points to the given run. + */ + public boolean is(Run r) { + return r.getNumber()==number && r.getParent().getName().equals(name); + } + + /** + * Returns true if {@link BuildPtr} points to the given job. + */ + public boolean is(Job job) { + return job.getName().equals(name); + } + } + + /** + * Range of build numbers [start,end). Immutable. + */ + public static final class Range { + final int start; + final int end; + + public Range(int start, int end) { + assert start ranges; + + public RangeSet() { + this(new ArrayList()); + } + + private RangeSet(List data) { + this.ranges = data; + } + + /** + * Gets all the ranges. + */ + public synchronized List getRanges() { + return new ArrayList(ranges); + } + + /** + * Expands the range set to include the given value. + * If the set already includes this number, this will be a no-op. + */ + public synchronized void add(int n) { + for( int i=0; i0) buf.append(','); + buf.append(r); + } + return buf.toString(); + } + + public synchronized boolean isEmpty() { + return ranges.isEmpty(); + } + + /** + * Returns true if all the integers logically in this {@link RangeSet} + * is smaller than the given integer. For example, {[1,3)} is smaller than 3, + * but {[1,3),[100,105)} is not smaller than anything less than 105. + * + * Note that {} is smaller than any n. + */ + public synchronized boolean isSmallerThan(int n) { + if(ranges.isEmpty()) return true; + + return ranges.get(ranges.size() - 1).isSmallerThan(n); + } + + static final class ConverterImpl implements Converter { + private final Converter collectionConv; // used to convert ArrayList in it + + public ConverterImpl(Converter collectionConv) { + this.collectionConv = collectionConv; + } + + public boolean canConvert(Class type) { + return type==RangeSet.class; + } + + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + collectionConv.marshal( ((RangeSet)source).getRanges(), writer, context ); + } + + public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) { + return new RangeSet((List)(collectionConv.unmarshal(reader,context))); + } + } + } + + private final Date timestamp; + + /** + * Null if this fingerprint is for a file that's + * apparently produced outside. + */ + private final BuildPtr original; + + private final byte[] md5sum; + + private final String fileName; + + /** + * Range of builds that use this file keyed by a job name. + */ + private final Hashtable usages = new Hashtable(); + + public Fingerprint(Run build, String fileName, byte[] md5sum) throws IOException { + this.original = build==null ? null : new BuildPtr(build); + this.md5sum = md5sum; + this.fileName = fileName; + this.timestamp = new Date(); + save(); + } + + /** + * The first build in which this file showed up, + * if the file looked like it's created there. + *

+ * This is considered as the "source" of this file, + * or the owner, in the sense that this project "owns" + * this file. + * + * @return null + * if the file is apparently created outside Hudson. + */ + public BuildPtr getOriginal() { + return original; + } + + public String getDisplayName() { + return fileName; + } + + /** + * The file name (like "foo.jar" without path). + */ + public String getFileName() { + return fileName; + } + + /** + * Gets the MD5 hash string. + */ + public String getHashString() { + return Util.toHexString(md5sum); + } + + /** + * Gets the timestamp when this record is created. + */ + public Date getTimestamp() { + return timestamp; + } + + /** + * Gets the string that says how long since this build has scheduled. + * + * @return + * string like "3 minutes" "1 day" etc. + */ + public String getTimestampString() { + long duration = System.currentTimeMillis()-timestamp.getTime(); + return Util.getTimeSpanString(duration); + + } + + /** + * Gets the build range set for the given job name. + * + *

+ * These builds of this job has used this file. + */ + public RangeSet getRangeSet(String jobName) { + RangeSet r = usages.get(jobName); + if(r==null) r = new RangeSet(); + return r; + } + + public RangeSet getRangeSet(Job job) { + return getRangeSet(job.getName()); + } + + /** + * Gets the sorted list of job names where this jar is used. + */ + public List getJobs() { + List r = new ArrayList(); + r.addAll(usages.keySet()); + Collections.sort(r); + return r; + } + + public Hashtable getUsages() { + return usages; + } + + public synchronized void add(Build b) throws IOException { + add(b.getParent().getName(),b.getNumber()); + } + + /** + * Records that a build of a job has used this file. + */ + public synchronized void add(String jobName, int n) throws IOException { + synchronized(usages) { + RangeSet r = usages.get(jobName); + if(r==null) { + r = new RangeSet(); + usages.put(jobName,r); + } + r.add(n); + } + save(); + } + + /** + * Returns true if any of the builds recorded in this fingerprint + * is still retained. + * + *

+ * This is used to find out old fingerprint records that can be removed + * without losing too much information. + */ + public synchronized boolean isAlive() { + if(original.isAlive()) + return true; + + for (Entry e : usages.entrySet()) { + Job j = Hudson.getInstance().getJob(e.getKey()); + if(j==null) + continue; + + int oldest = j.getFirstBuild().getNumber(); + if(!e.getValue().isSmallerThan(oldest)) + return true; + } + return false; + } + + /** + * Save the settings to a file. + */ + public synchronized void save() throws IOException { + XmlFile f = getConfigFile(getFingerprintFile(md5sum)); + f.mkdirs(); + f.write(this); + } + + /** + * The file we save our configuration. + */ + private static XmlFile getConfigFile(File file) { + return new XmlFile(XSTREAM,file); + } + + /** + * Determines the file name from md5sum. + */ + private static File getFingerprintFile(byte[] md5sum) { + assert md5sum.length==16; + return new File( Hudson.getInstance().getRootDir(), + "fingerprints/"+ Util.toHexString(md5sum,0,1)+'/'+Util.toHexString(md5sum,1,1)+'/'+Util.toHexString(md5sum,2,md5sum.length-2)+".xml"); + } + + /** + * Loads a {@link Fingerprint} from a file in the image. + */ + /*package*/ static Fingerprint load(byte[] md5sum) throws IOException { + return load(getFingerprintFile(md5sum)); + } + /*package*/ static Fingerprint load(File file) throws IOException { + XmlFile configFile = getConfigFile(file); + if(!configFile.exists()) + return null; + try { + return (Fingerprint)configFile.read(); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to load "+configFile,e); + throw e; + } + } + + private static final XStream XSTREAM = new XStream2(); + static { + XSTREAM.alias("fingerprint",Fingerprint.class); + XSTREAM.alias("range",Range.class); + XSTREAM.alias("ranges",RangeSet.class); + XSTREAM.registerConverter(new HexBinaryConverter(),10); + XSTREAM.registerConverter(new RangeSet.ConverterImpl( + new CollectionConverter(XSTREAM.getClassMapper()) { + protected Object createCollection(Class type) { + return new ArrayList(); + } + } + ),10); + } + + private static final Logger logger = Logger.getLogger(Fingerprint.class.getName()); +} diff --git a/core/src/main/java/hudson/model/FingerprintCleanupThread.java b/core/src/main/java/hudson/model/FingerprintCleanupThread.java new file mode 100644 index 0000000000..95b4c2a29a --- /dev/null +++ b/core/src/main/java/hudson/model/FingerprintCleanupThread.java @@ -0,0 +1,94 @@ +package hudson.model; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.logging.Level; +import java.util.regex.Pattern; + +/** + * Scans the fingerprint database and remove old records + * that are no longer relevant. + * + *

+ * A {@link Fingerprint} is removed when none of the builds that + * it point to is available in the records. + * + * @author Kohsuke Kawaguchi + */ +public final class FingerprintCleanupThread extends PeriodicWork { + + private static FingerprintCleanupThread theInstance; + + public FingerprintCleanupThread() { + super("Fingerprint cleanup"); + theInstance = this; + } + + public static void invoke() { + theInstance.run(); + } + + protected void execute() { + int numFiles = 0; + + File root = new File(Hudson.getInstance().getRootDir(),"fingerprints"); + File[] files1 = root.listFiles(LENGTH2DIR_FILTER); + if(files1!=null) { + for (File file1 : files1) { + File[] files2 = file1.listFiles(LENGTH2DIR_FILTER); + for(File file2 : files2) { + File[] files3 = file2.listFiles(FINGERPRINTFILE_FILTER); + for(File file3 : files3) { + if(check(file3)) + numFiles++; + } + deleteIfEmpty(file2); + } + deleteIfEmpty(file1); + } + } + + logger.log(Level.INFO, "Cleaned up "+numFiles+" records"); + } + + /** + * Deletes a directory if it's empty. + */ + private void deleteIfEmpty(File dir) { + String[] r = dir.list(); + if(r==null) return; // can happen in a rare occasion + if(r.length==0) + dir.delete(); + } + + /** + * Examines the file and returns true if a file was deleted. + */ + private boolean check(File fingerprintFile) { + try { + Fingerprint fp = Fingerprint.load(fingerprintFile); + if(!fp.isAlive()) { + fingerprintFile.delete(); + return true; + } + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to process "+fingerprintFile, e); + } + return false; + } + + private static final FileFilter LENGTH2DIR_FILTER = new FileFilter() { + public boolean accept(File f) { + return f.isDirectory() && f.getName().length()==2; + } + }; + + private static final FileFilter FINGERPRINTFILE_FILTER = new FileFilter() { + private final Pattern PATTERN = Pattern.compile("[0-9a-f]{28}\\.xml"); + + public boolean accept(File f) { + return f.isFile() && PATTERN.matcher(f.getName()).matches(); + } + }; +} diff --git a/core/src/main/java/hudson/model/FingerprintMap.java b/core/src/main/java/hudson/model/FingerprintMap.java new file mode 100644 index 0000000000..153a0b584e --- /dev/null +++ b/core/src/main/java/hudson/model/FingerprintMap.java @@ -0,0 +1,79 @@ +package hudson.model; + +import hudson.Util; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; + +/** + * Cache of {@link Fingerprint}s. + * + *

+ * This implementation makes sure that no two {@link Fingerprint} objects + * lie around for the same hash code, and that unused {@link Fingerprint} + * will be adequately GC-ed to prevent memory leak. + * + * @author Kohsuke Kawaguchi + */ +public final class FingerprintMap { + private final Map> core = new HashMap>(); + + /** + * Returns true if there's some data in the fingerprint database. + */ + public boolean isReady() { + return new File( Hudson.getInstance().getRootDir(),"fingerprints").exists(); + } + + /** + * @param build + * set to non-null if {@link Fingerprint} to be created (if so) + * will have this build as the owner. Otherwise null, to indicate + * an owner-less build. + */ + public synchronized Fingerprint getOrCreate(Build build, String fileName, byte[] md5sum) throws IOException { + return getOrCreate(build,fileName, Util.toHexString(md5sum)); + } + + public synchronized Fingerprint getOrCreate(Build build, String fileName, String md5sum) throws IOException { + assert build!=null; + assert fileName!=null; + Fingerprint fp = get(md5sum); + if(fp!=null) + return fp; // found it. + + // not found. need to create one. + // creates a new one + fp = new Fingerprint(build,fileName,toByteArray(md5sum)); + + core.put(md5sum,new WeakReference(fp)); + + return fp; + } + + public synchronized Fingerprint get(String md5sum) throws IOException { + if(md5sum.length()!=32) + return null; // illegal input + md5sum = md5sum.toLowerCase(); + + WeakReference wfp = core.get(md5sum); + if(wfp!=null) { + Fingerprint fp = wfp.get(); + if(fp!=null) + return fp; // found it + } + + return Fingerprint.load(toByteArray(md5sum)); + } + + private byte[] toByteArray(String md5sum) { + byte[] data = new byte[16]; + for( int i=0; i computers = new HashMap(); + + /** + * Number of executors of the master node. + */ + private int numExecutors = 2; + + /** + * False to enable anyone to do anything. + */ + private boolean useSecurity = false; + + /** + * Message displayed in the top page. + */ + private String systemMessage; + + /** + * Root directory of the system. + */ + public transient final File root; + + /** + * All {@link Job}s keyed by their names. + */ + /*package*/ transient final Map jobs = new TreeMap(); + + /** + * The sole instance. + */ + private static Hudson theInstance; + + private transient boolean isQuietingDown; + private transient boolean terminating; + + private List jdks; + + /** + * Set of installed cluster nodes. + * + * 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. + */ + private volatile List slaves; + + /** + * Quiet period. + * + * This is {@link Integer} so that we can initialize it to '5' for upgrading users. + */ + /*package*/ Integer quietPeriod; + + /** + * {@link View}s. + */ + private List views; // can't initialize it eagerly for backward compatibility + + private transient final FingerprintMap fingerprintMap = new FingerprintMap(); + + /** + * Loaded plugins. + */ + public transient final PluginManager pluginManager; + + public static Hudson getInstance() { + return theInstance; + } + + + public Hudson(File root, ServletContext context) throws IOException { + this.root = root; + if(theInstance!=null) + throw new IllegalStateException("second instance"); + theInstance = this; + + // load plugins. + pluginManager = new PluginManager(context); + + load(); + if(slaves==null) slaves = new ArrayList(); + updateComputerList(); + + getQueue().load(); + } + + /** + * If you are calling it o Hudson something is wrong. + * + * @deprecated + */ + public String getNodeName() { + return ""; + } + + public String getNodeDescription() { + return "the master Hudson node"; + } + + public String getDescription() { + return systemMessage; + } + + public PluginManager getPluginManager() { + return pluginManager; + } + + /** + * Gets the SCM descriptor by name. Primarily used for making them web-visible. + */ + public Descriptor getScm(String shortClassName) { + return findDescriptor(shortClassName,SCMS.SCMS); + } + + /** + * Gets the builder descriptor by name. Primarily used for making them web-visible. + */ + public Descriptor getBuilder(String shortClassName) { + return findDescriptor(shortClassName, BuildStep.BUILDERS); + } + + /** + * Gets the publisher descriptor by name. Primarily used for making them web-visible. + */ + public Descriptor getPublisher(String shortClassName) { + return findDescriptor(shortClassName, BuildStep.PUBLISHERS); + } + + /** + * Finds a descriptor that has the specified name. + */ + private > + Descriptor findDescriptor(String shortClassName, Collection> descriptors) { + String name = '.'+shortClassName; + for (Descriptor d : descriptors) { + if(d.clazz.getName().endsWith(name)) + return d; + } + return null; + } + + /** + * Gets the plugin object from its short name. + * + *

+ * This allows URL hudson/plugin/ID to be served by the views + * of the plugin class. + */ + public Plugin getPlugin(String shortName) { + PluginWrapper p = pluginManager.getPlugin(shortName); + if(p==null) return null; + return p.plugin; + } + + /** + * Synonym to {@link #getNodeDescription()}. + */ + public String getSystemMessage() { + return systemMessage; + } + + public Launcher createLauncher(TaskListener listener) { + return new Launcher(listener); + } + + /** + * Updates {@link #computers} by using {@link #getSlaves()}. + * + *

+ * This method tries to reuse existing {@link Computer} objects + * so that we won't upset {@link Executor}s running in it. + */ + private void updateComputerList() { + synchronized(computers) { + Map byName = new HashMap(); + for (Computer c : computers.values()) + byName.put(c.getNode().getNodeName(),c); + + Set old = new HashSet(computers.values()); + Set used = new HashSet(); + + updateComputer(this, byName, used); + for (Slave s : getSlaves()) + updateComputer(s, byName, used); + + // find out what computers are removed, and kill off all executors. + // when all executors exit, it will be removed from the computers map. + // so don't remove too quickly + old.removeAll(used); + for (Computer c : old) { + c.kill(); + } + } + getQueue().scheduleMaintenance(); + } + + private void updateComputer(Node n, Map byNameMap, Set used) { + Computer c; + c = byNameMap.get(n.getNodeName()); + if(c==null) { + if(n.getNumExecutors()>0) + computers.put(n,c=new Computer(n)); + } else { + c.setNode(n); + } + used.add(c); + } + + /*package*/ void removeComputer(Computer computer) { + synchronized(computers) { + Iterator> itr=computers.entrySet().iterator(); + while(itr.hasNext()) { + if(itr.next().getValue()==computer) { + itr.remove(); + return; + } + } + } + throw new IllegalStateException("Trying to remove unknown computer"); + } + + /** + * Gets the snapshot of all the jobs. + */ + public synchronized List getJobs() { + return new ArrayList(jobs.values()); + } + + /** + * Gets the snapshot of all the projects. + */ + public synchronized List getProjects() { + List r = new ArrayList(); + for (Job job : jobs.values()) { + if(job instanceof Project) + r.add((Project)job); + } + return r; + } + + /** + * Gets the names of all the {@link Job}s. + */ + public synchronized Collection getJobNames() { + return new AbstractList() { + private final List jobs = getJobs(); + public String get(int index) { + return jobs.get(index).getName(); + } + + public int size() { + return jobs.size(); + } + }; + } + + /** + * Every job belongs to us. + * + * @deprecated + * why are you calling a method that always return true? + */ + public boolean containsJob(Job job) { + return true; + } + + public synchronized JobCollection getView(String name) { + if(views!=null) { + for (View v : views) { + if(v.getViewName().equals(name)) + return v; + } + } + if(this.getViewName().equals(name)) + return this; + else + return null; + } + + /** + * Gets the read-only list of all {@link JobCollection}s. + */ + public synchronized JobCollection[] getViews() { + if(views==null) + views = new ArrayList(); + JobCollection[] r = new JobCollection[views.size()+1]; + views.toArray(r); + // sort Views and put "all" at the very beginning + r[r.length-1] = r[0]; + Arrays.sort(r,1,r.length,JobCollection.SORTER); + r[0] = this; + return r; + } + + public synchronized void deleteView(View view) throws IOException { + if(views!=null) { + views.remove(view); + save(); + } + } + + public String getViewName() { + return "All"; + } + + /** + * Gets the read-only list of all {@link Computer}s. + */ + public Computer[] getComputers() { + synchronized(computers) { + Computer[] r = computers.values().toArray(new Computer[computers.size()]); + Arrays.sort(r,new Comparator() { + public int compare(Computer lhs, Computer rhs) { + if(lhs.getNode()==Hudson.this) return -1; + if(rhs.getNode()==Hudson.this) return 1; + return lhs.getNode().getNodeName().compareTo(rhs.getNode().getNodeName()); + } + }); + return r; + } + } + + public Computer getComputer(String name) { + synchronized(computers) { + for (Computer c : computers.values()) { + if(c.getNode().getNodeName().equals(name)) + return c; + } + } + return null; + } + + public Queue getQueue() { + return queue; + } + + public String getDisplayName() { + return "Hudson"; + } + + public List getJDKs() { + if(jdks==null) + jdks = new ArrayList(); + return jdks; + } + + /** + * Gets the JDK installation of the given name, or returns null. + */ + public JDK getJDK(String name) { + for (JDK j : getJDKs()) { + if(j.getName().equals(name)) + return j; + } + return null; + } + + /** + * Gets the slave node of the give name, hooked under this Hudson. + */ + public Slave getSlave(String name) { + for (Slave s : getSlaves()) { + if(s.getNodeName().equals(name)) + return s; + } + return null; + } + + public List getSlaves() { + return Collections.unmodifiableList(slaves); + } + + /** + * Gets the system default quiet period. + */ + public int getQuietPeriod() { + return quietPeriod!=null ? quietPeriod : 5; + } + + public String getUrl() { + return ""; + } + + public File getRootDir() { + return root; + } + + public boolean isUseSecurity() { + return useSecurity; + } + + public void setUseSecurity(boolean useSecurity) { + this.useSecurity = useSecurity; + } + + /** + * Returns true if Hudson is quieting down. + *

+ * No further jobs will be executed unless it + * can be finished while other current pending builds + * are still in progress. + */ + public boolean isQuietingDown() { + return isQuietingDown; + } + + /** + * Returns true if the container initiated the termination of the web application. + */ + public boolean isTerminating() { + return terminating; + } + + /** + * Gets the job of the given name. + * + * @return null + * if such a project doesn't exist. + */ + public synchronized Job getJob(String name) { + return jobs.get(name); + } + + /** + * Gets the user of the given name. + * + * @return + * This method returns a non-null object for any user name, without validation. + */ + public User getUser(String name) { + return User.get(name); + } + + /** + * Creates a new job. + * + * @throws IllegalArgumentException + * if the project of the given name already exists. + */ + public synchronized Job createProject( JobDescriptor type, String name ) throws IOException { + if(jobs.containsKey(name)) + throw new IllegalArgumentException(); + + Job job; + try { + job = type.newInstance(name); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + + job.save(); + jobs.put(name,job); + return job; + } + + /** + * Called in response to {@link Job#doDoDelete(StaplerRequest, StaplerResponse)} + */ + /*package*/ void deleteJob(Job job) throws IOException { + jobs.remove(job.getName()); + if(views!=null) { + for (View v : views) { + synchronized(v) { + v.jobNames.remove(job.getName()); + } + } + save(); + } + } + + /** + * Called by {@link Job#renameTo(String)} to update relevant data structure. + * assumed to be synchronized on Hudson by the caller. + */ + /*package*/ void onRenamed(Job job, String oldName, String newName) throws IOException { + jobs.remove(oldName); + jobs.put(newName,job); + + if(views!=null) { + for (View v : views) { + synchronized(v) { + if(v.jobNames.remove(oldName)) + v.jobNames.add(newName); + } + } + save(); + } + } + + public FingerprintMap getFingerprintMap() { + return fingerprintMap; + } + + // if no fingrer print matches, display "not found page". + public Object getFingerprint( String md5sum ) throws IOException { + Fingerprint r = fingerprintMap.get(md5sum); + if(r==null) return new NoFingerprintMatch(md5sum); + else return r; + } + + /** + * Gets a {@link Fingerprint} object if it exists. + * Otherwise null. + */ + public Fingerprint _getFingerprint( String md5sum ) throws IOException { + return fingerprintMap.get(md5sum); + } + + /** + * The file we save our configuration. + */ + private XmlFile getConfigFile() { + return new XmlFile(XSTREAM, new File(root,"config.xml")); + } + + public int getNumExecutors() { + return numExecutors; + } + + public Mode getMode() { + return Mode.NORMAL; + } + + private synchronized void load() throws IOException { + XmlFile cfg = getConfigFile(); + if(cfg.exists()) + cfg.unmarshal(this); + + File projectsDir = new File(root,"jobs"); + if(!projectsDir.isDirectory() && !projectsDir.mkdirs()) { + if(projectsDir.exists()) + throw new IOException(projectsDir+" is not a directory"); + throw new IOException("Unable to create "+projectsDir+"\nPermission issue? Please create this directory manually."); + } + File[] subdirs = projectsDir.listFiles(new FileFilter() { + public boolean accept(File child) { + return child.isDirectory(); + } + }); + jobs.clear(); + for (File subdir : subdirs) { + try { + Job p = Job.load(this,subdir); + jobs.put(p.getName(), p); + } catch (IOException e) { + e.printStackTrace(); // TODO: logging + } + } + } + + /** + * Save the settings to a file. + */ + public synchronized void save() throws IOException { + getConfigFile().write(this); + } + + + /** + * Called to shut down the system. + */ + public void cleanUp() { + terminating = true; + synchronized(computers) { + for( Computer c : computers.values() ) + c.interrupt(); + } + ExternalJob.reloadThread.interrupt(); + Trigger.timer.cancel(); + + if(pluginManager!=null) // be defensive. there could be some ugly timing related issues + pluginManager.stop(); + + getQueue().save(); + } + + + +// +// +// actions +// +// + /** + * Accepts submission from the configuration page. + */ + public synchronized void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + try { + if(!Hudson.adminCheck(req,rsp)) + return; + + req.setCharacterEncoding("UTF-8"); + + useSecurity = req.getParameter("use_security")!=null; + + numExecutors = Integer.parseInt(req.getParameter("numExecutors")); + quietPeriod = Integer.parseInt(req.getParameter("quiet_period")); + + systemMessage = Util.nullify(req.getParameter("system_message")); + + {// update slave list + List newSlaves = new ArrayList(); + String[] names = req.getParameterValues("slave_name"); + String[] descriptions = req.getParameterValues("slave_description"); + String[] executors = req.getParameterValues("slave_executors"); + String[] cmds = req.getParameterValues("slave_command"); + String[] rfs = req.getParameterValues("slave_remoteFS"); + String[] lfs = req.getParameterValues("slave_localFS"); + String[] mode = req.getParameterValues("slave_mode"); + if(names!=null && descriptions!=null && executors!=null && cmds!=null && rfs!=null && lfs!=null && mode!=null) { + int len = Util.min(names.length,descriptions.length,executors.length,cmds.length,rfs.length, lfs.length, mode.length); + for(int i=0;i d : BuildStep.BUILDERS ) + result &= d.configure(req); + + for( Descriptor d : BuildStep.PUBLISHERS ) + result &= d.configure(req); + + for( Descriptor scmd : SCMS.SCMS ) + result &= scmd.configure(req); + + save(); + if(result) + rsp.sendRedirect("."); // go to the top page + else + rsp.sendRedirect("configure"); // back to config + } catch (FormException e) { + sendError(e,req,rsp); + } + } + + /** + * Accepts the new description. + */ + public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + + req.setCharacterEncoding("UTF-8"); + systemMessage = req.getParameter("description"); + save(); + rsp.sendRedirect("."); + } + + public synchronized void doQuietDown( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + isQuietingDown = true; + rsp.sendRedirect2("."); + } + + public synchronized void doCancelQuietDown( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + isQuietingDown = false; + getQueue().scheduleMaintenance(); + rsp.sendRedirect2("."); + } + + public synchronized Job doCreateJob( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return null; + + req.setCharacterEncoding("UTF-8"); + String name = req.getParameter("name").trim(); + String jobType = req.getParameter("type"); + String mode = req.getParameter("mode"); + + try { + checkGoodName(name); + } catch (ParseException e) { + sendError(e,req,rsp); + return null; + } + + if(getJob(name)!=null) { + sendError("A job already exists with the name '"+name+"'",req,rsp); + return null; + } + + if(mode==null) { + rsp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return null; + } + + Job result; + + if(mode.equals("newJob")) { + if(jobType ==null) { + // request forged? + rsp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return null; + } + // redirect to the project config screen + result = createProject(Jobs.getDescriptor(jobType), name); + } else { + Job src = getJob(req.getParameter("from")); + if(src==null) { + rsp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return null; + } + + result = createProject((JobDescriptor)src.getDescriptor(),name); + + // copy config + Util.copyFile(src.getConfigFile(),result.getConfigFile()); + + // reload from the new config + result = Job.load(this,result.getRootDir()); + result.nextBuildNumber = 1; // reset the next build number + jobs.put(name,result); + } + + rsp.sendRedirect2(req.getContextPath()+'/'+result.getUrl()+"configure"); + return result; + } + + public synchronized void doCreateView( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + + req.setCharacterEncoding("UTF-8"); + + String name = req.getParameter("name"); + + try { + checkGoodName(name); + } catch (ParseException e) { + sendError(e, req, rsp); + return; + } + + View v = new View(this, name); + if(views==null) + views = new Vector(); + views.add(v); + save(); + + // redirect to the config screen + rsp.sendRedirect2("./"+v.getUrl()+"configure"); + } + + /** + * Check if the given name is suitable as a name + * for job, view, etc. + * + * @throws ParseException + * if the given name is not good + */ + public static void checkGoodName(String name) throws ParseException { + if(name==null || name.length()==0) + throw new ParseException("No name is specified",0); + + for( int i=0; i".indexOf(ch)!=-1) + throw new ParseException("'"+ch+"' is an unsafe character",i); + } + + // looks good + } + + /** + * Called once the user logs in. Just forward to the top page. + */ + public synchronized void doLoginEntry( StaplerRequest req, StaplerResponse rsp ) throws IOException { + rsp.sendRedirect2(req.getContextPath()+"/"); + } + + /** + * Called once the user logs in. Just forward to the top page. + */ + public synchronized void doLogout( StaplerRequest req, StaplerResponse rsp ) throws IOException { + HttpSession session = req.getSession(false); + if(session!=null) + session.invalidate(); + rsp.sendRedirect2(req.getContextPath()+"/"); + } + + /** + * Reloads the configuration. + */ + public synchronized void doReload( StaplerRequest req, StaplerResponse rsp ) throws IOException { + if(!Hudson.adminCheck(req,rsp)) + return; + + load(); + rsp.sendRedirect2(req.getContextPath()+"/"); + } + + /** + * Uploads a plugin. + */ + public void doUploadPlugin( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + try { + if(!Hudson.adminCheck(req,rsp)) + return; + + ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory()); + + // Parse the request + FileItem fileItem = (FileItem) upload.parseRequest(req).get(0); + String fileName = Util.getFileName(fileItem.getName()); + if(!fileName.endsWith(".hpi")) { + sendError(fileName+" is not a Hudson plugin",req,rsp); + return; + } + fileItem.write(new File(getPluginManager().rootDir, fileName)); + + fileItem.delete(); + + rsp.sendRedirect2("managePlugins"); + } catch (IOException e) { + throw e; + } catch (Exception e) {// grrr. fileItem.write throws this + throw new ServletException(e); + } + } + + /** + * Do a finger-print check. + */ + public void doDoFingerprintCheck( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + try { + ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory()); + + // Parse the request + List items = upload.parseRequest(req); + + rsp.sendRedirect2(req.getContextPath()+"/fingerprint/"+ + getDigestOf(items.get(0).getInputStream())+'/'); + + // if an error occur and we fail to do this, it will still be cleaned up + // when GC-ed. + for (FileItem item : items) + item.delete(); + } catch (FileUploadException e) { + throw new ServletException(e); // I'm not sure what the implication of this + } + } + + public String getDigestOf(InputStream source) throws IOException, ServletException { + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + + DigestInputStream in =new DigestInputStream(source,md5); + byte[] buf = new byte[8192]; + try { + while(in.read(buf)>0) + ; // simply discard the input + } finally { + in.close(); + } + return Util.toHexString(md5.digest()); + } catch (NoSuchAlgorithmException e) { + throw new ServletException(e); // impossible + } + } + + /** + * Serves static resources without the "Last-Modified" header to work around + * a bug in Firefox. + * + * @see https://bugzilla.mozilla.org/show_bug.cgi?id=89419 + */ + public void doNocacheImages( StaplerRequest req, StaplerResponse rsp ) throws IOException { + String path = req.getRestOfPath(); + + if(path.length()==0) + path = "/"; + + if(path.indexOf("..")!=-1 || path.length()<1) { + // don't serve anything other than files in the artifacts dir + rsp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + File f = new File(req.getServletContext().getRealPath("/images"),path.substring(1)); + if(!f.exists()) { + rsp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + if(f.isDirectory()) { + // listing not allowed + rsp.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + + FileInputStream in = new FileInputStream(f); + // serve the file + String contentType = req.getServletContext().getMimeType(f.getPath()); + rsp.setContentType(contentType); + rsp.setContentLength((int)f.length()); + byte[] buf = new byte[1024]; + int len; + while((len=in.read(buf))>0) + rsp.getOutputStream().write(buf,0,len); + in.close(); + } + + /** + * For debugging. Expose URL to perfrom GC. + */ + public void doGc( StaplerRequest req, StaplerResponse rsp ) throws IOException { + System.gc(); + rsp.setStatus(HttpServletResponse.SC_OK); + rsp.setContentType("text/plain"); + rsp.getWriter().println("GCed"); + } + + /** + * For system diagnostics. + * Run arbitraary Groovy script. + */ + public void doScript( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!adminCheck(req,rsp)) + return; // ability to run arbitrary script is dangerous + + String text = req.getParameter("script"); + if(text!=null) { + GroovyShell shell = new GroovyShell(); + + StringWriter out = new StringWriter(); + PrintWriter pw = new PrintWriter(out); + shell.setVariable("out", pw); + try { + Object output = shell.evaluate(text); + if(output!=null) + pw.println("Result: "+output); + } catch (Throwable t) { + t.printStackTrace(pw); + } + req.setAttribute("output",out); + } + + req.getView(this,"_script.jelly").forward(req,rsp); + } + + public void doFingerprintCleanup( StaplerRequest req, StaplerResponse rsp ) throws IOException { + FingerprintCleanupThread.invoke(); + rsp.setStatus(HttpServletResponse.SC_OK); + rsp.setContentType("text/plain"); + rsp.getWriter().println("Invoked"); + } + + public void doWorkspaceCleanup( StaplerRequest req, StaplerResponse rsp ) throws IOException { + WorkspaceCleanupThread.invoke(); + rsp.setStatus(HttpServletResponse.SC_OK); + rsp.setContentType("text/plain"); + rsp.getWriter().println("Invoked"); + } + + /** + * Checks if the path is a valid path. + */ + public void doCheckLocalFSRoot( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + // this can be used to check the existence of a file on the server, so needs to be protected + new FormFieldValidator(req,rsp,true) { + public void check() throws IOException, ServletException { + File f = getFileParameter("value"); + if(f.isDirectory()) {// OK + ok(); + } else {// nope + if(f.exists()) { + error(f+" is not a directory"); + } else { + error("No such directory: "+f); + } + } + } + }.process(); + } + + /** + * Checks if the JAVA_HOME is a valid JAVA_HOME path. + */ + public void doJavaHomeCheck( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + // this can be used to check the existence of a file on the server, so needs to be protected + new FormFieldValidator(req,rsp,true) { + public void check() throws IOException, ServletException { + File f = getFileParameter("value"); + if(!f.isDirectory()) { + error(f+" is not a directory"); + return; + } + + File toolsJar = new File(f,"lib/tools.jar"); + if(!toolsJar.exists()) { + error(f+" doesn't look like a JDK directory"); + return; + } + + ok(); + } + }.process(); + } + + + public static boolean isWindows() { + return File.pathSeparatorChar==';'; + } + + + /** + * Returns all {@code CVSROOT} strings used in the current Hudson installation. + * + *

+ * Ideally this shouldn't be defined in here + * but EL doesn't provide a convenient way of invoking a static function, + * so I'm putting it here for now. + */ + public Set getAllCvsRoots() { + Set r = new TreeSet(); + for( Project p : getProjects() ) { + SCM scm = p.getScm(); + if (scm instanceof CVSSCM) { + CVSSCM cvsscm = (CVSSCM) scm; + r.add(cvsscm.getCvsRoot()); + } + } + + return r; + } + + public static boolean adminCheck(StaplerRequest req,StaplerResponse rsp) throws IOException { + if(!getInstance().isUseSecurity()) + return true; + + if(req.isUserInRole("admin")) + return true; + + rsp.sendError(StaplerResponse.SC_FORBIDDEN); + return false; + } + + /** + * Live view of recent {@link LogRecord}s produced by Hudson. + */ + public static List logRecords = Collections.EMPTY_LIST; // initialized to dummy value to avoid NPE + + /** + * Thread-safe reusable {@link XStream}. + */ + private static final XStream XSTREAM = new XStream2(); + + static { + XSTREAM.alias("hudson",Hudson.class); + XSTREAM.alias("slave",Slave.class); + XSTREAM.alias("view",View.class); + XSTREAM.alias("jdk",JDK.class); + } +} diff --git a/core/src/main/java/hudson/model/JDK.java b/core/src/main/java/hudson/model/JDK.java new file mode 100644 index 0000000000..d34c0f18f7 --- /dev/null +++ b/core/src/main/java/hudson/model/JDK.java @@ -0,0 +1,79 @@ +package hudson.model; + +import hudson.EnvVars; + +import java.io.File; +import java.util.Map; + +/** + * Information about JDK installation. + * + * @author Kohsuke Kawaguchi + */ +public final class JDK { + private final String name; + private final String javaHome; + + public JDK(String name, String javaHome) { + this.name = name; + this.javaHome = javaHome; + } + + /** + * install directory. + */ + public String getJavaHome() { + return javaHome; + } + + /** + * Human readable display name. + */ + public String getName() { + return name; + } + + /** + * Gets the path to the bin directory. + */ + public File getBinDir() { + return new File(getJavaHome(),"bin"); + } + /** + * Gets the path to 'java'. + */ + private File getExecutable() { + String execName; + if(File.separatorChar=='\\') + execName = "java.exe"; + else + execName = "java"; + + return new File(getJavaHome(),"bin/"+execName); + } + + /** + * Returns true if the executable exists. + */ + public boolean getExists() { + return getExecutable().exists(); + } + + /** + * Sets PATH and JAVA_HOME from this JDK. + */ + public void buildEnvVars(Map env) { + String path = env.get("PATH"); + if(path==null) + path = EnvVars.masterEnvVars.get("PATH"); + + if(path==null) + path = getBinDir().getPath(); + else + path = getBinDir().getPath()+File.pathSeparator+path; + env.put("PATH",path); + env.put("JAVA_HOME",javaHome); + if(!env.containsKey("HUDSON_HOME")) + env.put("HUDSON_HOME", Hudson.getInstance().getRootDir().getPath() ); + } +} diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java new file mode 100644 index 0000000000..4fb8b1d52f --- /dev/null +++ b/core/src/main/java/hudson/model/Job.java @@ -0,0 +1,661 @@ +package hudson.model; + +import com.thoughtworks.xstream.XStream; +import hudson.ExtensionPoint; +import hudson.Util; +import hudson.XmlFile; +import hudson.tasks.BuildTrigger; +import hudson.tasks.LogRotator; +import hudson.util.ChartUtil; +import hudson.util.DataSetBuilder; +import hudson.util.IOException2; +import hudson.util.RunList; +import hudson.util.ShiftedCategoryAxis; +import hudson.util.TextFile; +import hudson.util.XStream2; +import org.apache.tools.ant.taskdefs.Copy; +import org.apache.tools.ant.types.FileSet; +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.AreaRendererEndType; +import org.jfree.chart.renderer.category.AreaRenderer; +import org.jfree.data.category.CategoryDataset; +import org.jfree.ui.RectangleInsets; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import java.awt.Color; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.SortedMap; +import java.util.Collections; + +/** + * A job is an runnable entity under the monitoring of Hudson. + * + *

+ * Every time it "runs", it will be recorded as a {@link Run} object. + * + *

+ * To register a custom {@link Job} class from a plugin, add it to + * {@link Jobs#JOBS}. Also see {@link Job#XSTREAM}. + * + * @author Kohsuke Kawaguchi + */ +public abstract class Job, RunT extends Run> + extends DirectoryHolder implements Describable>, ExtensionPoint { + /** + * Project name. + */ + protected /*final*/ transient String name; + + /** + * Project description. Can be HTML. + */ + protected String description; + + /** + * Root directory for this job. + */ + protected transient File root; + + /** + * Next bulid number. + * Kept in a separate file because this is the only information + * that gets updated often. This allows the rest of the configuration + * to be in the VCS. + *

+ * In 1.28 and earlier, this field was stored in the project configuration file, + * so even though this is marked as transient, don't move it around. + */ + protected transient int nextBuildNumber = 1; + private transient Hudson parent; + + private LogRotator logRotator; + + private boolean keepDependencies; + + protected Job(Hudson parent,String name) { + this.parent = parent; + doSetName(name); + getBuildDir().mkdirs(); + } + + /** + * Called when a {@link Job} is loaded from disk. + */ + protected void onLoad(Hudson root, String name) throws IOException { + this.parent = root; + doSetName(name); + + TextFile f = getNextBuildNumberFile(); + if(f.exists()) { + // starting 1.28, we store nextBuildNumber in a separate file. + // but old Hudson didn't do it, so if the file doesn't exist, + // assume that nextBuildNumber was read from config.xml + try { + this.nextBuildNumber = Integer.parseInt(f.readTrim()); + } catch (NumberFormatException e) { + throw new IOException2(f+" doesn't contain a number",e); + } + } else { + // this must be the old Hudson. create this file now. + saveNextBuildNumber(); + save(); // and delete it from the config.xml + } + } + + /** + * Just update {@link #name} and {@link #root}, since they are linked. + */ + private void doSetName(String name) { + this.name = name; + this.root = new File(new File(parent.root,"jobs"),name); + } + + public File getRootDir() { + return root; + } + + private TextFile getNextBuildNumberFile() { + return new TextFile(new File(this.root,"nextBuildNumber")); + } + + private void saveNextBuildNumber() throws IOException { + getNextBuildNumberFile().write(String.valueOf(nextBuildNumber)+'\n'); + } + + public final Hudson getParent() { + return parent; + } + + public boolean isInQueue() { + return false; + } + + /** + * If true, it will keep all the build logs of dependency components. + */ + public boolean isKeepDependencies() { + return keepDependencies; + } + + /** + * Allocates a new buildCommand number. + */ + public synchronized int assignBuildNumber() throws IOException { + int r = nextBuildNumber++; + saveNextBuildNumber(); + return r; + } + + public int getNextBuildNumber() { + return nextBuildNumber; + } + + /** + * Gets the project description HTML. + */ + public String getDescription() { + return description; + } + + /** + * Sets the project description HTML. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Returns the log rotator for this job, or null if none. + */ + public LogRotator getLogRotator() { + return logRotator; + } + + public void setLogRotator(LogRotator logRotator) { + this.logRotator = logRotator; + } + + public String getName() { + return name; + } + + public String getDisplayName() { + return getName(); + } + + /** + * Renames a job. + */ + public void renameTo(String newName) throws IOException { + // always synchronize from bigger objects first + synchronized(parent) { + synchronized(this) { + // sanity check + if(newName==null) + throw new IllegalArgumentException("New name is not given"); + if(parent.getJob(newName)!=null) + throw new IllegalArgumentException("Job "+newName+" already exists"); + + // noop? + if(this.name.equals(newName)) + return; + + + String oldName = this.name; + File oldRoot = this.root; + + doSetName(newName); + File newRoot = this.root; + + {// rename data files + boolean interrupted=false; + boolean renamed = false; + + // try to rename the job directory. + // this may fail on Windows due to some other processes accessing a file. + // so retry few times before we fall back to copy. + for( int retry=0; retry<5; retry++ ) { + if(oldRoot.renameTo(newRoot)) { + renamed = true; + break; // succeeded + } + try { + Thread.sleep(500); + } catch (InterruptedException e) { + // process the interruption later + interrupted = true; + } + } + + if(interrupted) + Thread.currentThread().interrupt(); + + if(!renamed) { + // failed to rename. it must be that some lengthy process is going on + // to prevent a rename operation. So do a copy. Ideally we'd like to + // later delete the old copy, but we can't reliably do so, as before the VM + // shuts down there might be a new job created under the old name. + Copy cp = new Copy(); + cp.setProject(new org.apache.tools.ant.Project()); + cp.setTodir(newRoot); + FileSet src = new FileSet(); + src.setDir(getRootDir()); + cp.addFileset(src); + cp.setOverwrite(true); + cp.setPreserveLastModified(true); + cp.setFailOnError(false); // keep going even if there's an error + cp.execute(); + + // try to delete as much as possible + try { + Util.deleteRecursive(oldRoot); + } catch (IOException e) { + // but ignore the error, since we expect that + e.printStackTrace(); + } + } + } + + parent.onRenamed(this,oldName,newName); + + // update BuildTrigger of other projects that point to this object. + // can't we generalize this? + for( Project p : parent.getProjects() ) { + BuildTrigger t = (BuildTrigger) p.getPublishers().get(BuildTrigger.DESCRIPTOR); + if(t!=null) { + if(t.onJobRenamed(oldName,newName)) + p.save(); + } + } + } + } + } + + /** + * Returns true if we should display "build now" icon + */ + public abstract boolean isBuildable(); + + /** + * Gets all the builds. + * + * @return + * never null. The first entry is the latest buildCommand. + */ + public List getBuilds() { + return new ArrayList(_getRuns().values()); + } + + /** + * Gets all the builds in a map. + */ + public SortedMap getBuildsAsMap() { + return Collections.unmodifiableSortedMap(_getRuns()); + } + + /** + * @deprecated + * This is only used to support backward compatibility with + * old URLs. + */ + public RunT getBuild(String id) { + for (RunT r : _getRuns().values()) { + if(r.getId().equals(id)) + return r; + } + return null; + } + + /** + * @param n + * The build number. + * @see Run#getNumber() + */ + public RunT getBuildByNumber(int n) { + return _getRuns().get(n); + } + + public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { + try { + // try to interpret the token as build number + return _getRuns().get(Integer.valueOf(token)); + } catch (NumberFormatException e) { + return super.getDynamic(token,req,rsp); + } + } + + /** + * The file we save our configuration. + */ + protected static XmlFile getConfigFile(File dir) { + return new XmlFile(XSTREAM,new File(dir,"config.xml")); + } + + File getConfigFile() { + return new File(root,"config.xml"); + } + + /** + * Directory for storing {@link Run} records. + *

+ * Some {@link Job}s may not have backing data store for {@link Run}s, + * but those {@link Job}s that use file system for storing data + * should use this directory for consistency. + * + * @see RunMap + */ + protected File getBuildDir() { + return new File(root,"builds"); + } + + /** + * Returns the URL of this project. + */ + public String getUrl() { + return "job/"+name+'/'; + } + + /** + * Gets all the runs. + * + * The resulting map must be immutable (by employing copy-on-write semantics.) + */ + protected abstract SortedMap _getRuns(); + + /** + * Called from {@link Run} to remove it from this job. + * + * The files are deleted already. So all the callee needs to do + * is to remove a reference from this {@link Job}. + */ + protected abstract void removeRun(RunT run); + + /** + * Returns the last build. + */ + public RunT getLastBuild() { + SortedMap runs = _getRuns(); + + if(runs.isEmpty()) return null; + return runs.get(runs.firstKey()); + } + + /** + * Returns the oldest build in the record. + */ + public RunT getFirstBuild() { + SortedMap runs = _getRuns(); + + if(runs.isEmpty()) return null; + return runs.get(runs.lastKey()); + } + + /** + * Returns the last successful build, if any. Otherwise null. + */ + public RunT getLastSuccessfulBuild() { + RunT r = getLastBuild(); + // temporary hack till we figure out what's causing this bug + while(r!=null && (r.isBuilding() || r.getResult()==null || r.getResult().isWorseThan(Result.UNSTABLE))) + r=r.getPreviousBuild(); + return r; + } + + /** + * Returns the last stable build, if any. Otherwise null. + */ + public RunT getLastStableBuild() { + RunT r = getLastBuild(); + while(r!=null && (r.isBuilding() || r.getResult().isWorseThan(Result.SUCCESS))) + r=r.getPreviousBuild(); + return r; + } + + /** + * Returns the last failed build, if any. Otherwise null. + */ + public RunT getLastFailedBuild() { + RunT r = getLastBuild(); + while(r!=null && (r.isBuilding() || r.getResult()!=Result.FAILURE)) + r=r.getPreviousBuild(); + return r; + } + + /** + * Used as the color of the status ball for the project. + */ + public String getIconColor() { + RunT lastBuild = getLastBuild(); + while(lastBuild!=null && lastBuild.hasntStartedYet()) + lastBuild = lastBuild.getPreviousBuild(); + + if(lastBuild!=null) + return lastBuild.getIconColor(); + else + return "grey"; + } + + + /** + * Save the settings to a file. + */ + public synchronized void save() throws IOException { + getConfigFile(root).write(this); + } + + /** + * Loads a project from a config file. + */ + static Job load(Hudson root, File dir) throws IOException { + Job job = (Job)getConfigFile(dir).read(); + job.onLoad(root,dir.getName()); + return job; + } + + + +// +// +// actions +// +// + /** + * Accepts submission from the configuration page. + */ + public synchronized void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + + req.setCharacterEncoding("UTF-8"); + + description = req.getParameter("description"); + + if(req.getParameter("logrotate")!=null) + logRotator = LogRotator.DESCRIPTOR.newInstance(req); + else + logRotator = null; + + keepDependencies = req.getParameter("keepDependencies")!=null; + + save(); + + String newName = req.getParameter("name"); + if(newName!=null && !newName.equals(name)) { + rsp.sendRedirect("rename?newName="+newName); + } else { + rsp.sendRedirect("."); + } + } + + /** + * Accepts the new description. + */ + public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + + req.setCharacterEncoding("UTF-8"); + description = req.getParameter("description"); + save(); + rsp.sendRedirect("."); // go to the top page + } + + /** + * Returns the image that shows the current buildCommand status. + */ + public void doBuildStatus( StaplerRequest req, StaplerResponse rsp ) throws IOException { + rsp.sendRedirect2(req.getContextPath()+"/nocacheImages/48x48/"+getBuildStatusUrl()); + } + + public String getBuildStatusUrl() { + return getIconColor()+".gif"; + } + + /** + * Returns the graph that shows how long each build took. + */ + public void doBuildTimeGraph( StaplerRequest req, StaplerResponse rsp ) throws IOException { + class Label implements Comparable

+ * The text file is assumed to be in the system default encoding. + * + * @param start + * The byte offset in the input file where the write operation starts. + * + * @return + * if the file is still being written, this method writes the file + * until the last newline character and returns the offset to start + * the next write operation. + */ + public long writeLogTo(long start, Writer w) throws IOException { + CountingOutputStream os = new CountingOutputStream(new WriterOutputStream(w)); + + RandomAccessFile f = new RandomAccessFile(file,"r"); + f.seek(start); + + if(completed) { + // write everything till EOF + byte[] buf = new byte[1024]; + int sz; + while((sz=f.read(buf))>=0) + os.write(buf,0,sz); + } else { + ByteBuf buf = new ByteBuf(null,f); + HeadMark head = new HeadMark(buf); + TailMark tail = new TailMark(buf); + + while(tail.moveToNextLine(f)) { + head.moveTo(tail,os); + } + head.finish(os); + } + + f.close(); + os.flush(); + + return os.getCount()+start; + } + + /** + * Points to a byte in the buffer. + */ + private static class Mark { + protected ByteBuf buf; + protected int pos; + + public Mark(ByteBuf buf) { + this.buf = buf; + } + } + + /** + * Points to the start of the region that's not committed + * to the ouput yet. + */ + private static final class HeadMark extends Mark { + public HeadMark(ByteBuf buf) { + super(buf); + } + + /** + * Moves this mark to 'that' mark, and writes the data + * to {@link OutputStream} if necessary. + */ + void moveTo(Mark that, OutputStream os) throws IOException { + while(this.buf!=that.buf) { + os.write(buf.buf,0,buf.size); + buf = buf.next; + pos = 0; + } + + this.pos = that.pos; + } + + void finish(OutputStream os) throws IOException { + os.write(buf.buf,0,pos); + } + } + + /** + * Points to the end of the region. + */ + private static final class TailMark extends Mark { + public TailMark(ByteBuf buf) { + super(buf); + } + + boolean moveToNextLine(RandomAccessFile f) throws IOException { + while(true) { + while(pos==buf.size) { + if(!buf.isFull()) { + // read until EOF + return false; + } else { + // read into the next buffer + buf = new ByteBuf(buf,f); + pos = 0; + } + } + byte b = buf.buf[pos++]; + if(b=='\r' || b=='\n') + return true; + } + } + } + + private static final class ByteBuf { + private final byte[] buf = new byte[1024]; + private int size = 0; + private ByteBuf next; + + public ByteBuf(ByteBuf previous, RandomAccessFile f) throws IOException { + if(previous!=null) { + assert previous.next==null; + previous.next = this; + } + + while(!this.isFull()) { + int chunk = f.read(buf, size, buf.length - size); + if(chunk==-1) + return; + size+= chunk; + } + } + + public boolean isFull() { + return buf.length==size; + } + } +} diff --git a/core/src/main/java/hudson/model/ModelObject.java b/core/src/main/java/hudson/model/ModelObject.java new file mode 100644 index 0000000000..512a48f732 --- /dev/null +++ b/core/src/main/java/hudson/model/ModelObject.java @@ -0,0 +1,10 @@ +package hudson.model; + +/** + * A model object has a URL. + * + * @author Kohsuke Kawaguchi + */ +public interface ModelObject { + String getDisplayName(); +} diff --git a/core/src/main/java/hudson/model/NoFingerprintMatch.java b/core/src/main/java/hudson/model/NoFingerprintMatch.java new file mode 100644 index 0000000000..a5bcc943eb --- /dev/null +++ b/core/src/main/java/hudson/model/NoFingerprintMatch.java @@ -0,0 +1,16 @@ +package hudson.model; + +/** + * @author Kohsuke Kawaguchi + */ +public class NoFingerprintMatch implements ModelObject { + private final String md5sum; + + public NoFingerprintMatch(String md5sum) { + this.md5sum = md5sum; + } + + public String getDisplayName() { + return md5sum; + } +} diff --git a/core/src/main/java/hudson/model/Node.java b/core/src/main/java/hudson/model/Node.java new file mode 100644 index 0000000000..b1150326f6 --- /dev/null +++ b/core/src/main/java/hudson/model/Node.java @@ -0,0 +1,62 @@ +package hudson.model; + +import hudson.Launcher; + +/** + * Commonality between {@link Slave} and master {@link Hudson}. + * + * @author Kohsuke Kawaguchi + */ +public interface Node { + /** + * Name of this node. + * + * @return + * "" if this is master + */ + String getNodeName(); + + /** + * Human-readable description of this node. + */ + String getNodeDescription(); + + /** + * Returns a {@link Launcher} for executing programs on this node. + */ + Launcher createLauncher(TaskListener listener); + + /** + * Returns the number of {@link Executor}s. + * + * This may be different from getExecutors().size() + * because it takes time to adjust the number of executors. + */ + int getNumExecutors(); + + /** + * Returns true if this node is only available + * for those jobs that exclusively specifies this node + * as the assigned node. + */ + Mode getMode(); + + public enum Mode { + NORMAL("Utilize this slave as much as possible"), + EXCLUSIVE("Leave this machine for tied jobs only"); + + private final String description; + + public String getDescription() { + return description; + } + + public String getName() { + return name(); + } + + Mode(String description) { + this.description = description; + } + } +} diff --git a/core/src/main/java/hudson/model/PeriodicWork.java b/core/src/main/java/hudson/model/PeriodicWork.java new file mode 100644 index 0000000000..717780655f --- /dev/null +++ b/core/src/main/java/hudson/model/PeriodicWork.java @@ -0,0 +1,53 @@ +package hudson.model; + +import java.util.TimerTask; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Abstract base class for a periodic work. + * + * @author Kohsuke Kawaguchi + */ +abstract class PeriodicWork extends TimerTask { + + /** + * Name of the work. + */ + private final String name; + private Thread thread; + + protected final Logger logger = Logger.getLogger(getClass().getName()); + + protected PeriodicWork(String name) { + this.name = name; + } + + /** + * Schedules this periodic work now in a new thread, if one isn't already running. + */ + public final void run() { + try { + if(thread!=null && thread.isAlive()) { + logger.log(Level.INFO, name+" thread is still running. Execution aborted."); + return; + } + thread = new Thread(new Runnable() { + public void run() { + logger.log(Level.INFO, "Started "+name); + long startTime = System.currentTimeMillis(); + + execute(); + + logger.log(Level.INFO, "Finished "+name+". "+ + (System.currentTimeMillis()-startTime)+" ms"); + } + },name+" thread"); + thread.start(); + } catch (Throwable t) { + logger.log(Level.SEVERE, name+" thread failed with error", t); + } + } + + protected abstract void execute(); +} diff --git a/core/src/main/java/hudson/model/Project.java b/core/src/main/java/hudson/model/Project.java new file mode 100644 index 0000000000..5c681fb7a9 --- /dev/null +++ b/core/src/main/java/hudson/model/Project.java @@ -0,0 +1,730 @@ +package hudson.model; + +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.Descriptor.FormException; +import hudson.model.Fingerprint.RangeSet; +import hudson.model.RunMap.Constructor; +import hudson.scm.NullSCM; +import hudson.scm.SCM; +import hudson.scm.SCMS; +import hudson.tasks.BuildStep; +import hudson.tasks.BuildTrigger; +import hudson.tasks.Builder; +import hudson.tasks.Fingerprinter; +import hudson.tasks.Publisher; +import hudson.tasks.test.AbstractTestResultAction; +import hudson.triggers.Trigger; +import hudson.triggers.Triggers; +import org.kohsuke.stapler.Ancestor; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.StringTokenizer; +import java.util.TreeMap; +import java.util.Vector; + +/** + * Buildable software project. + * + * @author Kohsuke Kawaguchi + */ +public class Project extends Job { + + /** + * All the builds keyed by their build number. + */ + private transient /*almost final*/ RunMap builds = new RunMap(); + + private SCM scm = new NullSCM(); + + /** + * List of all {@link Trigger}s for this project. + */ + private List triggers = new Vector(); + + /** + * List of active {@link Builder}s configured for this project. + */ + private List builders = new Vector(); + + /** + * List of active {@link Publisher}s configured for this project. + */ + private List publishers = new Vector(); + + /** + * {@link Action}s contributed from {@link #triggers}, {@link #builders}, + * and {@link #publishers}. + * + * We don't want to persist them separately, and these actions + * come and go as configuration change, so it's kept separate. + */ + private transient /*final*/ List transientActions = new Vector(); + + /** + * Identifies {@link JDK} to be used. + * Null if no explicit configuration is required. + * + *

+ * Can't store {@link JDK} directly because {@link Hudson} and {@link Project} + * are saved independently. + * + * @see Hudson#getJDK(String) + */ + private String jdk; + + /** + * The quiet period. Null to delegate to the system default. + */ + private Integer quietPeriod = null; + + /** + * If this project is configured to be only built on a certain node, + * this value will be set to that node. Null to indicate the affinity + * with the master node. + * + * see #canRoam + */ + private String assignedNode; + + /** + * True if this project can be built on any node. + * + *

+ * This somewhat ugly flag combination is so that we can migrate + * existing Hudson installations nicely. + */ + private boolean canRoam; + + /** + * True to suspend new builds. + */ + private boolean disabled; + + /** + * Creates a new project. + */ + public Project(Hudson parent,String name) { + super(parent,name); + + if(!parent.getSlaves().isEmpty()) { + // if a new job is configured with Hudson that already has slave nodes + // make it roamable by default + canRoam = true; + } + } + + /** + * If this project is configured to be always built on this node, + * return that {@link Node}. Otherwise null. + */ + public Node getAssignedNode() { + if(canRoam) + return null; + + if(assignedNode ==null) + return Hudson.getInstance(); + return getParent().getSlave(assignedNode); + } + + public JDK getJDK() { + return getParent().getJDK(jdk); + } + + public int getQuietPeriod() { + return quietPeriod!=null ? quietPeriod : getParent().getQuietPeriod(); + } + + // ugly name because of EL + public boolean getHasCustomQuietPeriod() { + return quietPeriod!=null; + } + + + protected void onLoad(Hudson root, String name) throws IOException { + super.onLoad(root, name); + + if(triggers==null) + // it didn't exist in < 1.28 + triggers = new Vector(); + + this.builds = new RunMap(); + this.builds.load(this,new Constructor() { + public Build create(File dir) throws IOException { + return new Build(Project.this,dir); + } + }); + + for (Trigger t : triggers) + t.start(this); + + updateTransientActions(); + } + + public boolean isBuildable() { + return !isDisabled(); + } + + public boolean isDisabled() { + return disabled; + } + + public SCM getScm() { + return scm; + } + + public void setScm(SCM scm) { + this.scm = scm; + } + + @Override + public String getIconColor() { + if(isDisabled()) + // use grey to indicate that the build is disabled + return "grey"; + else + return super.getIconColor(); + } + + public synchronized Map,Trigger> getTriggers() { + return Descriptor.toMap(triggers); + } + + public synchronized Map,Builder> getBuilders() { + return Descriptor.toMap(builders); + } + + public synchronized Map,Publisher> getPublishers() { + return Descriptor.toMap(publishers); + } + + /** + * Adds a new {@link BuildStep} to this {@link Project} and saves the configuration. + */ + private synchronized void addPublisher(Publisher buildStep) throws IOException { + for( int i=0; i descriptor) throws IOException { + for( int i=0; i _getRuns() { + return builds.getView(); + } + + public void removeRun(Build run) { + this.builds.remove(run); + } + + /** + * Creates a new build of this project for immediate execution. + */ + public Build newBuild() throws IOException { + Build lastBuild = new Build(this); + builds.put(lastBuild); + return lastBuild; + } + + public boolean checkout(Build build, Launcher launcher, BuildListener listener, File changelogFile) throws IOException { + if(scm==null) + return true; // no SCM + + FilePath workspace = getWorkspace(); + workspace.mkdirs(); + + return scm.checkout(build, launcher, workspace, listener, changelogFile); + } + + /** + * Checks if there's any update in SCM, and returns true if any is found. + * + *

+ * The caller is responsible for coordinating the mutual exclusion between + * a build and polling, as both touches the workspace. + */ + public boolean pollSCMChanges( TaskListener listener ) { + if(scm==null) { + listener.getLogger().println("No SCM"); + return false; // no SCM + } + + + FilePath workspace = getWorkspace(); + if(!workspace.exists()) { + // no workspace. build now, or nothing will ever be built + listener.getLogger().println("No workspace is available, so can't check for updates."); + listener.getLogger().println("Scheduling a new build to get a workspace."); + return true; + } + + try { + // TODO: do this by using the right slave + return scm.pollChanges(this, new Launcher(listener), workspace, listener ); + } catch (IOException e) { + e.printStackTrace(listener.fatalError(e.getMessage())); + return false; + } + } + + /** + * Gets the {@link Node} where this project was last built on. + * + * @return + * null if no information is available (for example, + * if no build was done yet.) + */ + public Node getLastBuiltOn() { + // where was it built on? + Build b = getLastBuild(); + if(b==null) + return null; + else + return b.getBuiltOn(); + } + + /** + * Gets the directory where the module is checked out. + */ + public FilePath getWorkspace() { + Node node = getLastBuiltOn(); + + if(node==null) + node = getParent(); + + if(node instanceof Slave) + return ((Slave)node).getWorkspaceRoot().child(getName()); + else + return new FilePath(new File(getRootDir(),"workspace")); + } + + /** + * Returns the root directory of the checked-out module. + * + * @return + * When running remotely, this returns a remote fs directory. + */ + public FilePath getModuleRoot() { + return getScm().getModuleRoot(getWorkspace()); + } + + /** + * Gets the dependency relationship map between this project (as the source) + * and that project (as the sink.) + * + * @return + * can be empty but not null. build number of this project to the build + * numbers of that project. + */ + public SortedMap getRelationship(Project that) { + TreeMap r = new TreeMap(REVERSE_INTEGER_COMPARATOR); + + checkAndRecord(that, r, this.getBuilds()); + // checkAndRecord(that, r, that.getBuilds()); + + return r; + } + + public List getDownstreamProjects() { + BuildTrigger buildTrigger = (BuildTrigger) getPublishers().get(BuildTrigger.DESCRIPTOR); + if(buildTrigger==null) + return new ArrayList(); + else + return buildTrigger.getChildProjects(); + } + + public List getUpstreamProjects() { + List r = new ArrayList(); + for( Project p : Hudson.getInstance().getProjects() ) { + synchronized(p) { + for (BuildStep step : p.publishers) { + if (step instanceof BuildTrigger) { + BuildTrigger trigger = (BuildTrigger) step; + if(trigger.getChildProjects().contains(this)) + r.add(p); + } + } + } + } + return r; + } + + /** + * Helper method for getDownstreamRelationship. + * + * For each given build, find the build number range of the given project and put that into the map. + */ + private void checkAndRecord(Project that, TreeMap r, Collection builds) { + for (Build build : builds) { + RangeSet rs = build.getDownstreamRelationship(that); + if(rs==null || rs.isEmpty()) + continue; + + int n = build.getNumber(); + + RangeSet value = r.get(n); + if(value==null) + r.put(n,rs); + else + value.add(rs); + } + } + + /** + * Schedules a build of this project. + */ + public void scheduleBuild() { + if(!disabled) + getParent().getQueue().add(this); + } + + /** + * Returns true if the build is in the queue. + */ + @Override + public boolean isInQueue() { + return getParent().getQueue().contains(this); + } + + /** + * Schedules the SCM polling. If a polling is already in progress + * or a build is in progress, polling will take place after that. + * Otherwise the polling will be started immediately on a separate thread. + * + *

+ * In any case this method returns immediately. + */ + public void scheduleSCMPolling() { + // TODO + } + + /** + * Returns true if the fingerprint record is configured in this project. + */ + public boolean isFingerprintConfigured() { + synchronized(publishers) { + for (Publisher p : publishers) { + if(p instanceof Fingerprinter) + return true; + } + } + return false; + } + + + +// +// +// actions +// +// + /** + * Schedules a new build command. + */ + public void doBuild( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + scheduleBuild(); + rsp.forwardToPreviousPage(req); + } + + /** + * Cancels a scheduled build. + */ + public void doCancelQueue( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + + getParent().getQueue().cancel(this); + rsp.forwardToPreviousPage(req); + } + + /** + * Accepts submission from the configuration page. + */ + public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + + Set upstream = Collections.EMPTY_SET; + + synchronized(this) { + try { + if(!Hudson.adminCheck(req,rsp)) + return; + + req.setCharacterEncoding("UTF-8"); + + int scmidx = Integer.parseInt(req.getParameter("scm")); + scm = SCMS.SCMS.get(scmidx).newInstance(req); + + disabled = req.getParameter("disable")!=null; + + jdk = req.getParameter("jdk"); + if(req.getParameter("hasCustomQuietPeriod")!=null) { + quietPeriod = Integer.parseInt(req.getParameter("quiet_period")); + } else { + quietPeriod = null; + } + + if(req.getParameter("hasSlaveAffinity")!=null) { + canRoam = false; + assignedNode = req.getParameter("slave"); + if(assignedNode !=null) { + if(Hudson.getInstance().getSlave(assignedNode)==null) { + assignedNode = null; // no such slave + } + } + } else { + canRoam = true; + assignedNode = null; + } + + buildDescribable(req, BuildStep.BUILDERS, builders, "builder"); + buildDescribable(req, BuildStep.PUBLISHERS, publishers, "publisher"); + + for (Trigger t : triggers) + t.stop(); + buildDescribable(req, Triggers.TRIGGERS, triggers, "trigger"); + for (Trigger t : triggers) + t.start(this); + + updateTransientActions(); + + super.doConfigSubmit(req,rsp); + } catch (FormException e) { + sendError(e,req,rsp); + } + } + + if(req.getParameter("pseudoUpstreamTrigger")!=null) { + upstream = new HashSet(Project.fromNameList(req.getParameter("upstreamProjects"))); + } + + // this needs to be done after we release the lock on this, + // or otherwise we could dead-lock + for (Project p : Hudson.getInstance().getProjects()) { + boolean isUpstream = upstream.contains(p); + synchronized(p) { + List newChildProjects = p.getDownstreamProjects(); + + if(isUpstream) { + if(!newChildProjects.contains(this)) + newChildProjects.add(this); + } else { + newChildProjects.remove(this); + } + + if(newChildProjects.isEmpty()) { + p.removePublisher(BuildTrigger.DESCRIPTOR); + } else { + p.addPublisher(new BuildTrigger(newChildProjects)); + } + } + } + } + + private void updateTransientActions() { + if(transientActions==null) + transientActions = new Vector(); // happens when loaded from disk + synchronized(transientActions) { + transientActions.clear(); + for (BuildStep step : builders) { + Action a = step.getProjectAction(this); + if(a!=null) + transientActions.add(a); + } + for (BuildStep step : publishers) { + Action a = step.getProjectAction(this); + if(a!=null) + transientActions.add(a); + } + for (Trigger trigger : triggers) { + Action a = trigger.getProjectAction(); + if(a!=null) + transientActions.add(a); + } + } + } + + public synchronized List getActions() { + // add all the transient actions, too + List actions = new Vector(super.getActions()); + actions.addAll(transientActions); + return actions; + } + + public List getProminentActions() { + List a = getActions(); + List pa = new Vector(); + for (Action action : a) { + if(action instanceof ProminentProjectAction) + pa.add((ProminentProjectAction) action); + } + return pa; + } + + private > void buildDescribable(StaplerRequest req, List> descriptors, List result, String prefix) + throws FormException { + + result.clear(); + for( int i=0; i< descriptors.size(); i++ ) { + if(req.getParameter(prefix +i)!=null) { + T instance = descriptors.get(i).newInstance(req); + result.add(instance); + } + } + } + + /** + * Serves the workspace files. + */ + public void doWs( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + File dir = getWorkspace().getLocal(); + if(!dir.exists()) { + // if there's no workspace, report a nice error message + rsp.forward(this,"noWorkspace",req); + } else { + serveFile(req, rsp, dir, "folder.gif", true); + } + } + + /** + * Display the test result trend. + */ + public void doTestResultTrend( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + Build b = getLastSuccessfulBuild(); + if(b!=null) { + AbstractTestResultAction a = b.getTestResultAction(); + if(a!=null) { + a.doGraph(req,rsp); + return; + } + } + + // error + rsp.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + + /** + * Changes the test result report display mode. + */ + public void doFlipTestResultTrend( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + boolean failureOnly = false; + + // check the current preference value + Cookie[] cookies = req.getCookies(); + if(cookies!=null) { + for (Cookie cookie : cookies) { + if(cookie.getName().equals(FAILURE_ONLY_COOKIE)) + failureOnly = Boolean.parseBoolean(cookie.getValue()); + } + } + + // flip! + failureOnly = !failureOnly; + + // set the updated value + Cookie cookie = new Cookie(FAILURE_ONLY_COOKIE,String.valueOf(failureOnly)); + List anc = req.getAncestors(); + Ancestor a = (Ancestor) anc.get(anc.size()-1); // last + cookie.setPath(a.getUrl()); // just for this chart + cookie.setMaxAge(Integer.MAX_VALUE); + rsp.addCookie(cookie); + + // back to the project page + rsp.sendRedirect("."); + } + + /** + * @deprecated + * left for legacy config file compatibility + */ + private transient String slave; + + private static final String FAILURE_ONLY_COOKIE = "TestResultAction_failureOnly"; + + /** + * Converts a list of projects into a camma-separated names. + */ + public static String toNameList(Collection projects) { + StringBuilder buf = new StringBuilder(); + for (Project project : projects) { + if(buf.length()>0) + buf.append(", "); + buf.append(project.getName()); + } + return buf.toString(); + } + + /** + * Does the opposite of {@link #toNameList(Collection)}. + */ + public static List fromNameList(String list) { + Hudson hudson = Hudson.getInstance(); + + List r = new ArrayList(); + StringTokenizer tokens = new StringTokenizer(list,","); + while(tokens.hasMoreTokens()) { + String projectName = tokens.nextToken().trim(); + Job job = hudson.getJob(projectName); + if(!(job instanceof Project)) { + continue; // ignore this token + } + r.add((Project) job); + } + return r; + } + + private static final Comparator REVERSE_INTEGER_COMPARATOR = new Comparator() { + public int compare(Integer o1, Integer o2) { + return o2-o1; + } + }; + + public JobDescriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final JobDescriptor DESCRIPTOR = new JobDescriptor(Project.class) { + public String getDisplayName() { + return "Building a software project"; + } + + public Project newInstance(String name) { + return new Project(Hudson.getInstance(),name); + } + }; +} diff --git a/core/src/main/java/hudson/model/ProminentProjectAction.java b/core/src/main/java/hudson/model/ProminentProjectAction.java new file mode 100644 index 0000000000..02a969153b --- /dev/null +++ b/core/src/main/java/hudson/model/ProminentProjectAction.java @@ -0,0 +1,11 @@ +package hudson.model; + +/** + * Marker interface for {@link Action}s that should be displayed + * at the top of the project page. + * + * @author Kohsuke Kawaguchi + */ +public interface ProminentProjectAction extends Action { + // TODO: do the rendering of the part from the action page +} diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java new file mode 100644 index 0000000000..19556cb804 --- /dev/null +++ b/core/src/main/java/hudson/model/Queue.java @@ -0,0 +1,480 @@ +package hudson.model; + +import hudson.model.Node.Mode; +import hudson.util.OneShotEvent; + +import java.util.Calendar; +import java.util.Comparator; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.io.PrintWriter; +import java.io.FileOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.InputStreamReader; + +/** + * Build queue. + * + *

+ * This class implements the core scheduling logic. + * + * @author Kohsuke Kawaguchi + */ +public class Queue { + + private static final Comparator itemComparator = new Comparator() { + public int compare(Item lhs, Item rhs) { + int r = lhs.timestamp.getTime().compareTo(rhs.timestamp.getTime()); + if(r!=0) return r; + + return lhs.id-rhs.id; + } + }; + + /** + * Items in the queue ordered by {@link Item#timestamp}. + * + *

+ * This consists of {@link Item}s that cannot be run yet + * because its time has not yet come. + */ + private final Set queue = new TreeSet(itemComparator); + + /** + * {@link Project}s that can be built immediately + * but blocked because another build is in progress. + */ + private final Set blockedProjects = new HashSet(); + + /** + * {@link Project}s that can be built immediately + * that are waiting for available {@link Executor}. + */ + private final List buildables = new LinkedList(); + + /** + * Data structure created for each idle {@link Executor}. + * This is an offer from the queue to an executor. + * + *

+ * It eventually receives a {@link #project} to build. + */ + private static class JobOffer { + final Executor executor; + + /** + * Used to wake up an executor, when it has an offered + * {@link Project} to build. + */ + final OneShotEvent event = new OneShotEvent(); + /** + * The project that this {@link Executor} is going to build. + * (Or null, in which case event is used to trigger a queue maintenance.) + */ + Project project; + + public JobOffer(Executor executor) { + this.executor = executor; + } + + public void set(Project p) { + this.project = p; + event.signal(); + } + + public boolean isAvailable() { + return project==null && !executor.getOwner().isTemporarilyOffline(); + } + + public Node getNode() { + return executor.getOwner().getNode(); + } + + public boolean isNotExclusive() { + return getNode().getMode()== Mode.NORMAL; + } + } + + private final Map parked = new HashMap(); + + /** + * Loads the queue contents that was {@link #save() saved}. + */ + public synchronized void load() { + // write out the contents of the queue + try { + File queueFile = getQueueFile(); + if(!queueFile.exists()) + return; + + BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(queueFile))); + String line; + while((line=in.readLine())!=null) { + Job j = Hudson.getInstance().getJob(line); + if(j instanceof Project) + ((Project)j).scheduleBuild(); + } + in.close(); + // discard the queue file now that we are done + queueFile.delete(); + } catch(IOException e) { + LOGGER.log(Level.WARNING, "Failed to load the queue file "+getQueueFile(),e); + } + } + + /** + * Persists the queue contents to the disk. + */ + public synchronized void save() { + // write out the contents of the queue + try { + PrintWriter w = new PrintWriter(new FileOutputStream( + getQueueFile())); + for (Item i : getItems()) + w.println(i.getProject().getName()); + w.close(); + } catch(IOException e) { + LOGGER.log(Level.WARNING, "Failed to write out the queue file "+getQueueFile(),e); + } + } + + private File getQueueFile() { + return new File(Hudson.getInstance().getRootDir(),"queue.txt"); + } + + /** + * Schedule a new build for this project. + */ + public synchronized void add( Project p ) { + if(contains(p)) + return; // no double queueing + + // put the item in the queue + Calendar due = new GregorianCalendar(); + due.add(Calendar.SECOND, p.getQuietPeriod()); + queue.add(new Item(due,p)); + + scheduleMaintenance(); // let an executor know that a new item is in the queue. + } + + public synchronized void cancel( Project p ) { + for (Iterator itr = queue.iterator(); itr.hasNext();) { + Item item = (Item) itr.next(); + if(item.project==p) { + itr.remove(); + return; + } + } + blockedProjects.remove(p); + buildables.remove(p); + } + + public synchronized boolean isEmpty() { + return queue.isEmpty() && blockedProjects.isEmpty() && buildables.isEmpty(); + } + + private synchronized Item peek() { + return queue.iterator().next(); + } + + /** + * Gets a snapshot of items in the queue. + */ + public synchronized Item[] getItems() { + Item[] r = new Item[queue.size()+blockedProjects.size()+buildables.size()]; + queue.toArray(r); + int idx=queue.size(); + Calendar now = new GregorianCalendar(); + for (Project p : blockedProjects) { + r[idx++] = new Item(now, p); + } + for (Project p : buildables) { + r[idx++] = new Item(now, p); + } + return r; + } + + /** + * Returns true if this queue contaisn the said project. + */ + public synchronized boolean contains(Project p) { + // if this project is already scheduled, + // don't do anything + if(blockedProjects.contains(p) || buildables.contains(p)) + return true; + for (Item item : queue) { + if (item.project == p) + return true; + } + return false; + } + + /** + * Called by the executor to fetch something to build next. + * + * This method blocks until a next project becomes buildable. + */ + public Project pop() throws InterruptedException { + final Executor exec = Executor.currentExecutor(); + boolean successfulReturn = false; + + try { + while(true) { + final JobOffer offer = new JobOffer(exec); + long sleep = -1; + + synchronized(this) { + // consider myself parked + assert !parked.containsKey(exec); + parked.put(exec,offer); + + // reuse executor thread to do a queue maintainance. + // at the end of this we get all the buildable jobs + // in the buildables field. + maintain(); + + // allocate buildable jobs to executors + Iterator itr = buildables.iterator(); + while(itr.hasNext()) { + Project p = itr.next(); + JobOffer runner = choose(p); + if(runner==null) + // if we couldn't find the executor that fits, + // just leave it in the buildables list and + // check if we can execute other projects + continue; + + // found a matching executor. use it. + runner.set(p); + itr.remove(); + } + + // we went over all the buildable projects and awaken + // all the executors that got work to do. now, go to sleep + // until this thread is awakened. If this executor assigned a job to + // itself above, the block method will return immediately. + + if(!queue.isEmpty()) { + // wait until the first item in the queue is due + sleep = peek().timestamp.getTimeInMillis()-new GregorianCalendar().getTimeInMillis(); + if(sleep <100) sleep =100; // avoid wait(0) + } + } + + // this needs to be done outside synchronized block, + // so that executors can maintain a queue while others are sleeping + if(sleep ==-1) + offer.event.block(); + else + offer.event.block(sleep); + + synchronized(this) { + // am I woken up because I have a project to build? + if(offer.project!=null) { + // if so, just build it + successfulReturn = true; + return offer.project; + } + // otherwise run a queue maintenance + } + } + } finally { + synchronized(this) { + // remove myself from the parked list + JobOffer offer = parked.get(exec); + if(offer!=null) { + if(!successfulReturn && offer.project!=null) { + // we are already assigned a project, + // ask for someone else to build it. + // note that while this thread is waiting for CPU + // someone else can schedule this build again. + if(!contains(offer.project)) + buildables.add(offer.project); + } + + // since this executor might have been chosen for + // maintenance, schedule another one. Worst case + // we'll just run a pointless maintenance, and that's + // fine. + scheduleMaintenance(); + } + } + } + } + + /** + * Choses the executor to carry out the build for the given project. + * + * @return + * null if no {@link Executor} can run it. + */ + private JobOffer choose(Project p) { + if(Hudson.getInstance().isQuietingDown()) { + // if we are quieting down, don't run anything so that + // all executors will be free. + return null; + } + + Node n = p.getAssignedNode(); + if(n!=null) { + // if a project has assigned node, it can be only built on it + for (JobOffer offer : parked.values()) { + if(offer.isAvailable() && offer.getNode()==n) + return offer; + } + return null; + } + + // otherwise let's see if the last node that this project was built is available + // it has up-to-date workspace, so that's usually preferable. + // (but we can't use an exclusive node) + n = p.getLastBuiltOn(); + if(n!=null && n.getMode()==Mode.NORMAL) { + for (JobOffer offer : parked.values()) { + if(offer.isAvailable() && offer.getNode()==n) + return offer; + } + } + + // duration of a build on a slave tends not to have an impact on + // the master/slave communication, so that means we should favor + // running long jobs on slaves. + Build succ = p.getLastSuccessfulBuild(); + if(succ!=null && succ.getDuration()>15*60*1000) { + // consider a long job to be > 15 mins + for (JobOffer offer : parked.values()) { + if(offer.isAvailable() && offer.getNode() instanceof Slave && offer.isNotExclusive()) + return offer; + } + } + + // lastly, just look for any idle executor + for (JobOffer offer : parked.values()) { + if(offer.isAvailable() && offer.isNotExclusive()) + return offer; + } + + // nothing available + return null; + } + + /** + * Checks the queue and runs anything that can be run. + * + *

+ * When conditions are changed, this method should be invoked. + * + * This wakes up one {@link Executor} so that it will maintain a queue. + */ + public synchronized void scheduleMaintenance() { + // this code assumes that after this method is called + // no more executors will be offered job except by + // the pop() code. + for (Entry av : parked.entrySet()) { + if(av.getValue().project==null) { + av.getValue().event.signal(); + return; + } + } + } + + + /** + * Queue maintainance. + * + * Move projects between {@link #queue}, {@link #blockedProjects}, and {@link #buildables} + * appropriately. + */ + private synchronized void maintain() { + Iterator itr = blockedProjects.iterator(); + while(itr.hasNext()) { + Project p = itr.next(); + Build lastBuild = p.getLastBuild(); + if (lastBuild == null || !lastBuild.isBuilding()) { + // ready to be executed + itr.remove(); + buildables.add(p); + } + } + + while(!queue.isEmpty()) { + Item top = peek(); + + if(!top.timestamp.before(new GregorianCalendar())) + return; // finished moving all ready items from queue + + Build lastBuild = top.project.getLastBuild(); + if(lastBuild==null || !lastBuild.isBuilding()) { + // ready to be executed immediately + queue.remove(top); + buildables.add(top.project); + } else { + // this can't be built know because another build is in progress + // set this project aside. + queue.remove(top); + blockedProjects.add(top.project); + } + } + } + + /** + * Item in a queue. + */ + public class Item { + /** + * This item can be run after this time. + */ + final Calendar timestamp; + + /** + * Project to be built. + */ + final Project project; + + /** + * Unique number of this {@link Item}. + * Used to differenciate {@link Item}s with the same due date. + */ + final int id; + + public Item(Calendar timestamp, Project project) { + this.timestamp = timestamp; + this.project = project; + synchronized(Queue.this) { + this.id = iota++; + } + } + + public Calendar getTimestamp() { + return timestamp; + } + + public Project getProject() { + return project; + } + + public int getId() { + return id; + } + } + + /** + * Unique number generator + */ + private int iota=0; + + private static final Logger LOGGER = Logger.getLogger(Queue.class.getName()); +} diff --git a/core/src/main/java/hudson/model/RSS.java b/core/src/main/java/hudson/model/RSS.java new file mode 100644 index 0000000000..1d7d0062dc --- /dev/null +++ b/core/src/main/java/hudson/model/RSS.java @@ -0,0 +1,71 @@ +package hudson.model; + +import hudson.FeedAdapter; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Iterator; + +/** + * RSS related code. + * + * @author Kohsuke Kawaguchi + */ +final class RSS { + + /** + * Parses trackback ping. + */ + public static void doTrackback( Object it, StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + req.setCharacterEncoding("UTF-8"); + + String title = req.getParameter("title"); + String url = req.getParameter("url"); + String excerpt = req.getParameter("excerpt"); + String blog_name = req.getParameter("blog_name"); + + rsp.setStatus(HttpServletResponse.SC_OK); + rsp.setContentType("application/xml; charset=UTF-8"); + PrintWriter pw = rsp.getWriter(); + pw.println(""); + pw.println(""+(url!=null?0:1)+""); + if(url==null) { + pw.println("url must be specified"); + } + pw.println(""); + pw.close(); + } + + /** + * Sends the RSS feed to the client. + * + * @param title + * Title of the feed. + * @param url + * URL of the model object that owns this feed + * @param entries + * Entries to be listed in the RSS feed. + * @param adapter + * Controls how to render entries to RSS. + */ + public static void forwardToRss(String title, String url, Collection entries, FeedAdapter adapter, StaplerRequest req, HttpServletResponse rsp) throws IOException, ServletException { + req.setAttribute("adapter",adapter); + req.setAttribute("title",title); + req.setAttribute("url",url); + req.setAttribute("entries",entries); + + String flavor = req.getParameter("flavor"); + if(flavor==null) flavor="atom"; + + req.getView(Hudson.getInstance(),"/hudson/"+flavor+".jelly").forward(req,rsp); + } +} diff --git a/core/src/main/java/hudson/model/Result.java b/core/src/main/java/hudson/model/Result.java new file mode 100644 index 0000000000..5ab1e77c69 --- /dev/null +++ b/core/src/main/java/hudson/model/Result.java @@ -0,0 +1,73 @@ +package hudson.model; + +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.basic.AbstractBasicConverter; + +/** + * The build outcome. + * + * @author Kohsuke Kawaguchi + */ +public final class Result { + /** + * The build didn't have any fatal errors not errors. + */ + public static final Result SUCCESS = new Result("SUCCESS",0); + /** + * The build didn't have any fatal errors but some errors. + */ + public static final Result UNSTABLE = new Result("UNSTABLE",1); + /** + * The build had a fatal error. + */ + public static final Result FAILURE = new Result("FAILURE",2); + /** + * The build was manually aborted. + */ + public static final Result ABORTED = new Result("ABORTED",3); + + private final String name; + + /** + * Bigger numbers are worse. + */ + private final int ordinal; + + private Result(String name, int ordinal) { + this.name = name; + this.ordinal = ordinal; + } + + /** + * Combines two {@link Result}s and returns the worse one. + */ + public Result combine(Result that) { + if(this.ordinal < that.ordinal) + return that; + else + return this; + } + + public boolean isWorseThan(Result that) { + return this.ordinal > that.ordinal; + } + + public String toString() { + return name; + } + + private static final Result[] all = new Result[] {SUCCESS,UNSTABLE,FAILURE,ABORTED}; + + public static final Converter conv = new AbstractBasicConverter () { + public boolean canConvert(Class clazz) { + return clazz==Result.class; + } + + protected Object fromString(String s) { + for (Result r : all) + if (s.equals(r.name)) + return r; + return FAILURE; + } + }; +} diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java new file mode 100644 index 0000000000..ac38f718c8 --- /dev/null +++ b/core/src/main/java/hudson/model/Run.java @@ -0,0 +1,800 @@ +package hudson.model; + +import static hudson.Util.combine; +import com.thoughtworks.xstream.XStream; +import hudson.CloseProofOutputStream; +import hudson.ExtensionPoint; +import hudson.Util; +import hudson.XmlFile; +import hudson.FeedAdapter; +import hudson.tasks.BuildStep; +import hudson.tasks.LogRotator; +import hudson.tasks.test.AbstractTestResultAction; +import hudson.util.CharSpool; +import hudson.util.IOException2; +import hudson.util.XStream2; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Comparator; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +/** + * A particular execution of {@link Job}. + * + *

+ * Custom {@link Run} type is always used in conjunction with + * a custom {@link Job} type, so there's no separate registration + * mechanism for custom {@link Run} types. + * + * @author Kohsuke Kawaguchi + */ +public abstract class Run ,RunT extends Run> + extends DirectoryHolder implements ExtensionPoint { + + protected transient final JobT project; + + /** + * Build number. + * + *

+ * In earlier versions < 1.24, this number is not unique nor continuous, + * but going forward, it will, and this really replaces the build id. + */ + public /*final*/ int number; + + /** + * Previous build. Can be null. + * These two fields are maintained and updated by {@link RunMap}. + */ + protected volatile transient RunT previousBuild; + /** + * Next build. Can be null. + */ + protected volatile transient RunT nextBuild; + + /** + * When the build is scheduled. + */ + protected transient final Calendar timestamp; + + /** + * The build result. + * This value may change while the state is in {@link State#BUILDING}. + */ + protected volatile Result result; + + /** + * Human-readable description. Can be null. + */ + protected volatile String description; + + /** + * The current build state. + */ + protected volatile transient State state; + + private static enum State { + NOT_STARTED, + BUILDING, + COMPLETED + } + + /** + * Number of milli-seconds it took to run this build. + */ + protected long duration; + + /** + * Keeps this log entries. + */ + private boolean keepLog; + + protected static final SimpleDateFormat ID_FORMATTER = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); + + /** + * Creates a new {@link Run}. + */ + protected Run(JobT job) throws IOException { + this(job, new GregorianCalendar()); + this.number = project.assignBuildNumber(); + } + + /** + * Constructor for creating a {@link Run} object in + * an arbitrary state. + */ + protected Run(JobT job, Calendar timestamp) { + this.project = job; + this.timestamp = timestamp; + this.state = State.NOT_STARTED; + } + + /** + * Loads a run from a log file. + */ + protected Run(JobT project, File buildDir) throws IOException { + this(project, new GregorianCalendar()); + try { + this.timestamp.setTime(ID_FORMATTER.parse(buildDir.getName())); + } catch (ParseException e) { + throw new IOException2("Invalid directory name "+buildDir,e); + } catch (NumberFormatException e) { + throw new IOException2("Invalid directory name "+buildDir,e); + } + this.state = State.COMPLETED; + this.result = Result.FAILURE; // defensive measure. value should be overwritten by unmarshal, but just in case the saved data is inconsistent + getDataFile().unmarshal(this); // load the rest of the data + } + + /** + * Returns the build result. + * + *

+ * When a build is {@link #isBuilding() in progress}, this method + * may return null or a temporary intermediate result. + */ + public final Result getResult() { + return result; + } + + public void setResult(Result r) { + // state can change only when we are building + assert state==State.BUILDING; + + StackTraceElement caller = findCaller(Thread.currentThread().getStackTrace(),"setResult"); + + + // result can only get worse + if(result==null) { + result = r; + LOGGER.info(toString()+" : result is set to "+r+" by "+caller); + } else { + if(r.isWorseThan(result)) { + LOGGER.info(toString()+" : result is set to "+r+" by "+caller); + result = r; + } + } + } + + private StackTraceElement findCaller(StackTraceElement[] stackTrace, String callee) { + for(int i=0; i getArtifacts() { + List r = new ArrayList(); + addArtifacts(getArtifactsDir(),"",r); + return r; + } + + /** + * Returns true if this run has any artifacts. + * + *

+ * The strange method name is so that we can access it from EL. + */ + public boolean getHasArtifacts() { + return !getArtifacts().isEmpty(); + } + + private void addArtifacts( File dir, String path, List r ) { + String[] children = dir.list(); + if(children==null) return; + for (String child : children) { + if(r.size()>CUTOFF) + return; + File sub = new File(dir, child); + if (sub.isDirectory()) { + addArtifacts(sub, path + child + '/', r); + } else { + r.add(new Artifact(path + child)); + } + } + } + + private static final int CUTOFF = 17; // 0, 1,... 16, and then "too many" + + /** + * A build artifact. + */ + public class Artifact { + /** + * Relative path name from {@link Run#getArtifactsDir()} + */ + private final String relativePath; + + private Artifact(String relativePath) { + this.relativePath = relativePath; + } + + /** + * Gets the artifact file. + */ + public File getFile() { + return new File(getArtifactsDir(),relativePath); + } + + /** + * Returns just the file name portion, without the path. + */ + public String getFileName() { + return getFile().getName(); + } + + public String toString() { + return relativePath; + } + } + + /** + * Returns the log file. + */ + public File getLogFile() { + return new File(getRootDir(),"log"); + } + + /** + * Deletes this build and its entire log + * + * @throws IOException + * if we fail to delete. + */ + public synchronized void delete() throws IOException { + File rootDir = getRootDir(); + File tmp = new File(rootDir.getParentFile(),'.'+rootDir.getName()); + + if(!rootDir.renameTo(tmp)) + throw new IOException(rootDir+" is in use"); + + Util.deleteRecursive(tmp); + + getParent().removeRun((RunT)this); + } + + protected static interface Runner { + Result run( BuildListener listener ) throws Exception; + + void post( BuildListener listener ); + } + + protected final void run(Runner job) { + if(result!=null) + return; // already built. + + onStartBuilding(); + try { + // to set the state to COMPLETE in the end, even if the thread dies abnormally. + // otherwise the queue state becomes inconsistent + + long start = System.currentTimeMillis(); + BuildListener listener=null; + + try { + try { + final PrintStream log = new PrintStream(new FileOutputStream(getLogFile())); + listener = new BuildListener() { + final PrintWriter pw = new PrintWriter(new CloseProofOutputStream(log),true); + + public void started() {} + + public PrintStream getLogger() { + return log; + } + + public PrintWriter error(String msg) { + pw.println("ERROR: "+msg); + return pw; + } + + public PrintWriter fatalError(String msg) { + return error(msg); + } + + public void finished(Result result) { + pw.close(); + log.close(); + } + }; + + listener.started(); + + result = job.run(listener); + + LOGGER.info(toString()+" main build action completed: "+result); + } catch (ThreadDeath t) { + throw t; + } catch( Throwable e ) { + handleFatalBuildProblem(listener,e); + result = Result.FAILURE; + } + + // even if the main buidl fails fatally, try to run post build processing + job.post(listener); + + } catch (ThreadDeath t) { + throw t; + } catch( Throwable e ) { + handleFatalBuildProblem(listener,e); + result = Result.FAILURE; + } + + long end = System.currentTimeMillis(); + duration = end-start; + + if(listener!=null) + listener.finished(result); + + try { + save(); + } catch (IOException e) { + e.printStackTrace(); + } + + try { + LogRotator lr = getParent().getLogRotator(); + if(lr!=null) + lr.perform(getParent()); + } catch (IOException e) { + e.printStackTrace(); + } + } finally { + onEndBuilding(); + } + } + + /** + * Handles a fatal build problem (exception) that occured during the build. + */ + private void handleFatalBuildProblem(BuildListener listener, Throwable e) { + if(listener!=null) { + if(e instanceof IOException) + Util.displayIOException((IOException)e,listener); + + Writer w = listener.fatalError(e.getMessage()); + if(w!=null) { + try { + e.printStackTrace(new PrintWriter(w)); + w.close(); + } catch (IOException e1) { + // ignore + } + } + } + } + + /** + * Called when a job started building. + */ + protected void onStartBuilding() { + state = State.BUILDING; + } + + /** + * Called when a job finished building normally or abnormally. + */ + protected void onEndBuilding() { + state = State.COMPLETED; + if(result==null) { + // shouldn't happen, but be defensive until we figure out why + result = Result.FAILURE; + LOGGER.warning(toString()+": No build result is set, so marking as failure. This shouldn't happen"); + } + } + + /** + * Save the settings to a file. + */ + public synchronized void save() throws IOException { + getDataFile().write(this); + } + + private XmlFile getDataFile() { + return new XmlFile(XSTREAM,new File(getRootDir(),"build.xml")); + } + + /** + * Gets the log of the build as a string. + * + * I know, this isn't terribly efficient! + */ + public String getLog() throws IOException { + return Util.loadFile(getLogFile()); + } + + public void doBuildStatus( StaplerRequest req, StaplerResponse rsp ) throws IOException { + // see Hudson.doNocacheImages. this is a work around for a bug in Firefox + rsp.sendRedirect2(req.getContextPath()+"/nocacheImages/48x48/"+getBuildStatusUrl()); + } + + public String getBuildStatusUrl() { + return getIconColor()+".gif"; + } + + public static class Summary { + /** + * Is this build worse or better, compared to the previous build? + */ + public boolean isWorse; + public String message; + + public Summary(boolean worse, String message) { + this.isWorse = worse; + this.message = message; + } + } + + /** + * Gets an object that computes the single line summary of this build. + */ + public Summary getBuildStatusSummary() { + Run prev = getPreviousBuild(); + + if(getResult()==Result.SUCCESS) { + if(prev==null || prev.getResult()== Result.SUCCESS) + return new Summary(false,"stable"); + else + return new Summary(false,"back to normal"); + } + + if(getResult()==Result.FAILURE) { + RunT since = getPreviousNotFailedBuild(); + if(since==null) + return new Summary(false,"broken for a long time"); + if(since==prev) + return new Summary(true,"broken since this build"); + return new Summary(false,"broekn since "+since.getDisplayName()); + } + + if(getResult()==Result.ABORTED) + return new Summary(false,"aborted"); + + if(getResult()==Result.UNSTABLE) { + if(((Run)this) instanceof Build) { + AbstractTestResultAction trN = ((Build)(Run)this).getTestResultAction(); + AbstractTestResultAction trP = prev==null ? null : ((Build) prev).getTestResultAction(); + if(trP==null) { + if(trN!=null && trN.getFailCount()>0) + return new Summary(false,combine(trN.getFailCount(),"test faliure")); + else // ??? + return new Summary(false,"unstable"); + } + if(trP.getFailCount()==0) + return new Summary(true,combine(trP.getFailCount(),"test")+" started to fail"); + if(trP.getFailCount() < trN.getFailCount()) + return new Summary(true,combine(trN.getFailCount()-trP.getFailCount(),"more test") + +" are failing ("+trN.getFailCount()+" total)"); + if(trP.getFailCount() > trN.getFailCount()) + return new Summary(false,combine(trP.getFailCount()-trN.getFailCount(),"less test") + +" are failing ("+trN.getFailCount()+" total)"); + + return new Summary(false,combine(trN.getFailCount(),"test")+" are still failing"); + } + } + + return new Summary(false,"?"); + } + + /** + * Serves the artifacts. + */ + public void doArtifact( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + serveFile(req, rsp, getArtifactsDir(), "package.gif", true); + } + + /** + * Returns the build number in the body. + */ + public void doBuildNumber( StaplerRequest req, StaplerResponse rsp ) throws IOException { + rsp.setContentType("text/plain"); + rsp.setCharacterEncoding("US-ASCII"); + rsp.setStatus(HttpServletResponse.SC_OK); + rsp.getWriter().print(number); + } + + /** + * Handles incremental log output. + */ + public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException { + rsp.setContentType("text/plain"); + rsp.setCharacterEncoding("UTF-8"); + rsp.setStatus(HttpServletResponse.SC_OK); + + boolean completed = !isBuilding(); + File logFile = getLogFile(); + if(!logFile.exists()) { + // file doesn't exist yet + rsp.addHeader("X-Text-Size","0"); + rsp.addHeader("X-More-Data","true"); + return; + } + LargeText text = new LargeText(logFile,completed); + long start = 0; + String s = req.getParameter("start"); + if(s!=null) + start = Long.parseLong(s); + + CharSpool spool = new CharSpool(); + long r = text.writeLogTo(start,spool); + + rsp.addHeader("X-Text-Size",String.valueOf(r)); + if(!completed) + rsp.addHeader("X-More-Data","true"); + + spool.writeTo(rsp.getWriter()); + } + + public void doToggleLogKeep( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + + keepLog = !keepLog; + save(); + rsp.forwardToPreviousPage(req); + } + + /** + * Accepts the new description. + */ + public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + + req.setCharacterEncoding("UTF-8"); + description = req.getParameter("description"); + save(); + rsp.sendRedirect("."); // go to the top page + } + + /** + * Returns the map that contains environmental variables for this build. + * + * Used by {@link BuildStep}s that invoke external processes. + */ + public Map getEnvVars() { + Map env = new HashMap(); + env.put("BUILD_NUMBER",String.valueOf(number)); + env.put("BUILD_ID",getId()); + env.put("BUILD_TAG","hudson-"+getParent().getName()+"-"+number); + env.put("JOB_NAME",getParent().getName()); + return env; + } + + private static final XStream XSTREAM = new XStream2(); + static { + XSTREAM.alias("build",Build.class); + XSTREAM.registerConverter(Result.conv); + } + + private static final Logger LOGGER = Logger.getLogger(Run.class.getName()); + + /** + * Sort by date. Newer ones first. + */ + public static final Comparator ORDER_BY_DATE = new Comparator() { + public int compare(Run lhs, Run rhs) { + return -lhs.getTimestamp().compareTo(rhs.getTimestamp()); + } + }; + + /** + * {@link FeedAdapter} to produce feed from the summary of this build. + */ + public static final FeedAdapter FEED_ADAPTER = new FeedAdapter() { + public String getEntryTitle(Run entry) { + return entry+" ("+entry.getResult()+")"; + } + + public String getEntryUrl(Run entry) { + return entry.getUrl(); + } + + public String getEntryID(Run entry) { + return "tag:"+entry.getParent().getName()+':'+entry.getId(); + } + + public Calendar getEntryTimestamp(Run entry) { + return entry.getTimestamp(); + } + }; +} diff --git a/core/src/main/java/hudson/model/RunMap.java b/core/src/main/java/hudson/model/RunMap.java new file mode 100644 index 0000000000..afb1101e4d --- /dev/null +++ b/core/src/main/java/hudson/model/RunMap.java @@ -0,0 +1,184 @@ +package hudson.model; + +import java.util.AbstractMap; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.Comparator; +import java.util.Collections; +import java.util.Map; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; + +/** + * {@link Map} from build number to {@link Run}. + * + *

+ * This class is multi-thread safe by using copy-on-write technique, + * and it also updates the bi-directional links within {@link Run} + * accordingly. + * + * @author Kohsuke Kawaguchi + */ +public final class RunMap> extends AbstractMap implements SortedMap { + // copy-on-write map + private transient volatile SortedMap builds = + new TreeMap(COMPARATOR); + + /** + * Read-only view of this map. + */ + private final SortedMap view = Collections.unmodifiableSortedMap(this); + + public Set> entrySet() { + // since the map is copy-on-write, make sure no one modifies it + return Collections.unmodifiableSet(builds.entrySet()); + } + + public synchronized R put(R value) { + return put(value.getNumber(),value); + } + + public synchronized R put(Integer key, R value) { + // copy-on-write update + TreeMap m = new TreeMap(builds); + + R r = update(m, key, value); + + this.builds = m; + return r; + } + + public synchronized void putAll(Map rhs) { + // copy-on-write update + TreeMap m = new TreeMap(builds); + + for (Map.Entry e : rhs.entrySet()) + update(m, e.getKey(), e.getValue()); + + this.builds = m; + } + + private R update(TreeMap m, Integer key, R value) { + // things are bit tricky because this map is order so that the newest one comes first, + // yet 'nextBuild' refers to the newer build. + R first = m.isEmpty() ? null : m.get(m.firstKey()); + R r = m.put(key, value); + SortedMap head = m.headMap(key); + if(!head.isEmpty()) { + R prev = m.get(head.lastKey()); + value.previousBuild = prev.previousBuild; + value.nextBuild = prev; + if(value.previousBuild!=null) + value.previousBuild.nextBuild = value; + prev.previousBuild=value; + } else { + value.previousBuild = first; + value.nextBuild = null; + if(first!=null) + first.nextBuild = value; + } + return r; + } + + public synchronized boolean remove(R run) { + if(run.nextBuild!=null) + run.nextBuild.previousBuild = run.previousBuild; + if(run.previousBuild!=null) + run.previousBuild.nextBuild = run.nextBuild; + + // copy-on-write update + TreeMap m = new TreeMap(builds); + R r = m.remove(run.getNumber()); + this.builds = m; + + return r!=null; + } + + public synchronized void reset(TreeMap builds) { + this.builds = new TreeMap(COMPARATOR); + putAll(builds); + } + + /** + * Gets the read-only view of this map. + */ + public SortedMap getView() { + return view; + } + +// +// SortedMap delegation +// + public Comparator comparator() { + return builds.comparator(); + } + + public SortedMap subMap(Integer fromKey, Integer toKey) { + return builds.subMap(fromKey, toKey); + } + + public SortedMap headMap(Integer toKey) { + return builds.headMap(toKey); + } + + public SortedMap tailMap(Integer fromKey) { + return builds.tailMap(fromKey); + } + + public Integer firstKey() { + return builds.firstKey(); + } + + public Integer lastKey() { + return builds.lastKey(); + } + + public static final Comparator COMPARATOR = new Comparator() { + public int compare(Comparable o1, Comparable o2) { + return -o1.compareTo(o2); + } + }; + + /** + * {@link Run} factory. + */ + public interface Constructor> { + R create(File dir) throws IOException; + } + + /** + * Fills in {@link RunMap} by loading build records from the file system. + * + * @param job + * Job that owns this map. + * @param cons + * Used to create new instance of {@link Run}. + */ + public synchronized void load(Job job, Constructor cons) { + TreeMap builds = new TreeMap(RunMap.COMPARATOR); + File buildDir = job.getBuildDir(); + buildDir.mkdirs(); + String[] buildDirs = buildDir.list(new FilenameFilter() { + public boolean accept(File dir, String name) { + return new File(dir,name).isDirectory(); + } + }); + + for( String build : buildDirs ) { + File d = new File(buildDir,build); + if(new File(d,"build.xml").exists()) { + // if the build result file isn't in the directory, ignore it. + try { + R b = cons.create(d); + builds.put( b.getNumber(), b ); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + reset(builds); + } +} diff --git a/core/src/main/java/hudson/model/Slave.java b/core/src/main/java/hudson/model/Slave.java new file mode 100644 index 0000000000..7424ed3fb1 --- /dev/null +++ b/core/src/main/java/hudson/model/Slave.java @@ -0,0 +1,224 @@ +package hudson.model; + +import hudson.FilePath; +import hudson.Launcher; +import hudson.Proc; +import hudson.Util; +import hudson.util.ArgumentListBuilder; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; + +/** + * Information about a Hudson slave node. + * + * @author Kohsuke Kawaguchi + */ +public final class Slave implements Node { + /** + * Name of this slave node. + */ + private final String name; + + /** + * Description of this node. + */ + private final String description; + + /** + * Commands to run to post a job on this machine. + */ + private final String command; + + /** + * Path to the root of the workspace + * from within this node, such as "/hudson" + */ + private final String remoteFS; + + /** + * Path to the root of the remote workspace of this node, + * such as "/net/slave1/hudson" + */ + private final File localFS; + + /** + * Number of executors of this node. + */ + private int numExecutors = 2; + + /** + * Job allocation strategy. + */ + private Mode mode; + + public Slave(String name, String description, String command, String remoteFS, File localFS, int numExecutors, Mode mode) { + this.name = name; + this.description = description; + this.command = command; + this.remoteFS = remoteFS; + this.localFS = localFS; + this.numExecutors = numExecutors; + this.mode = mode; + } + + public String getNodeName() { + return name; + } + + public String getCommand() { + return command; + } + + public String[] getCommandTokens() { + return Util.tokenize(command); + } + + public String getRemoteFS() { + return remoteFS; + } + + public File getLocalFS() { + return localFS; + } + + public String getNodeDescription() { + return description; + } + + public FilePath getFilePath() { + return new FilePath(localFS,remoteFS); + } + + public int getNumExecutors() { + return numExecutors; + } + + public Mode getMode() { + return mode; + } + + /** + * Estimates the clock difference with this slave. + * + * @return + * difference in milli-seconds. + * a large positive value indicates that the master is ahead of the slave, + * and negative value indicates otherwise. + */ + public long getClockDifference() throws IOException { + File testFile = new File(localFS,"clock.skew"); + FileOutputStream os = new FileOutputStream(testFile); + long now = new Date().getTime(); + os.close(); + + long r = now - testFile.lastModified(); + + testFile.delete(); + + return r; + } + + /** + * Gets the clock difference in HTML string. + */ + public String getClockDifferenceString() { + try { + long diff = getClockDifference(); + if(-1000100*60) // more than a minute difference + s = ""+s+""; + + return s; + } catch (IOException e) { + return "Unable to check"; + } + } + + public Launcher createLauncher(TaskListener listener) { + if(command.length()==0) // local alias + return new Launcher(listener); + + + return new Launcher(listener) { + @Override + public Proc launch(String[] cmd, String[] env, OutputStream out, FilePath workDir) throws IOException { + return super.launch(prepend(cmd,env,workDir), env, null, out); + } + + @Override + public Proc launch(String[] cmd, String[] env, InputStream in, OutputStream out) throws IOException { + return super.launch(prepend(cmd,env,CURRENT_DIR), env, in, out); + } + + @Override + public boolean isUnix() { + // Err on Unix, since we expect that to be the common slaves + return remoteFS.indexOf('\\')==-1; + } + + private String[] prepend(String[] cmd, String[] env, FilePath workDir) { + ArgumentListBuilder r = new ArgumentListBuilder(); + r.add(getCommandTokens()); + r.add(getFilePath().child("bin").child("slave").getRemote()); + r.addQuoted(workDir.getRemote()); + for (String s : env) { + int index =s.indexOf('='); + r.add(s.substring(0,index)); + r.add(s.substring(index+1)); + } + r.add("--"); + for (String c : cmd) { + // ssh passes the command and parameters in one string. + // see RFC 4254 section 6.5. + // so the consequence that we need to give + // {"ssh",...,"ls","\"a b\""} to list a file "a b". + // If we just do + // {"ssh",...,"ls","a b"} (which is correct if this goes directly to Runtime.exec), + // then we end up executing "ls","a","b" on the other end. + // + // I looked at rsh source code, and that behave the same way. + if(c.indexOf(' ')>=0) + r.addQuoted(c); + else + r.add(c); + } + return r.toCommandArray(); + } + }; + } + + public FilePath getWorkspaceRoot() { + return getFilePath().child("workspace"); + } + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Slave that = (Slave) o; + + return name.equals(that.name); + + } + + public int hashCode() { + return name.hashCode(); + } + + private static final FilePath CURRENT_DIR = new FilePath(new File(".")); +} diff --git a/core/src/main/java/hudson/model/StreamBuildListener.java b/core/src/main/java/hudson/model/StreamBuildListener.java new file mode 100644 index 0000000000..d79c7c15d7 --- /dev/null +++ b/core/src/main/java/hudson/model/StreamBuildListener.java @@ -0,0 +1,50 @@ +package hudson.model; + +import hudson.util.WriterOutputStream; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Writer; + +/** + * {@link BuildListener} that writes to a {@link Writer}. + * @author Kohsuke Kawaguchi + */ +public class StreamBuildListener implements BuildListener { + private final PrintWriter w; + + private final PrintStream ps; + + public StreamBuildListener(Writer w) { + this(new PrintWriter(w)); + } + + public StreamBuildListener(PrintWriter w) { + this.w = w; + // unless we auto-flash, PrintStream will use BufferedOutputStream internally, + // and break ordering + this.ps = new PrintStream(new WriterOutputStream(w),true); + } + + public void started() { + w.println("started"); + } + + public PrintStream getLogger() { + return ps; + } + + public PrintWriter error(String msg) { + w.println("ERROR: "+msg); + return w; + } + + public PrintWriter fatalError(String msg) { + w.println("FATAL: "+msg); + return w; + } + + public void finished(Result result) { + w.println("finished: "+result); + } +} diff --git a/core/src/main/java/hudson/model/TaskListener.java b/core/src/main/java/hudson/model/TaskListener.java new file mode 100644 index 0000000000..d83b57a3a4 --- /dev/null +++ b/core/src/main/java/hudson/model/TaskListener.java @@ -0,0 +1,44 @@ +package hudson.model; + +import hudson.util.StreamTaskListener; +import hudson.util.NullStream; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * Receives events that happen during some task execution, + * such as a build or SCM change polling. + * + * @author Kohsuke Kawaguchi + */ +public interface TaskListener { + /** + * This writer will receive the output of the build. + * + * @return + * must be non-null. + */ + PrintStream getLogger(); + + /** + * An error in the build. + * + * @return + * A writer to receive details of the error. Not null. + */ + PrintWriter error(String msg); + + /** + * A fatal error in the build. + * + * @return + * A writer to receive details of the error. Not null. + */ + PrintWriter fatalError(String msg); + + /** + * {@link TaskListener} that discards the output. + */ + public static final TaskListener NULL = new StreamTaskListener(new NullStream()); +} diff --git a/core/src/main/java/hudson/model/User.java b/core/src/main/java/hudson/model/User.java new file mode 100644 index 0000000000..38eb24c1ec --- /dev/null +++ b/core/src/main/java/hudson/model/User.java @@ -0,0 +1,261 @@ +package hudson.model; + +import com.thoughtworks.xstream.XStream; +import hudson.FeedAdapter; +import hudson.XmlFile; +import hudson.model.Descriptor.FormException; +import hudson.scm.ChangeLogSet; +import hudson.util.RunList; +import hudson.util.XStream2; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a user. + * + * @author Kohsuke Kawaguchi + */ +public class User extends AbstractModelObject { + + private transient final String id; + + private volatile String fullName; + + private volatile String description; + + /** + * List of {@link UserProperty}s configured for this project. + * Copy-on-write semantics. + */ + private volatile List properties = new ArrayList(); + + + private User(String id) { + this.id = id; + this.fullName = id; // fullName defaults to name + + for (UserPropertyDescriptor d : UserProperties.LIST) { + UserProperty up = d.newInstance(this); + if(up!=null) + properties.add(up); + } + + // load the other data from disk if it's available + XmlFile config = getConfigFile(); + try { + if(config.exists()) + config.unmarshal(this); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to load "+config,e); + } + + for (UserProperty p : properties) + p.setUser(this); + } + + public String getId() { + return id; + } + + public String getUrl() { + return "user/"+ id; + } + + /** + * Gets the human readable name of this user. + * This is configurable by the user. + * + * @return + * never null. + */ + public String getFullName() { + return fullName; + } + + public String getDescription() { + return description; + } + + /** + * Gets the user properties configured for this user. + */ + public Map,UserProperty> getProperties() { + return Descriptor.toMap(properties); + } + + /** + * Gets the specific property, or null. + */ + public T getProperty(Class clazz) { + for (UserProperty p : properties) { + if(clazz.isInstance(p)) + return (T)p; // can't use Class.cast as that's 5.0 feature + } + return null; + } + + /** + * Accepts the new description. + */ + public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + req.setCharacterEncoding("UTF-8"); + + description = req.getParameter("description"); + save(); + + rsp.sendRedirect("."); // go to the top page + } + + + + public static User get(String name) { + if(name==null) + return null; + synchronized(byName) { + User u = byName.get(name); + if(u==null) { + u = new User(name); + byName.put(name,u); + } + return u; + } + } + + /** + * Returns the user name. + */ + public String getDisplayName() { + return getFullName(); + } + + /** + * Gets the list of {@link Build}s that include changes by this user, + * by the timestamp order. + * + * TODO: do we need some index for this? + */ + public List getBuilds() { + List r = new ArrayList(); + for (Project p : Hudson.getInstance().getProjects()) { + for (Build b : p.getBuilds()) { + for (ChangeLogSet.Entry e : b.getChangeSet()) { + if(e.getAuthor()==this) { + r.add(b); + break; + } + } + } + } + Collections.sort(r,Run.ORDER_BY_DATE); + return r; + } + + public String toString() { + return fullName; + } + + /** + * The file we save our configuration. + */ + protected final XmlFile getConfigFile() { + return new XmlFile(XSTREAM,new File(Hudson.getInstance().getRootDir(),"users/"+ id +"/config.xml")); + } + + /** + * Save the settings to a file. + */ + public synchronized void save() throws IOException { + XmlFile config = getConfigFile(); + config.mkdirs(); + config.write(this); + } + + /** + * Accepts submission from the configuration page. + */ + public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + + req.setCharacterEncoding("UTF-8"); + + try { + fullName = req.getParameter("fullName"); + description = req.getParameter("description"); + + List props = new ArrayList(); + for (Descriptor d : UserProperties.LIST) + props.add(d.newInstance(req)); + this.properties = props; + + save(); + + rsp.sendRedirect("."); + } catch (FormException e) { + sendError(e,req,rsp); + } + } + + public void doRssAll( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + rss(req, rsp, " all builds", RunList.fromRuns(getBuilds())); + } + + public void doRssFailed( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + rss(req, rsp, " regression builds", RunList.fromRuns(getBuilds()).regressionOnly()); + } + + private void rss(StaplerRequest req, StaplerResponse rsp, String suffix, RunList runs) throws IOException, ServletException { + RSS.forwardToRss(getDisplayName()+ suffix, getUrl(), + runs.newBuilds(), FEED_ADAPTER, req, rsp ); + } + + + /** + * Keyed by {@link User#id}. + */ + private static final Map byName = new HashMap(); + + /** + * Used to load/save user configuration. + */ + private static final XStream XSTREAM = new XStream2(); + + private static final Logger LOGGER = Logger.getLogger(User.class.getName()); + + static { + XSTREAM.alias("user",User.class); + } + + /** + * {@link FeedAdapter} to produce build status summary in the feed. + */ + public static final FeedAdapter FEED_ADAPTER = new FeedAdapter() { + public String getEntryTitle(Run entry) { + return entry+" : "+entry.getBuildStatusSummary().message; + } + + public String getEntryUrl(Run entry) { + return entry.getUrl(); + } + + public String getEntryID(Run entry) { + return "tag:"+entry.getParent().getName()+':'+entry.getId(); + } + + public Calendar getEntryTimestamp(Run entry) { + return entry.getTimestamp(); + } + }; +} diff --git a/core/src/main/java/hudson/model/UserProperties.java b/core/src/main/java/hudson/model/UserProperties.java new file mode 100644 index 0000000000..f7d3c6d8a3 --- /dev/null +++ b/core/src/main/java/hudson/model/UserProperties.java @@ -0,0 +1,15 @@ +package hudson.model; + +import hudson.tasks.Mailer; + +import java.util.List; + +/** + * List of all installed {@link UserProperty} types. + * @author Kohsuke Kawaguchi + */ +public class UserProperties { + public static final List LIST = Descriptor.toList( + Mailer.UserProperty.DESCRIPTOR + ); +} diff --git a/core/src/main/java/hudson/model/UserProperty.java b/core/src/main/java/hudson/model/UserProperty.java new file mode 100644 index 0000000000..bcf99f58da --- /dev/null +++ b/core/src/main/java/hudson/model/UserProperty.java @@ -0,0 +1,28 @@ +package hudson.model; + +import hudson.Plugin; +import hudson.ExtensionPoint; + +/** + * Extensible property of {@link User}. + * + *

+ * {@link Plugin}s can extend this to define custom properties + * for {@link User}s. {@link UserProperty}s show up in the user + * configuration screen, and they are persisted with the user object. + * + * @author Kohsuke Kawaguchi + * @see UserProperties#LIST + */ +public abstract class UserProperty implements Describable, ExtensionPoint { + /** + * The user object that owns this property. + * This value will be set by the Hudson code. + * Derived classes can expect this value to be always set. + */ + protected transient User user; + + /*package*/ final void setUser(User u) { + this.user = u; + } +} diff --git a/core/src/main/java/hudson/model/UserPropertyDescriptor.java b/core/src/main/java/hudson/model/UserPropertyDescriptor.java new file mode 100644 index 0000000000..91460c58d6 --- /dev/null +++ b/core/src/main/java/hudson/model/UserPropertyDescriptor.java @@ -0,0 +1,21 @@ +package hudson.model; + +/** + * {@link Descriptor} for {@link UserProperty}. + * + * @author Kohsuke Kawaguchi + */ +public abstract class UserPropertyDescriptor extends Descriptor { + protected UserPropertyDescriptor(Class clazz) { + super(clazz); + } + + /** + * Creates a default instance of {@link UserProperty} to be associated + * with {@link User} that doesn't have any back up data store. + * + * @return null + * if the implementation choose not to add any proeprty object for such user. + */ + public abstract UserProperty newInstance(User user); +} diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java new file mode 100644 index 0000000000..a2655e9000 --- /dev/null +++ b/core/src/main/java/hudson/model/View.java @@ -0,0 +1,136 @@ +package hudson.model; + +import hudson.Util; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * Represents a collection of {@link Job}s. + * + * @author Kohsuke Kawaguchi + */ +public class View extends JobCollection { + + private final Hudson owner; + + /** + * List of job names. This is what gets serialized. + */ + /*package*/ final Set jobNames = new TreeSet(); + + /** + * Name of this view. + */ + private String name; + + /** + * Message displayed in the view page. + */ + private String description; + + + public View(Hudson owner, String name) { + this.name = name; + this.owner = owner; + } + + /** + * Returns a read-only view of all {@link Job}s in this view. + * + *

+ * This method returns a separate copy each time to avoid + * concurrent modification issue. + */ + public synchronized List getJobs() { + Job[] jobs = new Job[jobNames.size()]; + int i=0; + for (String name : jobNames) + jobs[i++] = owner.getJob(name); + return Arrays.asList(jobs); + } + + public boolean containsJob(Job job) { + return jobNames.contains(job.getName()); + } + + public String getViewName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getDisplayName() { + return name; + } + + public Job doCreateJob(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return null; + + Job job = owner.doCreateJob(req, rsp); + if(job!=null) { + jobNames.add(job.getName()); + owner.save(); + } + return job; + } + + public String getUrl() { + return "view/"+name+'/'; + } + + /** + * Accepts submission from the configuration page. + */ + public synchronized void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException { + if(!Hudson.adminCheck(req,rsp)) + return; + + req.setCharacterEncoding("UTF-8"); + + jobNames.clear(); + for (Job job : owner.getJobs()) { + if(req.getParameter(job.getName())!=null) + jobNames.add(job.getName()); + } + + description = Util.nullify(req.getParameter("description")); + + owner.save(); + + rsp.sendRedirect("."); + } + + /** + * Accepts the new description. + */ + public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + if(!Hudson.adminCheck(req,rsp)) + return; + + req.setCharacterEncoding("UTF-8"); + description = req.getParameter("description"); + owner.save(); + rsp.sendRedirect("."); // go to the top page + } + + /** + * Deletes this view. + */ + public synchronized void doDoDelete( StaplerRequest req, StaplerResponse rsp ) throws IOException { + if(!Hudson.adminCheck(req,rsp)) + return; + + owner.deleteView(this); + rsp.sendRedirect2(req.getContextPath()+"/"); + } +} diff --git a/core/src/main/java/hudson/model/ViewJob.java b/core/src/main/java/hudson/model/ViewJob.java new file mode 100644 index 0000000000..e5145e6f7c --- /dev/null +++ b/core/src/main/java/hudson/model/ViewJob.java @@ -0,0 +1,148 @@ +package hudson.model; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.SortedMap; + +/** + * {@link Job} that monitors activities that happen outside Hudson, + * which requires occasional batch reload activity to obtain the up-to-date information. + * + *

+ * This can be used as a base class to derive custom {@link Job} type. + * + * @author Kohsuke Kawaguchi + */ +public abstract class ViewJob, RunT extends Run> + extends Job { + + /** + * We occasionally update the list of {@link Run}s from a file system. + * The next scheduled update time. + */ + private transient long nextUpdate = 0; + + /** + * All {@link Run}s. Copy-on-write semantics. + */ + protected transient /*almost final*/ RunMap runs = new RunMap(); + + private transient boolean notLoaded = true; + + /** + * If the reloading of runs are in progress (in another thread, + * set to true.) + */ + private transient volatile boolean reloadingInProgress; + + /** + * {@link ExternalJob}s that need to be reloaded. + * + * This is a set, so no {@link ExternalJob}s are scheduled twice, yet + * it's order is predictable, avoiding starvation. + */ + private static final LinkedHashSet reloadQueue = new LinkedHashSet(); + /*package*/ static final Thread reloadThread = new ReloadThread(); + static { + reloadThread.start(); + } + + protected ViewJob(Hudson parent, String name) { + super(parent, name); + } + + public boolean isBuildable() { + return false; + } + + protected SortedMap _getRuns() { + if(notLoaded || runs==null) { + // if none is loaded yet, do so immediately. + synchronized(this) { + if(runs==null) + runs = new RunMap(); + if(notLoaded) { + notLoaded = false; + _reload(); + } + } + } + if(nextUpdate + * The loaded {@link Run}s should be set to {@link #runs}. + */ + protected abstract void reload(); + + public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + super.doConfigSubmit(req,rsp); + // make sure to reload to reflect this config change. + nextUpdate = 0; + } + + + /** + * Thread that reloads the {@link Run}s. + */ + private static final class ReloadThread extends Thread { + private ViewJob getNext() throws InterruptedException { + synchronized(reloadQueue) { + while(reloadQueue.isEmpty()) + reloadQueue.wait(); + ViewJob job = reloadQueue.iterator().next(); + reloadQueue.remove(job); + return job; + } + } + + public void run() { + while (true) { + try { + getNext()._reload(); + } catch (InterruptedException e) { + // treat this as a death signal + return; + } catch (Throwable t) { + // otherwise ignore any error + t.printStackTrace(); + } + } + } + } + + // private static final Logger logger = Logger.getLogger(ViewJob.class.getName()); +} diff --git a/core/src/main/java/hudson/model/WorkspaceCleanupThread.java b/core/src/main/java/hudson/model/WorkspaceCleanupThread.java new file mode 100644 index 0000000000..20dc8ce52c --- /dev/null +++ b/core/src/main/java/hudson/model/WorkspaceCleanupThread.java @@ -0,0 +1,119 @@ +package hudson.model; + +import hudson.Util; +import hudson.util.StreamTaskListener; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Date; +import java.util.logging.Level; + +/** + * Clean up old left-over workspaces from slaves. + * + * @author Kohsuke Kawaguchi + */ +public class WorkspaceCleanupThread extends PeriodicWork { + private static WorkspaceCleanupThread theInstance; + + public WorkspaceCleanupThread() { + super("Workspace clean-up"); + theInstance = this; + } + + public static void invoke() { + theInstance.run(); + } + + private TaskListener listener; + + protected void execute() { + Hudson h = Hudson.getInstance(); + try { + // don't buffer this, so that the log shows what the worker thread is up to in real time + OutputStream os = new FileOutputStream( + new File(h.getRootDir(),"workspace-cleanup.log")); + try { + listener = new StreamTaskListener(os); + + for (Slave s : h.getSlaves()) { + process(s); + } + + process(h); + } finally { + os.close(); + } + } catch (IOException e) { + logger.log(Level.SEVERE, "Failed to access log file",e); + } + } + + private void process(Hudson h) { + File jobs = new File(h.getRootDir(), "jobs"); + File[] dirs = jobs.listFiles(DIR_FILTER); + if(dirs==null) return; + for (File dir : dirs) { + File ws = new File(dir, "workspace"); + if(shouldBeDeleted(dir.getName(),ws,h)) { + delete(ws); + } + } + } + + private boolean shouldBeDeleted(String jobName, File dir, Node n) { + Job job = Hudson.getInstance().getJob(jobName); + if(job==null) + // no such project anymore + return true; + + if(!dir.exists()) + return false; + + if (job instanceof Project) { + Project p = (Project) job; + Node lb = p.getLastBuiltOn(); + if(lb!=null && lb.equals(n)) + // this is the active workspace. keep it. + return false; + } + + // if older than a month, delete + return dir.lastModified() + 30 * DAY < new Date().getTime(); + + } + + private void process(Slave s) { + // TODO: we should be using launcher to execute remote rm -rf + + listener.getLogger().println("Scanning "+s.getNodeName()); + + File[] dirs = s.getWorkspaceRoot().getLocal().listFiles(DIR_FILTER); + if(dirs ==null) return; + for (File dir : dirs) { + if(shouldBeDeleted(dir.getName(),dir,s)) + delete(dir); + } + } + + private void delete(File dir) { + try { + listener.getLogger().println("Deleting "+dir); + Util.deleteRecursive(dir); + } catch (IOException e) { + e.printStackTrace(listener.error("Failed to delete "+dir)); + } + } + + + private static final FileFilter DIR_FILTER = new FileFilter() { + public boolean accept(File f) { + return f.isDirectory(); + } + }; + + private static final long DAY = 1000*60*60*24; +} diff --git a/core/src/main/java/hudson/model/package.html b/core/src/main/java/hudson/model/package.html new file mode 100644 index 0000000000..9148f13cc6 --- /dev/null +++ b/core/src/main/java/hudson/model/package.html @@ -0,0 +1,3 @@ + +Core object model that are bound to URLs via stapler, rooted at Hudson. + \ No newline at end of file diff --git a/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/CVSEntry.java b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/CVSEntry.java new file mode 100644 index 0000000000..3d8efbfe53 --- /dev/null +++ b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/CVSEntry.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002,2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package hudson.org.apache.tools.ant.taskdefs.cvslib; + +import java.util.Date; +import java.util.Vector; + +/** + * CVS Entry. + * + * @version $Revision$ $Date$ + */ +class CVSEntry { + private Date m_date; + private String m_author; + private final String m_comment; + private final Vector m_files = new Vector(); + + public CVSEntry(Date date, String author, String comment) { + m_date = date; + m_author = author; + m_comment = comment; + } + + public void addFile(String file, String revision, String previousRevision, String branch, boolean dead) { + m_files.addElement(new RCSFile(file, revision, previousRevision, branch, dead)); + } + + // maybe null, in case of error + Date getDate() { + return m_date; + } + + void setAuthor(final String author) { + m_author = author; + } + + String getAuthor() { + return m_author; + } + + String getComment() { + return m_comment; + } + + Vector getFiles() { + return m_files; + } + + /** + * Checks if any of the entries include a change to a branch. + * + * @param branch + * can be null to indicate the trunk. + */ + public boolean containsBranch(String branch) { + for (RCSFile file : m_files) { + String b = file.getBranch(); + if(b==null && branch==null) + return true; + if(b==null || branch==null) + continue; + if(b.equals(branch)) + return true; + } + return false; + } + + public String toString() { + return getAuthor() + "\n" + getDate() + "\n" + getFiles() + "\n" + + getComment(); + } +} diff --git a/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogParser.java b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogParser.java new file mode 100644 index 0000000000..9ee412be63 --- /dev/null +++ b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogParser.java @@ -0,0 +1,336 @@ +/* + * Copyright 2002-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package hudson.org.apache.tools.ant.taskdefs.cvslib; +// patched to work around http://issues.apache.org/bugzilla/show_bug.cgi?id=38583 + +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.TimeZone; +import java.util.Map; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +/** + * A class used to parse the output of the CVS log command. + * + * @version $Revision$ $Date$ + */ +class ChangeLogParser { + //private static final int GET_ENTRY = 0; + private static final int GET_FILE = 1; + private static final int GET_DATE = 2; + private static final int GET_COMMENT = 3; + private static final int GET_REVISION = 4; + private static final int GET_PREVIOUS_REV = 5; + private static final int GET_SYMBOLIC_NAMES = 6; + + /** + * input format for dates read in from cvs log. + * + * Some users reported that they see different formats, + * so this is extended from original Ant version to cover different formats. + */ + private static final SimpleDateFormat[] c_inputDate + = new SimpleDateFormat[]{ + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"), + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), + }; + + static { + TimeZone utc = TimeZone.getTimeZone("UTC"); + for (SimpleDateFormat df : c_inputDate) { + df.setTimeZone(utc); + } + } + + //The following is data used while processing stdout of CVS command + private String m_file; + private String m_date; + private String m_author; + private String m_comment; + private String m_revision; + private String m_previousRevision; + /** + * All branches available on the current file. + * Keyed by branch revision prefix (like "1.2.3." if files in the branch have revision numbers like + * "1.2.3.4") and the value is the branch name. + */ + private final Map branches = new HashMap(); + /** + * True if the log record indicates deletion; + */ + private boolean m_dead; + + private int m_status = GET_FILE; + + /** rcs entries */ + private final Hashtable m_entries = new Hashtable(); + + private final Task owner; + + public ChangeLogParser(Task owner) { + this.owner = owner; + } + + /** + * Get a list of rcs entries as an array. + * + * @return a list of rcs entries as an array + */ + CVSEntry[] getEntrySetAsArray() { + final CVSEntry[] array = new CVSEntry[ m_entries.size() ]; + Enumeration e = m_entries.elements(); + int i = 0; + while (e.hasMoreElements()) { + array[i++] = (CVSEntry) e.nextElement(); + } + return array; + } + + private boolean dead = false; + + /** + * Receive notification about the process writing + * to standard output. + */ + public void stdout(final String line) { + if(dead) + return; + try { + switch(m_status) { + case GET_FILE: + // make sure attributes are reset when + // working on a 'new' file. + reset(); + processFile(line); + break; + case GET_SYMBOLIC_NAMES: + processSymbolicName(line); + break; + + case GET_REVISION: + processRevision(line); + break; + + case GET_DATE: + processDate(line); + break; + + case GET_COMMENT: + processComment(line); + break; + + case GET_PREVIOUS_REV: + processGetPreviousRevision(line); + break; + } + } catch (Exception e) { + // we don't know how to handle the input any more. don't accept any more input + dead = true; + } + } + + /** + * Process a line while in "GET_COMMENT" state. + * + * @param line the line + */ + private void processComment(final String line) { + final String lineSeparator = System.getProperty("line.separator"); + if (line.startsWith("======")) { + //We have ended changelog for that particular file + //so we can save it + final int end + = m_comment.length() - lineSeparator.length(); //was -1 + m_comment = m_comment.substring(0, end); + saveEntry(); + m_status = GET_FILE; + } else if (line.startsWith("----------------------------")) { + final int end + = m_comment.length() - lineSeparator.length(); //was -1 + m_comment = m_comment.substring(0, end); + m_status = GET_PREVIOUS_REV; + } else { + m_comment += line + lineSeparator; + } + } + + /** + * Process a line while in "GET_FILE" state. + * + * @param line the line + */ + private void processFile(final String line) { + if (line.startsWith("Working file:")) { + m_file = line.substring(14, line.length()); + m_status = GET_SYMBOLIC_NAMES; + } + } + + /** + * Obtains the revision name list + */ + private void processSymbolicName(String line) { + if (line.startsWith("\t")) { + line = line.trim(); + int idx = line.lastIndexOf(':'); + if(idx<0) { + // ??? + return; + } + + String symbol = line.substring(0,idx); + Matcher m = DOT_PATTERN.matcher(line.substring(idx + 2)); + if(!m.matches()) + return; // not a branch name + + branches.put(m.group(1)+m.group(3)+'.',symbol); + } else + if (line.startsWith("keyword substitution:")) { + m_status = GET_REVISION; + } + } + + private static final Pattern DOT_PATTERN = Pattern.compile("(([0-9]+\\.)+)0\\.([0-9]+)"); + + /** + * Process a line while in "REVISION" state. + * + * @param line the line + */ + private void processRevision(final String line) { + if (line.startsWith("revision")) { + m_revision = line.substring(9); + m_status = GET_DATE; + } else if (line.startsWith("======")) { + //There was no revisions in this changelog + //entry so lets move unto next file + m_status = GET_FILE; + } + } + + /** + * Process a line while in "DATE" state. + * + * @param line the line + */ + private void processDate(final String line) { + if (line.startsWith("date:")) { + m_date = line.substring(6, 25); + String lineData = line.substring(line.indexOf(";") + 1); + m_author = lineData.substring(10, lineData.indexOf(";")); + + m_status = GET_COMMENT; + + m_dead = lineData.indexOf("state: dead;")!=-1; + + //Reset comment to empty here as we can accumulate multiple lines + //in the processComment method + m_comment = ""; + } + } + + /** + * Process a line while in "GET_PREVIOUS_REVISION" state. + * + * @param line the line + */ + private void processGetPreviousRevision(final String line) { + if (!line.startsWith("revision")) { + throw new IllegalStateException("Unexpected line from CVS: " + + line); + } + m_previousRevision = line.substring(9); + + saveEntry(); + + m_revision = m_previousRevision; + m_status = GET_DATE; + } + + /** + * Utility method that saves the current entry. + */ + private void saveEntry() { + final String entryKey = m_date + m_author + m_comment; + CVSEntry entry; + if (!m_entries.containsKey(entryKey)) { + entry = new CVSEntry(parseDate(m_date), m_author, m_comment); + m_entries.put(entryKey, entry); + } else { + entry = m_entries.get(entryKey); + } + + entry.addFile(m_file, m_revision, m_previousRevision, findBranch(m_revision), m_dead); + } + + /** + * Finds the branch name that matches the revision, or null if not found. + */ + private String findBranch(String revision) { + if(revision==null) return null; // defensive check + for (Entry e : branches.entrySet()) { + if(revision.startsWith(e.getKey()) && revision.substring(e.getKey().length()).indexOf('.')==-1) + return e.getValue(); + } + return null; + } + + /** + * Parse date out from expected format. + * + * @param date the string holding dat + * @return the date object or null if unknown date format + */ + private Date parseDate(String date) { + for (SimpleDateFormat df : c_inputDate) { + try { + return df.parse(date); + } catch (ParseException e) { + // try next if one fails + } + } + + // nothing worked + owner.log("Failed to parse "+date+"\n", Project.MSG_ERR); + //final String message = REZ.getString( "changelog.bat-date.error", date ); + //getContext().error( message ); + return null; + } + + /** + * reset all internal attributes except status. + */ + private void reset() { + m_file = null; + m_date = null; + m_author = null; + m_comment = null; + m_revision = null; + m_previousRevision = null; + m_dead = false; + branches.clear(); + } +} diff --git a/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogTask.java b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogTask.java new file mode 100644 index 0000000000..f82c870e6f --- /dev/null +++ b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogTask.java @@ -0,0 +1,429 @@ +/* + * Copyright 2002-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package hudson.org.apache.tools.ant.taskdefs.cvslib; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.taskdefs.AbstractCvsTask; +import org.apache.tools.ant.taskdefs.cvslib.CvsVersion; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; +import java.util.Vector; + +/** + * Examines the output of cvs log and group related changes together. + * + * It produces an XML output representing the list of changes. + *

+ * <!-- Root element -->
+ * <!ELEMENT changelog (entry+)>
+ * <!-- CVS Entry -->
+ * <!ELEMENT entry (date,author,file+,msg)>
+ * <!-- Date of cvs entry -->
+ * <!ELEMENT date (#PCDATA)>
+ * <!-- Author of change -->
+ * <!ELEMENT author (#PCDATA)>
+ * <!-- List of files affected -->
+ * <!ELEMENT msg (#PCDATA)>
+ * <!-- File changed -->
+ * <!ELEMENT file (name,revision,prevrevision?)>
+ * <!-- Name of the file -->
+ * <!ELEMENT name (#PCDATA)>
+ * <!-- Revision number -->
+ * <!ELEMENT revision (#PCDATA)>
+ * <!-- Previous revision number -->
+ * <!ELEMENT prevrevision (#PCDATA)>
+ * 
+ * + * @version $Revision$ $Date$ + * @since Ant 1.5 + * @ant.task name="cvschangelog" category="scm" + */ +public class ChangeLogTask extends AbstractCvsTask { + /** User list */ + private File m_usersFile; + + /** User list */ + private Vector m_cvsUsers = new Vector(); + + /** Input dir */ + private File m_dir; + + /** Output file */ + private File m_destfile; + + /** The earliest date at which to start processing entries. */ + private Date m_start; + + /** The latest date at which to stop processing entries. */ + private Date m_stop; + + /** + * To filter out change logs for a certain branch, this variable will be the branch name. + * Otherwise null. + */ + private String branch; + + /** + * Filesets containing list of files against which the cvs log will be + * performed. If empty then all files will in the working directory will + * be checked. + */ + private List m_filesets = new ArrayList(); + + + /** + * Set the base dir for cvs. + * + * @param dir The new dir value + */ + public void setDir(final File dir) { + m_dir = dir; + } + + + /** + * Set the output file for the log. + * + * @param destfile The new destfile value + */ + public void setDestfile(final File destfile) { + m_destfile = destfile; + } + + + /** + * Set a lookup list of user names & addresses + * + * @param usersFile The file containing the users info. + */ + public void setUsersfile(final File usersFile) { + m_usersFile = usersFile; + } + + + /** + * Add a user to list changelog knows about. + * + * @param user the user + */ + public void addUser(final CvsUser user) { + m_cvsUsers.addElement(user); + } + + + /** + * Set the date at which the changelog should start. + * + * @param start The date at which the changelog should start. + */ + public void setStart(final Date start) { + m_start = start; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + + /** + * Set the date at which the changelog should stop. + * + * @param stop The date at which the changelog should stop. + */ + public void setEnd(final Date stop) { + m_stop = stop; + } + + + /** + * Set the number of days worth of log entries to process. + * + * @param days the number of days of log to process. + */ + public void setDaysinpast(final int days) { + final long time = System.currentTimeMillis() + - (long) days * 24 * 60 * 60 * 1000; + + setStart(new Date(time)); + } + + + /** + * Adds a file about which cvs logs will be generated. + * + * @param fileName + * fileName relative to {@link #setDir(File)}. + */ + public void addFile(String fileName) { + m_filesets.add(fileName); + } + + public void setFile(List files) { + m_filesets = files; + } + + + /** + * Execute task + * + * @exception BuildException if something goes wrong executing the + * cvs command + */ + public void execute() throws BuildException { + File savedDir = m_dir; // may be altered in validate + + try { + + validate(); + final Properties userList = new Properties(); + + loadUserlist(userList); + + for (Enumeration e = m_cvsUsers.elements(); + e.hasMoreElements();) { + final CvsUser user = (CvsUser) e.nextElement(); + + user.validate(); + userList.put(user.getUserID(), user.getDisplayname()); + } + + + setCommand("log"); + + if (getTag() != null) { + CvsVersion myCvsVersion = new CvsVersion(); + myCvsVersion.setProject(getProject()); + myCvsVersion.setTaskName("cvsversion"); + myCvsVersion.setCvsRoot(getCvsRoot()); + myCvsVersion.setCvsRsh(getCvsRsh()); + myCvsVersion.setPassfile(getPassFile()); + myCvsVersion.setDest(m_dir); + myCvsVersion.execute(); + if (myCvsVersion.supportsCvsLogWithSOption()) { + addCommandArgument("-S"); + } + } + if (null != m_start) { + final SimpleDateFormat outputDate = + new SimpleDateFormat("yyyy-MM-dd"); + + // Kohsuke patch: + // probably due to timezone difference between server/client and + // the lack of precise specification in the protocol or something, + // sometimes the java.net CVS server (and probably others) don't + // always report all the changes that have happened in the given day. + // so let's take the date range bit wider, to make sure that + // the server sends us all the logs that we care. + // + // the only downside of this change is that it will increase the traffic + // unnecessarily, but given that in Hudson we already narrow down the scope + // by specifying files, this should be acceptable increase. + + Date safeStart = new Date(m_start.getTime()-1000L*60*60*24); + + // Kohsuke patch until here + + // We want something of the form: -d ">=YYYY-MM-dd" + final String dateRange = ">=" + outputDate.format(safeStart); + + // Supply '-d' as a separate argument - Bug# 14397 + addCommandArgument("-d"); + addCommandArgument(dateRange); + } + + // Check if list of files to check has been specified + if (!m_filesets.isEmpty()) { + for (String file : m_filesets) { + addCommandArgument(file); + } + } + + final ChangeLogParser parser = new ChangeLogParser(this); + final RedirectingStreamHandler handler = + new RedirectingStreamHandler(parser); + + log(getCommand(), Project.MSG_VERBOSE); + + setDest(m_dir); + setExecuteStreamHandler(handler); + try { + super.execute(); + } finally { + final String errors = handler.getErrors(); + + if (null != errors && errors.length()!=0) { + log(errors, Project.MSG_ERR); + } + } + + final CVSEntry[] entrySet = parser.getEntrySetAsArray(); + final CVSEntry[] filteredEntrySet = filterEntrySet(entrySet); + + replaceAuthorIdWithName(userList, filteredEntrySet); + + writeChangeLog(filteredEntrySet); + + } finally { + m_dir = savedDir; + } + } + + /** + * Validate the parameters specified for task. + * + * @throws BuildException if fails validation checks + */ + private void validate() + throws BuildException { + if (null == m_dir) { + m_dir = getProject().getBaseDir(); + } + if (null == m_destfile) { + final String message = "Destfile must be set."; + + throw new BuildException(message); + } + if (!m_dir.exists()) { + final String message = "Cannot find base dir " + + m_dir.getAbsolutePath(); + + throw new BuildException(message); + } + if (null != m_usersFile && !m_usersFile.exists()) { + final String message = "Cannot find user lookup list " + + m_usersFile.getAbsolutePath(); + + throw new BuildException(message); + } + } + + /** + * Load the userlist from the userList file (if specified) and add to + * list of users. + * + * @param userList the file of users + * @throws BuildException if file can not be loaded for some reason + */ + private void loadUserlist(final Properties userList) + throws BuildException { + if (null != m_usersFile) { + try { + userList.load(new FileInputStream(m_usersFile)); + } catch (final IOException ioe) { + throw new BuildException(ioe.toString(), ioe); + } + } + } + + /** + * Filter the specified entries according to an appropriate rule. + * + * @param entrySet the entry set to filter + * @return the filtered entry set + */ + private CVSEntry[] filterEntrySet(final CVSEntry[] entrySet) { + final Vector results = new Vector(); + + for (int i = 0; i < entrySet.length; i++) { + final CVSEntry cvsEntry = entrySet[i]; + final Date date = cvsEntry.getDate(); + + if(date==null) + // skip dates that didn't parse. + continue; + + if (null != m_start && m_start.after(date)) { + //Skip dates that are too early + continue; + } + if (null != m_stop && m_stop.before(date)) { + //Skip dates that are too late + continue; + } + if (!cvsEntry.containsBranch(branch)) + // didn't match the branch + continue; + results.addElement(cvsEntry); + } + + final CVSEntry[] resultArray = new CVSEntry[results.size()]; + + results.copyInto(resultArray); + return resultArray; + } + + /** + * replace all known author's id's with their maven specified names + */ + private void replaceAuthorIdWithName(final Properties userList, + final CVSEntry[] entrySet) { + for (int i = 0; i < entrySet.length; i++) { + + final CVSEntry entry = entrySet[ i ]; + if (userList.containsKey(entry.getAuthor())) { + entry.setAuthor(userList.getProperty(entry.getAuthor())); + } + } + } + + /** + * Print changelog to file specified in task. + * + * @param entrySet the entry set to write. + * @throws BuildException if there is an error writing changelog. + */ + private void writeChangeLog(final CVSEntry[] entrySet) + throws BuildException { + FileOutputStream output = null; + + try { + output = new FileOutputStream(m_destfile); + + final PrintWriter writer = + new PrintWriter(new OutputStreamWriter(output, "UTF-8")); + + final ChangeLogWriter serializer = new ChangeLogWriter(); + + serializer.printChangeLog(writer, entrySet); + } catch (final UnsupportedEncodingException uee) { + getProject().log(uee.toString(), Project.MSG_ERR); + } catch (final IOException ioe) { + throw new BuildException(ioe.toString(), ioe); + } finally { + if (null != output) { + try { + output.close(); + } catch (final IOException ioe) { + } + } + } + } +} + diff --git a/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogWriter.java b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogWriter.java new file mode 100644 index 0000000000..5438943801 --- /dev/null +++ b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogWriter.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package hudson.org.apache.tools.ant.taskdefs.cvslib; + +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Enumeration; +import java.util.TimeZone; + +/** + * Class used to generate an XML changelog. + * + * @version $Revision$ $Date$ + */ +class ChangeLogWriter { + /** output format for dates written to xml file */ + private static final SimpleDateFormat c_outputDate + = new SimpleDateFormat("yyyy-MM-dd"); + /** output format for times written to xml file */ + private static final SimpleDateFormat c_outputTime + = new SimpleDateFormat("HH:mm"); + + static { + TimeZone utc = TimeZone.getTimeZone("UTC"); + c_outputDate.setTimeZone(utc); + c_outputTime.setTimeZone(utc); + } + + /** + * Print out the specified entries. + * + * @param output writer to which to send output. + * @param entries the entries to be written. + */ + public void printChangeLog(final PrintWriter output, + final CVSEntry[] entries) { + output.println(""); + output.println(""); + for (int i = 0; i < entries.length; i++) { + final CVSEntry entry = entries[i]; + + printEntry(output, entry); + } + output.println(""); + output.flush(); + output.close(); + } + + + /** + * Print out an individual entry in changelog. + * + * @param entry the entry to print + * @param output writer to which to send output. + */ + private void printEntry(final PrintWriter output, final CVSEntry entry) { + output.println("\t"); + output.println("\t\t" + c_outputDate.format(entry.getDate()) + + ""); + output.println("\t\t"); + output.println("\t\t"); + + final Enumeration enumeration = entry.getFiles().elements(); + + while (enumeration.hasMoreElements()) { + final RCSFile file = (RCSFile) enumeration.nextElement(); + + output.println("\t\t"); + output.println("\t\t\t" + file.getName() + ""); + output.println("\t\t\t" + file.getRevision() + + ""); + + final String previousRevision = file.getPreviousRevision(); + + if (previousRevision != null) { + output.println("\t\t\t" + previousRevision + + ""); + } + + if(file.isDead()) + output.println("\t\t\t"); + + output.println("\t\t"); + } + output.println("\t\t"); + output.println("\t"); + } +} + diff --git a/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/CvsUser.java b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/CvsUser.java new file mode 100644 index 0000000000..3502c67075 --- /dev/null +++ b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/CvsUser.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package hudson.org.apache.tools.ant.taskdefs.cvslib; + +import org.apache.tools.ant.BuildException; + +/** + * Represents a CVS user with a userID and a full name. + * + * @version $Revision$ $Date$ + */ +public class CvsUser { + /** The user's Id */ + private String m_userID; + /** The user's full name */ + private String m_displayName; + + + /** + * Set the user's fullname + * + * @param displayName the user's full name + */ + public void setDisplayname(final String displayName) { + m_displayName = displayName; + } + + + /** + * Set the user's id + * + * @param userID the user's new id value. + */ + public void setUserid(final String userID) { + m_userID = userID; + } + + + /** + * Get the user's id. + * + * @return The userID value + */ + String getUserID() { + return m_userID; + } + + + /** + * Get the user's full name + * + * @return the user's full name + */ + String getDisplayname() { + return m_displayName; + } + + + /** + * validate that this object is configured. + * + * @exception BuildException if the instance has not be correctly + * configured. + */ + void validate() throws BuildException { + if (null == m_userID) { + final String message = "Username attribute must be set."; + + throw new BuildException(message); + } + if (null == m_displayName) { + final String message = + "Displayname attribute must be set for userID " + m_userID; + + throw new BuildException(message); + } + } +} + diff --git a/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/RCSFile.java b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/RCSFile.java new file mode 100644 index 0000000000..bd994c5eed --- /dev/null +++ b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/RCSFile.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package hudson.org.apache.tools.ant.taskdefs.cvslib; + +/** + * Represents a RCS File change. + * + * @version $Revision$ $Date$ + */ +class RCSFile { + private String m_name; + private String m_revision; + private String m_previousRevision; + private boolean m_dead; + private String m_branch; + + + RCSFile(final String name, + final String revision, + final String previousRevision, + final String branch, + final boolean dead) { + m_name = name; + m_revision = revision; + if (!revision.equals(previousRevision)) { + m_previousRevision = previousRevision; + } + m_branch = branch; + m_dead = dead; + } + + + String getName() { + return m_name; + } + + + String getRevision() { + return m_revision; + } + + String getPreviousRevision() { + return m_previousRevision; + } + + boolean isDead() { + return m_dead; + } + + /** + * Gets the name of this branch, if available. + */ + String getBranch() { + return m_branch; + } +} + diff --git a/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/RedirectingOutputStream.java b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/RedirectingOutputStream.java new file mode 100644 index 0000000000..b3a8adf80c --- /dev/null +++ b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/RedirectingOutputStream.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002,2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package hudson.org.apache.tools.ant.taskdefs.cvslib; + +import org.apache.tools.ant.taskdefs.LogOutputStream; + +/** + * A dummy stream that just passes stuff to the parser. + * + * @version $Revision$ $Date$ + */ +class RedirectingOutputStream + extends LogOutputStream { + private final ChangeLogParser parser; + + + /** + * Creates a new instance of this class. + * + * @param parser the parser to which output is sent. + */ + public RedirectingOutputStream(final ChangeLogParser parser) { + super(null, 0); + this.parser = parser; + } + + + /** + * Logs a line to the log system of ant. + * + * @param line the line to log. + */ + protected void processLine(final String line) { + parser.stdout(line); + } +} + diff --git a/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/RedirectingStreamHandler.java b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/RedirectingStreamHandler.java new file mode 100644 index 0000000000..8411d639d6 --- /dev/null +++ b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/RedirectingStreamHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002,2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package hudson.org.apache.tools.ant.taskdefs.cvslib; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.taskdefs.PumpStreamHandler; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * A dummy stream handler that just passes stuff to the parser. + * + * @version $Revision$ $Date$ + */ +class RedirectingStreamHandler + extends PumpStreamHandler { + RedirectingStreamHandler(final ChangeLogParser parser) { + super(new RedirectingOutputStream(parser), + new ByteArrayOutputStream()); + } + + + String getErrors() { + try { + final ByteArrayOutputStream error + = (ByteArrayOutputStream) getErr(); + + return error.toString("ASCII"); + } catch (final Exception e) { + return null; + } + } + + + public void stop() { + super.stop(); + try { + getErr().close(); + getOut().close(); + } catch (final IOException e) { + // plain impossible + throw new BuildException(e); + } + } +} + diff --git a/core/src/main/java/hudson/scheduler/BaseParser.java b/core/src/main/java/hudson/scheduler/BaseParser.java new file mode 100644 index 0000000000..7ca1578b10 --- /dev/null +++ b/core/src/main/java/hudson/scheduler/BaseParser.java @@ -0,0 +1,70 @@ +package hudson.scheduler; + +import antlr.ANTLRException; +import antlr.LLkParser; +import antlr.ParserSharedInputState; +import antlr.SemanticException; +import antlr.Token; +import antlr.TokenBuffer; +import antlr.TokenStream; +import antlr.TokenStreamException; + +/** + * @author Kohsuke Kawaguchi + */ +abstract class BaseParser extends LLkParser { + private static final int[] LOWER_BOUNDS = new int[] {0,0,1,0,0}; + private static final int[] UPPER_BOUNDS = new int[] {59,23,31,12,7}; + + protected BaseParser(int i) { + super(i); + } + + protected BaseParser(ParserSharedInputState parserSharedInputState, int i) { + super(parserSharedInputState, i); + } + + protected BaseParser(TokenBuffer tokenBuffer, int i) { + super(tokenBuffer, i); + } + + protected BaseParser(TokenStream tokenStream, int i) { + super(tokenStream, i); + } + + protected long doRange(int start, int end, int step, int field) throws ANTLRException { + rangeCheck(start, field); + rangeCheck(end, field); + if (step <= 0) + error("step must be positive, but found " + step); + if (start>end) + error("You mean "+end+'-'+start+'?'); + + long bits = 0; + for (int i = start; i <= end; i += step) { + bits |= 1L << i; + } + return bits; + } + + protected long doRange( int step, int field ) throws ANTLRException { + return doRange( LOWER_BOUNDS[field], UPPER_BOUNDS[field], step, field ); + } + + protected void rangeCheck(int value, int field) throws ANTLRException { + if( value true scheduled + */ + final long[] bits = new long[4]; + + int dayOfWeek; + + public CronTab(String format) throws ANTLRException { + this(format,1); + } + + public CronTab(String format, int line) throws ANTLRException { + CrontabLexer lexer = new CrontabLexer(new StringReader(format)); + lexer.setLine(line); + CrontabParser parser = new CrontabParser(lexer); + + parser.startRule(this); + if((dayOfWeek&(1<<7))!=0) + dayOfWeek |= 1; // copy bit 7 over to bit 0 + } + + + /** + * Returns true if the given calendar matches + */ + boolean check(Calendar cal) { + if(!checkBits(bits[0],cal.get(Calendar.MINUTE))) + return false; + if(!checkBits(bits[1],cal.get(Calendar.HOUR_OF_DAY))) + return false; + if(!checkBits(bits[2],cal.get(Calendar.DAY_OF_MONTH))) + return false; + if(!checkBits(bits[3],cal.get(Calendar.MONTH)+1)) + return false; + if(!checkBits(dayOfWeek,cal.get(Calendar.DAY_OF_WEEK)-1)) + return false; + + return true; + } + + /** + * Returns true if n-th bit is on. + */ + private boolean checkBits(long bitMask, int n) { + return (bitMask|(1L< + * I couldn't call this class "CVS" because that would cause the view folder name + * to collide with CVS control files. + * + * @author Kohsuke Kawaguchi + */ +public class CVSSCM extends AbstractCVSFamilySCM { + /** + * CVSSCM connection string. + */ + private String cvsroot; + + /** + * Module names. + * + * This could be a whitespace-separate list of multiple modules. + */ + private String module; + + /** + * Branch to build. Null to indicate the trunk. + */ + private String branch; + + private String cvsRsh; + + private boolean canUseUpdate; + + /** + * True to avoid creating a sub-directory inside the workspace. + * (Works only when there's just one module.) + */ + private boolean flatten; + + + public CVSSCM(String cvsroot, String module,String branch,String cvsRsh,boolean canUseUpdate, boolean flatten) { + this.cvsroot = cvsroot; + this.module = module.trim(); + this.branch = nullify(branch); + this.cvsRsh = nullify(cvsRsh); + this.canUseUpdate = canUseUpdate; + this.flatten = flatten && module.indexOf(' ')==-1; + } + + public String getCvsRoot() { + return cvsroot; + } + + /** + * If there are multiple modules, return the module directory of the first one. + * @param workspace + */ + public FilePath getModuleRoot(FilePath workspace) { + if(flatten) + return workspace; + + int idx = module.indexOf(' '); + if(idx>=0) return workspace.child(module.substring(0,idx)); + else return workspace.child(module); + } + + public ChangeLogParser createChangeLogParser() { + return new CVSChangeLogParser(); + } + + public String getAllModules() { + return module; + } + + public String getBranch() { + return branch; + } + + public String getCvsRsh() { + return cvsRsh; + } + + public boolean getCanUseUpdate() { + return canUseUpdate; + } + + public boolean isFlatten() { + return flatten; + } + + public boolean pollChanges(Project project, Launcher launcher, FilePath dir, TaskListener listener) throws IOException { + List changedFiles = update(true, launcher, dir, listener); + + return changedFiles!=null && !changedFiles.isEmpty(); + } + + public boolean checkout(Build build, Launcher launcher, FilePath dir, BuildListener listener, File changelogFile) throws IOException { + List changedFiles = null; // files that were affected by update. null this is a check out + + if(canUseUpdate && isUpdatable(dir.getLocal())) { + changedFiles = update(false,launcher,dir,listener); + if(changedFiles==null) + return false; // failed + } else { + dir.deleteContents(); + + ArgumentListBuilder cmd = new ArgumentListBuilder(); + cmd.add("cvs","-Q","-z9","-d",cvsroot,"co"); + if(branch!=null) + cmd.add("-r",branch); + if(flatten) + cmd.add("-d",dir.getName()); + cmd.addTokenized(module); + + if(!run(launcher,cmd,listener, flatten ? dir.getParent() : dir)) + return false; + } + + // archive the workspace to support later tagging + // TODO: doing this partially remotely would be faster + File archiveFile = getArchiveFile(build); + ZipOutputStream zos = new ZipOutputStream(archiveFile); + if(flatten) { + archive(build.getProject().getWorkspace().getLocal(), module, zos); + } else { + StringTokenizer tokens = new StringTokenizer(module); + while(tokens.hasMoreTokens()) { + String m = tokens.nextToken(); + archive(new File(build.getProject().getWorkspace().getLocal(),m),m,zos); + } + } + zos.close(); + + // contribute the tag action + build.getActions().add(new TagAction(build)); + + return calcChangeLog(build, changedFiles, changelogFile, listener); + } + + /** + * Returns the file name used to archive the build. + */ + private static File getArchiveFile(Build build) { + return new File(build.getRootDir(),"workspace.zip"); + } + + private void archive(File dir,String relPath,ZipOutputStream zos) throws IOException { + Set knownFiles = new HashSet(); + // see http://www.monkey.org/openbsd/archive/misc/9607/msg00056.html for what Entries.Log is for + parseCVSEntries(new File(dir,"CVS/Entries"),knownFiles); + parseCVSEntries(new File(dir,"CVS/Entries.Log"),knownFiles); + parseCVSEntries(new File(dir,"CVS/Entries.Extra"),knownFiles); + boolean hasCVSdirs = !knownFiles.isEmpty(); + knownFiles.add("CVS"); + + File[] files = dir.listFiles(); + if(files==null) + throw new IOException("No such directory exists. Did you specify the correct branch?: "+dir); + + for( File f : files ) { + String name = relPath+'/'+f.getName(); + if(f.isDirectory()) { + if(hasCVSdirs && !knownFiles.contains(f.getName())) { + // not controlled in CVS. Skip. + // but also make sure that we archive CVS/*, which doesn't have CVS/CVS + continue; + } + archive(f,name,zos); + } else { + if(!dir.getName().equals("CVS")) + // we only need to archive CVS control files, not the actual workspace files + continue; + zos.putNextEntry(new ZipEntry(name)); + FileInputStream fis = new FileInputStream(f); + Util.copyStream(fis,zos); + fis.close(); + zos.closeEntry(); + } + } + } + + /** + * Parses the CVS/Entries file and adds file/directory names to the list. + */ + private void parseCVSEntries(File entries, Set knownFiles) throws IOException { + if(!entries.exists()) + return; + + BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(entries))); + String line; + while((line=in.readLine())!=null) { + String[] tokens = line.split("/+"); + if(tokens==null || tokens.length<2) continue; // invalid format + knownFiles.add(tokens[1]); + } + in.close(); + } + + /** + * Updates the workspace as well as locate changes. + * + * @return + * List of affected file names, relative to the workspace directory. + * Null if the operation failed. + */ + public List update(boolean dryRun, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException { + + List changedFileNames = new ArrayList(); // file names relative to the workspace + + ArgumentListBuilder cmd = new ArgumentListBuilder(); + cmd.add("cvs","-q","-z9"); + if(dryRun) + cmd.add("-n"); + cmd.add("update","-PdC"); + + if(flatten) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + if(!run(launcher,cmd,listener,workspace, + new ForkOutputStream(baos,listener.getLogger()))) + return null; + + parseUpdateOutput("",baos, changedFileNames); + } else { + StringTokenizer tokens = new StringTokenizer(module); + while(tokens.hasMoreTokens()) { + String moduleName = tokens.nextToken(); + + // capture the output during update + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + if(!run(launcher,cmd,listener, + new FilePath(workspace, moduleName), + new ForkOutputStream(baos,listener.getLogger()))) + return null; + + // we'll run one "cvs log" command with workspace as the base, + // so use path names that are relative to moduleName. + parseUpdateOutput(moduleName+'/',baos, changedFileNames); + } + } + + return changedFileNames; + } + + // see http://www.network-theory.co.uk/docs/cvsmanual/cvs_153.html for the output format. + // we don't care '?' because that's not in the repository + private static final Pattern UPDATE_LINE = Pattern.compile("[UPARMC] (.+)"); + + private static final Pattern REMOVAL_LINE = Pattern.compile("cvs (server|update): (.+) is no longer in the repository"); + private static final Pattern NEWDIRECTORY_LINE = Pattern.compile("cvs server: New directory `(.+)' -- ignored"); + + /** + * Parses the output from CVS update and list up files that might have been changed. + * + * @param result + * list of file names whose changelog should be checked. This may include files + * that are no longer present. The path names are relative to the workspace, + * hence "String", not {@link File}. + */ + private void parseUpdateOutput(String baseName, ByteArrayOutputStream output, List result) throws IOException { + BufferedReader in = new BufferedReader(new InputStreamReader( + new ByteArrayInputStream(output.toByteArray()))); + String line; + while((line=in.readLine())!=null) { + Matcher matcher = UPDATE_LINE.matcher(line); + if(matcher.matches()) { + result.add(baseName+matcher.group(1)); + continue; + } + + matcher= REMOVAL_LINE.matcher(line); + if(matcher.matches()) { + result.add(baseName+matcher.group(2)); + continue; + } + + // this line is added in an attempt to capture newly created directories in the repository, + // but it turns out that this line always hit if the workspace is missing a directory + // that the server has, even if that directory contains nothing in it + //matcher= NEWDIRECTORY_LINE.matcher(line); + //if(matcher.matches()) { + // result.add(baseName+matcher.group(1)); + //} + } + } + + /** + * Returns true if we can use "cvs update" instead of "cvs checkout" + */ + private boolean isUpdatable(File dir) { + if(flatten) { + return isUpdatableModule(dir); + } else { + StringTokenizer tokens = new StringTokenizer(module); + while(tokens.hasMoreTokens()) { + File module = new File(dir,tokens.nextToken()); + if(!isUpdatableModule(module)) + return false; + } + return true; + } + } + + private boolean isUpdatableModule(File module) { + File cvs = new File(module,"CVS"); + if(!cvs.exists()) + return false; + + // check cvsroot + if(!checkContents(new File(cvs,"Root"),cvsroot)) + return false; + if(branch!=null) { + if(!checkContents(new File(cvs,"Tag"),'T'+branch)) + return false; + } else { + if(new File(cvs,"Tag").exists()) + return false; + } + + return true; + } + + /** + * Returns true if the contents of the file is equal to the given string. + * + * @return false in all the other cases. + */ + private boolean checkContents(File file, String contents) { + try { + Reader r = new FileReader(file); + try { + String s = new BufferedReader(r).readLine(); + if (s == null) return false; + return s.trim().equals(contents.trim()); + } finally { + r.close(); + } + } catch (IOException e) { + return false; + } + } + + /** + * Computes the changelog into an XML file. + * + *

+ * When we update the workspace, we'll compute the changelog by using its output to + * make it faster. In general case, we'll fall back to the slower approach where + * we check all files in the workspace. + * + * @param changedFiles + * Files whose changelog should be checked for updates. + * This is provided if the previous operation is update, otherwise null, + * which means we have to fall back to the default slow computation. + */ + private boolean calcChangeLog(Build build, List changedFiles, File changelogFile, final BuildListener listener) { + if(build.getPreviousBuild()==null || (changedFiles!=null && changedFiles.isEmpty())) { + // nothing to compare against, or no changes + // (note that changedFiles==null means fallback, so we have to run cvs log. + listener.getLogger().println("$ no changes detected"); + return createEmptyChangeLog(changelogFile,listener, "changelog"); + } + + listener.getLogger().println("$ computing changelog"); + + final StringWriter errorOutput = new StringWriter(); + final boolean[] hadError = new boolean[1]; + + ChangeLogTask task = new ChangeLogTask() { + public void log(String msg, int msgLevel) { + // send error to listener. This seems like the route in which the changelog task + // sends output + if(msgLevel==org.apache.tools.ant.Project.MSG_ERR) { + hadError[0] = true; + errorOutput.write(msg); + errorOutput.write('\n'); + } + } + }; + task.setProject(new org.apache.tools.ant.Project()); + File baseDir = build.getProject().getWorkspace().getLocal(); + task.setDir(baseDir); + if(DESCRIPTOR.getCvspassFile().length()!=0) + task.setPassfile(new File(DESCRIPTOR.getCvspassFile())); + task.setCvsRoot(cvsroot); + task.setCvsRsh(cvsRsh); + task.setFailOnError(true); + task.setDestfile(changelogFile); + task.setBranch(branch); + task.setStart(build.getPreviousBuild().getTimestamp().getTime()); + task.setEnd(build.getTimestamp().getTime()); + if(changedFiles!=null) { + // if the directory doesn't exist, cvs changelog will die, so filter them out. + // this means we'll lose the log of those changes + for (String filePath : changedFiles) { + if(new File(baseDir,filePath).getParentFile().exists()) + task.addFile(filePath); + } + } else { + // fallback + if(!flatten) + task.setPackage(module); + } + + try { + task.execute(); + if(hadError[0]) { + // non-fatal error must have occurred, such as cvs changelog parsing error.s + listener.getLogger().print(errorOutput); + } + return true; + } catch( BuildException e ) { + // capture output from the task for diagnosis + listener.getLogger().print(errorOutput); + // then report an error + PrintWriter w = listener.error(e.getMessage()); + w.println("Working directory is "+baseDir); + e.printStackTrace(w); + return false; + } catch( RuntimeException e ) { + // an user reported a NPE inside the changeLog task. + // we don't want a bug in Ant to prevent a build. + e.printStackTrace(listener.error(e.getMessage())); + return true; // so record the message but continue + } + } + + public DescriptorImpl getDescriptor() { + return DESCRIPTOR; + } + + public void buildEnvVars(Map env) { + if(cvsRsh!=null) + env.put("CVS_RSH",cvsRsh); + String cvspass = DESCRIPTOR.getCvspassFile(); + if(cvspass.length()!=0) + env.put("CVS_PASSFILE",cvspass); + } + + static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); + + public static final class DescriptorImpl extends Descriptor implements ModelObject { + DescriptorImpl() { + super(CVSSCM.class); + } + + public String getDisplayName() { + return "CVS"; + } + + public SCM newInstance(StaplerRequest req) { + return new CVSSCM( + req.getParameter("cvs_root"), + req.getParameter("cvs_module"), + req.getParameter("cvs_branch"), + req.getParameter("cvs_rsh"), + req.getParameter("cvs_use_update")!=null, + req.getParameter("cvs_legacy")==null + ); + } + + public String getCvspassFile() { + String value = (String)getProperties().get("cvspass"); + if(value==null) + value = ""; + return value; + } + + public void setCvspassFile(String value) { + getProperties().put("cvspass",value); + save(); + } + + /** + * Gets the URL that shows the diff. + */ + public String getDiffURL(String cvsRoot, String pathName, String oldRev, String newRev) { + String url = getProperties().get("repository-browser.diff." + cvsRoot).toString(); + if(url==null) return null; + return url.replaceAll("%%P",pathName).replace("%%r",oldRev).replace("%%R",newRev); + + } + + public boolean configure( HttpServletRequest req ) { + setCvspassFile(req.getParameter("cvs_cvspass")); + + Map properties = getProperties(); + + int i=0; + while(true) { + String root = req.getParameter("cvs_repobrowser_cvsroot" + i); + if(root==null) break; + + setBrowser(req.getParameter("cvs_repobrowser"+i), properties, root, "repository-browser."); + setBrowser(req.getParameter("cvs_repobrowser_diff"+i), properties, root, "repository-browser.diff."); + i++; + } + + save(); + + return true; + } + + private void setBrowser(String key, Map properties, String root, String prefi) { + String value = Util.nullify(key); + if(value==null) { + properties.remove(prefi +root); + } else { + properties.put(prefi +root,value); + } + } + + public Map getProperties() { + return super.getProperties(); + } + + // + // web methods + // + + public void doCvsPassCheck(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + // this method can be used to check if a file exists anywhere in the file system, + // so it should be protected. + new FormFieldValidator(req,rsp,true) { + protected void check() throws IOException, ServletException { + String v = fixEmpty(request.getParameter("value")); + if(v==null) { + // default. + ok(); + } else { + File cvsPassFile = new File(v); + + if(cvsPassFile.exists()) { + ok(); + } else { + error("No such file exists"); + } + } + } + }.process(); + } + + /** + * Displays "cvs --version" for trouble shooting. + */ + public void doVersion(StaplerRequest req, StaplerResponse rsp) throws IOException { + rsp.setContentType("text/plain"); + Proc proc = Hudson.getInstance().createLauncher(TaskListener.NULL).launch( + new String[]{"cvs", "--version"}, new String[0], rsp.getOutputStream(), FilePath.RANDOM); + proc.join(); + } + + /** + * Checks the entry to the CVSROOT field. + *

+ * Also checks if .cvspass file contains the entry for this. + */ + public void doCvsrootCheck(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + new FormFieldValidator(req,rsp,false) { + protected void check() throws IOException, ServletException { + String v = fixEmpty(request.getParameter("value")); + if(v==null) { + error("CVSROOT is mandatory"); + return; + } + + // CVSROOT format isn't really that well defined. So it's hard to check this rigorously. + if(v.startsWith(":pserver") || v.startsWith(":ext")) { + if(!CVSROOT_PSERVER_PATTERN.matcher(v).matches()) { + error("Invalid CVSROOT string"); + return; + } + // I can't really test if the machine name exists, either. + // some cvs, such as SOCKS-enabled cvs can resolve host names that Hudson might not + // be able to. If :ext is used, all bets are off anyway. + } + + // check .cvspass file to see if it has entry. + // CVS handles authentication only if it's pserver. + if(v.startsWith(":pserver")) { + String cvspass = getCvspassFile(); + File passfile; + if(cvspass.equals("")) { + passfile = new File(new File(System.getProperty("user.home")),".cvspass"); + } else { + passfile = new File(cvspass); + } + + if(passfile.exists()) { + // It's possible that we failed to locate the correct .cvspass file location, + // so don't report an error if we couldn't locate this file. + // + // if this is explicitly specified, then our system config page should have + // reported an error. + if(!scanCvsPassFile(passfile, v)) { + error("It doesn't look like this CVSROOT has its password set." + + " Would you like to set it now?"); + return; + } + } + } + + // all tests passed so far + ok(); + } + }.process(); + } + + /** + * Checks if the given pserver CVSROOT value exists in the pass file. + */ + private boolean scanCvsPassFile(File passfile, String cvsroot) throws IOException { + cvsroot += ' '; + String cvsroot2 = "/1 "+cvsroot; // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5006835 + BufferedReader in = new BufferedReader(new FileReader(passfile)); + try { + String line; + while((line=in.readLine())!=null) { + // "/1 " version always have the port number in it, so examine a much with + // default port 2401 left out + int portIndex = line.indexOf(":2401/"); + String line2 = ""; + if(portIndex>=0) + line2 = line.substring(0,portIndex+1)+line.substring(portIndex+5); // leave '/' + + if(line.startsWith(cvsroot) || line.startsWith(cvsroot2) || line2.startsWith(cvsroot2)) + return true; + } + return false; + } finally { + in.close(); + } + } + + private static final Pattern CVSROOT_PSERVER_PATTERN = + Pattern.compile(":(ext|pserver):[^@:]+@[^:]+:(\\d+:)?.+"); + + /** + * Runs cvs login command. + * + * TODO: this apparently doesn't work. Probably related to the fact that + * cvs does some tty magic to disable ecoback or whatever. + */ + public void doPostPassword(StaplerRequest req, StaplerResponse rsp) throws IOException { + if(!Hudson.adminCheck(req,rsp)) + return; + + String cvsroot = req.getParameter("cvsroot"); + String password = req.getParameter("password"); + + if(cvsroot==null || password==null) { + rsp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + rsp.setContentType("text/plain"); + Proc proc = Hudson.getInstance().createLauncher(TaskListener.NULL).launch( + new String[]{"cvs", "-d",cvsroot,"login"}, new String[0], + new ByteArrayInputStream((password+"\n").getBytes()), + rsp.getOutputStream()); + proc.join(); + } + } + + /** + * Action for a build that performs the tagging. + */ + public final class TagAction implements Action { + private final Build build; + + /** + * If non-null, that means the build is already tagged. + */ + private String tagName; + + /** + * If non-null, that means the tagging is in progress + * (asynchronously.) + */ + private transient TagWorkerThread workerThread; + + public TagAction(Build build) { + this.build = build; + } + + public String getIconFileName() { + return "save.gif"; + } + + public String getDisplayName() { + return "Tag this build"; + } + + public String getUrlName() { + return "tagBuild"; + } + + public String getTagName() { + return tagName; + } + + public TagWorkerThread getWorkerThread() { + return workerThread; + } + + public Build getBuild() { + return build; + } + + public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + req.setAttribute("build",build); + req.getView(this,chooseAction()).forward(req,rsp); + } + + private synchronized String chooseAction() { + if(tagName!=null) + return "alreadyTagged.jelly"; + if(workerThread!=null) + return "inProgress.jelly"; + return "tagForm.jelly"; + } + + /** + * Invoked to actually tag the workspace. + */ + public synchronized void doSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + String name = req.getParameter("name"); + if(name==null || name.length()==0) { + // invalid tag name + doIndex(req,rsp); + return; + } + + if(workerThread==null) { + workerThread = new TagWorkerThread(name); + workerThread.start(); + } + + doIndex(req,rsp); + } + + /** + * Clears the error status. + */ + public synchronized void doClearError(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + if(workerThread!=null && !workerThread.isAlive()) + workerThread = null; + doIndex(req,rsp); + } + + public final class TagWorkerThread extends Thread { + private final String tagName; + // StringWriter is synchronized + private final StringWriter log = new StringWriter(); + + public TagWorkerThread(String tagName) { + this.tagName = tagName; + } + + public String getLog() { + // this method can be invoked from another thread. + return log.toString(); + } + + public String getTagName() { + return tagName; + } + + public void run() { + BuildListener listener = new StreamBuildListener(log); + + Result result = Result.FAILURE; + File destdir = null; + listener.started(); + try { + destdir = Util.createTempDir(); + + // unzip the archive + listener.getLogger().println("expanding the workspace archive into "+destdir); + Expand e = new Expand(); + e.setProject(new org.apache.tools.ant.Project()); + e.setDest(destdir); + e.setSrc(getArchiveFile(build)); + e.setTaskType("unzip"); + e.execute(); + + // run cvs tag command + listener.getLogger().println("tagging the workspace"); + StringTokenizer tokens = new StringTokenizer(CVSSCM.this.module); + while(tokens.hasMoreTokens()) { + String m = tokens.nextToken(); + ArgumentListBuilder cmd = new ArgumentListBuilder(); + cmd.add("cvs","tag","-R",tagName); + if(!CVSSCM.this.run(new Launcher(listener),cmd,listener,new FilePath(destdir).child(m))) { + listener.getLogger().println("tagging failed"); + return; + } + } + + // completed successfully + synchronized(TagAction.this) { + TagAction.this.tagName = this.tagName; + TagAction.this.workerThread = null; + } + build.save(); + + } catch (Throwable e) { + e.printStackTrace(listener.fatalError(e.getMessage())); + } finally { + try { + if(destdir!=null) { + listener.getLogger().println("cleaning up "+destdir); + Util.deleteRecursive(destdir); + } + } catch (IOException e) { + e.printStackTrace(listener.fatalError(e.getMessage())); + } + listener.finished(result); + } + } + } + } +} diff --git a/core/src/main/java/hudson/scm/ChangeLogParser.java b/core/src/main/java/hudson/scm/ChangeLogParser.java new file mode 100644 index 0000000000..09980cd986 --- /dev/null +++ b/core/src/main/java/hudson/scm/ChangeLogParser.java @@ -0,0 +1,20 @@ +package hudson.scm; + +import hudson.model.Build; +import hudson.scm.ChangeLogSet.Entry; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; + +/** + * Encapsulates the file format of the changelog. + * + * Instances should be stateless, but + * persisted as a part of {@link Build}. + * + * @author Kohsuke Kawaguchi + */ +public abstract class ChangeLogParser { + public abstract ChangeLogSet parse(Build build, File changelogFile) throws IOException, SAXException; +} diff --git a/core/src/main/java/hudson/scm/ChangeLogSet.java b/core/src/main/java/hudson/scm/ChangeLogSet.java new file mode 100644 index 0000000000..5bc840a8b4 --- /dev/null +++ b/core/src/main/java/hudson/scm/ChangeLogSet.java @@ -0,0 +1,63 @@ +package hudson.scm; + +import hudson.model.User; + +import java.util.Collections; + +/** + * Represents SCM change list. + * + * Use the "index" view of this object to render the changeset detail page, + * and use the "digest" view of this object to render the summary page. + * + * @author Kohsuke Kawaguchi + */ +public abstract class ChangeLogSet implements Iterable { + /** + * Returns true if there's no change. + */ + public abstract boolean isEmptySet(); + + /** + * Constant instance that represents no changes. + */ + public static final ChangeLogSet EMPTY = new CVSChangeLogSet(Collections.EMPTY_LIST); + + public static abstract class Entry { + + public abstract String getMsg(); + + /** + * The user who made this change. + * + * @return + * never null. + */ + public abstract User getAuthor(); + + /** + * Message escaped for HTML + */ + public String getMsgEscaped() { + String msg = getMsg(); + StringBuffer buf = new StringBuffer(msg.length()+64); + for( int i=0; i"); + else + if(ch=='<') + buf.append("<"); + else + if(ch=='&') + buf.append("&"); + else + if(ch==' ') + buf.append(" "); + else + buf.append(ch); + } + return buf.toString(); + } + } +} diff --git a/core/src/main/java/hudson/scm/EditType.java b/core/src/main/java/hudson/scm/EditType.java new file mode 100644 index 0000000000..f0222df3bf --- /dev/null +++ b/core/src/main/java/hudson/scm/EditType.java @@ -0,0 +1,28 @@ +package hudson.scm; + +/** + * Designates the SCM operation. + * + * @author Kohsuke Kawaguchi + */ +public final class EditType { + private String name; + private String description; + + public EditType(String name, String description) { + this.name = name; + this.description = description; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public static final EditType ADD = new EditType("add","The file was added"); + public static final EditType EDIT = new EditType("edit","The file was modified"); + public static final EditType DELETE = new EditType("delete","The file was removed"); +} diff --git a/core/src/main/java/hudson/scm/NullChangeLogParser.java b/core/src/main/java/hudson/scm/NullChangeLogParser.java new file mode 100644 index 0000000000..8e600050a9 --- /dev/null +++ b/core/src/main/java/hudson/scm/NullChangeLogParser.java @@ -0,0 +1,17 @@ +package hudson.scm; + +import hudson.model.Build; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; + +/** + * {@link ChangeLogParser} for no SCM. + * @author Kohsuke Kawaguchi + */ +public class NullChangeLogParser extends ChangeLogParser { + public ChangeLogSet parse(Build build, File changelogFile) throws IOException, SAXException { + return ChangeLogSet.EMPTY; + } +} diff --git a/core/src/main/java/hudson/scm/NullSCM.java b/core/src/main/java/hudson/scm/NullSCM.java new file mode 100644 index 0000000000..633d30a97b --- /dev/null +++ b/core/src/main/java/hudson/scm/NullSCM.java @@ -0,0 +1,57 @@ +package hudson.scm; + +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.model.Project; +import hudson.model.TaskListener; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import org.kohsuke.stapler.StaplerRequest; + +/** + * No {@link SCM}. + * + * @author Kohsuke Kawaguchi + */ +public class NullSCM implements SCM { + public boolean pollChanges(Project project, Launcher launcher, FilePath dir, TaskListener listener) throws IOException { + // no change + return false; + } + + public boolean checkout(Build build, Launcher launcher, FilePath remoteDir, BuildListener listener, File changeLogFile) throws IOException { + return true; + } + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + public void buildEnvVars(Map env) { + // noop + } + + public FilePath getModuleRoot(FilePath workspace) { + return workspace; + } + + public ChangeLogParser createChangeLogParser() { + return new NullChangeLogParser(); + } + + static final Descriptor DESCRIPTOR = new Descriptor(NullSCM.class) { + public String getDisplayName() { + return "None"; + } + + public SCM newInstance(StaplerRequest req) { + return new NullSCM(); + } + }; +} diff --git a/core/src/main/java/hudson/scm/SCM.java b/core/src/main/java/hudson/scm/SCM.java new file mode 100644 index 0000000000..5fa284c005 --- /dev/null +++ b/core/src/main/java/hudson/scm/SCM.java @@ -0,0 +1,86 @@ +package hudson.scm; + +import hudson.FilePath; +import hudson.Launcher; +import hudson.ExtensionPoint; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Describable; +import hudson.model.Project; +import hudson.model.TaskListener; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +/** + * Captures the configuration information in it. + * + *

+ * To register a custom {@link SCM} implementation from a plugin, + * add it to {@link SCMS#SCMS}. + * + * @author Kohsuke Kawaguchi + */ +public interface SCM extends Describable, ExtensionPoint { + + /** + * Checks if there has been any changes to this module in the repository. + * + * TODO: we need to figure out a better way to communicate an error back, + * so that we won't keep retrying the same node (for example a slave might be down.) + * + * @param project + * The project to check for updates + * @param launcher + * Abstraction of the machine where the polling will take place. + * @param workspace + * The workspace directory that contains baseline files. + * @param listener + * Logs during the polling should be sent here. + * + * @return true + * if the change is detected. + */ + boolean pollChanges(Project project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException; + + /** + * Obtains a fresh workspace of the module(s) into the specified directory + * of the specified machine. + * + *

+ * The "update" operation can be performed instead of a fresh checkout if + * feasible. + * + *

+ * This operation should also capture the information necessary to tag the workspace later. + * + * @param launcher + * Abstracts away the machine that the files will be checked out. + * @param workspace + * a directory to check out the source code. May contain left-over + * from the previous build. + * @param changelogFile + * Upon a successful return, this file should capture the changelog. + * @return + * null if the operation fails. The error should be reported to the listener. + * Otherwise return the changes included in this update (if this was an update.) + */ + boolean checkout(Build build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException; + + /** + * Adds environmental variables for the builds to the given map. + */ + void buildEnvVars(Map env); + + /** + * Gets the top directory of the checked out module. + * @param workspace + */ + FilePath getModuleRoot(FilePath workspace); + + /** + * The returned object will be used to parse changelog.xml. + */ + ChangeLogParser createChangeLogParser(); +} diff --git a/core/src/main/java/hudson/scm/SCMS.java b/core/src/main/java/hudson/scm/SCMS.java new file mode 100644 index 0000000000..a0024ec052 --- /dev/null +++ b/core/src/main/java/hudson/scm/SCMS.java @@ -0,0 +1,16 @@ +package hudson.scm; + +import hudson.model.Descriptor; + +import java.util.List; + +/** + * @author Kohsuke Kawaguchi + */ +public class SCMS { + /** + * List of all installed SCMs. + */ + public static final List> SCMS = + Descriptor.toList(NullSCM.DESCRIPTOR,CVSSCM.DESCRIPTOR,SubversionSCM.DESCRIPTOR); +} diff --git a/core/src/main/java/hudson/scm/SubversionChangeLogParser.java b/core/src/main/java/hudson/scm/SubversionChangeLogParser.java new file mode 100644 index 0000000000..3fd3d8639c --- /dev/null +++ b/core/src/main/java/hudson/scm/SubversionChangeLogParser.java @@ -0,0 +1,43 @@ +package hudson.scm; + +import hudson.model.Build; +import hudson.scm.SubversionChangeLogSet.LogEntry; +import hudson.scm.SubversionChangeLogSet.Path; +import org.apache.commons.digester.Digester; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +/** + * {@link ChangeLogParser} for Subversion. + * + * @author Kohsuke Kawaguchi + */ +public class SubversionChangeLogParser extends ChangeLogParser { + public SubversionChangeLogSet parse(Build build, File changelogFile) throws IOException, SAXException { + // http://svn.collab.net/repos/svn/trunk/subversion/svn/schema/ + + Digester digester = new Digester(); + ArrayList r = new ArrayList(); + digester.push(r); + + digester.addObjectCreate("*/logentry", LogEntry.class); + digester.addSetProperties("*/logentry"); + digester.addBeanPropertySetter("*/logentry/author","user"); + digester.addBeanPropertySetter("*/logentry/date"); + digester.addBeanPropertySetter("*/logentry/msg"); + digester.addSetNext("*/logentry","add"); + + digester.addObjectCreate("*/logentry/paths/path", Path.class); + digester.addSetProperties("*/logentry/paths/path"); + digester.addBeanPropertySetter("*/logentry/paths/path","value"); + digester.addSetNext("*/logentry/paths/path","addPath"); + + digester.parse(changelogFile); + + return new SubversionChangeLogSet(build,r); + } + +} diff --git a/core/src/main/java/hudson/scm/SubversionChangeLogSet.java b/core/src/main/java/hudson/scm/SubversionChangeLogSet.java new file mode 100644 index 0000000000..3858d5775c --- /dev/null +++ b/core/src/main/java/hudson/scm/SubversionChangeLogSet.java @@ -0,0 +1,131 @@ +package hudson.scm; + +import hudson.model.Build; +import hudson.model.User; +import hudson.scm.SubversionChangeLogSet.LogEntry; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Iterator; + +/** + * {@link ChangeLogSet} for Subversion. + * + * @author Kohsuke Kawaguchi + */ +public final class SubversionChangeLogSet extends ChangeLogSet { + private final List logs; + private final Build build; + + /** + * @GuardedBy this + */ + private Map revisionMap; + + /*package*/ SubversionChangeLogSet(Build build, List logs) { + this.build = build; + this.logs = Collections.unmodifiableList(logs); + } + + public boolean isEmptySet() { + return logs.isEmpty(); + } + + public List getLogs() { + return logs; + } + + + public Iterator iterator() { + return logs.iterator(); + } + + public synchronized Map getRevisionMap() throws IOException { + if(revisionMap==null) + revisionMap = SubversionSCM.parseRevisionFile(build); + return revisionMap; + } + + /** + * One commit. + */ + public static class LogEntry extends ChangeLogSet.Entry { + private int revision; + private User author; + private String date; + private String msg; + private List paths = new ArrayList(); + + public int getRevision() { + return revision; + } + + public void setRevision(int revision) { + this.revision = revision; + } + + public User getAuthor() { + return author; + } + + public void setUser(String author) { + this.author = User.get(author); + } + + public String getUser() {// digester wants read/write property, even though it never reads. Duh. + return author.getDisplayName(); + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public void addPath( Path p ) { + paths.add(p); + } + + public List getPaths() { + return paths; + } + } + + public static class Path { + private char action; + private String value; + + public void setAction(String action) { + this.action = action.charAt(0); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public EditType getEditType() { + if( action=='A' ) + return EditType.ADD; + if( action=='D' ) + return EditType.DELETE; + return EditType.EDIT; + } + } +} diff --git a/core/src/main/java/hudson/scm/SubversionSCM.java b/core/src/main/java/hudson/scm/SubversionSCM.java new file mode 100644 index 0000000000..addb998b28 --- /dev/null +++ b/core/src/main/java/hudson/scm/SubversionSCM.java @@ -0,0 +1,532 @@ +package hudson.scm; + +import hudson.FilePath; +import hudson.Launcher; +import hudson.Proc; +import hudson.Util; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.model.Project; +import hudson.model.TaskListener; +import hudson.util.ArgumentListBuilder; +import hudson.util.FormFieldValidator; +import org.apache.commons.digester.Digester; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.xml.sax.SAXException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Subversion. + * + * Check http://svn.collab.net/repos/svn/trunk/subversion/svn/schema/ for + * various output formats. + * + * @author Kohsuke Kawaguchi + */ +public class SubversionSCM extends AbstractCVSFamilySCM { + private final String modules; + private boolean useUpdate; + private String username; + private String otherOptions; + + SubversionSCM( String modules, boolean useUpdate, String username, String otherOptions ) { + StringBuilder normalizedModules = new StringBuilder(); + StringTokenizer tokens = new StringTokenizer(modules); + while(tokens.hasMoreTokens()) { + if(normalizedModules.length()>0) normalizedModules.append(' '); + String m = tokens.nextToken(); + if(m.endsWith("/")) + // the normalized name is always without the trailing '/' + m = m.substring(0,m.length()-1); + normalizedModules.append(m); + } + + this.modules = normalizedModules.toString(); + this.useUpdate = useUpdate; + this.username = nullify(username); + this.otherOptions = nullify(otherOptions); + } + + public String getModules() { + return modules; + } + + public boolean isUseUpdate() { + return useUpdate; + } + + public String getUsername() { + return username; + } + + public String getOtherOptions() { + return otherOptions; + } + + private Collection getModuleDirNames() { + List dirs = new ArrayList(); + StringTokenizer tokens = new StringTokenizer(modules); + while(tokens.hasMoreTokens()) { + dirs.add(getLastPathComponent(tokens.nextToken())); + } + return dirs; + } + + private boolean calcChangeLog(Build build, File changelogFile, Launcher launcher, BuildListener listener) throws IOException { + if(build.getPreviousBuild()==null) { + // nothing to compare against + return createEmptyChangeLog(changelogFile, listener, "log"); + } + + PrintStream logger = listener.getLogger(); + + Map previousRevisions = parseRevisionFile(build.getPreviousBuild()); + Map thisRevisions = parseRevisionFile(build); + + Map env = createEnvVarMap(true); + + for( String module : getModuleDirNames() ) { + Integer prevRev = previousRevisions.get(module); + if(prevRev==null) { + logger.println("no revision recorded for "+module+" in the previous build"); + continue; + } + Integer thisRev = thisRevisions.get(module); + if(thisRev!=null && thisRev.equals(prevRev)) { + logger.println("no change for "+module+" since the previous build"); + continue; + } + + String cmd = DESCRIPTOR.getSvnExe()+" log -v --xml --non-interactive -r "+(prevRev+1)+":BASE "+module; + OutputStream os = new BufferedOutputStream(new FileOutputStream(changelogFile)); + try { + int r = launcher.launch(cmd,env,os,build.getProject().getWorkspace()).join(); + if(r!=0) { + listener.fatalError("revision check failed"); + // report the output + FileInputStream log = new FileInputStream(changelogFile); + try { + Util.copyStream(log,listener.getLogger()); + } finally { + log.close(); + } + return false; + } + } finally { + os.close(); + } + } + + return true; + } + + /*package*/ static Map parseRevisionFile(Build build) throws IOException { + Map revisions = new HashMap(); // module -> revision + {// read the revision file of the last build + File file = getRevisionFile(build); + if(!file.exists()) + // nothing to compare against + return revisions; + + BufferedReader br = new BufferedReader(new FileReader(file)); + String line; + while((line=br.readLine())!=null) { + int index = line.indexOf('/'); + if(index<0) { + continue; // invalid line? + } + try { + revisions.put(line.substring(0,index), Integer.parseInt(line.substring(index+1))); + } catch (NumberFormatException e) { + // perhaps a corrupted line. ignore + } + } + } + + return revisions; + } + + public boolean checkout(Build build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException { + boolean result; + + if(useUpdate && isUpdatable(workspace,listener)) { + result = update(launcher,workspace,listener); + if(!result) + return false; + } else { + workspace.deleteContents(); + StringTokenizer tokens = new StringTokenizer(modules); + while(tokens.hasMoreTokens()) { + ArgumentListBuilder cmd = new ArgumentListBuilder(); + cmd.add(DESCRIPTOR.getSvnExe(),"co","-q","--non-interactive"); + if(username!=null) + cmd.add("--username",username); + if(otherOptions!=null) + cmd.add(Util.tokenize(otherOptions)); + cmd.add(tokens.nextToken()); + + result = run(launcher,cmd,listener,workspace); + if(!result) + return false; + } + } + + // write out the revision file + PrintWriter w = new PrintWriter(new FileOutputStream(getRevisionFile(build))); + try { + Map revMap = buildRevisionMap(workspace,listener); + for (Entry e : revMap.entrySet()) { + w.println( e.getKey() +'/'+ e.getValue().revision ); + } + } finally { + w.close(); + } + + return calcChangeLog(build, changelogFile, launcher, listener); + } + + /** + * Output from "svn info" command. + */ + public static class SvnInfo { + /** The remote URL of this directory */ + String url; + /** Current workspace revision. */ + int revision = -1; + + private SvnInfo() {} + + /** + * Returns true if this object is fully populated. + */ + public boolean isComplete() { + return url!=null && revision!=-1; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setRevision(int revision) { + this.revision = revision; + } + + /** + * Executes "svn info" command and returns the parsed output + * + * @param subject + * The target to run "svn info". Either local path or remote URL. + */ + public static SvnInfo parse(String subject, Map env, FilePath workspace, TaskListener listener) throws IOException { + String cmd = DESCRIPTOR.getSvnExe()+" info --xml "+subject; + listener.getLogger().println("$ "+cmd); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + int r = new Proc(cmd,env,baos,workspace.getLocal()).join(); + if(r!=0) { + // failed. to allow user to diagnose the problem, send output to log + listener.getLogger().write(baos.toByteArray()); + throw new IOException("svn info failed"); + } + + SvnInfo info = new SvnInfo(); + + Digester digester = new Digester(); + digester.push(info); + + digester.addBeanPropertySetter("info/entry/url"); + digester.addSetProperties("info/entry/commit","revision","revision"); // set attributes. in particular @revision + + try { + digester.parse(new ByteArrayInputStream(baos.toByteArray())); + } catch (SAXException e) { + // failed. to allow user to diagnose the problem, send output to log + listener.getLogger().write(baos.toByteArray()); + e.printStackTrace(listener.fatalError("Failed to parse Subversion output")); + throw new IOException("Unabled to parse svn info output"); + } + + if(!info.isComplete()) + throw new IOException("No revision in the svn info output"); + + return info; + } + + } + + /** + * Checks .svn files in the workspace and finds out revisions of the modules + * that the workspace has. + * + * @return + * null if the parsing somehow fails. Otherwise a map from module names to revisions. + */ + private Map buildRevisionMap(FilePath workspace, TaskListener listener) throws IOException { + PrintStream logger = listener.getLogger(); + + Map revisions = new HashMap(); + + Map env = createEnvVarMap(false); + + // invoke the "svn info" + for( String module : getModuleDirNames() ) { + // parse the output + SvnInfo info = SvnInfo.parse(module,env,workspace,listener); + revisions.put(module,info); + logger.println("Revision:"+info.revision); + } + + return revisions; + } + + /** + * Gets the file that stores the revision. + */ + private static File getRevisionFile(Build build) { + return new File(build.getRootDir(),"revision.txt"); + } + + public boolean update(Launcher launcher, FilePath remoteDir, BuildListener listener) throws IOException { + ArgumentListBuilder cmd = new ArgumentListBuilder(); + cmd.add(DESCRIPTOR.getSvnExe(), "update", "-q", "--non-interactive"); + + if(username!=null) + cmd.add(" --username ",username); + if(otherOptions!=null) + cmd.add(Util.tokenize(otherOptions)); + + StringTokenizer tokens = new StringTokenizer(modules); + while(tokens.hasMoreTokens()) { + if(!run(launcher,cmd,listener,new FilePath(remoteDir,getLastPathComponent(tokens.nextToken())))) + return false; + } + return true; + } + + /** + * Returns true if we can use "svn update" instead of "svn checkout" + */ + private boolean isUpdatable(FilePath workspace,BuildListener listener) { + StringTokenizer tokens = new StringTokenizer(modules); + while(tokens.hasMoreTokens()) { + String url = tokens.nextToken(); + String moduleName = getLastPathComponent(url); + File module = workspace.child(moduleName).getLocal(); + + try { + SvnInfo svnInfo = SvnInfo.parse(moduleName, createEnvVarMap(false), workspace, listener); + if(!svnInfo.url.equals(url)) { + listener.getLogger().println("Checking out a fresh workspace because the workspace is not "+url); + return false; + } + } catch (IOException e) { + listener.getLogger().println("Checking out a fresh workspace because Hudson failed to detect the current workspace "+module); + e.printStackTrace(listener.error(e.getMessage())); + return false; + } + } + return true; + } + + public boolean pollChanges(Project project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException { + // current workspace revision + Map wsRev = buildRevisionMap(workspace,listener); + + Map env = createEnvVarMap(false); + + // check the corresponding remote revision + for (SvnInfo localInfo : wsRev.values()) { + SvnInfo remoteInfo = SvnInfo.parse(localInfo.url,env,workspace,listener); + if(remoteInfo.revision > localInfo.revision) + return true; // change found + } + + return false; // no change + } + + public ChangeLogParser createChangeLogParser() { + return new SubversionChangeLogParser(); + } + + + public DescriptorImpl getDescriptor() { + return DESCRIPTOR; + } + + public void buildEnvVars(Map env) { + // no environment variable + } + + public FilePath getModuleRoot(FilePath workspace) { + String s; + + // if multiple URLs are specified, pick the first one + int idx = modules.indexOf(' '); + if(idx>=0) s = modules.substring(0,idx); + else s = modules; + + return workspace.child(getLastPathComponent(s)); + } + + private String getLastPathComponent(String s) { + String[] tokens = s.split("/"); + return tokens[tokens.length-1]; // return the last token + } + + static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); + + public static final class DescriptorImpl extends Descriptor { + DescriptorImpl() { + super(SubversionSCM.class); + } + + public String getDisplayName() { + return "Subversion"; + } + + public SCM newInstance(StaplerRequest req) { + return new SubversionSCM( + req.getParameter("svn_modules"), + req.getParameter("svn_use_update")!=null, + req.getParameter("svn_username"), + req.getParameter("svn_other_options") + ); + } + + public String getSvnExe() { + String value = (String)getProperties().get("svn_exe"); + if(value==null) + value = "svn"; + return value; + } + + public void setSvnExe(String value) { + getProperties().put("svn_exe",value); + save(); + } + + public boolean configure( HttpServletRequest req ) { + setSvnExe(req.getParameter("svn_exe")); + return true; + } + + /** + * Returns the Subversion version information. + * + * @return + * null if failed to obtain. + */ + public Version version(Launcher l, String svnExe) { + try { + if(svnExe==null || svnExe.equals("")) svnExe="svn"; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + l.launch(new String[]{svnExe,"--version"},new String[0],out,FilePath.RANDOM).join(); + + // parse the first line for version + BufferedReader r = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(out.toByteArray()))); + String line; + while((line = r.readLine())!=null) { + Matcher m = SVN_VERSION.matcher(line); + if(m.matches()) + return new Version(Integer.parseInt(m.group(2)), m.group(1)); + } + + // ancient version of subversions didn't have the fixed version number line. + // or maybe something else is going wrong. + LOGGER.log(Level.WARNING, "Failed to parse the first line from svn output: "+line); + return new Version(0,"(unknown)"); + } catch (IOException e) { + // Stack trace likely to be overkill for a problem that isn't necessarily a problem at all: + LOGGER.log(Level.WARNING, "Failed to check svn version: {0}", e.toString()); + return null; // failed to obtain + } + } + + // web methods + + public void doVersionCheck(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + // this method runs a new process, so it needs to be protected + new FormFieldValidator(req,rsp,true) { + protected void check() throws IOException, ServletException { + String svnExe = request.getParameter("exe"); + + Version v = version(new Launcher(TaskListener.NULL),svnExe); + if(v==null) { + error("Failed to check subversion version info. Is this a valid path?"); + return; + } + if(v.isOK()) { + ok(); + } else { + error("Version "+v.versionId+" found, but 1.3.0 is required"); + } + } + }.process(); + } + } + + public static final class Version { + private final int revision; + private String versionId; + + public Version(int revision, String versionId) { + this.revision = revision; + this.versionId = versionId; + } + + /** + * Repository revision ID of this build. + */ + public int getRevision() { + return revision; + } + + /** + * Human-readable version string. + */ + public String getVersionId() { + return versionId; + } + + /** + * We use "svn info --xml", which is new in 1.3.0 + */ + public boolean isOK() { + return revision>=17949; + } + } + + private static final Pattern SVN_VERSION = Pattern.compile("svn, .+ ([0-9.]+) \\(r([0-9]+)\\)"); + + private static final Logger LOGGER = Logger.getLogger(SubversionSCM.class.getName()); +} diff --git a/core/src/main/java/hudson/scm/package.html b/core/src/main/java/hudson/scm/package.html new file mode 100644 index 0000000000..95c3d1cf14 --- /dev/null +++ b/core/src/main/java/hudson/scm/package.html @@ -0,0 +1,3 @@ + +Hudson's interface with source code management systems. Start with SCM + \ No newline at end of file diff --git a/core/src/main/java/hudson/tasks/Ant.java b/core/src/main/java/hudson/tasks/Ant.java new file mode 100644 index 0000000000..3fd3013eaf --- /dev/null +++ b/core/src/main/java/hudson/tasks/Ant.java @@ -0,0 +1,226 @@ +package hudson.tasks; + +import hudson.Launcher; +import hudson.Util; +import hudson.util.FormFieldValidator; +import hudson.model.Action; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.model.Project; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.ServletException; +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +/** + * @author Kohsuke Kawaguchi + */ +public class Ant extends Builder { + + private final String targets; + + /** + * Identifies {@link AntInstallation} to be used. + */ + private final String antName; + + public Ant(String targets,String antName) { + this.targets = targets; + this.antName = antName; + } + + public String getTargets() { + return targets; + } + + /** + * Gets the Ant to invoke, + * or null to invoke the default one. + */ + public AntInstallation getAnt() { + for( AntInstallation i : DESCRIPTOR.getInstallations() ) { + if(antName!=null && i.getName().equals(antName)) + return i; + } + return null; + } + + public boolean perform(Build build, Launcher launcher, BuildListener listener) { + Project proj = build.getProject(); + + String cmd; + + String execName; + if(onWindows) + execName = "ant.bat"; + else + execName = "ant"; + + AntInstallation ai = getAnt(); + if(ai==null) + cmd = execName+' '+targets; + else { + File exec = ai.getExecutable(); + if(!ai.getExists()) { + listener.fatalError(exec+" doesn't exist"); + return false; + } + cmd = exec.getPath()+' '+targets; + } + + Map env = build.getEnvVars(); + if(ai!=null) + env.put("ANT_HOME",ai.getAntHome()); + + if(onWindows) { + // on Windows, executing batch file can't return the correct error code, + // so we need to wrap it into cmd.exe. + // double %% is needed because we want ERRORLEVEL to be expanded after + // batch file executed, not before. This alone shows how broken Windows is... + cmd = "cmd.exe /C "+cmd+" && exit %%ERRORLEVEL%%"; + } + + try { + int r = launcher.launch(cmd,env,listener.getLogger(),proj.getModuleRoot()).join(); + return r==0; + } catch (IOException e) { + Util.displayIOException(e,listener); + e.printStackTrace( listener.fatalError("command execution failed") ); + return false; + } + } + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); + + public static final class DescriptorImpl extends Descriptor { + private DescriptorImpl() { + super(Ant.class); + } + + public String getHelpFile() { + return "/help/project-config/ant.html"; + } + + public String getDisplayName() { + return "Invoke top-level Ant targets"; + } + + public AntInstallation[] getInstallations() { + AntInstallation[] r = (AntInstallation[]) getProperties().get("installations"); + + if(r==null) + return new AntInstallation[0]; + + return r.clone(); + } + + public boolean configure(HttpServletRequest req) { + boolean r = true; + + int i; + String[] names = req.getParameterValues("ant_name"); + String[] homes = req.getParameterValues("ant_home"); + int len; + if(names!=null && homes!=null) + len = Math.min(names.length,homes.length); + else + len = 0; + AntInstallation[] insts = new AntInstallation[len]; + + for( i=0; i getDescriptor() { + return DESCRIPTOR; + } + + + public static final Descriptor DESCRIPTOR = new Descriptor(ArtifactArchiver.class) { + public String getDisplayName() { + return "Archive the artifacts"; + } + + public String getHelpFile() { + return "/help/project-config/archive-artifact.html"; + } + + public Publisher newInstance(StaplerRequest req) { + return new ArtifactArchiver( + req.getParameter("artifacts").trim(), + req.getParameter("artifacts_latest_only")!=null); + } + }; +} diff --git a/core/src/main/java/hudson/tasks/BatchFile.java b/core/src/main/java/hudson/tasks/BatchFile.java new file mode 100644 index 0000000000..2df903ef85 --- /dev/null +++ b/core/src/main/java/hudson/tasks/BatchFile.java @@ -0,0 +1,89 @@ +package hudson.tasks; + +import hudson.FilePath; +import hudson.Launcher; +import hudson.Util; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.model.Project; +import org.kohsuke.stapler.StaplerRequest; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; + +/** + * Executes commands by using Windows batch file. + * + * @author Kohsuke Kawaguchi + */ +public class BatchFile extends Builder { + private final String command; + + public BatchFile(String command) { + this.command = command; + } + + public String getCommand() { + return command; + } + + public boolean perform(Build build, Launcher launcher, BuildListener listener) { + Project proj = build.getProject(); + FilePath ws = proj.getWorkspace(); + FilePath script=null; + try { + try { + script = ws.createTempFile("hudson",".bat"); + Writer w = new FileWriter(script.getLocal()); + w.write(command); + w.write("\r\nexit %ERRORLEVEL%"); + w.close(); + } catch (IOException e) { + Util.displayIOException(e,listener); + e.printStackTrace( listener.fatalError("Unable to produce a batch file") ); + return false; + } + + String[] cmd = new String[] {script.getRemote()}; + + int r; + try { + r = launcher.launch(cmd,build.getEnvVars(),listener.getLogger(),ws).join(); + } catch (IOException e) { + Util.displayIOException(e,listener); + e.printStackTrace( listener.fatalError("command execution failed") ); + r = -1; + } + return r==0; + } finally { + if(script!=null) + script.delete(); + } + } + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); + + public static final class DescriptorImpl extends Descriptor { + private DescriptorImpl() { + super(BatchFile.class); + } + + public String getHelpFile() { + return "/help/project-config/batch.html"; + } + + public String getDisplayName() { + return "Execute Windows batch command"; + } + + public Builder newInstance(StaplerRequest req) { + return new BatchFile(req.getParameter("batchFile")); + } + } +} diff --git a/core/src/main/java/hudson/tasks/BuildStep.java b/core/src/main/java/hudson/tasks/BuildStep.java new file mode 100644 index 0000000000..69965dff1b --- /dev/null +++ b/core/src/main/java/hudson/tasks/BuildStep.java @@ -0,0 +1,75 @@ +package hudson.tasks; + +import hudson.Launcher; +import hudson.model.Action; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.model.Project; +import hudson.tasks.junit.JUnitResultArchiver; + +import java.util.List; + +/** + * One step of the whole build process. + * + * @author Kohsuke Kawaguchi + */ +public interface BuildStep { + + /** + * Runs before the build begins. + * + * @return + * true if the build can continue, false if there was an error + * and the build needs to be aborted. + */ + boolean prebuild( Build build, BuildListener listener ); + + /** + * Runs the step over the given build and reports the progress to the listener. + * + * @return + * true if the build can continue, false if there was an error + * and the build needs to be aborted. + */ + boolean perform(Build build, Launcher launcher, BuildListener listener); + + /** + * Returns an action object if this {@link BuildStep} has an action + * to contribute to a {@link Project}. + * + * @param project + * {@link Project} that owns this build step, + * since {@link BuildStep} object doesn't usually have this "parent" pointer. + */ + Action getProjectAction(Project project); + + /** + * List of all installed builders. + * + * Builders are invoked to perform the build itself. + */ + public static final List> BUILDERS = Descriptor.toList( + Shell.DESCRIPTOR, + BatchFile.DESCRIPTOR, + Ant.DESCRIPTOR, + Maven.DESCRIPTOR + ); + + /** + * List of all installed publishers. + * + * Publishers are invoked after the build is completed, normally to perform + * some post-actions on build results, such as sending notifications, collecting + * results, etc. + */ + public static final List> PUBLISHERS = Descriptor.toList( + ArtifactArchiver.DESCRIPTOR, + Fingerprinter.DESCRIPTOR, + JavadocArchiver.DESCRIPTOR, + JUnitResultArchiver.DESCRIPTOR, + Mailer.DESCRIPTOR, + BuildTrigger.DESCRIPTOR + ); +} diff --git a/core/src/main/java/hudson/tasks/BuildTrigger.java b/core/src/main/java/hudson/tasks/BuildTrigger.java new file mode 100644 index 0000000000..c17c4b59f0 --- /dev/null +++ b/core/src/main/java/hudson/tasks/BuildTrigger.java @@ -0,0 +1,103 @@ +package hudson.tasks; + +import hudson.Launcher; +import hudson.model.Action; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.model.Job; +import hudson.model.Project; +import hudson.model.Result; + +import java.util.List; + +import org.kohsuke.stapler.StaplerRequest; + +/** + * Triggers builds of other projects. + * + * @author Kohsuke Kawaguchi + */ +public class BuildTrigger extends Publisher { + + /** + * Comma-separated list of other projects to be scheduled. + */ + private String childProjects; + + public BuildTrigger(String childProjects) { + this.childProjects = childProjects; + } + + public BuildTrigger(List childProjects) { + this(Project.toNameList(childProjects)); + } + + public String getChildProjectsValue() { + return childProjects; + } + + public List getChildProjects() { + return Project.fromNameList(childProjects); + } + + public boolean perform(Build build, Launcher launcher, BuildListener listener) { + if(build.getResult()== Result.SUCCESS) { + for (Project p : getChildProjects()) { + listener.getLogger().println("Triggering a new build of "+p.getName()); + p.scheduleBuild(); + } + } + + return true; + } + + /** + * Called from {@link Job#renameTo(String)} when a job is renamed. + * + * @return true + * if this {@link BuildTrigger} is changed and needs to be saved. + */ + public boolean onJobRenamed(String oldName, String newName) { + // quick test + if(!childProjects.contains(oldName)) + return false; + + boolean changed = false; + + // we need to do this per string, since old Project object is already gone. + String[] projects = childProjects.split(","); + for( int i=0; i0) b.append(','); + b.append(p); + } + childProjects = b.toString(); + } + + return changed; + } + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + + public static final Descriptor DESCRIPTOR = new Descriptor(BuildTrigger.class) { + public String getDisplayName() { + return "Build other projects"; + } + + public Publisher newInstance(StaplerRequest req) { + return new BuildTrigger(req.getParameter("childProjects")); + } + }; +} diff --git a/core/src/main/java/hudson/tasks/Builder.java b/core/src/main/java/hudson/tasks/Builder.java new file mode 100644 index 0000000000..169be88132 --- /dev/null +++ b/core/src/main/java/hudson/tasks/Builder.java @@ -0,0 +1,33 @@ +package hudson.tasks; + +import hudson.model.Describable; +import hudson.model.Action; +import hudson.model.Project; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.ExtensionPoint; + +/** + * {@link BuildStep}s that perform the actual build. + * + *

+ * To register a custom {@link Builder} from a plugin, + * add it to {@link BuildStep#BUILDERS}. + * + * @author Kohsuke Kawaguchi + */ +public abstract class Builder implements BuildStep, Describable, ExtensionPoint { + /** + * Default implementation that does nothing. + */ + public boolean prebuild(Build build, BuildListener listener) { + return true; + } + + /** + * Default implementation that does nothing. + */ + public Action getProjectAction(Project project) { + return null; + } +} diff --git a/core/src/main/java/hudson/tasks/Fingerprinter.java b/core/src/main/java/hudson/tasks/Fingerprinter.java new file mode 100644 index 0000000000..7903b638ca --- /dev/null +++ b/core/src/main/java/hudson/tasks/Fingerprinter.java @@ -0,0 +1,241 @@ +package hudson.tasks; + +import hudson.Launcher; +import hudson.model.Action; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.model.Fingerprint; +import hudson.model.Hudson; +import hudson.model.Project; +import hudson.model.Result; +import hudson.model.Fingerprint.BuildPtr; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.types.FileSet; +import org.kohsuke.stapler.StaplerRequest; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.Set; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Records fingerprints of the specified files. + * + * @author Kohsuke Kawaguchi + */ +public class Fingerprinter extends Publisher { + + /** + * Comma-separated list of files/directories to be fingerprinted. + */ + private final String targets; + + /** + * Also record all the finger prints of the build artifacts. + */ + private final boolean recordBuildArtifacts; + + public Fingerprinter(String targets, boolean recordBuildArtifacts) { + this.targets = targets; + this.recordBuildArtifacts = recordBuildArtifacts; + } + + public String getTargets() { + return targets; + } + + public boolean getRecordBuildArtifacts() { + return recordBuildArtifacts; + } + + public boolean perform(Build build, Launcher launcher, BuildListener listener) { + listener.getLogger().println("Recording fingerprints"); + + Map record = new HashMap(); + + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + // I don't think this is possible, but check anyway + e.printStackTrace(listener.error("MD5 not installed")); + build.setResult(Result.FAILURE); + return true; + } + + if(targets.length()!=0) + record(build, md5, listener, record, targets); + + if(recordBuildArtifacts) { + ArtifactArchiver aa = (ArtifactArchiver) build.getProject().getPublishers().get(ArtifactArchiver.DESCRIPTOR); + if(aa==null) { + // configuration error + listener.error("Build artifacts are supposed to be fingerprinted, but build artifact archiving is not configured"); + build.setResult(Result.FAILURE); + return true; + } + record(build, md5, listener, record, aa.getArtifacts() ); + } + + build.getActions().add(new FingerprintAction(build,record)); + + return true; + } + + private void record(Build build, MessageDigest md5, BuildListener listener, Map record, String targets) { + Project p = build.getProject(); + + FileSet src = new FileSet(); + File baseDir = p.getWorkspace().getLocal(); + src.setDir(baseDir); + src.setIncludes(targets); + + byte[] buf = new byte[8192]; + + DirectoryScanner ds = src.getDirectoryScanner(new org.apache.tools.ant.Project()); + for( String f : ds.getIncludedFiles() ) { + File file = new File(baseDir,f); + + // consider the file to be produced by this build only if the timestamp + // is newer than when the build has started. + boolean produced = build.getTimestamp().getTimeInMillis() <= file.lastModified(); + + try { + md5.reset(); // technically not necessary, but hey, just to be safe + DigestInputStream in =new DigestInputStream(new FileInputStream(file),md5); + try { + while(in.read(buf)>0) + ; // simply discard the input + } finally { + in.close(); + } + + Fingerprint fp = Hudson.getInstance().getFingerprintMap().getOrCreate( + produced?build:null, file.getName(), md5.digest()); + if(fp==null) { + listener.error("failed to record fingerprint for "+file); + continue; + } + fp.add(build); + record.put(f,fp.getHashString()); + } catch (IOException e) { + e.printStackTrace(listener.error("Failed to compute digest for "+file)); + } + } + } + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + + public static final Descriptor DESCRIPTOR = new Descriptor(Fingerprinter.class) { + public String getDisplayName() { + return "Record fingerprints of files to track usage"; + } + + public String getHelpFile() { + return "/help/project-config/fingerprint.html"; + } + + public Publisher newInstance(StaplerRequest req) { + return new Fingerprinter( + req.getParameter("fingerprint_targets").trim(), + req.getParameter("fingerprint_artifacts")!=null); + } + }; + + + /** + * Action for displaying fingerprints. + */ + public static final class FingerprintAction implements Action { + private final Build build; + + private final Map record; + + private transient WeakReference> ref; + + public FingerprintAction(Build build, Map record) { + this.build = build; + this.record = record; + } + + public String getIconFileName() { + return "fingerprint.gif"; + } + + public String getDisplayName() { + return "See fingerprints"; + } + + public String getUrlName() { + return "fingerprints"; + } + + public Build getBuild() { + return build; + } + + /** + * Map from file names of the fingeprinted file to its fingerprint record. + */ + public synchronized Map getFingerprints() { + if(ref!=null) { + Map m = ref.get(); + if(m!=null) + return m; + } + + Hudson h = Hudson.getInstance(); + + Map m = new TreeMap(); + for (Entry r : record.entrySet()) { + try { + m.put(r.getKey(), h._getFingerprint(r.getValue()) ); + } catch (IOException e) { + logger.log(Level.WARNING,e.getMessage(),e); + } + } + + m = Collections.unmodifiableMap(m); + ref = new WeakReference>(m); + return m; + } + + /** + * Gets the dependency to other builds in a map. + * Returns build numbers instead of {@link Build}, since log records may be gone. + */ + public Map getDependencies() { + Map r = new HashMap(); + + for (Fingerprint fp : getFingerprints().values()) { + BuildPtr bp = fp.getOriginal(); + if(bp==null) continue; // outside Hudson + if(bp.is(build)) continue; // we are the owner + + Integer existing = r.get(bp.getJob()); + if(existing!=null && existing>bp.getNumber()) + continue; // the record in the map is already up to date + r.put((Project)bp.getJob(),bp.getNumber()); + } + + return r; + } + } + + private static final Logger logger = Logger.getLogger(Fingerprinter.class.getName()); +} diff --git a/core/src/main/java/hudson/tasks/JavadocArchiver.java b/core/src/main/java/hudson/tasks/JavadocArchiver.java new file mode 100644 index 0000000000..d19b062e36 --- /dev/null +++ b/core/src/main/java/hudson/tasks/JavadocArchiver.java @@ -0,0 +1,117 @@ +package hudson.tasks; + +import hudson.Launcher; +import hudson.model.Action; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.model.DirectoryHolder; +import hudson.model.Project; +import hudson.model.ProminentProjectAction; +import org.apache.tools.ant.taskdefs.Copy; +import org.apache.tools.ant.types.FileSet; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import java.io.File; +import java.io.IOException; + +/** + * Saves javadoc for the project and publish them. + * + * @author Kohsuke Kawaguchi + */ +public class JavadocArchiver extends AntBasedPublisher { + /** + * Path to the javadoc directory in the workspace. + */ + private final String javadocDir; + + public JavadocArchiver(String javadocDir) { + this.javadocDir = javadocDir; + } + + public String getJavadocDir() { + return javadocDir; + } + + /** + * Gets the directory where the javadoc is stored for the given project. + */ + private static File getJavadocDir(Project project) { + return new File(project.getRootDir(),"javadoc"); + } + + public boolean perform(Build build, Launcher launcher, BuildListener listener) { + // TODO: run tar or something for better remote copy + File javadoc = new File(build.getParent().getWorkspace().getLocal(), javadocDir); + if(!javadoc.exists()) { + listener.error("The specified javadoc directory doesn't exist: "+javadoc); + return false; + } + if(!javadoc.isDirectory()) { + listener.error("The specified javadoc directory isn't a directory: "+javadoc); + return false; + } + + listener.getLogger().println("Publishing javadoc"); + + File target = getJavadocDir(build.getParent()); + target.mkdirs(); + + Copy copyTask = new Copy(); + copyTask.setProject(new org.apache.tools.ant.Project()); + copyTask.setTodir(target); + FileSet src = new FileSet(); + src.setDir(javadoc); + copyTask.addFileset(src); + + execTask(copyTask, listener); + + return true; + } + + public Action getProjectAction(Project project) { + return new JavadocAction(project); + } + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + + public static final Descriptor DESCRIPTOR = new Descriptor(JavadocArchiver.class) { + public String getDisplayName() { + return "Publish javadoc"; + } + + public Publisher newInstance(StaplerRequest req) { + return new JavadocArchiver(req.getParameter("javadoc_dir")); + } + }; + + public static final class JavadocAction extends DirectoryHolder implements ProminentProjectAction { + private final Project project; + + public JavadocAction(Project project) { + this.project = project; + } + + public String getUrlName() { + return "javadoc"; + } + + public String getDisplayName() { + return "Javadoc"; + } + + public String getIconFileName() { + return "help.gif"; + } + + public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + serveFile(req, rsp, getJavadocDir(project), "help.gif", false); + } + } +} diff --git a/core/src/main/java/hudson/tasks/LogRotator.java b/core/src/main/java/hudson/tasks/LogRotator.java new file mode 100644 index 0000000000..6bd61785df --- /dev/null +++ b/core/src/main/java/hudson/tasks/LogRotator.java @@ -0,0 +1,113 @@ +package hudson.tasks; + +import hudson.model.Describable; +import hudson.model.Descriptor; +import hudson.model.Job; +import hudson.model.Run; +import hudson.scm.SCM; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Calendar; +import java.util.GregorianCalendar; + +import org.kohsuke.stapler.StaplerRequest; + +/** + * Deletes old log files. + * + * TODO: is there any other task that follows the same pattern? + * try to generalize this just like {@link SCM} or {@link BuildStep}. + * + * @author Kohsuke Kawaguchi + */ +public class LogRotator implements Describable { + + /** + * If not -1, history is only kept up to this days. + */ + private final int daysToKeep; + + /** + * If not -1, only this number of build logs are kept. + */ + private final int numToKeep; + + public LogRotator(int daysToKeep, int numToKeep) { + this.daysToKeep = daysToKeep; + this.numToKeep = numToKeep; + } + + public void perform(Job job) throws IOException { + // keep the last successful build regardless of the status + Run lsb = job.getLastSuccessfulBuild(); + + if(numToKeep!=-1) { + Run[] builds = job.getBuilds().toArray(new Run[0]); + for( int i=numToKeep; i { + private LRDescriptor() { + super(LogRotator.class); + } + + public String getDisplayName() { + return "Log Rotation"; + } + + public LogRotator newInstance(StaplerRequest req) { + return new LogRotator( + parse(req,"logrotate_days"), + parse(req,"logrotate_nums") ); + } + + private int parse(HttpServletRequest req, String name) { + String p = req.getParameter(name); + if(p==null) return -1; + try { + return Integer.parseInt(p); + } catch (NumberFormatException e) { + return -1; + } + } + } +} diff --git a/core/src/main/java/hudson/tasks/Mailer.java b/core/src/main/java/hudson/tasks/Mailer.java new file mode 100644 index 0000000000..1ccf452c7e --- /dev/null +++ b/core/src/main/java/hudson/tasks/Mailer.java @@ -0,0 +1,416 @@ +package hudson.tasks; + +import hudson.Launcher; +import hudson.Util; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.model.Result; +import hudson.model.User; +import hudson.model.UserPropertyDescriptor; +import hudson.scm.ChangeLogSet.Entry; +import org.apache.tools.ant.types.selectors.SelectorUtils; +import org.kohsuke.stapler.StaplerRequest; + +import javax.mail.Authenticator; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.PasswordAuthentication; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.Address; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Sends the build result in e-mail. + * + * @author Kohsuke Kawaguchi + */ +public class Mailer extends Publisher { + + private static final Logger LOGGER = Logger.getLogger(Mailer.class.getName()); + + private static final int MAX_LOG_LINES = 250; + + /** + * Whitespace-separated list of e-mail addresses that represent recipients. + */ + public String recipients; + + /** + * If true, only the first unstable build will be reported. + */ + public boolean dontNotifyEveryUnstableBuild; + + /** + * If true, individuals will receive e-mails regarding who broke the build. + */ + public boolean sendToIndividuals; + + // TODO: left so that XStream won't get angry. figure out how to set the error handling behavior + // in XStream. + private transient String from; + private transient String subject; + private transient boolean failureOnly; + + public boolean perform(Build build, Launcher launcher, BuildListener listener) { + try { + MimeMessage mail = getMail(build); + if(mail!=null) { + StringBuffer buf = new StringBuffer("Sending e-mails to "); + for (Address a : mail.getAllRecipients()) + buf.append(' ').append(a); + listener.getLogger().println(buf); + Transport.send(mail); + } + } catch (MessagingException e) { + e.printStackTrace( listener.error(e.getMessage()) ); + } + + return true; + } + + private MimeMessage getMail(Build build) throws MessagingException { + if(build.getResult()==Result.FAILURE) { + return createFailureMail(build); + } + + if(build.getResult()==Result.UNSTABLE) { + Build prev = build.getPreviousBuild(); + if(!dontNotifyEveryUnstableBuild) + return createUnstableMail(build); + if(prev!=null) { + if(prev.getResult()==Result.SUCCESS) + return createUnstableMail(build); + } + } + + if(build.getResult()==Result.SUCCESS) { + Build prev = build.getPreviousBuild(); + if(prev!=null) { + if(prev.getResult()==Result.FAILURE) + return createBackToNormalMail(build, "normal"); + if(prev.getResult()==Result.UNSTABLE) + return createBackToNormalMail(build, "stable"); + } + } + + return null; + } + + private MimeMessage createBackToNormalMail(Build build, String subject) throws MessagingException { + MimeMessage msg = createEmptyMail(build); + + msg.setSubject(getSubject(build,"Hudson build is back to "+subject +": ")); + StringBuffer buf = new StringBuffer(); + appendBuildUrl(build,buf); + msg.setText(buf.toString()); + + return msg; + } + + private MimeMessage createUnstableMail(Build build) throws MessagingException { + MimeMessage msg = createEmptyMail(build); + + msg.setSubject(getSubject(build,"Hudson build became unstable: ")); + StringBuffer buf = new StringBuffer(); + appendBuildUrl(build,buf); + msg.setText(buf.toString()); + + return msg; + } + + private void appendBuildUrl(Build build, StringBuffer buf) { + String baseUrl = DESCRIPTOR.getUrl(); + if(baseUrl!=null) { + buf.append("See ").append(baseUrl).append(Util.encode(build.getUrl())).append("\n\n"); + } + } + + private MimeMessage createFailureMail(Build build) throws MessagingException { + MimeMessage msg = createEmptyMail(build); + + msg.setSubject(getSubject(build, "Build failed in Hudson: ")); + + StringBuffer buf = new StringBuffer(); + appendBuildUrl(build,buf); + + buf.append("---------\n"); + + try { + String log = build.getLog(); + String[] lines = log.split("\n"); + int start = 0; + if (lines.length > MAX_LOG_LINES) { + // Avoid sending enormous logs over email. + // Interested users can always look at the log on the web server. + buf.append("[...truncated " + (lines.length - MAX_LOG_LINES) + " lines...]\n"); + start = lines.length - MAX_LOG_LINES; + } + String workspaceUrl = null, artifactUrl = null; + Pattern wsPattern = null; + String baseUrl = DESCRIPTOR.getUrl(); + if (baseUrl != null) { + // Hyperlink local file paths to the repository workspace or build artifacts. + // Note that it is possible for a failure mail to refer to a file using a workspace + // URL which has already been corrected in a subsequent build. To fix, archive. + workspaceUrl = baseUrl + Util.encode(build.getProject().getUrl()) + "ws/"; + artifactUrl = baseUrl + Util.encode(build.getUrl()) + "artifact/"; + File workspaceDir = build.getProject().getWorkspace().getLocal(); + // Match either file or URL patterns, i.e. either + // c:\hudson\workdir\jobs\foo\workspace\src\Foo.java + // file:/c:/hudson/workdir/jobs/foo/workspace/src/Foo.java + // will be mapped to one of: + // http://host/hudson/job/foo/ws/src/Foo.java + // http://host/hudson/job/foo/123/artifact/src/Foo.java + // Careful with path separator between $1 and $2: + // workspaceDir will not normally end with one; + // workspaceDir.toURI() will end with '/' if and only if workspaceDir.exists() at time of call + wsPattern = Pattern.compile("(\\Q" + workspaceDir + "\\E|\\Q" + workspaceDir.toURI() + "\\E)[/\\\\]?([^:#\\s]*)"); + } + for (int i = start; i < lines.length; i++) { + String line = lines[i]; + if (wsPattern != null) { + // Perl: $line =~ s{$rx}{$path = $2; $path =~ s!\\\\!/!g; $workspaceUrl . $path}eg; + Matcher m = wsPattern.matcher(line); + int pos = 0; + while (m.find(pos)) { + String path = m.group(2).replace(File.separatorChar, '/'); + String linkUrl = DESCRIPTOR.artifactMatches(path, build) ? artifactUrl : workspaceUrl; + // Append ' ' to make sure mail readers do not interpret following ':' as part of URL: + String prefix = line.substring(0, m.start()) + linkUrl + Util.encode(path) + ' '; + pos = prefix.length(); + line = prefix + line.substring(m.end()); + // XXX better style to reuse Matcher and fix offsets, but more work + m = wsPattern.matcher(line); + } + } + buf.append(line); + buf.append('\n'); + } + } catch (IOException e) { + // somehow failed to read the contents of the log + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + buf.append("Failed to access build log\n\n").append(sw); + } + + msg.setText(buf.toString()); + + return msg; + } + + private MimeMessage createEmptyMail(Build build) throws MessagingException { + MimeMessage msg = new MimeMessage(DESCRIPTOR.createSession()); + // TODO: I'd like to put the URL to the page in here, + // but how do I obtain that? + msg.setContent("","text/plain"); + msg.setFrom(new InternetAddress(DESCRIPTOR.getAdminAddress())); + + List rcp = new ArrayList(); + StringTokenizer tokens = new StringTokenizer(recipients); + while(tokens.hasMoreTokens()) + rcp.add(new InternetAddress(tokens.nextToken())); + if(sendToIndividuals) { + Set users = new HashSet(); + for (Entry change : build.getChangeSet()) { + User a = change.getAuthor(); + if(users.add(a)) + rcp.add(new InternetAddress(a.getProperty(UserProperty.class).getAddress())); + } + } + msg.setRecipients(Message.RecipientType.TO, rcp.toArray(new InternetAddress[rcp.size()])); + return msg; + } + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + private String getSubject(Build build, String caption) { + return caption +build.getProject().getName()+" #"+build.getNumber(); + } + + + public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); + + public static final class DescriptorImpl extends Descriptor { + + public DescriptorImpl() { + super(Mailer.class); + } + + public String getDisplayName() { + return "E-mail Notification"; + } + + public String getHelpFile() { + return "/help/project-config/mailer.html"; + } + + public String getDefaultSuffix() { + return (String)getProperties().get("mail.default.suffix"); + } + + /** JavaMail session. */ + public Session createSession() { + Properties props = new Properties(System.getProperties()); + // can't use putAll + for (Map.Entry o : ((Map)getProperties()).entrySet()) { + if(o.getValue()!=null) + props.put(o.getKey(),o.getValue()); + } + + return Session.getInstance(props,getAuthenticator()); + } + + private Authenticator getAuthenticator() { + final String un = getSmtpAuthUserName(); + if(un==null) return null; + return new Authenticator() { + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(getSmtpAuthUserName(),getSmtpAuthPassword()); + } + }; + } + + public boolean configure(HttpServletRequest req) throws FormException { + // this code is brain dead + getProperties().put("mail.smtp.host",nullify(req.getParameter("mailer_smtp_server"))); + getProperties().put("mail.admin.address",req.getParameter("mailer_admin_address")); + getProperties().put("mail.default.suffix",nullify(req.getParameter("mailer_default_suffix"))); + String url = nullify(req.getParameter("mailer_hudson_url")); + if(url!=null && !url.endsWith("/")) + url += '/'; + getProperties().put("mail.hudson.url",url); + + getProperties().put("mail.hudson.smtpauth.username",nullify(req.getParameter("mailer.SMTPAuth.userName"))); + getProperties().put("mail.hudson.smtpauth.password",nullify(req.getParameter("mailer.SMTPAuth.password"))); + + save(); + return super.configure(req); + } + + private String nullify(String v) { + if(v!=null && v.length()==0) v=null; + return v; + } + + public String getSmtpServer() { + return (String)getProperties().get("mail.smtp.host"); + } + + public String getAdminAddress() { + String v = (String)getProperties().get("mail.admin.address"); + if(v==null) v = "address not configured yet "; + return v; + } + + public String getUrl() { + return (String)getProperties().get("mail.hudson.url"); + } + + public String getSmtpAuthUserName() { + return (String)getProperties().get("mail.hudson.smtpauth.username"); + } + + public String getSmtpAuthPassword() { + return (String)getProperties().get("mail.hudson.smtpauth.password"); + } + + /** Check whether a path (/-separated) will be archived. */ + public boolean artifactMatches(String path, Build build) { + ArtifactArchiver aa = (ArtifactArchiver) build.getProject().getPublishers().get(ArtifactArchiver.DESCRIPTOR); + if (aa == null) { + LOGGER.finer("No ArtifactArchiver found"); + return false; + } + String artifacts = aa.getArtifacts(); + for (String include : artifacts.split("[, ]+")) { + String pattern = include.replace(File.separatorChar, '/'); + if (pattern.endsWith("/")) { + pattern += "**"; + } + if (SelectorUtils.matchPath(pattern, path)) { + LOGGER.log(Level.FINER, "DescriptorImpl.artifactMatches true for {0} against {1}", new Object[] {path, pattern}); + return true; + } + } + LOGGER.log(Level.FINER, "DescriptorImpl.artifactMatches for {0} matched none of {1}", new Object[] {path, artifacts}); + return false; + } + + public Publisher newInstance(StaplerRequest req) { + Mailer m = new Mailer(); + req.bindParameters(m,"mailer_"); + return m; + } + } + + /** + * Per user property that is e-mail address. + */ + public static class UserProperty extends hudson.model.UserProperty { + public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); + + /** + * The user's e-mail address. + * Null to leave it to default. + */ + private final String emailAddress; + + public UserProperty(String emailAddress) { + this.emailAddress = emailAddress; + } + + public String getAddress() { + if(emailAddress!=null) + return emailAddress; + + String ds = Mailer.DESCRIPTOR.getDefaultSuffix(); + if(ds!=null) + return user.getId()+ds; + else + return null; + } + + public DescriptorImpl getDescriptor() { + return DESCRIPTOR; + } + + public static final class DescriptorImpl extends UserPropertyDescriptor { + public DescriptorImpl() { + super(UserProperty.class); + } + + public String getDisplayName() { + return "E-mail"; + } + + public UserProperty newInstance(User user) { + return new UserProperty(null); + } + + public UserProperty newInstance(StaplerRequest req) throws FormException { + return new UserProperty(req.getParameter("email.address")); + } + } + } +} diff --git a/core/src/main/java/hudson/tasks/Maven.java b/core/src/main/java/hudson/tasks/Maven.java new file mode 100644 index 0000000000..b554d1cd87 --- /dev/null +++ b/core/src/main/java/hudson/tasks/Maven.java @@ -0,0 +1,230 @@ +package hudson.tasks; + +import hudson.Launcher; +import hudson.Util; +import hudson.util.FormFieldValidator; +import hudson.model.Action; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.model.Project; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.ServletException; +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +/** + * Build by using Maven. + * + * @author Kohsuke Kawaguchi + */ +public class Maven extends Builder { + + private final String targets; + + /** + * Identifies {@link MavenInstallation} to be used. + */ + private final String mavenName; + + public Maven(String targets,String mavenName) { + this.targets = targets; + this.mavenName = mavenName; + } + + public String getTargets() { + return targets; + } + + /** + * Gets the Maven to invoke, + * or null to invoke the default one. + */ + public MavenInstallation getMaven() { + for( MavenInstallation i : DESCRIPTOR.getInstallations() ) { + if(mavenName !=null && i.getName().equals(mavenName)) + return i; + } + return null; + } + + public boolean perform(Build build, Launcher launcher, BuildListener listener) { + Project proj = build.getProject(); + + String cmd; + + String execName; + if(launcher.isUnix()) + execName = "maven"; + else + execName = "maven.bat"; + + MavenInstallation ai = getMaven(); + if(ai==null) + cmd = execName+' '+targets; + else { + File exec = ai.getExecutable(); + if(exec==null) { + listener.fatalError("Couldn't find any executable in "+ai.getMavenHome()); + return false; + } + if(!exec.exists()) { + listener.fatalError(exec+" doesn't exist"); + return false; + } + cmd = exec.getPath()+' '+targets; + } + + Map env = build.getEnvVars(); + if(ai!=null) + env.put("MAVEN_HOME",ai.getMavenHome()); + // just as a precaution + // see http://maven.apache.org/continuum/faqs.html#how-does-continuum-detect-a-successful-build + env.put("MAVEN_TERMINATE_CMD","on"); + + try { + int r = launcher.launch(cmd,env,listener.getLogger(),proj.getModuleRoot()).join(); + return r==0; + } catch (IOException e) { + Util.displayIOException(e,listener); + e.printStackTrace( listener.fatalError("command execution failed") ); + return false; + } + } + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); + + public static final class DescriptorImpl extends Descriptor { + private DescriptorImpl() { + super(Maven.class); + } + + public String getHelpFile() { + return "/help/project-config/maven.html"; + } + + public String getDisplayName() { + return "Invoke top-level Maven targets"; + } + + public MavenInstallation[] getInstallations() { + MavenInstallation[] r = (MavenInstallation[]) getProperties().get("installations"); + + if(r==null) + return new MavenInstallation[0]; + + return r.clone(); + } + + public boolean configure(HttpServletRequest req) { + boolean r = true; + + int i; + String[] names = req.getParameterValues("maven_name"); + String[] homes = req.getParameterValues("maven_home"); + int len; + if(names!=null && homes!=null) + len = Math.min(names.length,homes.length); + else + len = 0; + MavenInstallation[] insts = new MavenInstallation[len]; + + for( i=0; i + * To register a custom {@link Publisher} from a plugin, + * add it to {@link BuildStep#PUBLISHERS}. + * + * @author Kohsuke Kawaguchi + */ +public abstract class Publisher implements BuildStep, Describable, ExtensionPoint { + /** + * Default implementation that does nothing. + */ + public boolean prebuild(Build build, BuildListener listener) { + return true; + } + + /** + * Default implementation that does nothing. + */ + public Action getProjectAction(Project project) { + return null; + } +} diff --git a/core/src/main/java/hudson/tasks/Shell.java b/core/src/main/java/hudson/tasks/Shell.java new file mode 100644 index 0000000000..747e3ab279 --- /dev/null +++ b/core/src/main/java/hudson/tasks/Shell.java @@ -0,0 +1,129 @@ +package hudson.tasks; + +import hudson.FilePath; +import hudson.Launcher; +import hudson.Util; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import static hudson.model.Hudson.isWindows; +import hudson.model.Project; +import org.kohsuke.stapler.StaplerRequest; + +import javax.servlet.http.HttpServletRequest; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; + +/** + * Executes a series of commands by using a shell. + * + * @author Kohsuke Kawaguchi + */ +public class Shell extends Builder { + private final String command; + + public Shell(String command) { + this.command = fixCrLf(command); + } + + /** + * Fix CR/LF in the string according to the platform we are running on. + */ + private String fixCrLf(String s) { + // eliminate CR + int idx; + while((idx=s.indexOf("\r\n"))!=-1) + s = s.substring(0,idx)+s.substring(idx+1); + + // add CR back if this is for Windows + if(isWindows()) { + idx=0; + while(true) { + idx = s.indexOf('\n',idx); + if(idx==-1) break; + s = s.substring(0,idx)+'\r'+s.substring(idx); + idx+=2; + } + } + return s; + } + + public String getCommand() { + return command; + } + + public boolean perform(Build build, Launcher launcher, BuildListener listener) { + Project proj = build.getProject(); + FilePath ws = proj.getWorkspace(); + FilePath script=null; + try { + try { + script = ws.createTempFile("hudson","sh"); + Writer w = new FileWriter(script.getLocal()); + w.write(command); + w.close(); + } catch (IOException e) { + Util.displayIOException(e,listener); + e.printStackTrace( listener.fatalError("Unable to produce a script file") ); + return false; + } + + String[] cmd = new String[] { DESCRIPTOR.getShell(),"-xe",script.getRemote()}; + + int r; + try { + r = launcher.launch(cmd,build.getEnvVars(),listener.getLogger(),ws).join(); + } catch (IOException e) { + Util.displayIOException(e,listener); + e.printStackTrace( listener.fatalError("command execution failed") ); + r = -1; + } + return r==0; + } finally { + if(script!=null) + script.delete(); + } + } + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); + + public static final class DescriptorImpl extends Descriptor { + private DescriptorImpl() { + super(Shell.class); + } + + public String getShell() { + String shell = (String)getProperties().get("shell"); + if(shell==null) + shell = isWindows()?"sh":"/bin/sh"; + return shell; + } + + public void setShell(String shell) { + getProperties().put("shell",shell); + save(); + } + + public String getHelpFile() { + return "/help/project-config/shell.html"; + } + + public String getDisplayName() { + return "Execute shell"; + } + + public Builder newInstance(StaplerRequest req) { + return new Shell(req.getParameter("shell")); + } + + public boolean configure( HttpServletRequest req ) { + setShell(req.getParameter("shell")); + return true; + } + } +} diff --git a/core/src/main/java/hudson/tasks/junit/CaseResult.java b/core/src/main/java/hudson/tasks/junit/CaseResult.java new file mode 100644 index 0000000000..825f856ec5 --- /dev/null +++ b/core/src/main/java/hudson/tasks/junit/CaseResult.java @@ -0,0 +1,254 @@ +package hudson.tasks.junit; + +import hudson.model.Build; +import org.dom4j.Element; + +import java.util.Comparator; + +/** + * One test result. + * + * @author Kohsuke Kawaguchi + */ +public final class CaseResult extends TestObject implements Comparable { + private final String className; + private final String testName; + private final String errorStackTrace; + private transient SuiteResult parent; + + private transient ClassResult classResult; + + /** + * This test has been failing since this build number (not id.) + * + * If {@link #isPassed() passing}, this field is left unused to 0. + */ + private /*final*/ int failedSince; + + CaseResult(SuiteResult parent, Element testCase) { + String cn = testCase.attributeValue("classname"); + if(cn==null) + // Maven seems to skip classname, and that shows up in testSuite/@name + cn = parent.getName(); + className = cn.replace('/','_'); // avoid unsafe chars + testName = testCase.attributeValue("name").replace('/','_'); + errorStackTrace = getError(testCase); + } + + private String getError(Element testCase) { + String msg = testCase.elementText("error"); + if(msg!=null) + return msg; + return testCase.elementText("failure"); + } + + public String getDisplayName() { + return testName; + } + + /** + * Gets the name of the test, which is returned from {@code TestCase.getName()} + * + *

+ * Note that this may contain any URL-unfriendly character. + */ + public String getName() { + return testName; + } + + /** + * Gets the version of {@link #getName()} that's URL-safe. + */ + public String getSafeName() { + StringBuffer buf = new StringBuffer(testName); + for( int i=0; i BY_AGE = new Comparator() { + public int compare(CaseResult lhs, CaseResult rhs) { + return lhs.getAge()-rhs.getAge(); + } + }; +} diff --git a/core/src/main/java/hudson/tasks/junit/ClassResult.java b/core/src/main/java/hudson/tasks/junit/ClassResult.java new file mode 100644 index 0000000000..208ac8b182 --- /dev/null +++ b/core/src/main/java/hudson/tasks/junit/ClassResult.java @@ -0,0 +1,97 @@ +package hudson.tasks.junit; + +import hudson.model.Build; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Cumulative test result of a test class. + * + * @author Kohsuke Kawaguchi + */ +public final class ClassResult extends TabulatedResult implements Comparable { + private final String className; + + private final List cases = new ArrayList(); + + private int passCount,failCount; + + private final PackageResult parent; + + ClassResult(PackageResult parent, String className) { + this.parent = parent; + this.className = className; + } + + public PackageResult getParent() { + return parent; + } + + public Build getOwner() { + return parent.getOwner(); + } + + public ClassResult getPreviousResult() { + PackageResult pr = parent.getPreviousResult(); + if(pr==null) return null; + return pr.getDynamic(getName(),null,null); + } + + public String getTitle() { + return "Test Result : "+getName(); + } + + public String getName() { + int idx = className.lastIndexOf('.'); + if(idx<0) return className; + else return className.substring(idx+1); + } + + public CaseResult getDynamic(String name, StaplerRequest req, StaplerResponse rsp) { + for (CaseResult c : cases) { + if(c.getSafeName().equals(name)) + return c; + } + return null; + } + + + public List getChildren() { + return cases; + } + + public int getPassCount() { + return passCount; + } + + public int getFailCount() { + return failCount; + } + + public void add(CaseResult r) { + cases.add(r); + } + + void freeze() { + passCount=failCount=0; + for (CaseResult r : cases) { + r.setClass(this); + if(r.isPassed()) passCount++; + else failCount++; + } + Collections.sort(cases); + } + + + public int compareTo(ClassResult that) { + return this.className.compareTo(that.className); + } + + public String getDisplayName() { + return getName(); + } +} diff --git a/core/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java b/core/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java new file mode 100644 index 0000000000..677621c3f5 --- /dev/null +++ b/core/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java @@ -0,0 +1,81 @@ +package hudson.tasks.junit; + +import hudson.Launcher; +import hudson.model.Action; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.model.Result; +import hudson.tasks.AntBasedPublisher; +import hudson.tasks.Publisher; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.types.FileSet; +import org.kohsuke.stapler.StaplerRequest; + +/** + * Generates HTML report from JUnit test result XML files. + * + * @author Kohsuke Kawaguchi + */ +public class JUnitResultArchiver extends AntBasedPublisher { + + /** + * {@link FileSet} "includes" string, like "foo/bar/*.xml" + */ + private final String testResults; + + public JUnitResultArchiver(String testResults) { + this.testResults = testResults; + } + + public boolean perform(Build build, Launcher launcher, BuildListener listener) { + FileSet fs = new FileSet(); + Project p = new Project(); + fs.setProject(p); + fs.setDir(build.getProject().getWorkspace().getLocal()); + fs.setIncludes(testResults); + DirectoryScanner ds = fs.getDirectoryScanner(p); + + if(ds.getIncludedFiles().length==0) { + listener.getLogger().println("No test report files were found. Configuration error?"); + // no test result. Most likely a configuration error or fatal problem + build.setResult(Result.FAILURE); + } + + TestResultAction action = new TestResultAction(build, ds, listener); + build.getActions().add(action); + + TestResult r = action.getResult(); + + if(r.getPassCount()==0 && r.getFailCount()==0) { + listener.getLogger().println("Test reports were found but none of them are new. Did tests run?"); + // no test result. Most likely a configuration error or fatal problem + build.setResult(Result.FAILURE); + } + + if(r.getFailCount()>0) + build.setResult(Result.UNSTABLE); + + return true; + } + + public String getTestResults() { + return testResults; + } + + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final Descriptor DESCRIPTOR = new Descriptor(JUnitResultArchiver.class) { + public String getDisplayName() { + return "Publish JUnit test result report"; + } + + public Publisher newInstance(StaplerRequest req) { + return new JUnitResultArchiver(req.getParameter("junitreport_includes")); + } + }; +} diff --git a/core/src/main/java/hudson/tasks/junit/MetaTabulatedResult.java b/core/src/main/java/hudson/tasks/junit/MetaTabulatedResult.java new file mode 100644 index 0000000000..74d1277d8e --- /dev/null +++ b/core/src/main/java/hudson/tasks/junit/MetaTabulatedResult.java @@ -0,0 +1,21 @@ +package hudson.tasks.junit; + +import java.util.Collection; +import java.util.List; + +/** + * {@link TabulatedResult} whose immediate children + * are other {@link TabulatedResult}s. + * + * @author Kohsuke Kawaguchi + */ +abstract class MetaTabulatedResult extends TabulatedResult { + public abstract String getChildTitle(); + + /** + * All failed tests. + */ + public abstract List getFailedTests(); + + public abstract Collection getChildren(); +} diff --git a/core/src/main/java/hudson/tasks/junit/PackageResult.java b/core/src/main/java/hudson/tasks/junit/PackageResult.java new file mode 100644 index 0000000000..91c9f1c6f2 --- /dev/null +++ b/core/src/main/java/hudson/tasks/junit/PackageResult.java @@ -0,0 +1,111 @@ +package hudson.tasks.junit; + +import hudson.model.Build; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Cumulative test result for a package. + * + * @author Kohsuke Kawaguchi + */ +public final class PackageResult extends MetaTabulatedResult { + private final String packageName; + + /** + * All {@link ClassResult}s keyed by their short name. + */ + private final Map classes = new TreeMap(); + + private int passCount,failCount; + + private final TestResult parent; + + PackageResult(TestResult parent, String packageName) { + this.packageName = packageName; + this.parent = parent; + } + + public String getName() { + return packageName; + } + + public Build getOwner() { + return parent.getOwner(); + } + + public PackageResult getPreviousResult() { + TestResult tr = parent.getPreviousResult(); + if(tr==null) return null; + return tr.byPackage(getName()); + } + + public String getTitle() { + return "Test Result : "+getName(); + } + + public String getChildTitle() { + return "Class"; + } + + public int getPassCount() { + return passCount; + } + + public int getFailCount() { + return failCount; + } + + public ClassResult getDynamic(String name, StaplerRequest req, StaplerResponse rsp) { + return classes.get(name); + } + + public Collection getChildren() { + return classes.values(); + } + + public List getFailedTests() { + List r = new ArrayList(); + for (ClassResult clr : classes.values()) { + for (CaseResult cr : clr.getChildren()) { + if(!cr.isPassed()) + r.add(cr); + } + } + Collections.sort(r,CaseResult.BY_AGE); + return r; + } + + void add(CaseResult r) { + String n = r.getSimpleName(); + ClassResult c = classes.get(n); + if(c==null) + classes.put(n,c=new ClassResult(this,n)); + c.add(r); + } + + void freeze() { + passCount=failCount=0; + for (ClassResult cr : classes.values()) { + cr.freeze(); + passCount += cr.getPassCount(); + failCount += cr.getFailCount(); + } + } + + + public int compareTo(PackageResult that) { + return this.packageName.compareTo(that.packageName); + } + + public String getDisplayName() { + return packageName; + } +} diff --git a/core/src/main/java/hudson/tasks/junit/SuiteResult.java b/core/src/main/java/hudson/tasks/junit/SuiteResult.java new file mode 100644 index 0000000000..3858821663 --- /dev/null +++ b/core/src/main/java/hudson/tasks/junit/SuiteResult.java @@ -0,0 +1,95 @@ +package hudson.tasks.junit; + +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Result of one test suite. + * + *

+ * The notion of "test suite" is rather arbitrary in JUnit ant task. + * It's basically one invocation of junit. + * + *

+ * This object is really only used as a part of the persisted + * object tree. + * + * @author Kohsuke Kawaguchi + */ +public final class SuiteResult { + private final String name; + private final String stdout; + private final String stderr; + + /** + * All test cases. + */ + private final List cases = new ArrayList(); + private transient TestResult parent; + + SuiteResult(File xmlReport) throws DocumentException { + Document result = new SAXReader().read(xmlReport); + Element root = result.getRootElement(); + name = root.attributeValue("name"); + + stdout = root.elementText("system-out"); + stderr = root.elementText("system-err"); + + for (Element e : (List)root.elements("testcase")) { + cases.add(new CaseResult(this,e)); + } + } + + public String getName() { + return name; + } + + public String getStdout() { + return stdout; + } + + public String getStderr() { + return stderr; + } + + public TestResult getParent() { + return parent; + } + + public List getCases() { + return cases; + } + + public SuiteResult getPreviousResult() { + TestResult pr = parent.getPreviousResult(); + if(pr==null) return null; + return pr.getSuite(name); + } + + /** + * Returns the {@link CaseResult} whose {@link CaseResult#getName()} + * is the same as the given string. + * + *

+ * Note that test name needs not be unique. + */ + public CaseResult getCase(String name) { + for (CaseResult c : cases) { + if(c.getName().equals(name)) + return c; + } + return null; + } + + public void freeze(TestResult owner) { + this.parent = owner; + for (CaseResult c : cases) + c.freeze(this); + } +} diff --git a/core/src/main/java/hudson/tasks/junit/TabulatedResult.java b/core/src/main/java/hudson/tasks/junit/TabulatedResult.java new file mode 100644 index 0000000000..e358e5297b --- /dev/null +++ b/core/src/main/java/hudson/tasks/junit/TabulatedResult.java @@ -0,0 +1,39 @@ +package hudson.tasks.junit; + +import java.util.Collection; + +/** + * Cumulated result of multiple tests. + * + * @author Kohsuke Kawaguchi + */ +public abstract class TabulatedResult extends TestObject { + + /** + * Gets the human readable title of this result object. + */ + public abstract String getTitle(); + + /** + * Gets the total number of passed tests. + */ + public abstract int getPassCount(); + + /** + * Gets the total number of failed tests. + */ + public abstract int getFailCount(); + + /** + * Gets the total number of tests. + */ + public final int getTotalCount() { + return getPassCount()+getFailCount(); + } + + /** + * Gets the child test result objects. + */ + public abstract Collection getChildren(); + +} diff --git a/core/src/main/java/hudson/tasks/junit/TestObject.java b/core/src/main/java/hudson/tasks/junit/TestObject.java new file mode 100644 index 0000000000..f9e42661b6 --- /dev/null +++ b/core/src/main/java/hudson/tasks/junit/TestObject.java @@ -0,0 +1,21 @@ +package hudson.tasks.junit; + +import hudson.model.Build; +import hudson.model.ModelObject; + +/** + * Base class for all test result objects. + * + * @author Kohsuke Kawaguchi + */ +public abstract class TestObject implements ModelObject { + public abstract Build getOwner(); + + /** + * Gets the counter part of this {@link TestObject} in the previous run. + * + * @return null + * if no such counter part exists. + */ + public abstract TestObject getPreviousResult(); +} diff --git a/core/src/main/java/hudson/tasks/junit/TestResult.java b/core/src/main/java/hudson/tasks/junit/TestResult.java new file mode 100644 index 0000000000..cbe935167a --- /dev/null +++ b/core/src/main/java/hudson/tasks/junit/TestResult.java @@ -0,0 +1,168 @@ +package hudson.tasks.junit; + +import hudson.model.Build; +import hudson.model.BuildListener; +import org.apache.tools.ant.DirectoryScanner; +import org.dom4j.DocumentException; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Root of all the test results for one build. + * + * @author Kohsuke Kawaguchi + */ +public final class TestResult extends MetaTabulatedResult { + /** + * List of all {@link SuiteResult}s in this test. + * This is the core data structure to be persisted in the disk. + */ + private final List suites = new ArrayList(); + + /** + * {@link #suites} keyed by their names for faster lookup. + */ + private transient Map suitesByName; + + /** + * Results tabulated by package. + */ + private transient Map byPackages; + + /*package*/ transient TestResultAction parent; + + /** + * Number of all tests. + */ + private transient int totalTests; + /** + * Number of failed/error tests. + */ + private transient List failedTests; + + /** + * Creates an empty result. + */ + TestResult() { + freeze(); + } + + TestResult(TestResultAction parent, DirectoryScanner results, BuildListener listener) { + this.parent = parent; + String[] includedFiles = results.getIncludedFiles(); + File baseDir = results.getBasedir(); + + long buildTime = parent.owner.getTimestamp().getTimeInMillis(); + + for (String value : includedFiles) { + File reportFile = new File(baseDir, value); + try { + if(buildTime <= reportFile.lastModified()) + // only count files that were actually updated during this build + suites.add(new SuiteResult(reportFile)); + } catch (DocumentException e) { + e.printStackTrace(listener.error("Failed to read "+reportFile)); + } + } + + freeze(); + } + + public String getDisplayName() { + return "Test Result"; + } + + public Build getOwner() { + return parent.owner; + } + + @Override + public TestResult getPreviousResult() { + TestResultAction p = parent.getPreviousResult(); + if(p!=null) + return p.getResult(); + else + return null; + } + + public String getTitle() { + return "Test Result"; + } + + public String getChildTitle() { + return "Package"; + } + + @Override + public int getPassCount() { + return totalTests-getFailCount(); + } + + @Override + public int getFailCount() { + return failedTests.size(); + } + + @Override + public List getFailedTests() { + return failedTests; + } + + @Override + public Collection getChildren() { + return byPackages.values(); + } + + public PackageResult getDynamic(String packageName, StaplerRequest req, StaplerResponse rsp) { + return byPackage(packageName); + } + + public PackageResult byPackage(String packageName) { + return byPackages.get(packageName); + } + + public SuiteResult getSuite(String name) { + return suitesByName.get(name); + } + + /** + * Builds up the transient part of the data structure. + */ + void freeze() { + suitesByName = new HashMap(); + totalTests = 0; + failedTests = new ArrayList(); + byPackages = new TreeMap(); + for (SuiteResult s : suites) { + s.freeze(this); + + suitesByName.put(s.getName(),s); + + totalTests += s.getCases().size(); + for(CaseResult cr : s.getCases()) { + if(!cr.isPassed()) + failedTests.add(cr); + + String pkg = cr.getPackageName(); + PackageResult pr = byPackage(pkg); + if(pr==null) + byPackages.put(pkg,pr=new PackageResult(this,pkg)); + pr.add(cr); + } + } + + Collections.sort(failedTests,CaseResult.BY_AGE); + + for (PackageResult pr : byPackages.values()) + pr.freeze(); + } +} diff --git a/core/src/main/java/hudson/tasks/junit/TestResultAction.java b/core/src/main/java/hudson/tasks/junit/TestResultAction.java new file mode 100644 index 0000000000..bb1960bd25 --- /dev/null +++ b/core/src/main/java/hudson/tasks/junit/TestResultAction.java @@ -0,0 +1,125 @@ +package hudson.tasks.junit; + +import com.thoughtworks.xstream.XStream; +import hudson.XmlFile; +import hudson.model.Action; +import hudson.model.Build; +import hudson.model.BuildListener; +import hudson.tasks.test.AbstractTestResultAction; +import hudson.util.StringConverter2; +import hudson.util.XStream2; +import org.apache.tools.ant.DirectoryScanner; +import org.kohsuke.stapler.StaplerProxy; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * {@link Action} that displays the JUnit test result. + * + *

+ * The actual test reports are isolated by {@link WeakReference} + * so that it doesn't eat up too much memory. + * + * @author Kohsuke Kawaguchi + */ +public class TestResultAction extends AbstractTestResultAction implements StaplerProxy { + private transient WeakReference result; + + // Hudson < 1.25 didn't set these fields, so use Integer + // so that we can distinguish between 0 tests vs not-computed-yet. + private int failCount; + private Integer totalCount; + + + TestResultAction(Build owner, DirectoryScanner results, BuildListener listener) { + super(owner); + + TestResult r = new TestResult(this,results,listener); + + totalCount = r.getTotalCount(); + failCount = r.getFailCount(); + + // persist the data + try { + getDataFile().write(r); + } catch (IOException e) { + e.printStackTrace(listener.fatalError("Failed to save the JUnit test result")); + } + + this.result = new WeakReference(r); + } + + private XmlFile getDataFile() { + return new XmlFile(XSTREAM,new File(owner.getRootDir(), "junitResult.xml")); + } + + public synchronized TestResult getResult() { + if(result==null) { + TestResult r = load(); + result = new WeakReference(r); + return r; + } + TestResult r = result.get(); + if(r==null) { + r = load(); + result = new WeakReference(r); + } + if(totalCount==null) { + totalCount = r.getTotalCount(); + failCount = r.getFailCount(); + } + return r; + } + + @Override + public int getFailCount() { + if(totalCount==null) + getResult(); // this will compute the result + return failCount; + } + + @Override + public int getTotalCount() { + if(totalCount==null) + getResult(); // this will compute the result + return totalCount; + } + + /** + * Loads a {@link TestResult} from disk. + */ + private TestResult load() { + TestResult r; + try { + r = (TestResult)getDataFile().read(); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to load "+getDataFile(),e); + r = new TestResult(); // return a dummy + } + r.parent = this; + r.freeze(); + return r; + } + + public Object getTarget() { + return getResult(); + } + + + + private static final Logger logger = Logger.getLogger(TestResultAction.class.getName()); + + private static final XStream XSTREAM = new XStream2(); + + static { + XSTREAM.alias("result",TestResult.class); + XSTREAM.alias("suite",SuiteResult.class); + XSTREAM.alias("case",CaseResult.class); + XSTREAM.registerConverter(new StringConverter2(),100); + + } +} diff --git a/core/src/main/java/hudson/tasks/junit/package.html b/core/src/main/java/hudson/tasks/junit/package.html new file mode 100644 index 0000000000..c3dfd4b800 --- /dev/null +++ b/core/src/main/java/hudson/tasks/junit/package.html @@ -0,0 +1,3 @@ + +Model objects that represent JUnit test reports. + \ No newline at end of file diff --git a/core/src/main/java/hudson/tasks/package.html b/core/src/main/java/hudson/tasks/package.html new file mode 100644 index 0000000000..b597c62b53 --- /dev/null +++ b/core/src/main/java/hudson/tasks/package.html @@ -0,0 +1,4 @@ + +Built-in Builders and Publishers +that perform the actual heavy-lifting of a build. + \ No newline at end of file diff --git a/core/src/main/java/hudson/tasks/test/AbstractTestResultAction.java b/core/src/main/java/hudson/tasks/test/AbstractTestResultAction.java new file mode 100644 index 0000000000..0c346ccc51 --- /dev/null +++ b/core/src/main/java/hudson/tasks/test/AbstractTestResultAction.java @@ -0,0 +1,187 @@ +package hudson.tasks.test; + +import hudson.model.Action; +import hudson.model.Build; +import hudson.model.Project; +import hudson.model.Result; +import hudson.util.ChartUtil; +import hudson.util.DataSetBuilder; +import hudson.util.ShiftedCategoryAxis; +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.AreaRendererEndType; +import org.jfree.chart.renderer.category.AreaRenderer; +import org.jfree.data.category.CategoryDataset; +import org.jfree.ui.RectangleInsets; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import java.awt.Color; +import java.io.IOException; + +/** + * Common base class for recording test result. + * + *

+ * {@link Project} and {@link Build} recognizes {@link Action}s that derive from this, + * and displays it nicely (regardless of the underlying implementation.) + * + * @author Kohsuke Kawaguchi + */ +public abstract class AbstractTestResultAction implements Action { + public final Build owner; + + protected AbstractTestResultAction(Build owner) { + this.owner = owner; + } + + /** + * Gets the number of failed tests. + */ + public abstract int getFailCount(); + + /** + * Gets the total number of tests. + */ + public abstract int getTotalCount(); + + public String getDisplayName() { + return "Test Result"; + } + + public String getUrlName() { + return "testReport"; + } + + public String getIconFileName() { + return "clipboard.gif"; + } + + public T getPreviousResult() { + return (T)getPreviousResult(getClass()); + } + + private U getPreviousResult(Class type) { + Build b = owner; + while(true) { + b = b.getPreviousBuild(); + if(b==null) + return null; + if(b.getResult()== Result.FAILURE) + continue; + U r = b.getAction(type); + if(r!=null) + return r; + } + } + + /** + * Generates a PNG image for the test result trend. + */ + public void doGraph( StaplerRequest req, StaplerResponse rsp) throws IOException { + if(ChartUtil.awtProblem) { + // not available. send out error message + rsp.sendRedirect2(req.getContextPath()+"/images/headless.png"); + return; + } + + if(req.checkIfModified(owner.getTimestamp(),rsp)) + return; + + class BuildLabel implements Comparable { + private final Build build; + + public BuildLabel(Build build) { + this.build = build; + } + + public int compareTo(BuildLabel that) { + return this.build.number-that.build.number; + } + + public boolean equals(Object o) { + BuildLabel that = (BuildLabel) o; + return build==that.build; + } + + public int hashCode() { + return build.hashCode(); + } + + public String toString() { + return build.getDisplayName(); + } + } + + boolean failureOnly = Boolean.valueOf(req.getParameter("failureOnly")); + + DataSetBuilder dsb = new DataSetBuilder(); + + for( AbstractTestResultAction a=this; a!=null; a=a.getPreviousResult(AbstractTestResultAction.class) ) { + dsb.add( a.getFailCount(), "failed", new BuildLabel(a.owner)); + if(!failureOnly) + dsb.add( a.getTotalCount()-a.getFailCount(),"total", new BuildLabel(a.owner)); + } + + ChartUtil.generateGraph(req,rsp,createChart(dsb.build()),500,200); + } + + private JFreeChart createChart(CategoryDataset dataset) { + + final JFreeChart chart = ChartFactory.createStackedAreaChart( + null, // chart title + null, // unused + "count", // range axis label + dataset, // data + PlotOrientation.VERTICAL, // orientation + false, // include legend + true, // tooltips + false // urls + ); + + // NOW DO SOME OPTIONAL CUSTOMISATION OF THE CHART... + + // set the background color for the chart... + +// final StandardLegend legend = (StandardLegend) chart.getLegend(); +// legend.setAnchor(StandardLegend.SOUTH); + + chart.setBackgroundPaint(Color.white); + + final CategoryPlot plot = chart.getCategoryPlot(); + + // plot.setAxisOffset(new Spacer(Spacer.ABSOLUTE, 5.0, 5.0, 5.0, 5.0)); + plot.setBackgroundPaint(Color.WHITE); + plot.setOutlinePaint(null); + plot.setForegroundAlpha(0.8f); +// plot.setDomainGridlinesVisible(true); +// plot.setDomainGridlinePaint(Color.white); + plot.setRangeGridlinesVisible(true); + plot.setRangeGridlinePaint(Color.black); + + CategoryAxis domainAxis = new ShiftedCategoryAxis(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()); + + AreaRenderer ar = (AreaRenderer) plot.getRenderer(); + ar.setEndType(AreaRendererEndType.TRUNCATE); + ar.setSeriesPaint(0,new Color(0xEF,0x29,0x29)); + ar.setSeriesPaint(1,new Color(0x72,0x9F,0xCF)); + + // crop extra space around the graph + plot.setInsets(new RectangleInsets(0,0,0,5.0)); + + return chart; + } +} diff --git a/core/src/main/java/hudson/tasks/test/package.html b/core/src/main/java/hudson/tasks/test/package.html new file mode 100644 index 0000000000..84587710a6 --- /dev/null +++ b/core/src/main/java/hudson/tasks/test/package.html @@ -0,0 +1,5 @@ + +Defines contracts that need to be implemented by a test reporting +action (such as the built-in JUnit one). This contract allows Project +to display a test result trend history. + \ No newline at end of file diff --git a/core/src/main/java/hudson/triggers/SCMTrigger.java b/core/src/main/java/hudson/triggers/SCMTrigger.java new file mode 100644 index 0000000000..808f9be829 --- /dev/null +++ b/core/src/main/java/hudson/triggers/SCMTrigger.java @@ -0,0 +1,197 @@ +package hudson.triggers; + +import antlr.ANTLRException; +import hudson.Util; +import hudson.model.Action; +import hudson.model.Build; +import hudson.model.Descriptor; +import hudson.model.Project; +import hudson.model.TaskListener; +import hudson.util.StreamTaskListener; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Date; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kohsuke.stapler.StaplerRequest; + +/** + * {@link Trigger} that checks for SCM updates periodically. + * + * @author Kohsuke Kawaguchi + */ +public class SCMTrigger extends Trigger { + /** + * Non-null if the polling is in progress. + * @guardedBy this + */ + private transient boolean pollingScheduled; + + /** + * Non-null if the polling is in progress. + * @guardedBy this + */ + private transient Thread pollingThread; + + /** + * Signal to the polling thread to abort now. + */ + private transient boolean abortNow; + + public SCMTrigger(String cronTabSpec) throws ANTLRException { + super(cronTabSpec); + } + + protected synchronized void run() { + if(pollingScheduled) + return; // noop + pollingScheduled = true; + + // otherwise do it now + startPolling(); + } + + public Action getProjectAction() { + return new SCMAction(); + } + + /** + * Makes sure that the polling is aborted. + */ + public synchronized void abort() throws InterruptedException { + if(pollingThread!=null && pollingThread.isAlive()) { + System.out.println("killing polling"); + abortNow = true; + pollingThread.interrupt(); + pollingThread.join(); + abortNow = false; + } + } + + /** + * Start polling if it's scheduled. + */ + public synchronized void startPolling() { + Build b = project.getLastBuild(); + + if(b!=null && b.isBuilding()) + return; // build in progress + + if(pollingThread!=null && pollingThread.isAlive()) + return; // polling already in progress + + if(!pollingScheduled) + return; // not scheduled + + pollingScheduled = false; + pollingThread = new Thread() { + private boolean runPolling() { + try { + // to make sure that the log file contains up-to-date text, + // don't do buffering. + OutputStream fos = new FileOutputStream(getLogFile()); + TaskListener listener = new StreamTaskListener(fos); + + try { + LOGGER.info("Polling SCM changes of "+project.getName()); + + PrintStream logger = listener.getLogger(); + long start = System.currentTimeMillis(); + logger.println("Started on "+new Date().toLocaleString()); + boolean result = project.pollSCMChanges(listener); + logger.println("Done. Took "+Util.getTimeSpanString(System.currentTimeMillis()-start)); + if(result) + logger.println("Changes found"); + else + logger.println("No changes"); + return result; + } finally { + fos.close(); + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE,"Failed to record SCM polling",e); + return false; + } + } + + public void run() { + boolean repeat; + do { + if(runPolling()) { + LOGGER.info("SCM changes detected in "+project.getName()); + project.scheduleBuild(); + } + if(abortNow) + return; // terminate now + + synchronized(SCMTrigger.this) { + repeat = pollingScheduled; + pollingScheduled = false; + } + } while(repeat); + } + }; + pollingThread.start(); + } + + /** + * Returns the file that records the last/current polling activity. + */ + public File getLogFile() { + return new File(project.getRootDir(),"scm-polling.log"); + } + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final Descriptor DESCRIPTOR = new Descriptor(SCMTrigger.class) { + public String getDisplayName() { + return "Poll SCM"; + } + + public String getHelpFile() { + return "/help/project-config/poll-scm.html"; + } + + public Trigger newInstance(StaplerRequest req) throws FormException { + try { + return new SCMTrigger(req.getParameter("scmpoll_spec")); + } catch (ANTLRException e) { + throw new FormException(e.toString(),e,"scmpoll_spec"); + } + } + }; + + /** + * Action object for {@link Project}. Used to display the polling log. + */ + public final class SCMAction implements Action { + public Project getOwner() { + return project; + } + + public String getIconFileName() { + return "clipboard.gif"; + } + + public String getDisplayName() { + return project.getScm().getDescriptor().getDisplayName()+" Polling Log"; + } + + public String getUrlName() { + return "scmPollLog"; + } + + public String getLog() throws IOException { + return Util.loadFile(getLogFile()); + } + } + + private static final Logger LOGGER = Logger.getLogger(SCMTrigger.class.getName()); +} diff --git a/core/src/main/java/hudson/triggers/TimerTrigger.java b/core/src/main/java/hudson/triggers/TimerTrigger.java new file mode 100644 index 0000000000..e3cb45439c --- /dev/null +++ b/core/src/main/java/hudson/triggers/TimerTrigger.java @@ -0,0 +1,45 @@ +package hudson.triggers; + +import antlr.ANTLRException; +import hudson.model.Descriptor; + +import org.kohsuke.stapler.StaplerRequest; + +/** + * {@link Trigger} that runs a job periodically. + * + * @author Kohsuke Kawaguchi + */ +public class TimerTrigger extends Trigger { + public TimerTrigger(String cronTabSpec) throws ANTLRException { + super(cronTabSpec); + } + + protected void run() { + project.scheduleBuild(); + } + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final Descriptor DESCRIPTOR = new Descriptor(TimerTrigger.class) { + public String getDisplayName() { + return "Build periodically"; + } + + public String getHelpFile() { + return "/help/project-config/timer.html"; + } + + public Trigger newInstance(StaplerRequest req) throws FormException { + try { + return new TimerTrigger(req.getParameter("timer_spec")); + } catch (ANTLRException e) { + throw new FormException(e.toString(),e,"timer_spec"); + } + } + }; + + +} diff --git a/core/src/main/java/hudson/triggers/Trigger.java b/core/src/main/java/hudson/triggers/Trigger.java new file mode 100644 index 0000000000..531d06a8c2 --- /dev/null +++ b/core/src/main/java/hudson/triggers/Trigger.java @@ -0,0 +1,143 @@ +package hudson.triggers; + +import antlr.ANTLRException; +import hudson.model.Action; +import hudson.model.Build; +import hudson.model.Describable; +import hudson.model.FingerprintCleanupThread; +import hudson.model.Hudson; +import hudson.model.Project; +import hudson.model.WorkspaceCleanupThread; +import hudson.scheduler.CronTabList; +import hudson.ExtensionPoint; +import hudson.tasks.BuildStep; + +import java.io.InvalidObjectException; +import java.io.ObjectStreamException; +import java.util.Calendar; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.Timer; +import java.util.TimerTask; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Triggers a {@link Build}. + * + *

+ * To register a custom {@link Trigger} from a plugin, + * add it to {@link Triggers#TRIGGERS}. + * + * @author Kohsuke Kawaguchi + */ +public abstract class Trigger implements Describable, ExtensionPoint { + + /** + * Called when a {@link Trigger} is loaded into memory and started. + * + * @param project + * given so that the persisted form of this object won't have to have a back pointer. + */ + public void start(Project project) { + this.project = project; + } + + /** + * Executes the triggered task. + * + * This method is invoked when the crontab matches the current time. + */ + protected abstract void run(); + + /** + * Called before a {@link Trigger} is removed. + * Under some circumstances, this may be invoked more than once for + * a given {@link Trigger}, so be prepared for that. + */ + public void stop() {} + + /** + * Returns an action object if this {@link Trigger} has an action + * to contribute to a {@link Project}. + */ + public Action getProjectAction() { + return null; + } + + + + protected final String spec; + protected transient CronTabList tabs; + protected transient Project project; + + protected Trigger(String cronTabSpec) throws ANTLRException { + this.spec = cronTabSpec; + this.tabs = CronTabList.create(cronTabSpec); + } + + protected Trigger() { + this.spec = ""; + this.tabs = new CronTabList(Collections.EMPTY_LIST); + } + + public String getSpec() { + return spec; + } + + private Object readResolve() throws ObjectStreamException { + try { + tabs = CronTabList.create(spec); + } catch (ANTLRException e) { + InvalidObjectException x = new InvalidObjectException(e.getMessage()); + x.initCause(e); + throw x; + } + return this; + } + + + /** + * Runs every minute to check {@link TimerTrigger} and schedules build. + */ + private static class Cron extends TimerTask { + private final Calendar cal = new GregorianCalendar(); + + public void run() { + LOGGER.fine("cron checking "+cal.getTime().toLocaleString()); + + try { + Hudson inst = Hudson.getInstance(); + for (Project p : inst.getProjects()) { + for (Trigger t : p.getTriggers().values()) { + LOGGER.fine("cron checking "+p.getName()); + if(t.tabs.check(cal)) { + LOGGER.fine("cron triggered "+p.getName()); + t.run(); + } + } + } + } catch (Throwable e) { + LOGGER.log(Level.WARNING,"Cron thread throw an exception",e); + // bug in the code. Don't let the thread die. + e.printStackTrace(); + } + + cal.add(Calendar.MINUTE,1); + } + } + + private static final Logger LOGGER = Logger.getLogger(Trigger.class.getName()); + + public static final Timer timer = new Timer(); // "Hudson cron thread"); -- this is a new constructor since 1.5 + + public static void init() { + timer.scheduleAtFixedRate(new Cron(), 1000*60, 1000*60/*every minute*/); + + // clean up fingerprint once a day + long HOUR = 1000*60*60; + long DAY = HOUR*24; + timer.scheduleAtFixedRate(new FingerprintCleanupThread(),DAY,DAY); + timer.scheduleAtFixedRate(new WorkspaceCleanupThread(),DAY+4*HOUR,DAY); + } +} diff --git a/core/src/main/java/hudson/triggers/Triggers.java b/core/src/main/java/hudson/triggers/Triggers.java new file mode 100644 index 0000000000..7324db70d6 --- /dev/null +++ b/core/src/main/java/hudson/triggers/Triggers.java @@ -0,0 +1,18 @@ +package hudson.triggers; + +import hudson.model.Descriptor; + +import java.util.List; + +/** + * @author Kohsuke Kawaguchi + */ +public class Triggers { + /** + * List of all installed {@link Trigger}s. + */ + public static final List> TRIGGERS = Descriptor.toList( + SCMTrigger.DESCRIPTOR, + TimerTrigger.DESCRIPTOR + ); +} diff --git a/core/src/main/java/hudson/triggers/package.html b/core/src/main/java/hudson/triggers/package.html new file mode 100644 index 0000000000..720afcbc02 --- /dev/null +++ b/core/src/main/java/hudson/triggers/package.html @@ -0,0 +1,3 @@ + +Built-in Triggers that run periodically to kick a new build. + \ No newline at end of file diff --git a/core/src/main/java/hudson/util/ArgumentListBuilder.java b/core/src/main/java/hudson/util/ArgumentListBuilder.java new file mode 100644 index 0000000000..0123227e83 --- /dev/null +++ b/core/src/main/java/hudson/util/ArgumentListBuilder.java @@ -0,0 +1,52 @@ +package hudson.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +/** + * Used to build up arguments for a process invocation. + * + * @author Kohsuke Kawaguchi + */ +public class ArgumentListBuilder { + private final List args = new ArrayList(); + + public ArgumentListBuilder add(String a) { + args.add(a); + return this; + } + + /** + * Adds an argument by quoting it. + * This is necessary only in a rare circumstance, + * such as when adding argument for ssh and rsh. + * + * Normal process invcations don't need it, because each + * argument is treated as its own string and never merged into one. + */ + public ArgumentListBuilder addQuoted(String a) { + return add('"'+a+'"'); + } + + public ArgumentListBuilder add(String... args) { + for (String arg : args) { + add(arg); + } + return this; + } + + /** + * Decomposes the given token into multiple arguments by splitting via whitespace. + */ + public ArgumentListBuilder addTokenized(String s) { + StringTokenizer tokens = new StringTokenizer(s); + while(tokens.hasMoreTokens()) + add(tokens.nextToken()); + return this; + } + + public String[] toCommandArray() { + return args.toArray(new String[args.size()]); + } +} diff --git a/core/src/main/java/hudson/util/AtomicFileWriter.java b/core/src/main/java/hudson/util/AtomicFileWriter.java new file mode 100644 index 0000000000..9ab5819cee --- /dev/null +++ b/core/src/main/java/hudson/util/AtomicFileWriter.java @@ -0,0 +1,58 @@ +package hudson.util; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; + +/** + * Buffered {@link FileWriter} that uses UTF-8. + * + *

+ * The write operation is atomic when used for overwriting; + * it either leaves the original file intact, or it completely rewrites it with new contents. + * + * @author Kohsuke Kawaguchi + */ +public class AtomicFileWriter extends Writer { + + private final Writer core; + private final File tmpFile; + private final File destFile; + + public AtomicFileWriter(File f) throws IOException { + tmpFile = File.createTempFile("atomic",null,f.getParentFile()); + destFile = f; + core = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tmpFile),"UTF-8")); + } + + public void write(int c) throws IOException { + core.write(c); + } + + public void write(String str, int off, int len) throws IOException { + core.write(str,off,len); + } + + public void write(char cbuf[], int off, int len) throws IOException { + core.write(cbuf,off,len); + } + + public void flush() throws IOException { + core.flush(); + } + + public void close() throws IOException { + core.close(); + } + + public void commit() throws IOException { + close(); + if(destFile.exists() && !destFile.delete()) + throw new IOException("Unable to delete "+destFile); + tmpFile.renameTo(destFile); + } +} diff --git a/core/src/main/java/hudson/util/CharSpool.java b/core/src/main/java/hudson/util/CharSpool.java new file mode 100644 index 0000000000..67b8626dc2 --- /dev/null +++ b/core/src/main/java/hudson/util/CharSpool.java @@ -0,0 +1,62 @@ +package hudson.util; + +import java.io.IOException; +import java.io.Writer; +import java.util.LinkedList; +import java.util.List; + +/** + * {@link Writer} that spools the output and writes to another {@link Writer} later. + * + * @author Kohsuke Kawaguchi + */ +public final class CharSpool extends Writer { + private List buf; + + private char[] last = new char[1024]; + private int pos; + + public void write(char cbuf[], int off, int len) { + while(len>0) { + int sz = Math.min(last.length-pos,len); + System.arraycopy(cbuf,off,last,pos,sz); + len -= sz; + off += sz; + pos += sz; + renew(); + } + } + + private void renew() { + if(pos(); + buf.add(last); + last = new char[1024]; + pos = 0; + } + + public void write(int c) { + renew(); + last[pos++] = (char)c; + } + + public void flush() { + // noop + } + + public void close() { + // noop + } + + public void writeTo(Writer w) throws IOException { + if(buf!=null) { + for (char[] cb : buf) { + w.write(cb); + } + } + w.write(last,0,pos); + } +} diff --git a/core/src/main/java/hudson/util/ChartUtil.java b/core/src/main/java/hudson/util/ChartUtil.java new file mode 100644 index 0000000000..54d63d9667 --- /dev/null +++ b/core/src/main/java/hudson/util/ChartUtil.java @@ -0,0 +1,50 @@ +package hudson.util; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.jfree.chart.JFreeChart; + +import javax.servlet.ServletOutputStream; +import javax.imageio.ImageIO; +import java.awt.Font; +import java.awt.HeadlessException; +import java.awt.image.BufferedImage; +import java.io.IOException; + +/** + * See issue 93. Detect an error in X11 and handle it gracefully. + * + * @author Kohsuke Kawaguchi + */ +public class ChartUtil { + + /** + * See issue 93. Detect an error in X11 and handle it gracefully. + */ + public static boolean awtProblem = false; + + public static void generateGraph(StaplerRequest req, StaplerResponse rsp, JFreeChart chart, int defaultW, int defaultH) throws IOException { + try { + String w = req.getParameter("width"); + if(w==null) w=String.valueOf(defaultW); + String h = req.getParameter("height"); + if(h==null) h=String.valueOf(defaultH); + BufferedImage image = chart.createBufferedImage(Integer.parseInt(w),Integer.parseInt(h)); + rsp.setContentType("image/png"); + ServletOutputStream os = rsp.getOutputStream(); + ImageIO.write(image, "PNG", os); + os.close(); + } catch(HeadlessException e) { + // not available. send out error message + rsp.sendRedirect2(req.getContextPath()+"/images/headless.png"); + } + } + + static { + try { + new Font("SansSerif",Font.BOLD,18).toString(); + } catch (Throwable t) { + awtProblem = true; + } + } +} diff --git a/core/src/main/java/hudson/util/CountingOutputStream.java b/core/src/main/java/hudson/util/CountingOutputStream.java new file mode 100644 index 0000000000..9853508455 --- /dev/null +++ b/core/src/main/java/hudson/util/CountingOutputStream.java @@ -0,0 +1,37 @@ +package hudson.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * {@link FilterOutputStream} that counts the number of bytes that were written. + * + * @author Kohsuke Kawaguchi + */ +public class CountingOutputStream extends FilterOutputStream { + private int count = 0; + + public int getCount() { + return count; + } + + public CountingOutputStream(OutputStream out) { + super(out); + } + + public void write(int b) throws IOException { + out.write(b); + count++; + } + + public void write(byte b[]) throws IOException { + out.write(b); + count += b.length; + } + + public void write(byte b[], int off, int len) throws IOException { + out.write(b, off, len); + count += len; + } +} diff --git a/core/src/main/java/hudson/util/DataSetBuilder.java b/core/src/main/java/hudson/util/DataSetBuilder.java new file mode 100644 index 0000000000..6e102ec146 --- /dev/null +++ b/core/src/main/java/hudson/util/DataSetBuilder.java @@ -0,0 +1,48 @@ +package hudson.util; + +import org.jfree.data.category.CategoryDataset; +import org.jfree.data.category.DefaultCategoryDataset; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; + +/** + * Builds {@link CategoryDataset}. + * + *

+ * This code works around an issue in {@link DefaultCategoryDataset} where + * order of addition changes the way they are drawn. + */ +public final class DataSetBuilder { + + private List values = new ArrayList(); + private List rows = new ArrayList(); + private List columns = new ArrayList(); + + public void add( Number value, Row rowKey, Column columnKey ) { + values.add(value); + rows.add(rowKey); + columns.add(columnKey); + } + + public CategoryDataset build() { + DefaultCategoryDataset ds = new DefaultCategoryDataset(); + + TreeSet rowSet = new TreeSet(rows); + TreeSet colSet = new TreeSet(columns); + + Comparable[] _rows = rowSet.toArray(new Comparable[rowSet.size()]); + Comparable[] _cols = colSet.toArray(new Comparable[colSet.size()]); + + // insert rows and columns in the right order + for (Comparable r : _rows) + ds.setValue(null, r, _cols[0]); + for (Comparable c : _cols) + ds.setValue(null, _rows[0], c); + + for( int i=0; i"); + } + + /** + * Sends out an HTML fragment that indicates an error. + */ + public void error(String message) throws IOException, ServletException { + response.setContentType("text/html"); + response.getWriter().print("

"+message+"
"); + } +} diff --git a/core/src/main/java/hudson/util/HexBinaryConverter.java b/core/src/main/java/hudson/util/HexBinaryConverter.java new file mode 100644 index 0000000000..1e399de315 --- /dev/null +++ b/core/src/main/java/hudson/util/HexBinaryConverter.java @@ -0,0 +1,33 @@ +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 hudson.Util; + +/** + * @author Kohsuke Kawaguchi + */ +public class HexBinaryConverter implements Converter { + + public boolean canConvert(Class type) { + return type==byte[].class; + } + + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + byte[] data = (byte[]) source; + writer.setValue(Util.toHexString(data)); + } + + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + String data = reader.getValue(); // needs to be called before hasMoreChildren. + + byte[] r = new byte[data.length()/2]; + for( int i=0; i + * index.jelly would display a nice friendly error page. + * + * @author Kohsuke Kawaguchi + */ +public class IncompatibleVMDetected { + + public Map getSystemProperties() { + return System.getProperties(); + } +} diff --git a/core/src/main/java/hudson/util/NullStream.java b/core/src/main/java/hudson/util/NullStream.java new file mode 100644 index 0000000000..4e274ca4dd --- /dev/null +++ b/core/src/main/java/hudson/util/NullStream.java @@ -0,0 +1,19 @@ +package hudson.util; + +import java.io.OutputStream; + +/** + * @author Kohsuke Kawaguchi + */ +public final class NullStream extends OutputStream { + public NullStream() {} + + public void write(byte b[]) { + } + + public void write(byte b[], int off, int len) { + } + + public void write(int b) { + } +} diff --git a/core/src/main/java/hudson/util/OneShotEvent.java b/core/src/main/java/hudson/util/OneShotEvent.java new file mode 100644 index 0000000000..cff5a4fc6b --- /dev/null +++ b/core/src/main/java/hudson/util/OneShotEvent.java @@ -0,0 +1,49 @@ +package hudson.util; + +/** + * Concurrency primitive "event". + * + * @author Kohsuke Kawaguchi + */ +public final class OneShotEvent { + private boolean signaled; + + /** + * Non-blocking method that signals this event. + */ + public synchronized void signal() { + if(signaled) return; + this.signaled = true; + notify(); + } + + /** + * Blocks until the event becomes the signaled state. + * + *

+ * This method blocks infinitely until a value is offered. + */ + public synchronized void block() throws InterruptedException { + while(!signaled) + wait(); + } + + /** + * Blocks until the event becomes the signaled state. + * + *

+ * If the specified amount of time elapses, + * this method returns null even if the value isn't offered. + */ + public synchronized void block(long timeout) throws InterruptedException { + if(!signaled) + wait(timeout); + } + + /** + * Returns true if a value is offered. + */ + public synchronized boolean isSignaled() { + return signaled; + } +} diff --git a/core/src/main/java/hudson/util/RingBufferLogHandler.java b/core/src/main/java/hudson/util/RingBufferLogHandler.java new file mode 100644 index 0000000000..db8f67964b --- /dev/null +++ b/core/src/main/java/hudson/util/RingBufferLogHandler.java @@ -0,0 +1,59 @@ +package hudson.util; + +import java.util.AbstractList; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +/** + * Log {@link Handler} that stores the log records into a ring buffer. + * + * @author Kohsuke Kawaguchi + */ +public class RingBufferLogHandler extends Handler { + + private int start = 0; + private final LogRecord[] records; + private int size = 0; + + public RingBufferLogHandler() { + this(256); + } + + public RingBufferLogHandler(int ringSize) { + records = new LogRecord[ringSize]; + } + + public synchronized void publish(LogRecord record) { + int len = records.length; + records[(start+size)%len]=record; + if(size==len) { + start++; + } else { + size++; + } + } + + /** + * Returns the list view of {@link LogRecord}s in the ring buffer. + * + *

+ * New records are always placed early in the list. + */ + public List getView() { + return new AbstractList() { + public LogRecord get(int index) { + // flip the order + return records[(start+(size-(index+1)))%records.length]; + } + + public int size() { + return size; + } + }; + } + + // noop + public void flush() {} + public void close() throws SecurityException {} +} diff --git a/core/src/main/java/hudson/util/RobustCollectionConverter.java b/core/src/main/java/hudson/util/RobustCollectionConverter.java new file mode 100644 index 0000000000..2eefa6a7e8 --- /dev/null +++ b/core/src/main/java/hudson/util/RobustCollectionConverter.java @@ -0,0 +1,38 @@ +package hudson.util; + +import com.thoughtworks.xstream.converters.collections.CollectionConverter; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.mapper.Mapper; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.alias.CannotResolveClassException; + +import java.util.Collection; + +/** + * {@link CollectionConverter} that ignores {@link CannotResolveClassException}. + * + *

+ * This allows Hudson to load XML files that contain non-existent classes + * (the expected scenario is that those classes belong to plugins that were unloaded.) + * + * @author Kohsuke Kawaguchi + */ +public class RobustCollectionConverter extends CollectionConverter { + public RobustCollectionConverter(Mapper mapper) { + super(mapper); + } + + protected void populateCollection(HierarchicalStreamReader reader, UnmarshallingContext context, Collection collection) { + while (reader.hasMoreChildren()) { + reader.moveDown(); + try { + Object item = readItem(reader, context, collection); + collection.add(item); + } catch (CannotResolveClassException e) { + System.err.println("failed to locate class: "+e); + } + reader.moveUp(); + } + } + +} diff --git a/core/src/main/java/hudson/util/RunList.java b/core/src/main/java/hudson/util/RunList.java new file mode 100644 index 0000000000..3c8787c9a4 --- /dev/null +++ b/core/src/main/java/hudson/util/RunList.java @@ -0,0 +1,87 @@ +package hudson.util; + +import hudson.model.Job; +import hudson.model.Result; +import hudson.model.Run; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.Iterator; +import java.util.List; + +/** + * {@link List} of {@link Run}s. + * + * @author Kohsuke Kawaguchi + */ +public class RunList extends ArrayList { + + public RunList() { + } + + public RunList(Job j) { + addAll(j.getBuilds()); + } + + public RunList(Collection jobs) { + for (Job j : jobs) + addAll(j.getBuilds()); + Collections.sort(this,Run.ORDER_BY_DATE); + } + + public static RunList fromRuns(Collection runs) { + RunList r = new RunList(); + r.addAll(runs); + return r; + } + + /** + * Filter the list to non-successful builds only. + */ + public RunList failureOnly() { + for (Iterator itr = iterator(); itr.hasNext();) { + Run r = itr.next(); + if(r.getResult()==Result.SUCCESS) + itr.remove(); + } + return this; + } + + /** + * Filter the list to regression builds only. + */ + public RunList regressionOnly() { + for (Iterator itr = iterator(); itr.hasNext();) { + Run r = itr.next(); + if(!r.getBuildStatusSummary().isWorse) + itr.remove(); + } + return this; + } + + /** + * Reduce the size of the list by only leaving relatively new ones. + */ + public RunList newBuilds() { + GregorianCalendar threshold = new GregorianCalendar(); + threshold.add(Calendar.DAY_OF_YEAR,-7); + + int count=0; + + for (Iterator itr = iterator(); itr.hasNext();) { + Run r = itr.next(); + // at least put 10 items + if(count<10) { + count++; + continue; + } + // anything older than 7 days will be ignored + if(r.getTimestamp().before(threshold)) + itr.remove(); + } + return this; + } +} diff --git a/core/src/main/java/hudson/util/Service.java b/core/src/main/java/hudson/util/Service.java new file mode 100644 index 0000000000..a6c499a512 --- /dev/null +++ b/core/src/main/java/hudson/util/Service.java @@ -0,0 +1,56 @@ +package hudson.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.Collection; +import java.util.Enumeration; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Load classes by looking up META-INF/services. + * + * @author Kohsuke Kawaguchi + */ +public class Service { + /** + * Look up META-INF/service/SPICLASSNAME from the classloader + * and all the discovered classes into the given collection. + */ + public static void load(Class spi, ClassLoader cl, Collection> result) { + try { + Enumeration e = cl.getResources("META-INF/services/" + spi.getName()); + while(e.hasMoreElements()) { + BufferedReader r = null; + URL url = e.nextElement(); + try { + r = new BufferedReader(new InputStreamReader(url.openStream(),"UTF-8")); + String line; + while((line=r.readLine())!=null) { + if(line.startsWith("#")) + continue; // comment line + line = line.trim(); + if(line.length()==0) + continue; // empty line. ignore. + + try { + result.add(cl.loadClass(line).asSubclass(spi)); + } catch (ClassNotFoundException x) { + LOGGER.log(Level.WARNING, "Failed to load "+line, x); + } + } + } catch (IOException x) { + LOGGER.log(Level.WARNING, "Failed to load "+url, x); + } finally { + r.close(); + } + } + } catch (IOException x) { + LOGGER.log(Level.WARNING, "Failed to look up service providers for "+spi, x); + } + } + + private static final Logger LOGGER = Logger.getLogger(Service.class.getName()); +} diff --git a/core/src/main/java/hudson/util/ShiftedCategoryAxis.java b/core/src/main/java/hudson/util/ShiftedCategoryAxis.java new file mode 100644 index 0000000000..7ed8ce7e8a --- /dev/null +++ b/core/src/main/java/hudson/util/ShiftedCategoryAxis.java @@ -0,0 +1,36 @@ +package hudson.util; + +import org.jfree.chart.axis.CategoryAxis; +import org.jfree.ui.RectangleEdge; + +import java.awt.geom.Rectangle2D; + +/** + * {@link CategoryAxis} shifted to left to eliminate redundant space + * between area and the Y-axis. + */ +public final class ShiftedCategoryAxis extends CategoryAxis { + public ShiftedCategoryAxis(String label) { + super(label); + } + + protected double calculateCategorySize(int categoryCount, Rectangle2D area, RectangleEdge edge) { + // we cut the left-half of the first item and the right-half of the last item, + // so we have more space + return super.calculateCategorySize(categoryCount-1, area, edge); + } + + public double getCategoryEnd(int category, int categoryCount, Rectangle2D area, RectangleEdge edge) { + return super.getCategoryStart(category, categoryCount, area, edge) + + calculateCategorySize(categoryCount, area, edge) / 2; + } + + public double getCategoryMiddle(int category, int categoryCount, Rectangle2D area, RectangleEdge edge) { + return super.getCategoryStart(category, categoryCount, area, edge); + } + + public double getCategoryStart(int category, int categoryCount, Rectangle2D area, RectangleEdge edge) { + return super.getCategoryStart(category, categoryCount, area, edge) + - calculateCategorySize(categoryCount, area, edge) / 2; + } +} diff --git a/core/src/main/java/hudson/util/StreamTaskListener.java b/core/src/main/java/hudson/util/StreamTaskListener.java new file mode 100644 index 0000000000..9d82785349 --- /dev/null +++ b/core/src/main/java/hudson/util/StreamTaskListener.java @@ -0,0 +1,43 @@ +package hudson.util; + +import hudson.model.TaskListener; + +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Writer; + +/** + * {@link TaskListener} that generates output into a single stream. + * + * @author Kohsuke Kawaguchi + */ +public final class StreamTaskListener implements TaskListener { + private final PrintStream out; + + public StreamTaskListener(PrintStream out) { + this.out = out; + } + + public StreamTaskListener(OutputStream out) { + this(new PrintStream(out)); + } + + public StreamTaskListener(Writer w) { + this(new WriterOutputStream(w)); + } + + public PrintStream getLogger() { + return out; + } + + public PrintWriter error(String msg) { + out.println(msg); + return new PrintWriter(new OutputStreamWriter(out),true); + } + + public PrintWriter fatalError(String msg) { + return error(msg); + } +} diff --git a/core/src/main/java/hudson/util/StringConverter2.java b/core/src/main/java/hudson/util/StringConverter2.java new file mode 100644 index 0000000000..000eb73cb4 --- /dev/null +++ b/core/src/main/java/hudson/util/StringConverter2.java @@ -0,0 +1,27 @@ +package hudson.util; + +import com.thoughtworks.xstream.converters.basic.AbstractBasicConverter; +import com.thoughtworks.xstream.converters.basic.StringConverter; + +/** + * The default {@link StringConverter} in XStream + * uses {@link String#intern()}, which stresses the + * (rather limited) PermGen space with a large XML file. + * + *

+ * Use this to avoid that (instead those strings will + * now be allocated to the heap space.) + * + * @author Kohsuke Kawaguchi + */ +public class StringConverter2 extends AbstractBasicConverter { + + public boolean canConvert(Class type) { + return type.equals(String.class); + } + + protected Object fromString(String str) { + return str; + } + +} diff --git a/core/src/main/java/hudson/util/TextFile.java b/core/src/main/java/hudson/util/TextFile.java new file mode 100644 index 0000000000..cd25fa4a07 --- /dev/null +++ b/core/src/main/java/hudson/util/TextFile.java @@ -0,0 +1,62 @@ +package hudson.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Represents a text file. + * + * Provides convenience methods for reading and writing to it. + * + * @author Kohsuke Kawaguchi + */ +public class TextFile { + private final File file; + + public TextFile(File file) { + this.file = file; + } + + public boolean exists() { + return file.exists(); + } + + /** + * Reads the entire contents and returns it. + */ + public String read() throws IOException { + StringWriter out = new StringWriter(); + PrintWriter w = new PrintWriter(out); + BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF-8")); + try { + String line; + while((line=in.readLine())!=null) + w.println(line); + } finally{ + in.close(); + } + return out.toString(); + } + + /** + * Overwrites the file by the given string. + */ + public void write(String text) throws IOException { + AtomicFileWriter w = new AtomicFileWriter(file); + w.write(text); + w.commit(); + } + + public String readTrim() throws IOException { + return read().trim(); + } + + public String toString() { + return file.toString(); + } +} diff --git a/core/src/main/java/hudson/util/WriterOutputStream.java b/core/src/main/java/hudson/util/WriterOutputStream.java new file mode 100644 index 0000000000..cfbbaabf2d --- /dev/null +++ b/core/src/main/java/hudson/util/WriterOutputStream.java @@ -0,0 +1,93 @@ +package hudson.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; + +/** + * {@link OutputStream} that writes to {@link Writer} + * by assuming the platform default encoding. + * + * @author Kohsuke Kawaguchi + */ +public class WriterOutputStream extends OutputStream { + private final Writer writer; + private final CharsetDecoder decoder; + + private ByteBuffer buf = ByteBuffer.allocate(1024); + private CharBuffer out = CharBuffer.allocate(1024); + + public WriterOutputStream(Writer out) { + this.writer = out; + decoder = Charset.defaultCharset().newDecoder(); + } + + public void write(int b) throws IOException { + if(buf.remaining()==0) + decode(false); + buf.put((byte)b); + } + + public void write(byte b[], int off, int len) throws IOException { + while(len>0) { + if(buf.remaining()==0) + decode(false); + int sz = Math.min(buf.remaining(),len); + buf.put(b,off,sz); + off += sz; + len -= sz; + } + } + + public void flush() throws IOException { + decode(false); + flushOutput(); + writer.flush(); + } + + private void flushOutput() throws IOException { + writer.write(out.array(),0,out.position()); + out.clear(); + } + + public void close() throws IOException { + decode(true); + flushOutput(); + writer.close(); + + buf.rewind(); + } + + /** + * Decodes the contents of {@link #buf} as much as possible to {@link #out}. + * If necessary {@link #out} is further sent to {@link #writer}. + * + *

+ * When this method returns, the {@link #buf} is back to the 'accumulation' + * mode. + * + * @param last + * if true, tell the decoder that all the input bytes are ready. + */ + private void decode(boolean last) throws IOException { + buf.flip(); + while(true) { + CoderResult r = decoder.decode(buf, out, last); + if(r==CoderResult.OVERFLOW) { + flushOutput(); + continue; + } + if(r==CoderResult.UNDERFLOW) { + buf.compact(); + return; + } + // otherwise treat it as an error + r.throwException(); + } + } +} diff --git a/core/src/main/java/hudson/util/XStream2.java b/core/src/main/java/hudson/util/XStream2.java new file mode 100644 index 0000000000..9e4c362273 --- /dev/null +++ b/core/src/main/java/hudson/util/XStream2.java @@ -0,0 +1,38 @@ +package hudson.util; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.converters.DataHolder; +import com.thoughtworks.xstream.io.HierarchicalStreamDriver; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import hudson.model.Hudson; + +/** + * {@link XStream} enhanced for retroweaver support. + * @author Kohsuke Kawaguchi + */ +public class XStream2 extends XStream { + public XStream2() { + init(); + } + + public XStream2(HierarchicalStreamDriver hierarchicalStreamDriver) { + super(hierarchicalStreamDriver); + init(); + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, Object root, DataHolder dataHolder) { + // init() is too early to do this + // defensive because some use of XStream happens before plugins are initialized. + Hudson h = Hudson.getInstance(); + if(h!=null && h.pluginManager!=null && h.pluginManager.uberClassLoader!=null) { + setClassLoader(h.pluginManager.uberClassLoader); + } + + return super.unmarshal(reader,root,dataHolder); + } + + private void init() { + registerConverter(new RobustCollectionConverter(getClassMapper()),10); + } +} diff --git a/core/src/main/java/hudson/util/package.html b/core/src/main/java/hudson/util/package.html new file mode 100644 index 0000000000..e0d5674708 --- /dev/null +++ b/core/src/main/java/hudson/util/package.html @@ -0,0 +1,3 @@ + +Other miscellaneous utility code + \ No newline at end of file diff --git a/core/src/main/java/hudson/win32errors.properties b/core/src/main/java/hudson/win32errors.properties new file mode 100644 index 0000000000..4ba68a2d83 --- /dev/null +++ b/core/src/main/java/hudson/win32errors.properties @@ -0,0 +1,2052 @@ +# Win32 error messages by error code +# +error0= \ +The operation completed successfully +error1= \ +Incorrect function +error2= \ +The system cannot find the file specified +error3= \ +The system cannot find the path specified +error4= \ +The system cannot open the file +error5= \ +Access is denied +error6= \ +The handle is invalid +error7= \ +The storage control blocks were destroyed +error8= \ +Not enough storage is available to process this command +error9= \ +The storage control block address is invalid +error10= \ +The environment is incorrect +error11= \ +An attempt was made to load a program with an incorrect format +error12= \ +The access code is invalid +error13= \ +The data is invalid +error14= \ +Not enough storage is available to complete this operation +error15= \ +The system cannot find the drive specified +error16= \ +The directory cannot be removed +error17= \ +The system cannot move the file to a different disk drive +error18= \ +There are no more files +error19= \ +The media is write protected +error20= \ +The system cannot find the device specified +error21= \ +The device is not ready +error22= \ +The device does not recognize the command +error23= \ +Data error (cyclic redundancy check) +error24= \ +The program issued a command but the command length is incorrect +error25= \ +The drive cannot locate a specific area or track on the disk +error26= \ +The specified disk or diskette cannot be accessed +error27= \ +The drive cannot find the sector requested +error28= \ +The printer is out of paper +error29= \ +The system cannot write to the specified device +error30= \ +The system cannot read from the specified device +error31= \ +A device attached to the system is not functioning +error32= \ +The process cannot access the file because it is being used by another process +error33= \ +The process cannot access the file because another process has locked a portion of the file +error34= \ +The wrong diskette is in the drive. +Insert %2 (Volume Serial Number: %3) into drive %1 +error35= \ +Unknown error (0x23) +error36= \ +Too many files opened for sharing +error37= \ +Unknown error (0x25) +error38= \ +Reached the end of the file +error39= \ +The disk is full +error40= \ +Unknown error (0x28) +error41= \ +Unknown error (0x29) +error42= \ +Unknown error (0x2a) +error43= \ +Unknown error (0x2b) +error44= \ +Unknown error (0x2c) +error45= \ +Unknown error (0x2d) +error46= \ +Unknown error (0x2e) +error47= \ +Unknown error (0x2f) +error48= \ +Unknown error (0x30) +error49= \ +Unknown error (0x31) +error50= \ +The request is not supported +error51= \ +Windows cannot find the network path. Verify that the network path is correct and the destination computer is not busy or turned off. If Windows still cannot find the network path, contact your network administrator +error52= \ +You were not connected because a duplicate name exists on the network. Go to System in Control Panel to change the computer name and try again +error53= \ +The network path was not found +error54= \ +The network is busy +error55= \ +The specified network resource or device is no longer available +error56= \ +The network BIOS command limit has been reached +error57= \ +A network adapter hardware error occurred +error58= \ +The specified server cannot perform the requested operation +error59= \ +An unexpected network error occurred +error60= \ +The remote adapter is not compatible +error61= \ +The printer queue is full +error62= \ +Space to store the file waiting to be printed is not available on the server +error63= \ +Your file waiting to be printed was deleted +error64= \ +The specified network name is no longer available +error65= \ +Network access is denied +error66= \ +The network resource type is not correct +error67= \ +The network name cannot be found +error68= \ +The name limit for the local computer network adapter card was exceeded +error69= \ +The network BIOS session limit was exceeded +error70= \ +The remote server has been paused or is in the process of being started +error71= \ +No more connections can be made to this remote computer at this time because there are already as many connections as the computer can accept +error72= \ +The specified printer or disk device has been paused +error73= \ +Unknown error (0x49) +error74= \ +Unknown error (0x4a) +error75= \ +Unknown error (0x4b) +error76= \ +Unknown error (0x4c) +error77= \ +Unknown error (0x4d) +error78= \ +Unknown error (0x4e) +error79= \ +Unknown error (0x4f) +error80= \ +The file exists +error81= \ +Unknown error (0x51) +error82= \ +The directory or file cannot be created +error83= \ +Fail on INT 24 +error84= \ +Storage to process this request is not available +error85= \ +The local device name is already in use +error86= \ +The specified network password is not correct +error87= \ +The parameter is incorrect +error88= \ +A write fault occurred on the network +error89= \ +The system cannot start another process at this time +error90= \ +Unknown error (0x5a) +error91= \ +Unknown error (0x5b) +error92= \ +Unknown error (0x5c) +error93= \ +Unknown error (0x5d) +error94= \ +Unknown error (0x5e) +error95= \ +Unknown error (0x5f) +error96= \ +Unknown error (0x60) +error97= \ +Unknown error (0x61) +error98= \ +Unknown error (0x62) +error99= \ +Unknown error (0x63) +error100= \ +Cannot create another system semaphore +error101= \ +The exclusive semaphore is owned by another process +error102= \ +The semaphore is set and cannot be closed +error103= \ +The semaphore cannot be set again +error104= \ +Cannot request exclusive semaphores at interrupt time +error105= \ +The previous ownership of this semaphore has ended +error106= \ +Insert the diskette for drive %1 +error107= \ +The program stopped because an alternate diskette was not inserted +error108= \ +The disk is in use or locked by another process +error109= \ +The pipe has been ended +error110= \ +The system cannot open the device or file specified +error111= \ +The file name is too long +error112= \ +There is not enough space on the disk +error113= \ +No more internal file identifiers available +error114= \ +The target internal file identifier is incorrect +error115= \ +Unknown error (0x73) +error116= \ +Unknown error (0x74) +error117= \ +The IOCTL call made by the application program is not correct +error118= \ +The verify-on-write switch parameter value is not correct +error119= \ +The system does not support the command requested +error120= \ +This function is not supported on this system +error121= \ +The semaphore timeout period has expired +error122= \ +The data area passed to a system call is too small +error123= \ +The filename, directory name, or volume label syntax is incorrect +error124= \ +The system call level is not correct +error125= \ +The disk has no volume label +error126= \ +The specified module could not be found +error127= \ +The specified procedure could not be found +error128= \ +There are no child processes to wait for +error129= \ +The %1 application cannot be run in Win32 mode +error130= \ +Attempt to use a file handle to an open disk partition for an operation other than raw disk I/O +error131= \ +An attempt was made to move the file pointer before the beginning of the file +error132= \ +The file pointer cannot be set on the specified device or file +error133= \ +A JOIN or SUBST command cannot be used for a drive that contains previously joined drives +error134= \ +An attempt was made to use a JOIN or SUBST command on a drive that has already been joined +error135= \ +An attempt was made to use a JOIN or SUBST command on a drive that has already been substituted +error136= \ +The system tried to delete the JOIN of a drive that is not joined +error137= \ +The system tried to delete the substitution of a drive that is not substituted +error138= \ +The system tried to join a drive to a directory on a joined drive +error139= \ +The system tried to substitute a drive to a directory on a substituted drive +error140= \ +The system tried to join a drive to a directory on a substituted drive +error141= \ +The system tried to SUBST a drive to a directory on a joined drive +error142= \ +The system cannot perform a JOIN or SUBST at this time +error143= \ +The system cannot join or substitute a drive to or for a directory on the same drive +error144= \ +The directory is not a subdirectory of the root directory +error145= \ +The directory is not empty +error146= \ +The path specified is being used in a substitute +error147= \ +Not enough resources are available to process this command +error148= \ +The path specified cannot be used at this time +error149= \ +An attempt was made to join or substitute a drive for which a directory on the drive is the target of a previous substitute +error150= \ +System trace information was not specified in your CONFIG.SYS file, or tracing is disallowed +error151= \ +The number of specified semaphore events for DosMuxSemWait is not correct +error152= \ +DosMuxSemWait did not execute; too many semaphores are already set +error153= \ +The DosMuxSemWait list is not correct +error154= \ +The volume label you entered exceeds the label character limit of the target file system +error155= \ +Cannot create another thread +error156= \ +The recipient process has refused the signal +error157= \ +The segment is already discarded and cannot be locked +error158= \ +The segment is already unlocked +error159= \ +The address for the thread ID is not correct +error160= \ +One or more arguments are not correct +error161= \ +The specified path is invalid +error162= \ +A signal is already pending +error163= \ +Unknown error (0xa3) +error164= \ +No more threads can be created in the system +error165= \ +Unknown error (0xa5) +error166= \ +Unknown error (0xa6) +error167= \ +Unable to lock a region of a file +error168= \ +Unknown error (0xa8) +error169= \ +Unknown error (0xa9) +error170= \ +The requested resource is in use +error171= \ +Unknown error (0xab) +error172= \ +Unknown error (0xac) +error173= \ +A lock request was not outstanding for the supplied cancel region +error174= \ +The file system does not support atomic changes to the lock type +error175= \ +Unknown error (0xaf) +error176= \ +Unknown error (0xb0) +error177= \ +Unknown error (0xb1) +error178= \ +Unknown error (0xb2) +error179= \ +Unknown error (0xb3) +error180= \ +The system detected a segment number that was not correct +error181= \ +Unknown error (0xb5) +error182= \ +The operating system cannot run %1 +error183= \ +Cannot create a file when that file already exists +error184= \ +Unknown error (0xb8) +error185= \ +Unknown error (0xb9) +error186= \ +The flag passed is not correct +error187= \ +The specified system semaphore name was not found +error188= \ +The operating system cannot run %1 +error189= \ +The operating system cannot run %1 +error190= \ +The operating system cannot run %1 +error191= \ +Cannot run %1 in Win32 mode +error192= \ +The operating system cannot run %1 +error193= \ +%1 is not a valid Win32 application +error194= \ +The operating system cannot run %1 +error195= \ +The operating system cannot run %1 +error196= \ +The operating system cannot run this application program +error197= \ +The operating system is not presently configured to run this application +error198= \ +The operating system cannot run %1 +error199= \ +The operating system cannot run this application program +error200= \ +The code segment cannot be greater than or equal to 64K +error201= \ +The operating system cannot run %1 +error202= \ +The operating system cannot run %1 +error203= \ +The system could not find the environment option that was entered +error204= \ +Unknown error (0xcc) +error205= \ +No process in the command subtree has a signal handler +error206= \ +The filename or extension is too long +error207= \ +The ring 2 stack is in use +error208= \ +The global filename characters, * or ?, are entered incorrectly or too many global filename characters are specified +error209= \ +The signal being posted is not correct +error210= \ +The signal handler cannot be set +error211= \ +Unknown error (0xd3) +error212= \ +The segment is locked and cannot be reallocated +error213= \ +Unknown error (0xd5) +error214= \ +Too many dynamic-link modules are attached to this program or dynamic-link module +error215= \ +Cannot nest calls to LoadModule +error216= \ +The image file %1 is valid, but is for a machine type other than the current machine +error217= \ +Unknown error (0xd9) +error218= \ +Unknown error (0xda) +error219= \ +Unknown error (0xdb) +error220= \ +Unknown error (0xdc) +error221= \ +Unknown error (0xdd) +error222= \ +Unknown error (0xde) +error223= \ +Unknown error (0xdf) +error224= \ +Unknown error (0xe0) +error225= \ +Unknown error (0xe1) +error226= \ +Unknown error (0xe2) +error227= \ +Unknown error (0xe3) +error228= \ +Unknown error (0xe4) +error229= \ +Unknown error (0xe5) +error230= \ +The pipe state is invalid +error231= \ +All pipe instances are busy +error232= \ +The pipe is being closed +error233= \ +No process is on the other end of the pipe +error234= \ +More data is available +error235= \ +Unknown error (0xeb) +error236= \ +Unknown error (0xec) +error237= \ +Unknown error (0xed) +error238= \ +Unknown error (0xee) +error239= \ +Unknown error (0xef) +error240= \ +The session was canceled +error241= \ +Unknown error (0xf1) +error242= \ +Unknown error (0xf2) +error243= \ +Unknown error (0xf3) +error244= \ +Unknown error (0xf4) +error245= \ +Unknown error (0xf5) +error246= \ +Unknown error (0xf6) +error247= \ +Unknown error (0xf7) +error248= \ +Unknown error (0xf8) +error249= \ +Unknown error (0xf9) +error250= \ +Unknown error (0xfa) +error251= \ +Unknown error (0xfb) +error252= \ +Unknown error (0xfc) +error253= \ +Unknown error (0xfd) +error254= \ +The specified extended attribute name was invalid +error255= \ +The extended attributes are inconsistent +error256= \ +Unknown error (0x100) +error257= \ +Unknown error (0x101) +error258= \ +The wait operation timed out +error259= \ +No more data is available +error260= \ +Unknown error (0x104) +error261= \ +Unknown error (0x105) +error262= \ +Unknown error (0x106) +error263= \ +Unknown error (0x107) +error264= \ +Unknown error (0x108) +error265= \ +Unknown error (0x109) +error266= \ +The copy functions cannot be used +error267= \ +The directory name is invalid +error268= \ +Unknown error (0x10c) +error269= \ +Unknown error (0x10d) +error270= \ +Unknown error (0x10e) +error271= \ +Unknown error (0x10f) +error272= \ +Unknown error (0x110) +error273= \ +Unknown error (0x111) +error274= \ +Unknown error (0x112) +error275= \ +The extended attributes did not fit in the buffer +error276= \ +The extended attribute file on the mounted file system is corrupt +error277= \ +The extended attribute table file is full +error278= \ +The specified extended attribute handle is invalid +error279= \ +Unknown error (0x117) +error280= \ +Unknown error (0x118) +error281= \ +Unknown error (0x119) +error282= \ +The mounted file system does not support extended attributes +error283= \ +Unknown error (0x11b) +error284= \ +Unknown error (0x11c) +error285= \ +Unknown error (0x11d) +error286= \ +Unknown error (0x11e) +error287= \ +Unknown error (0x11f) +error288= \ +Attempt to release mutex not owned by caller +error289= \ +Unknown error (0x121) +error290= \ +Unknown error (0x122) +error291= \ +Unknown error (0x123) +error292= \ +Unknown error (0x124) +error293= \ +Unknown error (0x125) +error294= \ +Unknown error (0x126) +error295= \ +Unknown error (0x127) +error296= \ +Unknown error (0x128) +error297= \ +Unknown error (0x129) +error298= \ +Too many posts were made to a semaphore +error299= \ +Only part of a ReadProcessMemory or WriteProcessMemory request was completed +error300= \ +The oplock request is denied +error301= \ +An invalid oplock acknowledgment was received by the system +error302= \ +The volume is too fragmented to complete this operation +error303= \ +The file cannot be opened because it is in the process of being deleted +error304= \ +Unknown error (0x130) +error305= \ +Unknown error (0x131) +error306= \ +Unknown error (0x132) +error307= \ +Unknown error (0x133) +error308= \ +Unknown error (0x134) +error309= \ +Unknown error (0x135) +error310= \ +Unknown error (0x136) +error311= \ +Unknown error (0x137) +error312= \ +Unknown error (0x138) +error313= \ +Unknown error (0x139) +error314= \ +Unknown error (0x13a) +error315= \ +Unknown error (0x13b) +error316= \ +Unknown error (0x13c) +error317= \ +The system cannot find message text for message number 0x%1 in the message file for %2 +error318= \ +Unknown error (0x13e) +error319= \ +Unknown error (0x13f) +error320= \ +Unknown error (0x140) +error321= \ +Unknown error (0x141) +error322= \ +Unknown error (0x142) +error323= \ +Unknown error (0x143) +error324= \ +Unknown error (0x144) +error325= \ +Unknown error (0x145) +error326= \ +Unknown error (0x146) +error327= \ +Unknown error (0x147) +error328= \ +Unknown error (0x148) +error329= \ +Unknown error (0x149) +error330= \ +Unknown error (0x14a) +error331= \ +Unknown error (0x14b) +error332= \ +Unknown error (0x14c) +error333= \ +Unknown error (0x14d) +error334= \ +Unknown error (0x14e) +error335= \ +Unknown error (0x14f) +error336= \ +Unknown error (0x150) +error337= \ +Unknown error (0x151) +error338= \ +Unknown error (0x152) +error339= \ +Unknown error (0x153) +error340= \ +Unknown error (0x154) +error341= \ +Unknown error (0x155) +error342= \ +Unknown error (0x156) +error343= \ +Unknown error (0x157) +error344= \ +Unknown error (0x158) +error345= \ +Unknown error (0x159) +error346= \ +Unknown error (0x15a) +error347= \ +Unknown error (0x15b) +error348= \ +Unknown error (0x15c) +error349= \ +Unknown error (0x15d) +error350= \ +Unknown error (0x15e) +error351= \ +Unknown error (0x15f) +error352= \ +Unknown error (0x160) +error353= \ +Unknown error (0x161) +error354= \ +Unknown error (0x162) +error355= \ +Unknown error (0x163) +error356= \ +Unknown error (0x164) +error357= \ +Unknown error (0x165) +error358= \ +Unknown error (0x166) +error359= \ +Unknown error (0x167) +error360= \ +Unknown error (0x168) +error361= \ +Unknown error (0x169) +error362= \ +Unknown error (0x16a) +error363= \ +Unknown error (0x16b) +error364= \ +Unknown error (0x16c) +error365= \ +Unknown error (0x16d) +error366= \ +Unknown error (0x16e) +error367= \ +Unknown error (0x16f) +error368= \ +Unknown error (0x170) +error369= \ +Unknown error (0x171) +error370= \ +Unknown error (0x172) +error371= \ +Unknown error (0x173) +error372= \ +Unknown error (0x174) +error373= \ +Unknown error (0x175) +error374= \ +Unknown error (0x176) +error375= \ +Unknown error (0x177) +error376= \ +Unknown error (0x178) +error377= \ +Unknown error (0x179) +error378= \ +Unknown error (0x17a) +error379= \ +Unknown error (0x17b) +error380= \ +Unknown error (0x17c) +error381= \ +Unknown error (0x17d) +error382= \ +Unknown error (0x17e) +error383= \ +Unknown error (0x17f) +error384= \ +Unknown error (0x180) +error385= \ +Unknown error (0x181) +error386= \ +Unknown error (0x182) +error387= \ +Unknown error (0x183) +error388= \ +Unknown error (0x184) +error389= \ +Unknown error (0x185) +error390= \ +Unknown error (0x186) +error391= \ +Unknown error (0x187) +error392= \ +Unknown error (0x188) +error393= \ +Unknown error (0x189) +error394= \ +Unknown error (0x18a) +error395= \ +Unknown error (0x18b) +error396= \ +Unknown error (0x18c) +error397= \ +Unknown error (0x18d) +error398= \ +Unknown error (0x18e) +error399= \ +Unknown error (0x18f) +error400= \ +Unknown error (0x190) +error401= \ +Unknown error (0x191) +error402= \ +Unknown error (0x192) +error403= \ +Unknown error (0x193) +error404= \ +Unknown error (0x194) +error405= \ +Unknown error (0x195) +error406= \ +Unknown error (0x196) +error407= \ +Unknown error (0x197) +error408= \ +Unknown error (0x198) +error409= \ +Unknown error (0x199) +error410= \ +Unknown error (0x19a) +error411= \ +Unknown error (0x19b) +error412= \ +Unknown error (0x19c) +error413= \ +Unknown error (0x19d) +error414= \ +Unknown error (0x19e) +error415= \ +Unknown error (0x19f) +error416= \ +Unknown error (0x1a0) +error417= \ +Unknown error (0x1a1) +error418= \ +Unknown error (0x1a2) +error419= \ +Unknown error (0x1a3) +error420= \ +Unknown error (0x1a4) +error421= \ +Unknown error (0x1a5) +error422= \ +Unknown error (0x1a6) +error423= \ +Unknown error (0x1a7) +error424= \ +Unknown error (0x1a8) +error425= \ +Unknown error (0x1a9) +error426= \ +Unknown error (0x1aa) +error427= \ +Unknown error (0x1ab) +error428= \ +Unknown error (0x1ac) +error429= \ +Unknown error (0x1ad) +error430= \ +Unknown error (0x1ae) +error431= \ +Unknown error (0x1af) +error432= \ +Unknown error (0x1b0) +error433= \ +Unknown error (0x1b1) +error434= \ +Unknown error (0x1b2) +error435= \ +Unknown error (0x1b3) +error436= \ +Unknown error (0x1b4) +error437= \ +Unknown error (0x1b5) +error438= \ +Unknown error (0x1b6) +error439= \ +Unknown error (0x1b7) +error440= \ +Unknown error (0x1b8) +error441= \ +Unknown error (0x1b9) +error442= \ +Unknown error (0x1ba) +error443= \ +Unknown error (0x1bb) +error444= \ +Unknown error (0x1bc) +error445= \ +Unknown error (0x1bd) +error446= \ +Unknown error (0x1be) +error447= \ +Unknown error (0x1bf) +error448= \ +Unknown error (0x1c0) +error449= \ +Unknown error (0x1c1) +error450= \ +Unknown error (0x1c2) +error451= \ +Unknown error (0x1c3) +error452= \ +Unknown error (0x1c4) +error453= \ +Unknown error (0x1c5) +error454= \ +Unknown error (0x1c6) +error455= \ +Unknown error (0x1c7) +error456= \ +Unknown error (0x1c8) +error457= \ +Unknown error (0x1c9) +error458= \ +Unknown error (0x1ca) +error459= \ +Unknown error (0x1cb) +error460= \ +Unknown error (0x1cc) +error461= \ +Unknown error (0x1cd) +error462= \ +Unknown error (0x1ce) +error463= \ +Unknown error (0x1cf) +error464= \ +Unknown error (0x1d0) +error465= \ +Unknown error (0x1d1) +error466= \ +Unknown error (0x1d2) +error467= \ +Unknown error (0x1d3) +error468= \ +Unknown error (0x1d4) +error469= \ +Unknown error (0x1d5) +error470= \ +Unknown error (0x1d6) +error471= \ +Unknown error (0x1d7) +error472= \ +Unknown error (0x1d8) +error473= \ +Unknown error (0x1d9) +error474= \ +Unknown error (0x1da) +error475= \ +Unknown error (0x1db) +error476= \ +Unknown error (0x1dc) +error477= \ +Unknown error (0x1dd) +error478= \ +Unknown error (0x1de) +error479= \ +Unknown error (0x1df) +error480= \ +Unknown error (0x1e0) +error481= \ +Unknown error (0x1e1) +error482= \ +Unknown error (0x1e2) +error483= \ +Unknown error (0x1e3) +error484= \ +Unknown error (0x1e4) +error485= \ +Unknown error (0x1e5) +error486= \ +Unknown error (0x1e6) +error487= \ +Attempt to access invalid address +error488= \ +Unknown error (0x1e8) +error489= \ +Unknown error (0x1e9) +error490= \ +Unknown error (0x1ea) +error491= \ +Unknown error (0x1eb) +error492= \ +Unknown error (0x1ec) +error493= \ +Unknown error (0x1ed) +error494= \ +Unknown error (0x1ee) +error495= \ +Unknown error (0x1ef) +error496= \ +Unknown error (0x1f0) +error497= \ +Unknown error (0x1f1) +error498= \ +Unknown error (0x1f2) +error499= \ +Unknown error (0x1f3) +error500= \ +Unknown error (0x1f4) +error501= \ +Unknown error (0x1f5) +error502= \ +Unknown error (0x1f6) +error503= \ +Unknown error (0x1f7) +error504= \ +Unknown error (0x1f8) +error505= \ +Unknown error (0x1f9) +error506= \ +Unknown error (0x1fa) +error507= \ +Unknown error (0x1fb) +error508= \ +Unknown error (0x1fc) +error509= \ +Unknown error (0x1fd) +error510= \ +Unknown error (0x1fe) +error511= \ +Unknown error (0x1ff) +error512= \ +Unknown error (0x200) +error513= \ +Unknown error (0x201) +error514= \ +Unknown error (0x202) +error515= \ +Unknown error (0x203) +error516= \ +Unknown error (0x204) +error517= \ +Unknown error (0x205) +error518= \ +Unknown error (0x206) +error519= \ +Unknown error (0x207) +error520= \ +Unknown error (0x208) +error521= \ +Unknown error (0x209) +error522= \ +Unknown error (0x20a) +error523= \ +Unknown error (0x20b) +error524= \ +Unknown error (0x20c) +error525= \ +Unknown error (0x20d) +error526= \ +Unknown error (0x20e) +error527= \ +Unknown error (0x20f) +error528= \ +Unknown error (0x210) +error529= \ +Unknown error (0x211) +error530= \ +Unknown error (0x212) +error531= \ +Unknown error (0x213) +error532= \ +Unknown error (0x214) +error533= \ +Unknown error (0x215) +error534= \ +Arithmetic result exceeded 32 bits +error535= \ +There is a process on other end of the pipe +error536= \ +Waiting for a process to open the other end of the pipe +error537= \ +Unknown error (0x219) +error538= \ +Unknown error (0x21a) +error539= \ +Unknown error (0x21b) +error540= \ +Unknown error (0x21c) +error541= \ +Unknown error (0x21d) +error542= \ +Unknown error (0x21e) +error543= \ +Unknown error (0x21f) +error544= \ +Unknown error (0x220) +error545= \ +Unknown error (0x221) +error546= \ +Unknown error (0x222) +error547= \ +Unknown error (0x223) +error548= \ +Unknown error (0x224) +error549= \ +Unknown error (0x225) +error550= \ +Unknown error (0x226) +error551= \ +Unknown error (0x227) +error552= \ +Unknown error (0x228) +error553= \ +Unknown error (0x229) +error554= \ +Unknown error (0x22a) +error555= \ +Unknown error (0x22b) +error556= \ +Unknown error (0x22c) +error557= \ +Unknown error (0x22d) +error558= \ +Unknown error (0x22e) +error559= \ +Unknown error (0x22f) +error560= \ +Unknown error (0x230) +error561= \ +Unknown error (0x231) +error562= \ +Unknown error (0x232) +error563= \ +Unknown error (0x233) +error564= \ +Unknown error (0x234) +error565= \ +Unknown error (0x235) +error566= \ +Unknown error (0x236) +error567= \ +Unknown error (0x237) +error568= \ +Unknown error (0x238) +error569= \ +Unknown error (0x239) +error570= \ +Unknown error (0x23a) +error571= \ +Unknown error (0x23b) +error572= \ +Unknown error (0x23c) +error573= \ +Unknown error (0x23d) +error574= \ +Unknown error (0x23e) +error575= \ +Unknown error (0x23f) +error576= \ +Unknown error (0x240) +error577= \ +Unknown error (0x241) +error578= \ +Unknown error (0x242) +error579= \ +Unknown error (0x243) +error580= \ +Unknown error (0x244) +error581= \ +Unknown error (0x245) +error582= \ +Unknown error (0x246) +error583= \ +Unknown error (0x247) +error584= \ +Unknown error (0x248) +error585= \ +Unknown error (0x249) +error586= \ +Unknown error (0x24a) +error587= \ +Unknown error (0x24b) +error588= \ +Unknown error (0x24c) +error589= \ +Unknown error (0x24d) +error590= \ +Unknown error (0x24e) +error591= \ +Unknown error (0x24f) +error592= \ +Unknown error (0x250) +error593= \ +Unknown error (0x251) +error594= \ +Unknown error (0x252) +error595= \ +Unknown error (0x253) +error596= \ +Unknown error (0x254) +error597= \ +Unknown error (0x255) +error598= \ +Unknown error (0x256) +error599= \ +Unknown error (0x257) +error600= \ +Unknown error (0x258) +error601= \ +Unknown error (0x259) +error602= \ +Unknown error (0x25a) +error603= \ +Unknown error (0x25b) +error604= \ +Unknown error (0x25c) +error605= \ +Unknown error (0x25d) +error606= \ +Unknown error (0x25e) +error607= \ +Unknown error (0x25f) +error608= \ +Unknown error (0x260) +error609= \ +Unknown error (0x261) +error610= \ +Unknown error (0x262) +error611= \ +Unknown error (0x263) +error612= \ +Unknown error (0x264) +error613= \ +Unknown error (0x265) +error614= \ +Unknown error (0x266) +error615= \ +Unknown error (0x267) +error616= \ +Unknown error (0x268) +error617= \ +Unknown error (0x269) +error618= \ +Unknown error (0x26a) +error619= \ +Unknown error (0x26b) +error620= \ +Unknown error (0x26c) +error621= \ +Unknown error (0x26d) +error622= \ +Unknown error (0x26e) +error623= \ +Unknown error (0x26f) +error624= \ +Unknown error (0x270) +error625= \ +Unknown error (0x271) +error626= \ +Unknown error (0x272) +error627= \ +Unknown error (0x273) +error628= \ +Unknown error (0x274) +error629= \ +Unknown error (0x275) +error630= \ +Unknown error (0x276) +error631= \ +Unknown error (0x277) +error632= \ +Unknown error (0x278) +error633= \ +Unknown error (0x279) +error634= \ +Unknown error (0x27a) +error635= \ +Unknown error (0x27b) +error636= \ +Unknown error (0x27c) +error637= \ +Unknown error (0x27d) +error638= \ +Unknown error (0x27e) +error639= \ +Unknown error (0x27f) +error640= \ +Unknown error (0x280) +error641= \ +Unknown error (0x281) +error642= \ +Unknown error (0x282) +error643= \ +Unknown error (0x283) +error644= \ +Unknown error (0x284) +error645= \ +Unknown error (0x285) +error646= \ +Unknown error (0x286) +error647= \ +Unknown error (0x287) +error648= \ +Unknown error (0x288) +error649= \ +Unknown error (0x289) +error650= \ +Unknown error (0x28a) +error651= \ +Unknown error (0x28b) +error652= \ +Unknown error (0x28c) +error653= \ +Unknown error (0x28d) +error654= \ +Unknown error (0x28e) +error655= \ +Unknown error (0x28f) +error656= \ +Unknown error (0x290) +error657= \ +Unknown error (0x291) +error658= \ +Unknown error (0x292) +error659= \ +Unknown error (0x293) +error660= \ +Unknown error (0x294) +error661= \ +Unknown error (0x295) +error662= \ +Unknown error (0x296) +error663= \ +Unknown error (0x297) +error664= \ +Unknown error (0x298) +error665= \ +Unknown error (0x299) +error666= \ +Unknown error (0x29a) +error667= \ +Unknown error (0x29b) +error668= \ +Unknown error (0x29c) +error669= \ +Unknown error (0x29d) +error670= \ +Unknown error (0x29e) +error671= \ +Unknown error (0x29f) +error672= \ +Unknown error (0x2a0) +error673= \ +Unknown error (0x2a1) +error674= \ +Unknown error (0x2a2) +error675= \ +Unknown error (0x2a3) +error676= \ +Unknown error (0x2a4) +error677= \ +Unknown error (0x2a5) +error678= \ +Unknown error (0x2a6) +error679= \ +Unknown error (0x2a7) +error680= \ +Unknown error (0x2a8) +error681= \ +Unknown error (0x2a9) +error682= \ +Unknown error (0x2aa) +error683= \ +Unknown error (0x2ab) +error684= \ +Unknown error (0x2ac) +error685= \ +Unknown error (0x2ad) +error686= \ +Unknown error (0x2ae) +error687= \ +Unknown error (0x2af) +error688= \ +Unknown error (0x2b0) +error689= \ +Unknown error (0x2b1) +error690= \ +Unknown error (0x2b2) +error691= \ +Unknown error (0x2b3) +error692= \ +Unknown error (0x2b4) +error693= \ +Unknown error (0x2b5) +error694= \ +Unknown error (0x2b6) +error695= \ +Unknown error (0x2b7) +error696= \ +Unknown error (0x2b8) +error697= \ +Unknown error (0x2b9) +error698= \ +Unknown error (0x2ba) +error699= \ +Unknown error (0x2bb) +error700= \ +Unknown error (0x2bc) +error701= \ +Unknown error (0x2bd) +error702= \ +Unknown error (0x2be) +error703= \ +Unknown error (0x2bf) +error704= \ +Unknown error (0x2c0) +error705= \ +Unknown error (0x2c1) +error706= \ +Unknown error (0x2c2) +error707= \ +Unknown error (0x2c3) +error708= \ +Unknown error (0x2c4) +error709= \ +Unknown error (0x2c5) +error710= \ +Unknown error (0x2c6) +error711= \ +Unknown error (0x2c7) +error712= \ +Unknown error (0x2c8) +error713= \ +Unknown error (0x2c9) +error714= \ +Unknown error (0x2ca) +error715= \ +Unknown error (0x2cb) +error716= \ +Unknown error (0x2cc) +error717= \ +Unknown error (0x2cd) +error718= \ +Unknown error (0x2ce) +error719= \ +Unknown error (0x2cf) +error720= \ +Unknown error (0x2d0) +error721= \ +Unknown error (0x2d1) +error722= \ +Unknown error (0x2d2) +error723= \ +Unknown error (0x2d3) +error724= \ +Unknown error (0x2d4) +error725= \ +Unknown error (0x2d5) +error726= \ +Unknown error (0x2d6) +error727= \ +Unknown error (0x2d7) +error728= \ +Unknown error (0x2d8) +error729= \ +Unknown error (0x2d9) +error730= \ +Unknown error (0x2da) +error731= \ +Unknown error (0x2db) +error732= \ +Unknown error (0x2dc) +error733= \ +Unknown error (0x2dd) +error734= \ +Unknown error (0x2de) +error735= \ +Unknown error (0x2df) +error736= \ +Unknown error (0x2e0) +error737= \ +Unknown error (0x2e1) +error738= \ +Unknown error (0x2e2) +error739= \ +Unknown error (0x2e3) +error740= \ +Unknown error (0x2e4) +error741= \ +Unknown error (0x2e5) +error742= \ +Unknown error (0x2e6) +error743= \ +Unknown error (0x2e7) +error744= \ +Unknown error (0x2e8) +error745= \ +Unknown error (0x2e9) +error746= \ +Unknown error (0x2ea) +error747= \ +Unknown error (0x2eb) +error748= \ +Unknown error (0x2ec) +error749= \ +Unknown error (0x2ed) +error750= \ +Unknown error (0x2ee) +error751= \ +Unknown error (0x2ef) +error752= \ +Unknown error (0x2f0) +error753= \ +Unknown error (0x2f1) +error754= \ +Unknown error (0x2f2) +error755= \ +Unknown error (0x2f3) +error756= \ +Unknown error (0x2f4) +error757= \ +Unknown error (0x2f5) +error758= \ +Unknown error (0x2f6) +error759= \ +Unknown error (0x2f7) +error760= \ +Unknown error (0x2f8) +error761= \ +Unknown error (0x2f9) +error762= \ +Unknown error (0x2fa) +error763= \ +Unknown error (0x2fb) +error764= \ +Unknown error (0x2fc) +error765= \ +Unknown error (0x2fd) +error766= \ +Unknown error (0x2fe) +error767= \ +Unknown error (0x2ff) +error768= \ +Unknown error (0x300) +error769= \ +Unknown error (0x301) +error770= \ +Unknown error (0x302) +error771= \ +Unknown error (0x303) +error772= \ +Unknown error (0x304) +error773= \ +Unknown error (0x305) +error774= \ +Unknown error (0x306) +error775= \ +Unknown error (0x307) +error776= \ +Unknown error (0x308) +error777= \ +Unknown error (0x309) +error778= \ +Unknown error (0x30a) +error779= \ +Unknown error (0x30b) +error780= \ +Unknown error (0x30c) +error781= \ +Unknown error (0x30d) +error782= \ +Unknown error (0x30e) +error783= \ +Unknown error (0x30f) +error784= \ +Unknown error (0x310) +error785= \ +Unknown error (0x311) +error786= \ +Unknown error (0x312) +error787= \ +Unknown error (0x313) +error788= \ +Unknown error (0x314) +error789= \ +Unknown error (0x315) +error790= \ +Unknown error (0x316) +error791= \ +Unknown error (0x317) +error792= \ +Unknown error (0x318) +error793= \ +Unknown error (0x319) +error794= \ +Unknown error (0x31a) +error795= \ +Unknown error (0x31b) +error796= \ +Unknown error (0x31c) +error797= \ +Unknown error (0x31d) +error798= \ +Unknown error (0x31e) +error799= \ +Unknown error (0x31f) +error800= \ +Unknown error (0x320) +error801= \ +Unknown error (0x321) +error802= \ +Unknown error (0x322) +error803= \ +Unknown error (0x323) +error804= \ +Unknown error (0x324) +error805= \ +Unknown error (0x325) +error806= \ +Unknown error (0x326) +error807= \ +Unknown error (0x327) +error808= \ +Unknown error (0x328) +error809= \ +Unknown error (0x329) +error810= \ +Unknown error (0x32a) +error811= \ +Unknown error (0x32b) +error812= \ +Unknown error (0x32c) +error813= \ +Unknown error (0x32d) +error814= \ +Unknown error (0x32e) +error815= \ +Unknown error (0x32f) +error816= \ +Unknown error (0x330) +error817= \ +Unknown error (0x331) +error818= \ +Unknown error (0x332) +error819= \ +Unknown error (0x333) +error820= \ +Unknown error (0x334) +error821= \ +Unknown error (0x335) +error822= \ +Unknown error (0x336) +error823= \ +Unknown error (0x337) +error824= \ +Unknown error (0x338) +error825= \ +Unknown error (0x339) +error826= \ +Unknown error (0x33a) +error827= \ +Unknown error (0x33b) +error828= \ +Unknown error (0x33c) +error829= \ +Unknown error (0x33d) +error830= \ +Unknown error (0x33e) +error831= \ +Unknown error (0x33f) +error832= \ +Unknown error (0x340) +error833= \ +Unknown error (0x341) +error834= \ +Unknown error (0x342) +error835= \ +Unknown error (0x343) +error836= \ +Unknown error (0x344) +error837= \ +Unknown error (0x345) +error838= \ +Unknown error (0x346) +error839= \ +Unknown error (0x347) +error840= \ +Unknown error (0x348) +error841= \ +Unknown error (0x349) +error842= \ +Unknown error (0x34a) +error843= \ +Unknown error (0x34b) +error844= \ +Unknown error (0x34c) +error845= \ +Unknown error (0x34d) +error846= \ +Unknown error (0x34e) +error847= \ +Unknown error (0x34f) +error848= \ +Unknown error (0x350) +error849= \ +Unknown error (0x351) +error850= \ +Unknown error (0x352) +error851= \ +Unknown error (0x353) +error852= \ +Unknown error (0x354) +error853= \ +Unknown error (0x355) +error854= \ +Unknown error (0x356) +error855= \ +Unknown error (0x357) +error856= \ +Unknown error (0x358) +error857= \ +Unknown error (0x359) +error858= \ +Unknown error (0x35a) +error859= \ +Unknown error (0x35b) +error860= \ +Unknown error (0x35c) +error861= \ +Unknown error (0x35d) +error862= \ +Unknown error (0x35e) +error863= \ +Unknown error (0x35f) +error864= \ +Unknown error (0x360) +error865= \ +Unknown error (0x361) +error866= \ +Unknown error (0x362) +error867= \ +Unknown error (0x363) +error868= \ +Unknown error (0x364) +error869= \ +Unknown error (0x365) +error870= \ +Unknown error (0x366) +error871= \ +Unknown error (0x367) +error872= \ +Unknown error (0x368) +error873= \ +Unknown error (0x369) +error874= \ +Unknown error (0x36a) +error875= \ +Unknown error (0x36b) +error876= \ +Unknown error (0x36c) +error877= \ +Unknown error (0x36d) +error878= \ +Unknown error (0x36e) +error879= \ +Unknown error (0x36f) +error880= \ +Unknown error (0x370) +error881= \ +Unknown error (0x371) +error882= \ +Unknown error (0x372) +error883= \ +Unknown error (0x373) +error884= \ +Unknown error (0x374) +error885= \ +Unknown error (0x375) +error886= \ +Unknown error (0x376) +error887= \ +Unknown error (0x377) +error888= \ +Unknown error (0x378) +error889= \ +Unknown error (0x379) +error890= \ +Unknown error (0x37a) +error891= \ +Unknown error (0x37b) +error892= \ +Unknown error (0x37c) +error893= \ +Unknown error (0x37d) +error894= \ +Unknown error (0x37e) +error895= \ +Unknown error (0x37f) +error896= \ +Unknown error (0x380) +error897= \ +Unknown error (0x381) +error898= \ +Unknown error (0x382) +error899= \ +Unknown error (0x383) +error900= \ +Unknown error (0x384) +error901= \ +Unknown error (0x385) +error902= \ +Unknown error (0x386) +error903= \ +Unknown error (0x387) +error904= \ +Unknown error (0x388) +error905= \ +Unknown error (0x389) +error906= \ +Unknown error (0x38a) +error907= \ +Unknown error (0x38b) +error908= \ +Unknown error (0x38c) +error909= \ +Unknown error (0x38d) +error910= \ +Unknown error (0x38e) +error911= \ +Unknown error (0x38f) +error912= \ +Unknown error (0x390) +error913= \ +Unknown error (0x391) +error914= \ +Unknown error (0x392) +error915= \ +Unknown error (0x393) +error916= \ +Unknown error (0x394) +error917= \ +Unknown error (0x395) +error918= \ +Unknown error (0x396) +error919= \ +Unknown error (0x397) +error920= \ +Unknown error (0x398) +error921= \ +Unknown error (0x399) +error922= \ +Unknown error (0x39a) +error923= \ +Unknown error (0x39b) +error924= \ +Unknown error (0x39c) +error925= \ +Unknown error (0x39d) +error926= \ +Unknown error (0x39e) +error927= \ +Unknown error (0x39f) +error928= \ +Unknown error (0x3a0) +error929= \ +Unknown error (0x3a1) +error930= \ +Unknown error (0x3a2) +error931= \ +Unknown error (0x3a3) +error932= \ +Unknown error (0x3a4) +error933= \ +Unknown error (0x3a5) +error934= \ +Unknown error (0x3a6) +error935= \ +Unknown error (0x3a7) +error936= \ +Unknown error (0x3a8) +error937= \ +Unknown error (0x3a9) +error938= \ +Unknown error (0x3aa) +error939= \ +Unknown error (0x3ab) +error940= \ +Unknown error (0x3ac) +error941= \ +Unknown error (0x3ad) +error942= \ +Unknown error (0x3ae) +error943= \ +Unknown error (0x3af) +error944= \ +Unknown error (0x3b0) +error945= \ +Unknown error (0x3b1) +error946= \ +Unknown error (0x3b2) +error947= \ +Unknown error (0x3b3) +error948= \ +Unknown error (0x3b4) +error949= \ +Unknown error (0x3b5) +error950= \ +Unknown error (0x3b6) +error951= \ +Unknown error (0x3b7) +error952= \ +Unknown error (0x3b8) +error953= \ +Unknown error (0x3b9) +error954= \ +Unknown error (0x3ba) +error955= \ +Unknown error (0x3bb) +error956= \ +Unknown error (0x3bc) +error957= \ +Unknown error (0x3bd) +error958= \ +Unknown error (0x3be) +error959= \ +Unknown error (0x3bf) +error960= \ +Unknown error (0x3c0) +error961= \ +Unknown error (0x3c1) +error962= \ +Unknown error (0x3c2) +error963= \ +Unknown error (0x3c3) +error964= \ +Unknown error (0x3c4) +error965= \ +Unknown error (0x3c5) +error966= \ +Unknown error (0x3c6) +error967= \ +Unknown error (0x3c7) +error968= \ +Unknown error (0x3c8) +error969= \ +Unknown error (0x3c9) +error970= \ +Unknown error (0x3ca) +error971= \ +Unknown error (0x3cb) +error972= \ +Unknown error (0x3cc) +error973= \ +Unknown error (0x3cd) +error974= \ +Unknown error (0x3ce) +error975= \ +Unknown error (0x3cf) +error976= \ +Unknown error (0x3d0) +error977= \ +Unknown error (0x3d1) +error978= \ +Unknown error (0x3d2) +error979= \ +Unknown error (0x3d3) +error980= \ +Unknown error (0x3d4) +error981= \ +Unknown error (0x3d5) +error982= \ +Unknown error (0x3d6) +error983= \ +Unknown error (0x3d7) +error984= \ +Unknown error (0x3d8) +error985= \ +Unknown error (0x3d9) +error986= \ +Unknown error (0x3da) +error987= \ +Unknown error (0x3db) +error988= \ +Unknown error (0x3dc) +error989= \ +Unknown error (0x3dd) +error990= \ +Unknown error (0x3de) +error991= \ +Unknown error (0x3df) +error992= \ +Unknown error (0x3e0) +error993= \ +Unknown error (0x3e1) +error994= \ +Access to the extended attribute was denied +error995= \ +The I/O operation has been aborted because of either a thread exit or an application request +error996= \ +Overlapped I/O event is not in a signaled state +error997= \ +Overlapped I/O operation is in progress +error998= \ +Invalid access to memory location +error999= \ +Error performing inpage operation +error1000= \ +Unknown error (0x3e8) +error1001= \ +Recursion too deep; the stack overflowed +error1002= \ +The window cannot act on the sent message +error1003= \ +Cannot complete this function +error1004= \ +Invalid flags +error1005= \ +The volume does not contain a recognized file system. +Please make sure that all required file system drivers are loaded and that the volume is not corrupted +error1006= \ +The volume for a file has been externally altered so that the opened file is no longer valid +error1007= \ +The requested operation cannot be performed in full-screen mode +error1008= \ +An attempt was made to reference a token that does not exist +error1009= \ +The configuration registry database is corrupt +error1010= \ +The configuration registry key is invalid +error1011= \ +The configuration registry key could not be opened +error1012= \ +The configuration registry key could not be read +error1013= \ +The configuration registry key could not be written +error1014= \ +One of the files in the registry database had to be recovered by use of a log or alternate copy. The recovery was successful +error1015= \ +The registry is corrupted. The structure of one of the files containing registry data is corrupted, or the system''s memory image of the file is corrupted, or the file could not be recovered because the alternate copy or log was absent or corrupted +error1016= \ +An I/O operation initiated by the registry failed unrecoverably. The registry could not read in, or write out, or flush, one of the files that contain the system''s image of the registry +error1017= \ +The system has attempted to load or restore a file into the registry, but the specified file is not in a registry file format +error1018= \ +Illegal operation attempted on a registry key that has been marked for deletion +error1019= \ +System could not allocate the required space in a registry log +error1020= \ +Cannot create a symbolic link in a registry key that already has subkeys or values +error1021= \ +Cannot create a stable subkey under a volatile parent key +error1022= \ +A notify change request is being completed and the information is not being returned in the caller''s buffer. The caller now needs to enumerate the files to find the changes +error1023= \ +Unknown error (0x3ff) diff --git a/core/src/main/resources/hudson/atom.jelly b/core/src/main/resources/hudson/atom.jelly new file mode 100644 index 0000000000..d227078d99 --- /dev/null +++ b/core/src/main/resources/hudson/atom.jelly @@ -0,0 +1,33 @@ + + + + + + + ${title} + + + + + 2001-01-01T00:00:00Z + + + ${h.xsDate(adapter.getEntryTimestamp(entries[0]))} + + + + Hudson + + + + + ${adapter.getEntryTitle(e)} + + ${adapter.getEntryID(e)} + ${h.xsDate(adapter.getEntryTimestamp(e))} + ${h.xsDate(adapter.getEntryTimestamp(e))} + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/AbstractModelObject/descriptionForm.jelly b/core/src/main/resources/hudson/model/AbstractModelObject/descriptionForm.jelly new file mode 100644 index 0000000000..002074db2f --- /dev/null +++ b/core/src/main/resources/hudson/model/AbstractModelObject/descriptionForm.jelly @@ -0,0 +1,9 @@ + +

+ +
+ +
+
diff --git a/core/src/main/resources/hudson/model/AbstractModelObject/editDescription.jelly b/core/src/main/resources/hudson/model/AbstractModelObject/editDescription.jelly new file mode 100644 index 0000000000..a07aa2e9e5 --- /dev/null +++ b/core/src/main/resources/hudson/model/AbstractModelObject/editDescription.jelly @@ -0,0 +1,14 @@ + + + + + +
+ + +
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/AbstractModelObject/error.jelly b/core/src/main/resources/hudson/model/AbstractModelObject/error.jelly new file mode 100644 index 0000000000..78827a821d --- /dev/null +++ b/core/src/main/resources/hudson/model/AbstractModelObject/error.jelly @@ -0,0 +1,10 @@ + + + + + +

Error

+

+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Actionable/actions.jelly b/core/src/main/resources/hudson/model/Actionable/actions.jelly new file mode 100644 index 0000000000..9bd1bf49ab --- /dev/null +++ b/core/src/main/resources/hudson/model/Actionable/actions.jelly @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Build/changes.jelly b/core/src/main/resources/hudson/model/Build/changes.jelly new file mode 100644 index 0000000000..f0d4addeee --- /dev/null +++ b/core/src/main/resources/hudson/model/Build/changes.jelly @@ -0,0 +1,12 @@ + + + + + + Changes + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Build/index.jelly b/core/src/main/resources/hudson/model/Build/index.jelly new file mode 100644 index 0000000000..ba80779e09 --- /dev/null +++ b/core/src/main/resources/hudson/model/Build/index.jelly @@ -0,0 +1,115 @@ + + + + +
+ +
+ Started ${it.timestampString} ago +
+
+ + + Build #${it.number} + () + + +
+ +
+ + + + + + + + Latest Test Result + + + (no failures) + + + (1 failure) + + + (${tr.failCount} failures) + + + + + + + + + + + + + + Changes in dependency +
    + +
  1. + ${dep.project.displayName} + + + ${dep.from.displayName} + + → + + + ${dep.to.displayName} + + (detail) +
  2. +
    +
+
+
+
+ + + + + +

Upstream Builds

+ +
+ + +

Downstream Builds

+ +
+ + +

Permalinks

+ +
+
+
diff --git a/core/src/main/resources/hudson/model/Build/sidepanel.jelly b/core/src/main/resources/hudson/model/Build/sidepanel.jelly new file mode 100644 index 0000000000..915ccfdb04 --- /dev/null +++ b/core/src/main/resources/hudson/model/Build/sidepanel.jelly @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Computer/index.jelly b/core/src/main/resources/hudson/model/Computer/index.jelly new file mode 100644 index 0000000000..ad9fb7af99 --- /dev/null +++ b/core/src/main/resources/hudson/model/Computer/index.jelly @@ -0,0 +1,37 @@ + + + + + +
+
+ + + + + + +
+
+ +

+ + Slave ${it.displayName} +

+ +

Projects tied on ${it.displayName}

+ + + +

+ None +

+
+ + + +
+ +
+
+
diff --git a/core/src/main/resources/hudson/model/Computer/sidepanel.jelly b/core/src/main/resources/hudson/model/Computer/sidepanel.jelly new file mode 100644 index 0000000000..036875cb68 --- /dev/null +++ b/core/src/main/resources/hudson/model/Computer/sidepanel.jelly @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/DirectoryHolder/dir.jelly b/core/src/main/resources/hudson/model/DirectoryHolder/dir.jelly new file mode 100644 index 0000000000..4bdf8f5318 --- /dev/null +++ b/core/src/main/resources/hudson/model/DirectoryHolder/dir.jelly @@ -0,0 +1,46 @@ + + + + + +
+ +
+
+ + + ${p.title} + / + + + +
+
+ + + + + + + + + + + +
+ + + + ${t.title} + / + + + ${x.getSize()} + + fingerprint + +
+
+
+
+
diff --git a/core/src/main/resources/hudson/model/ExternalJob/sidepanel.jelly b/core/src/main/resources/hudson/model/ExternalJob/sidepanel.jelly new file mode 100644 index 0000000000..7b9c6a14cd --- /dev/null +++ b/core/src/main/resources/hudson/model/ExternalJob/sidepanel.jelly @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/ExternalRun/index.jelly b/core/src/main/resources/hudson/model/ExternalRun/index.jelly new file mode 100644 index 0000000000..ae6eabfd26 --- /dev/null +++ b/core/src/main/resources/hudson/model/ExternalRun/index.jelly @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/ExternalRun/sidepanel.jelly b/core/src/main/resources/hudson/model/ExternalRun/sidepanel.jelly new file mode 100644 index 0000000000..3917267364 --- /dev/null +++ b/core/src/main/resources/hudson/model/ExternalRun/sidepanel.jelly @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Fingerprint/index.jelly b/core/src/main/resources/hudson/model/Fingerprint/index.jelly new file mode 100644 index 0000000000..e81554def6 --- /dev/null +++ b/core/src/main/resources/hudson/model/Fingerprint/index.jelly @@ -0,0 +1,57 @@ + + + + + + + + + + + +

+ + ${it.fileName} +

+
+ MD5: ${it.hashString} +
+
+ Introduced ${it.timestampString} ago + + + outside Hudson + + + + + +
+

Usage

+

+ This file has been used in the following places: +

+ + + + + + + + + +
+ + + ${j} + + + ${j} + + + + +
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Hudson/_script.jelly b/core/src/main/resources/hudson/model/Hudson/_script.jelly new file mode 100644 index 0000000000..351fdf1716 --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/_script.jelly @@ -0,0 +1,28 @@ + + + + + + +

Script Console

+

+ Type in arbitrary Groovy script and + execute it on the server. Useful for trouble-shooting and diagnostics. + Use the 'println' command to see the output (if you use System.out, + it will go to the server's stdout, which is harder to see.) +

+
+ +
+ +
+
+ +

Result

+
${output}
+
+
+
+
diff --git a/core/src/main/resources/hudson/model/Hudson/configure.jelly b/core/src/main/resources/hudson/model/Hudson/configure.jelly new file mode 100644 index 0000000000..c6e1053793 --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/configure.jelly @@ -0,0 +1,148 @@ + + + + + + + + ${it.rootDir} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${s.clockDifferenceString} + + + + +
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/core/src/main/resources/hudson/model/Hudson/fingerprintCheck.jelly b/core/src/main/resources/hudson/model/Hudson/fingerprintCheck.jelly new file mode 100644 index 0000000000..f3b1e3fda1 --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/fingerprintCheck.jelly @@ -0,0 +1,29 @@ + + + + + +

+ + Check File Fingerprint +

+ + +
+ Got a jar file but don't know which version it is?
+ Find that out by checking the fingerprint against + the database in Hudson (more details) +
+
+ + + + + + +
+
+
+
diff --git a/core/src/main/resources/hudson/model/Hudson/legend.jelly b/core/src/main/resources/hudson/model/Hudson/legend.jelly new file mode 100644 index 0000000000..df48e3ab93 --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/legend.jelly @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + +
+ + The project has never been built before, or the project is disabled. +
+ + The first build of the project is in progress. +
+ + The last build was successful. +
+ + The last build was successful. A new build is in progress. +
+ + The last build was successful but unstable. + This is primarily used to represent test failures. +
+ + The last build was successful but unstable. A new build is in progress. +
+ + The last build fatally failed. +
+ + The last build fatally failed. A new build is in progress. +
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Hudson/log.jelly b/core/src/main/resources/hudson/model/Hudson/log.jelly new file mode 100644 index 0000000000..70653b28bd --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/log.jelly @@ -0,0 +1,14 @@ + + + + + +

Hudson Log

+ +
${h.printLogRecord(log)}
+
+
+
+
diff --git a/core/src/main/resources/hudson/model/Hudson/login.jelly b/core/src/main/resources/hudson/model/Hudson/login.jelly new file mode 100644 index 0000000000..6fc0d63cfa --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/login.jelly @@ -0,0 +1,23 @@ + + + + +
+ +
+ + + + + + + + + +
User:
Password
+ +
+
+
+
+
diff --git a/core/src/main/resources/hudson/model/Hudson/loginError.jelly b/core/src/main/resources/hudson/model/Hudson/loginError.jelly new file mode 100644 index 0000000000..c159889722 --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/loginError.jelly @@ -0,0 +1,13 @@ + + + + + +
+ Invalid login information. Please try again. +
+ Try again +
+
+
+
diff --git a/core/src/main/resources/hudson/model/Hudson/manage.jelly b/core/src/main/resources/hudson/model/Hudson/manage.jelly new file mode 100644 index 0000000000..17d63b1a0f --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/manage.jelly @@ -0,0 +1,50 @@ + + + + + + + + +
+ +
+
+
+
+ + + +

Manage Hudson

+ + + + + + + + Displays various environmental information to assist trouble-shooting. + + + System log captures output from java.util.logging output related to Hudson. + + + Executes arbitrary script for administration/trouble-shooting/diagnostics + + + + + + + + Stops executing new builds, so that the system can be eventually shut down safely. + + + +
+
+
+
diff --git a/core/src/main/resources/hudson/model/Hudson/managePlugins.jelly b/core/src/main/resources/hudson/model/Hudson/managePlugins.jelly new file mode 100644 index 0000000000..9b1a8a4b3e --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/managePlugins.jelly @@ -0,0 +1,91 @@ + + + + + + + +

Installed Plugins

+
+ +
+ + + + + + + +
+

${p.longName}

+
+ + + + +
+
+ + +
+
+
+ + + + +

Upload Plugin

+
+ Upload a plugin from your computer by using the form below, or + place your plugin at $HUDSON_HOME/plugins on the server. +
+
+ + + File: + +
+ +
+
+ +
+
+
diff --git a/core/src/main/resources/hudson/model/Hudson/newView.jelly b/core/src/main/resources/hudson/model/Hudson/newView.jelly new file mode 100644 index 0000000000..b479a9ab79 --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/newView.jelly @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/resources/hudson/model/Hudson/noJob.jelly b/core/src/main/resources/hudson/model/Hudson/noJob.jelly new file mode 100644 index 0000000000..8785ff3218 --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/noJob.jelly @@ -0,0 +1,6 @@ +
+ + Welcome to Hudson! Please create new jobs to get started. +
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Hudson/projectRelationship-help.jelly b/core/src/main/resources/hudson/model/Hudson/projectRelationship-help.jelly new file mode 100644 index 0000000000..c89969577c --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/projectRelationship-help.jelly @@ -0,0 +1,24 @@ + + + + +

What's "project relationship"?

+

+ When you have projects that depend on each other, Hudson can track which build of + the upstream project is used by which build of the downstream project, by using + the records created by + the fingerprint support. +

+

+ For this feature to work, the following conditions need to be met: +

+
    +
  1. The upstream project records the fingerprints of its build artifacts
  2. +
  3. The downstream project records the fingerprints of the upstream jar files it uses
  4. +
+

+ This allows Hudson to correlate two projects. +

+
+
+
diff --git a/core/src/main/resources/hudson/model/Hudson/projectRelationship.jelly b/core/src/main/resources/hudson/model/Hudson/projectRelationship.jelly new file mode 100644 index 0000000000..cf1316f010 --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/projectRelationship.jelly @@ -0,0 +1,70 @@ + + + + + +

Project Relationship

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ upstream project: + + -> + downstream project: + +
+ + +
+ No such project '${request.getParameter('lhs')}' +
+ No such project '${request.getParameter('rhs')}' +
+ There are no fingerprint records that connect these two projects. +
+ + + +
+
+
+
+
diff --git a/core/src/main/resources/hudson/model/Hudson/sidepanel2.jelly b/core/src/main/resources/hudson/model/Hudson/sidepanel2.jelly new file mode 100644 index 0000000000..7577b77008 --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/sidepanel2.jelly @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Hudson/systemInfo.jelly b/core/src/main/resources/hudson/model/Hudson/systemInfo.jelly new file mode 100644 index 0000000000..710a1ae28d --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/systemInfo.jelly @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + +
NameValue
+
+
+ + + +

System Properties

+ +

Environment Variables

+ +
+
+
+
diff --git a/core/src/main/resources/hudson/model/Job/buildHistory.jelly b/core/src/main/resources/hudson/model/Job/buildHistory.jelly new file mode 100644 index 0000000000..006dffb3b9 --- /dev/null +++ b/core/src/main/resources/hudson/model/Job/buildHistory.jelly @@ -0,0 +1,53 @@ + + + + + + + + + #${it.nextBuildNumber} + + + (pending) + + + + + + + + + + + #${build.number} + + + + + + + + + + + +
+ + + [cancel] +
+ +
+
+ + + for all + + for failures + + +
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Job/buildTimeTrend.jelly b/core/src/main/resources/hudson/model/Job/buildTimeTrend.jelly new file mode 100644 index 0000000000..c360e39205 --- /dev/null +++ b/core/src/main/resources/hudson/model/Job/buildTimeTrend.jelly @@ -0,0 +1,39 @@ + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + +
BuildDurationSlave
+ ${r.displayName} + + ${r.durationString} + + +
+
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Job/configure-entries.jelly b/core/src/main/resources/hudson/model/Job/configure-entries.jelly new file mode 100644 index 0000000000..11b6817a7e --- /dev/null +++ b/core/src/main/resources/hudson/model/Job/configure-entries.jelly @@ -0,0 +1,4 @@ + + diff --git a/core/src/main/resources/hudson/model/Job/configure.jelly b/core/src/main/resources/hudson/model/Job/configure.jelly new file mode 100644 index 0000000000..3941d21fba --- /dev/null +++ b/core/src/main/resources/hudson/model/Job/configure.jelly @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/resources/hudson/model/Job/delete.jelly b/core/src/main/resources/hudson/model/Job/delete.jelly new file mode 100644 index 0000000000..eb3e8124f8 --- /dev/null +++ b/core/src/main/resources/hudson/model/Job/delete.jelly @@ -0,0 +1,12 @@ + + + + + +
+ Are you sure about deleting the job? + +
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Job/index.jelly b/core/src/main/resources/hudson/model/Job/index.jelly new file mode 100644 index 0000000000..807df4bc01 --- /dev/null +++ b/core/src/main/resources/hudson/model/Job/index.jelly @@ -0,0 +1,40 @@ + + + + +

Project ${it.name}

+ + + + + +

Permalinks

+ +
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Job/main.jelly b/core/src/main/resources/hudson/model/Job/main.jelly new file mode 100644 index 0000000000..cfe84478dc --- /dev/null +++ b/core/src/main/resources/hudson/model/Job/main.jelly @@ -0,0 +1,2 @@ + + diff --git a/core/src/main/resources/hudson/model/Job/rename.jelly b/core/src/main/resources/hudson/model/Job/rename.jelly new file mode 100644 index 0000000000..411cb45109 --- /dev/null +++ b/core/src/main/resources/hudson/model/Job/rename.jelly @@ -0,0 +1,14 @@ + + + + + + +
+ Are you sure about renaming ${it.name} to ${newName}? + + +
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Job/rssHeader.jelly b/core/src/main/resources/hudson/model/Job/rssHeader.jelly new file mode 100644 index 0000000000..97f71ac18b --- /dev/null +++ b/core/src/main/resources/hudson/model/Job/rssHeader.jelly @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/JobCollection/index.jelly b/core/src/main/resources/hudson/model/JobCollection/index.jelly new file mode 100644 index 0000000000..8cdc2bbd8b --- /dev/null +++ b/core/src/main/resources/hudson/model/JobCollection/index.jelly @@ -0,0 +1,21 @@ + + + + + + +
+ +
+ + + + + + + + + +
+
+
diff --git a/core/src/main/resources/hudson/model/JobCollection/newJob.jelly b/core/src/main/resources/hudson/model/JobCollection/newJob.jelly new file mode 100644 index 0000000000..553d3b7d69 --- /dev/null +++ b/core/src/main/resources/hudson/model/JobCollection/newJob.jelly @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/resources/hudson/model/JobCollection/people.jelly b/core/src/main/resources/hudson/model/JobCollection/people.jelly new file mode 100644 index 0000000000..f03cd17cf1 --- /dev/null +++ b/core/src/main/resources/hudson/model/JobCollection/people.jelly @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + +
NameLast ActiveOn
${p.user}${p.lastChangeTimeString}${p.project.name}
+
+
+
diff --git a/core/src/main/resources/hudson/model/JobCollection/sidepanel.jelly b/core/src/main/resources/hudson/model/JobCollection/sidepanel.jelly new file mode 100644 index 0000000000..87f3338c61 --- /dev/null +++ b/core/src/main/resources/hudson/model/JobCollection/sidepanel.jelly @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/JobCollection/sidepanel2.jelly b/core/src/main/resources/hudson/model/JobCollection/sidepanel2.jelly new file mode 100644 index 0000000000..8a56e5e7e2 --- /dev/null +++ b/core/src/main/resources/hudson/model/JobCollection/sidepanel2.jelly @@ -0,0 +1,2 @@ + + diff --git a/core/src/main/resources/hudson/model/NoFingerprintMatch/index.jelly b/core/src/main/resources/hudson/model/NoFingerprintMatch/index.jelly new file mode 100644 index 0000000000..d50d3f99ab --- /dev/null +++ b/core/src/main/resources/hudson/model/NoFingerprintMatch/index.jelly @@ -0,0 +1,31 @@ + + + + + + + + + + +

+ + No matching record found +

+ +

+ The fingerprint ${id.displayName} did not match any of the recorded data. +

+
    +
  1. + Perhaps the file was not created under Hudson. + Maybe it's a version that someone built locally on his/her own machine. +
  2. +
  3. + Perhaps the projects are not set up correctly and not recoding + fingerprints. Check the project setting. +
  4. +
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Project/changes.jelly b/core/src/main/resources/hudson/model/Project/changes.jelly new file mode 100644 index 0000000000..93866ad8b0 --- /dev/null +++ b/core/src/main/resources/hudson/model/Project/changes.jelly @@ -0,0 +1,36 @@ + + + + + + + + + + Changes + from #${from} + to #${to} + + + +

+ ${b.displayName} : + +

+ +
    + +
  1. + + by + ${c.author} +
  2. +
    +
+
+
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Project/configure-entries.jelly b/core/src/main/resources/hudson/model/Project/configure-entries.jelly new file mode 100644 index 0000000000..10b3ea1539 --- /dev/null +++ b/core/src/main/resources/hudson/model/Project/configure-entries.jelly @@ -0,0 +1,111 @@ + + + + + (No new builds will be executed until the project is re-enabled.) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Project/main.jelly b/core/src/main/resources/hudson/model/Project/main.jelly new file mode 100644 index 0000000000..ce09a34ff6 --- /dev/null +++ b/core/src/main/resources/hudson/model/Project/main.jelly @@ -0,0 +1,107 @@ + + + + + + + + check relationship + + + + + + +
+ + + +
+ + + + + +
+ Test Result Trend +
+
+ +
+ +
+
+
+ + + + + + ${act.displayName} + + + + Workspace + + + + + + + Latest Test Result + + + (no failures) + + + (1 failure) + + + (${tr.failCount} failures) + + + + +
+ + + + +

Upstream Projects

+ +
+ + +

Downstream Projects

+ +
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Project/noWorkspace.jelly b/core/src/main/resources/hudson/model/Project/noWorkspace.jelly new file mode 100644 index 0000000000..8253af5774 --- /dev/null +++ b/core/src/main/resources/hudson/model/Project/noWorkspace.jelly @@ -0,0 +1,35 @@ + + + + + +

Error: no workspace

+ + +

+ A project won't have any workspace until at least one build is performed. +

+
+ +

+ There's no workspace for this project. Possible reasons are: +

+
    +
  1. + The project was renamed recently and no build was done under the new name +
  2. +
  3. + The slave this project has run on for the las time was removed +
  4. +
  5. + The workspace directory (${it.workspace.local}) is removed outside Hudson. +
  6. +
+
+
+

+ Run a build to have Hudson create a workspace. +

+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Project/sidepanel.jelly b/core/src/main/resources/hudson/model/Project/sidepanel.jelly new file mode 100644 index 0000000000..82a24c1f4e --- /dev/null +++ b/core/src/main/resources/hudson/model/Project/sidepanel.jelly @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Project/svn-password.jelly b/core/src/main/resources/hudson/model/Project/svn-password.jelly new file mode 100644 index 0000000000..227ed87043 --- /dev/null +++ b/core/src/main/resources/hudson/model/Project/svn-password.jelly @@ -0,0 +1,33 @@ + + + + + +

How to set Subversion password?

+

+ While subversion allows you to specify the '--password' option explicitly in the command line, + this is generally not desirable when you are using Hudson, because: +

+
    +
  1. People can read your password by using pargs
  2. +
  3. Password will be stored in a clear text in Hudson
  4. +
+

+ A preferrable approach is to do the following steps: +

+
    +
  1. Logon to the server that runs Hudson, by using the same user account Hudson uses
  2. +
  3. Manually run svn co ...
  4. +
  5. Subversion asks you the password interactively. Type in the password
  6. +
  7. + Subversion stores it in its authentication cache, and for successive svn co ... + it will use the password stored in the cache. +
  8. +
+

+ Note that this approach still doesn't really make your password secure, + it just makes it bit harder to read. +

+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Run/artifacts-index.jelly b/core/src/main/resources/hudson/model/Run/artifacts-index.jelly new file mode 100644 index 0000000000..899a2afdb2 --- /dev/null +++ b/core/src/main/resources/hudson/model/Run/artifacts-index.jelly @@ -0,0 +1,15 @@ + + + + + + Build Artifacts + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Run/console.jelly b/core/src/main/resources/hudson/model/Run/console.jelly new file mode 100644 index 0000000000..dd0d9b45b0 --- /dev/null +++ b/core/src/main/resources/hudson/model/Run/console.jelly @@ -0,0 +1,32 @@ + + + + + + + Console Output + + + + View as plain text + + + + + +

+          
+ +
+ +
+ + +
+
+
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Run/consoleText.jelly b/core/src/main/resources/hudson/model/Run/consoleText.jelly new file mode 100644 index 0000000000..9d37ec2918 --- /dev/null +++ b/core/src/main/resources/hudson/model/Run/consoleText.jelly @@ -0,0 +1,4 @@ + +${it.log} \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Run/logKeep.jelly b/core/src/main/resources/hudson/model/Run/logKeep.jelly new file mode 100644 index 0000000000..83eea5274b --- /dev/null +++ b/core/src/main/resources/hudson/model/Run/logKeep.jelly @@ -0,0 +1,15 @@ + + + +
+ + + + + + +
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/User/builds.jelly b/core/src/main/resources/hudson/model/User/builds.jelly new file mode 100644 index 0000000000..45d3a36421 --- /dev/null +++ b/core/src/main/resources/hudson/model/User/builds.jelly @@ -0,0 +1,47 @@ + + + + +

+ + Builds for ${it} +

+ + + + + + + + + + + + + + + + + + +
BuildDateStatus
+ + + + + ${b.project.name} + + #${b.number} + + ${b.timestampString} + + + + + + +
+ +
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/User/configure.jelly b/core/src/main/resources/hudson/model/User/configure.jelly new file mode 100644 index 0000000000..9020e054e5 --- /dev/null +++ b/core/src/main/resources/hudson/model/User/configure.jelly @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/resources/hudson/model/User/index.jelly b/core/src/main/resources/hudson/model/User/index.jelly new file mode 100644 index 0000000000..caab6befd4 --- /dev/null +++ b/core/src/main/resources/hudson/model/User/index.jelly @@ -0,0 +1,12 @@ + + + + +

+ + ${it.fullName} +

+ +
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/User/sidepanel.jelly b/core/src/main/resources/hudson/model/User/sidepanel.jelly new file mode 100644 index 0000000000..d9c189e7da --- /dev/null +++ b/core/src/main/resources/hudson/model/User/sidepanel.jelly @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/View/configure.jelly b/core/src/main/resources/hudson/model/View/configure.jelly new file mode 100644 index 0000000000..3512efa94a --- /dev/null +++ b/core/src/main/resources/hudson/model/View/configure.jelly @@ -0,0 +1,27 @@ + + + + + + + + + + + + + ${job.name} +
+
+
+ + + + +
+
+
+
diff --git a/core/src/main/resources/hudson/model/View/delete.jelly b/core/src/main/resources/hudson/model/View/delete.jelly new file mode 100644 index 0000000000..be5c7d6049 --- /dev/null +++ b/core/src/main/resources/hudson/model/View/delete.jelly @@ -0,0 +1,12 @@ + + + + + +
+ Are you sure about deleting the view? + +
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/View/noJob.jelly b/core/src/main/resources/hudson/model/View/noJob.jelly new file mode 100644 index 0000000000..7d9e11a883 --- /dev/null +++ b/core/src/main/resources/hudson/model/View/noJob.jelly @@ -0,0 +1,3 @@ +
+ This view has no jobs associated with it. Please add some +
\ No newline at end of file diff --git a/core/src/main/resources/hudson/model/View/sidepanel2.jelly b/core/src/main/resources/hudson/model/View/sidepanel2.jelly new file mode 100644 index 0000000000..cf1f99802c --- /dev/null +++ b/core/src/main/resources/hudson/model/View/sidepanel2.jelly @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/rss20.jelly b/core/src/main/resources/hudson/rss20.jelly new file mode 100644 index 0000000000..a687ba8acd --- /dev/null +++ b/core/src/main/resources/hudson/rss20.jelly @@ -0,0 +1,24 @@ + + + + + + + + ${title} + ${rootURL}/${url} + ${title} + + + + ${adapter.getEntryTitle(e)} + ${rootURL}/${adapter.getEntryUrl(e)} + ${adapter.getEntryID(e)} + ${h.xsDate(adapter.getEntryTimestamp(e))} + Hudson + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/scm/CVSChangeLogSet/digest.jelly b/core/src/main/resources/hudson/scm/CVSChangeLogSet/digest.jelly new file mode 100644 index 0000000000..65e2a62062 --- /dev/null +++ b/core/src/main/resources/hudson/scm/CVSChangeLogSet/digest.jelly @@ -0,0 +1,19 @@ + + + + + No changes. + + + Changes +
    + +
  1. ${cs.msgEscaped} (detail) +
  2. +
    +
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/scm/CVSChangeLogSet/index.jelly b/core/src/main/resources/hudson/scm/CVSChangeLogSet/index.jelly new file mode 100644 index 0000000000..831e65c1ad --- /dev/null +++ b/core/src/main/resources/hudson/scm/CVSChangeLogSet/index.jelly @@ -0,0 +1,32 @@ + + +

Summary

+
    + +
  1. +
    +
+ + + + + + + + + + + + + + +
+ +
+ ${cs.author}:
+ ${cs.msgEscaped} +
+
${f.revision}${f.name}
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/scm/CVSSCM/DescriptorImpl/enterPassword.jelly b/core/src/main/resources/hudson/scm/CVSSCM/DescriptorImpl/enterPassword.jelly new file mode 100644 index 0000000000..711e98bd48 --- /dev/null +++ b/core/src/main/resources/hudson/scm/CVSSCM/DescriptorImpl/enterPassword.jelly @@ -0,0 +1,25 @@ + + + + +

Enter CVS password

+

+ CVS stores passwords for :pserver CVSROOTs, per user. This page lets you add/replace + password to those entries. +

+ + + + + + + + + + + +
+
+
diff --git a/core/src/main/resources/hudson/scm/CVSSCM/TagAction/alreadyTagged.jelly b/core/src/main/resources/hudson/scm/CVSSCM/TagAction/alreadyTagged.jelly new file mode 100644 index 0000000000..f8f01daa3d --- /dev/null +++ b/core/src/main/resources/hudson/scm/CVSSCM/TagAction/alreadyTagged.jelly @@ -0,0 +1,12 @@ + + + + +

Build #${it.build.number}

+

+ This build is already tagged as ${it.tagName} +

+
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/scm/CVSSCM/TagAction/inProgress.jelly b/core/src/main/resources/hudson/scm/CVSSCM/TagAction/inProgress.jelly new file mode 100644 index 0000000000..309fceb851 --- /dev/null +++ b/core/src/main/resources/hudson/scm/CVSSCM/TagAction/inProgress.jelly @@ -0,0 +1,17 @@ + + + + +

Build #${it.build.number}

+

+ Tagging "${it.workerThread.tagName}" is in progress. +

+
+ +
+ +
+
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/scm/CVSSCM/TagAction/tagForm.jelly b/core/src/main/resources/hudson/scm/CVSSCM/TagAction/tagForm.jelly new file mode 100644 index 0000000000..62df2c92bf --- /dev/null +++ b/core/src/main/resources/hudson/scm/CVSSCM/TagAction/tagForm.jelly @@ -0,0 +1,20 @@ + + + + + +

Build #${it.build.number}

+
+

+ Choose the CVS tag name for this build: + + +

+
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/scm/CVSSCM/config.jelly b/core/src/main/resources/hudson/scm/CVSSCM/config.jelly new file mode 100644 index 0000000000..9ccf3e9d84 --- /dev/null +++ b/core/src/main/resources/hudson/scm/CVSSCM/config.jelly @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + (run CVS in a way compatible with older versions of Hudson <1.21) + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/scm/CVSSCM/global.jelly b/core/src/main/resources/hudson/scm/CVSSCM/global.jelly new file mode 100644 index 0000000000..651b1d37b7 --- /dev/null +++ b/core/src/main/resources/hudson/scm/CVSSCM/global.jelly @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/scm/NullSCM/config.jelly b/core/src/main/resources/hudson/scm/NullSCM/config.jelly new file mode 100644 index 0000000000..3e71a80c7f --- /dev/null +++ b/core/src/main/resources/hudson/scm/NullSCM/config.jelly @@ -0,0 +1,2 @@ + + diff --git a/core/src/main/resources/hudson/scm/NullSCM/global.jelly b/core/src/main/resources/hudson/scm/NullSCM/global.jelly new file mode 100644 index 0000000000..3e71a80c7f --- /dev/null +++ b/core/src/main/resources/hudson/scm/NullSCM/global.jelly @@ -0,0 +1,2 @@ + + diff --git a/core/src/main/resources/hudson/scm/SubversionChangeLogSet/digest.jelly b/core/src/main/resources/hudson/scm/SubversionChangeLogSet/digest.jelly new file mode 100644 index 0000000000..9d962b119e --- /dev/null +++ b/core/src/main/resources/hudson/scm/SubversionChangeLogSet/digest.jelly @@ -0,0 +1,40 @@ + + + + + + + + + Revision: + ${r.value} +
+
+ + Revisions +
    + + ${r.key} : ${r.value} + +
+
+
+ + + No changes. + + + Changes +
    + +
  1. + ${cs.msgEscaped} + (detail) +
  2. +
    +
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/scm/SubversionChangeLogSet/index.jelly b/core/src/main/resources/hudson/scm/SubversionChangeLogSet/index.jelly new file mode 100644 index 0000000000..40e8f7baed --- /dev/null +++ b/core/src/main/resources/hudson/scm/SubversionChangeLogSet/index.jelly @@ -0,0 +1,31 @@ + + +

Summary

+
    + +
  1. +
    +
+ + + + + + + + + + + + + +
+ +
+ Revision ${cs.revision} by ${cs.author}:
+ ${cs.msgEscaped} +
+
${p.value}
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/scm/SubversionSCM/config.jelly b/core/src/main/resources/hudson/scm/SubversionSCM/config.jelly new file mode 100644 index 0000000000..3103a468e9 --- /dev/null +++ b/core/src/main/resources/hudson/scm/SubversionSCM/config.jelly @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/scm/SubversionSCM/global.jelly b/core/src/main/resources/hudson/scm/SubversionSCM/global.jelly new file mode 100644 index 0000000000..4bf657314d --- /dev/null +++ b/core/src/main/resources/hudson/scm/SubversionSCM/global.jelly @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/Ant/config.jelly b/core/src/main/resources/hudson/tasks/Ant/config.jelly new file mode 100644 index 0000000000..d41cb51535 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/Ant/config.jelly @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/Ant/global.jelly b/core/src/main/resources/hudson/tasks/Ant/global.jelly new file mode 100644 index 0000000000..042305f992 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/Ant/global.jelly @@ -0,0 +1,26 @@ + + + + + + + + + + + + + +
+ +
+
+
+
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/ArtifactArchiver/config.jelly b/core/src/main/resources/hudson/tasks/ArtifactArchiver/config.jelly new file mode 100644 index 0000000000..aaa85058de --- /dev/null +++ b/core/src/main/resources/hudson/tasks/ArtifactArchiver/config.jelly @@ -0,0 +1,16 @@ + + + + + + + Discard all but the last successful artifact to save disk space + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/ArtifactArchiver/global.jelly b/core/src/main/resources/hudson/tasks/ArtifactArchiver/global.jelly new file mode 100644 index 0000000000..1c123d2ec0 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/ArtifactArchiver/global.jelly @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/BatchFile/config.jelly b/core/src/main/resources/hudson/tasks/BatchFile/config.jelly new file mode 100644 index 0000000000..0d835c1282 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/BatchFile/config.jelly @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/BatchFile/global.jelly b/core/src/main/resources/hudson/tasks/BatchFile/global.jelly new file mode 100644 index 0000000000..bb10d2e5d5 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/BatchFile/global.jelly @@ -0,0 +1,2 @@ + + diff --git a/core/src/main/resources/hudson/tasks/BuildTrigger/config.jelly b/core/src/main/resources/hudson/tasks/BuildTrigger/config.jelly new file mode 100644 index 0000000000..7eadfec7f0 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/BuildTrigger/config.jelly @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/BuildTrigger/global.jelly b/core/src/main/resources/hudson/tasks/BuildTrigger/global.jelly new file mode 100644 index 0000000000..1c123d2ec0 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/BuildTrigger/global.jelly @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/Fingerprinter/FingerprintAction/index.jelly b/core/src/main/resources/hudson/tasks/Fingerprinter/FingerprintAction/index.jelly new file mode 100644 index 0000000000..fa4092871d --- /dev/null +++ b/core/src/main/resources/hudson/tasks/Fingerprinter/FingerprintAction/index.jelly @@ -0,0 +1,53 @@ + + + + + +

+ + Recorded Fingerprints +

+ + + + + + + + + + + + + + + + +
FileOriginal ownerAge
+ ${e.key} + + + + outside Hudson + + + this build + + + + + + + ${f.timestampString} old + + + more details + +
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/Fingerprinter/config.jelly b/core/src/main/resources/hudson/tasks/Fingerprinter/config.jelly new file mode 100644 index 0000000000..d001e33f56 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/Fingerprinter/config.jelly @@ -0,0 +1,20 @@ + + + + + + + Fingerprint all archived artifacts + + + + Keep the build logs of dependencies + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/Fingerprinter/global.jelly b/core/src/main/resources/hudson/tasks/Fingerprinter/global.jelly new file mode 100644 index 0000000000..1c123d2ec0 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/Fingerprinter/global.jelly @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/JavadocArchiver/config.jelly b/core/src/main/resources/hudson/tasks/JavadocArchiver/config.jelly new file mode 100644 index 0000000000..e055d194c5 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/JavadocArchiver/config.jelly @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/JavadocArchiver/global.jelly b/core/src/main/resources/hudson/tasks/JavadocArchiver/global.jelly new file mode 100644 index 0000000000..1c123d2ec0 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/JavadocArchiver/global.jelly @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/LogRotator/config.jelly b/core/src/main/resources/hudson/tasks/LogRotator/config.jelly new file mode 100644 index 0000000000..75fe49307f --- /dev/null +++ b/core/src/main/resources/hudson/tasks/LogRotator/config.jelly @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/LogRotator/global.jelly b/core/src/main/resources/hudson/tasks/LogRotator/global.jelly new file mode 100644 index 0000000000..1c123d2ec0 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/LogRotator/global.jelly @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/Mailer/UserProperty/config.jelly b/core/src/main/resources/hudson/tasks/Mailer/UserProperty/config.jelly new file mode 100644 index 0000000000..70ad0fb7e3 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/Mailer/UserProperty/config.jelly @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/Mailer/config.jelly b/core/src/main/resources/hudson/tasks/Mailer/config.jelly new file mode 100644 index 0000000000..8e3605395b --- /dev/null +++ b/core/src/main/resources/hudson/tasks/Mailer/config.jelly @@ -0,0 +1,16 @@ + + + + + + + Don't send e-mail for every unstable build + + + + Send separate e-mails to individuals who broke the build + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/Mailer/global.jelly b/core/src/main/resources/hudson/tasks/Mailer/global.jelly new file mode 100644 index 0000000000..1d2e355835 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/Mailer/global.jelly @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/Maven/config.jelly b/core/src/main/resources/hudson/tasks/Maven/config.jelly new file mode 100644 index 0000000000..f2e17d71a3 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/Maven/config.jelly @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/Maven/global.jelly b/core/src/main/resources/hudson/tasks/Maven/global.jelly new file mode 100644 index 0000000000..b3f53a8a10 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/Maven/global.jelly @@ -0,0 +1,26 @@ + + + + + + + + + + + + + +
+ +
+
+
+
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/Shell/config.jelly b/core/src/main/resources/hudson/tasks/Shell/config.jelly new file mode 100644 index 0000000000..d310b957c8 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/Shell/config.jelly @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/Shell/global.jelly b/core/src/main/resources/hudson/tasks/Shell/global.jelly new file mode 100644 index 0000000000..1a4b741cd4 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/Shell/global.jelly @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/junit/CaseResult/index.jelly b/core/src/main/resources/hudson/tasks/junit/CaseResult/index.jelly new file mode 100644 index 0000000000..36bfea677b --- /dev/null +++ b/core/src/main/resources/hudson/tasks/junit/CaseResult/index.jelly @@ -0,0 +1,32 @@ + + + + + +

+ ${st.message} +

+

+ ${it.fullName} +

+ +
+ Failing for the past + ${h.addSuffix(it.age,'build','builds')} + (since #${it.failedSince}) +
+
+
+ + +

Standard Output

+
+
+ + +

Standard Error

+
+
+
+
+
diff --git a/core/src/main/resources/hudson/tasks/junit/ClassResult/body.jelly b/core/src/main/resources/hudson/tasks/junit/ClassResult/body.jelly new file mode 100644 index 0000000000..0085dea97d --- /dev/null +++ b/core/src/main/resources/hudson/tasks/junit/ClassResult/body.jelly @@ -0,0 +1,24 @@ + + +

All Tests

+ + + + + + + + + + + + + +
Test nameStatus
${p.name} + + + ${pst.message} + +
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/config.jelly b/core/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/config.jelly new file mode 100644 index 0000000000..cb7a7179c9 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/config.jelly @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/global.jelly b/core/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/global.jelly new file mode 100644 index 0000000000..1c123d2ec0 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/global.jelly @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/junit/MetaTabulatedResult/body.jelly b/core/src/main/resources/hudson/tasks/junit/MetaTabulatedResult/body.jelly new file mode 100644 index 0000000000..0554c8ffc0 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/junit/MetaTabulatedResult/body.jelly @@ -0,0 +1,50 @@ + + +

All Failed Tests

+ + + + + + + + + + + +
Test NameAge
+ ${f.fullName} + + ${f.age} +
+
+ + +

All Tests

+ + + + + + + + + + + + + + + + + + + + +
${it.childTitle}Fail(diff)Total(diff)
${p.name}${p.failCount} + ${h.getDiffString2(p.failCount-prev.failCount)} + ${p.totalCount} + ${h.getDiffString2(p.totalCount-prev.totalCount)} +
+
+
\ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/junit/TabulatedResult/index.jelly b/core/src/main/resources/hudson/tasks/junit/TabulatedResult/index.jelly new file mode 100644 index 0000000000..8e28f58b15 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/junit/TabulatedResult/index.jelly @@ -0,0 +1,14 @@ + + + + +

${it.title}

+ + + + + + +
+
+
diff --git a/core/src/main/resources/hudson/triggers/SCMTrigger/SCMAction/index.jelly b/core/src/main/resources/hudson/triggers/SCMTrigger/SCMAction/index.jelly new file mode 100644 index 0000000000..91a152b712 --- /dev/null +++ b/core/src/main/resources/hudson/triggers/SCMTrigger/SCMAction/index.jelly @@ -0,0 +1,9 @@ + + + + +

Last ${it.displayName}

+
${it.log}
+
+
+
diff --git a/core/src/main/resources/hudson/triggers/SCMTrigger/config.jelly b/core/src/main/resources/hudson/triggers/SCMTrigger/config.jelly new file mode 100644 index 0000000000..5c551ee2f3 --- /dev/null +++ b/core/src/main/resources/hudson/triggers/SCMTrigger/config.jelly @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/triggers/TimerTrigger/config.jelly b/core/src/main/resources/hudson/triggers/TimerTrigger/config.jelly new file mode 100644 index 0000000000..d25f4985e7 --- /dev/null +++ b/core/src/main/resources/hudson/triggers/TimerTrigger/config.jelly @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/util/IncompatibleVMDetected/index.jelly b/core/src/main/resources/hudson/util/IncompatibleVMDetected/index.jelly new file mode 100644 index 0000000000..8576072f78 --- /dev/null +++ b/core/src/main/resources/hudson/util/IncompatibleVMDetected/index.jelly @@ -0,0 +1,35 @@ + + + + + +

[!]Error

+

+ We detected that your JVM is not supported by Hudson. This is + due to the limitation is one of the libraries that Hudson uses, namely XStream. + See this FAQ for more details. +

+ +

Detected JVM

+ + + + + + + + + + + + + + + + + + +
Vendor: ${prop['java.vm.vendor']}
Version: ${prop['java.vm.version']}
VM Name: ${prop['java.vm.name']}
OS Name: ${prop['os.name']}
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/lib/foo.jelly b/core/src/main/resources/lib/foo.jelly new file mode 100644 index 0000000000..c94ae21f99 --- /dev/null +++ b/core/src/main/resources/lib/foo.jelly @@ -0,0 +1 @@ +JELLY TAG FILE ${value} \ No newline at end of file diff --git a/core/src/main/resources/lib/form/advanced.jelly b/core/src/main/resources/lib/form/advanced.jelly new file mode 100644 index 0000000000..ce49ab8bd6 --- /dev/null +++ b/core/src/main/resources/lib/form/advanced.jelly @@ -0,0 +1,17 @@ + + + + + + + + +
+ + +
\ No newline at end of file diff --git a/core/src/main/resources/lib/form/block.jelly b/core/src/main/resources/lib/form/block.jelly new file mode 100644 index 0000000000..ad75e14330 --- /dev/null +++ b/core/src/main/resources/lib/form/block.jelly @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/form/checkbox.jelly b/core/src/main/resources/lib/form/checkbox.jelly new file mode 100644 index 0000000000..2c385ce55f --- /dev/null +++ b/core/src/main/resources/lib/form/checkbox.jelly @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/core/src/main/resources/lib/form/description.jelly b/core/src/main/resources/lib/form/description.jelly new file mode 100644 index 0000000000..0630d3982c --- /dev/null +++ b/core/src/main/resources/lib/form/description.jelly @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/form/descriptorList.jelly b/core/src/main/resources/lib/form/descriptorList.jelly new file mode 100644 index 0000000000..7fccd0cb30 --- /dev/null +++ b/core/src/main/resources/lib/form/descriptorList.jelly @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/form/editableComboBox.jelly b/core/src/main/resources/lib/form/editableComboBox.jelly new file mode 100644 index 0000000000..613ceb794b --- /dev/null +++ b/core/src/main/resources/lib/form/editableComboBox.jelly @@ -0,0 +1,55 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/form/editableComboBoxValue.jelly b/core/src/main/resources/lib/form/editableComboBoxValue.jelly new file mode 100644 index 0000000000..48d1cecfc7 --- /dev/null +++ b/core/src/main/resources/lib/form/editableComboBoxValue.jelly @@ -0,0 +1,6 @@ + + + ${editableComboBox}_values.push("${value}"); + \ No newline at end of file diff --git a/core/src/main/resources/lib/form/entry.jelly b/core/src/main/resources/lib/form/entry.jelly new file mode 100644 index 0000000000..0e2297341d --- /dev/null +++ b/core/src/main/resources/lib/form/entry.jelly @@ -0,0 +1,33 @@ + + + + + + ${attrs.title} + + + + + + + Help for feature: ${title} + + + + + + + + ${description} + + + +
Loading...
+
+
diff --git a/core/src/main/resources/lib/form/form.jelly b/core/src/main/resources/lib/form/form.jelly new file mode 100644 index 0000000000..17ce1b8dfd --- /dev/null +++ b/core/src/main/resources/lib/form/form.jelly @@ -0,0 +1,16 @@ + + +
+ + +
+
+
\ No newline at end of file diff --git a/core/src/main/resources/lib/form/option.jelly b/core/src/main/resources/lib/form/option.jelly new file mode 100644 index 0000000000..fddf82e327 --- /dev/null +++ b/core/src/main/resources/lib/form/option.jelly @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/form/optionalBlock.jelly b/core/src/main/resources/lib/form/optionalBlock.jelly new file mode 100644 index 0000000000..2349ca44a4 --- /dev/null +++ b/core/src/main/resources/lib/form/optionalBlock.jelly @@ -0,0 +1,67 @@ + + + + + + + + + + + ${title} + + + + Help for feature: ${title} + + + + +
Loading...
+
+ + + + + + + +
diff --git a/core/src/main/resources/lib/form/radioBlock.jelly b/core/src/main/resources/lib/form/radioBlock.jelly new file mode 100644 index 0000000000..88d1cebc06 --- /dev/null +++ b/core/src/main/resources/lib/form/radioBlock.jelly @@ -0,0 +1,64 @@ + + + + + + + + + ${title} + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/form/repeatable.jelly b/core/src/main/resources/lib/form/repeatable.jelly new file mode 100644 index 0000000000..b64232029f --- /dev/null +++ b/core/src/main/resources/lib/form/repeatable.jelly @@ -0,0 +1,54 @@ + + + + + + + + +
+ + + +
+
+ + +
+ + \ No newline at end of file diff --git a/core/src/main/resources/lib/form/repeatableDeleteButton.jelly b/core/src/main/resources/lib/form/repeatableDeleteButton.jelly new file mode 100644 index 0000000000..e6bdb4657e --- /dev/null +++ b/core/src/main/resources/lib/form/repeatableDeleteButton.jelly @@ -0,0 +1,4 @@ + + diff --git a/core/src/main/resources/lib/form/section.jelly b/core/src/main/resources/lib/form/section.jelly new file mode 100644 index 0000000000..f5868b7f17 --- /dev/null +++ b/core/src/main/resources/lib/form/section.jelly @@ -0,0 +1,13 @@ + + + +
+ ${title} +
+
+ +
\ No newline at end of file diff --git a/core/src/main/resources/lib/form/taglib b/core/src/main/resources/lib/form/taglib new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/src/main/resources/lib/form/textbox.jelly b/core/src/main/resources/lib/form/textbox.jelly new file mode 100644 index 0000000000..118ab0b89b --- /dev/null +++ b/core/src/main/resources/lib/form/textbox.jelly @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/actions.jelly b/core/src/main/resources/lib/hudson/actions.jelly new file mode 100644 index 0000000000..37cbc22b18 --- /dev/null +++ b/core/src/main/resources/lib/hudson/actions.jelly @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/artifactList.jelly b/core/src/main/resources/lib/hudson/artifactList.jelly new file mode 100644 index 0000000000..4a04eb60fc --- /dev/null +++ b/core/src/main/resources/lib/hudson/artifactList.jelly @@ -0,0 +1,31 @@ + + + + + + + + + ${caption}
+ +
+ + + ${caption} + +
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/buildCaption.jelly b/core/src/main/resources/lib/hudson/buildCaption.jelly new file mode 100644 index 0000000000..17e5c16a40 --- /dev/null +++ b/core/src/main/resources/lib/hudson/buildCaption.jelly @@ -0,0 +1,23 @@ + + +

+ +
+ + +
+ Progress: + + + + [cancel] +
+
+
+ + + +

+
\ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/buildLink.jelly b/core/src/main/resources/lib/hudson/buildLink.jelly new file mode 100644 index 0000000000..fd46a81573 --- /dev/null +++ b/core/src/main/resources/lib/hudson/buildLink.jelly @@ -0,0 +1,26 @@ + + + + + ${jobName} #${number} + + + + + + ${jobName} #${number} + + + + ${jobName} #${number} + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/buildRangeLink.jelly b/core/src/main/resources/lib/hudson/buildRangeLink.jelly new file mode 100644 index 0000000000..fbb38982c7 --- /dev/null +++ b/core/src/main/resources/lib/hudson/buildRangeLink.jelly @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + - + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/buildStatusSummary.groovy b/core/src/main/resources/lib/hudson/buildStatusSummary.groovy new file mode 100644 index 0000000000..2957d8c369 --- /dev/null +++ b/core/src/main/resources/lib/hudson/buildStatusSummary.groovy @@ -0,0 +1,13 @@ +// displays one line HTML summary of the build, which includes the difference +// from the previous build +// +// Usage: + +jelly { + def s = build.getBuildStatusSummary(); + if(s.isWorse) { + output.write("${s.message}"); + } else { + output.write(s.message); + } +} diff --git a/core/src/main/resources/lib/hudson/editTypeIcon.jelly b/core/src/main/resources/lib/hudson/editTypeIcon.jelly new file mode 100644 index 0000000000..0584db0039 --- /dev/null +++ b/core/src/main/resources/lib/hudson/editTypeIcon.jelly @@ -0,0 +1,7 @@ + + diff --git a/core/src/main/resources/lib/hudson/editableDescription.jelly b/core/src/main/resources/lib/hudson/editableDescription.jelly new file mode 100644 index 0000000000..f368c801fa --- /dev/null +++ b/core/src/main/resources/lib/hudson/editableDescription.jelly @@ -0,0 +1,20 @@ + + +
+
+ ${it.description} +
+ + + + + + +
+
\ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/executors.jelly b/core/src/main/resources/lib/hudson/executors.jelly new file mode 100644 index 0000000000..de59a63700 --- /dev/null +++ b/core/src/main/resources/lib/hudson/executors.jelly @@ -0,0 +1,57 @@ + + + + + + + + No. + Status + + + + + + + ${c.displayName} + (offline) + + + + + + + + + ${eloop.index+1} + + + + + Dead (!) + + + + + + Idle + + + + + + + + + + terminate this build + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/node.jelly b/core/src/main/resources/lib/hudson/node.jelly new file mode 100644 index 0000000000..46b543cdd0 --- /dev/null +++ b/core/src/main/resources/lib/hudson/node.jelly @@ -0,0 +1,13 @@ + + + + + ${value.nodeName} + + + (master) + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/progressBar.jelly b/core/src/main/resources/lib/hudson/progressBar.jelly new file mode 100644 index 0000000000..ba95b51df3 --- /dev/null +++ b/core/src/main/resources/lib/hudson/progressBar.jelly @@ -0,0 +1,30 @@ + + + + progress-bar + + cursor:pointer + window.location='${href}' + + window.status='${href}';return true; + window.status=null;return true; + + + + + + + + + + + + + + + diff --git a/core/src/main/resources/lib/hudson/progressiveText.jelly b/core/src/main/resources/lib/hudson/progressiveText.jelly new file mode 100644 index 0000000000..0425f10c0f --- /dev/null +++ b/core/src/main/resources/lib/hudson/progressiveText.jelly @@ -0,0 +1,38 @@ + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/projectView.jelly b/core/src/main/resources/lib/hudson/projectView.jelly new file mode 100644 index 0000000000..d6b8cd8a2f --- /dev/null +++ b/core/src/main/resources/lib/hudson/projectView.jelly @@ -0,0 +1,96 @@ + + +
+ + + + + + + + + + + + + projectstatus + sortable pane + + margin-top:0px; border-top: none; + + + + Job + Last Success + Last Failure + Last Duration + + + + + + + + + + + + + + + + ${job.name} + + + + + + ${lsBuild.timestampString} + (#${lsBuild.number}) + + + N/A + + + + + + + ${lfBuild.timestampString} + (#${lfBuild.number}) + + + N/A + + + + + + + ${lsBuild.durationString} + + + N/A + + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/queue.jelly b/core/src/main/resources/lib/hudson/queue.jelly new file mode 100644 index 0000000000..5b7003fb20 --- /dev/null +++ b/core/src/main/resources/lib/hudson/queue.jelly @@ -0,0 +1,39 @@ + + + + + + + + No builds in the queue. + + + + + + + + Hudson is going to shut down. + No further builds will be performed. + + (cancel) + + + + + + + + ${item.project.name} + + + cancel this build + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/rssBar.jelly b/core/src/main/resources/lib/hudson/rssBar.jelly new file mode 100644 index 0000000000..f88c726aa1 --- /dev/null +++ b/core/src/main/resources/lib/hudson/rssBar.jelly @@ -0,0 +1,9 @@ + + + diff --git a/core/src/main/resources/lib/hudson/summary.jelly b/core/src/main/resources/lib/hudson/summary.jelly new file mode 100644 index 0000000000..789d560132 --- /dev/null +++ b/core/src/main/resources/lib/hudson/summary.jelly @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/hudson/taglib b/core/src/main/resources/lib/hudson/taglib new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/src/main/resources/lib/layout/header.jelly b/core/src/main/resources/lib/layout/header.jelly new file mode 100644 index 0000000000..90f27a9dca --- /dev/null +++ b/core/src/main/resources/lib/layout/header.jelly @@ -0,0 +1,5 @@ + + + + + diff --git a/core/src/main/resources/lib/layout/isAdmin.jelly b/core/src/main/resources/lib/layout/isAdmin.jelly new file mode 100644 index 0000000000..f9fad4bf46 --- /dev/null +++ b/core/src/main/resources/lib/layout/isAdmin.jelly @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/layout/layout.jelly b/core/src/main/resources/lib/layout/layout.jelly new file mode 100644 index 0000000000..131bc64f5f --- /dev/null +++ b/core/src/main/resources/lib/layout/layout.jelly @@ -0,0 +1,112 @@ + + + + + + + + ${h.appendIfNotNull(title, ' [Hudson]', 'Hudson')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + +
+ + +
+ + +
\ No newline at end of file diff --git a/core/src/main/resources/lib/layout/main-panel.jelly b/core/src/main/resources/lib/layout/main-panel.jelly new file mode 100644 index 0000000000..e9927939a7 --- /dev/null +++ b/core/src/main/resources/lib/layout/main-panel.jelly @@ -0,0 +1,5 @@ + + + + + diff --git a/core/src/main/resources/lib/layout/pane.jelly b/core/src/main/resources/lib/layout/pane.jelly new file mode 100644 index 0000000000..d223871dde --- /dev/null +++ b/core/src/main/resources/lib/layout/pane.jelly @@ -0,0 +1,14 @@ + + + + + +
+ ${title} +
+
\ No newline at end of file diff --git a/core/src/main/resources/lib/layout/readme.txt b/core/src/main/resources/lib/layout/readme.txt new file mode 100644 index 0000000000..792ace395a --- /dev/null +++ b/core/src/main/resources/lib/layout/readme.txt @@ -0,0 +1 @@ +Tags used to manage layouts. \ No newline at end of file diff --git a/core/src/main/resources/lib/layout/rightspace.jelly b/core/src/main/resources/lib/layout/rightspace.jelly new file mode 100644 index 0000000000..f8cf105a70 --- /dev/null +++ b/core/src/main/resources/lib/layout/rightspace.jelly @@ -0,0 +1,9 @@ + + +
+ +
+
\ No newline at end of file diff --git a/core/src/main/resources/lib/layout/side-panel.jelly b/core/src/main/resources/lib/layout/side-panel.jelly new file mode 100644 index 0000000000..bc9987d294 --- /dev/null +++ b/core/src/main/resources/lib/layout/side-panel.jelly @@ -0,0 +1,5 @@ + + + + + diff --git a/core/src/main/resources/lib/layout/tab.jelly b/core/src/main/resources/lib/layout/tab.jelly new file mode 100644 index 0000000000..ab0fd6f311 --- /dev/null +++ b/core/src/main/resources/lib/layout/tab.jelly @@ -0,0 +1,38 @@ + + + + + + + + ${name} + + + + + + + + + + + + + + + + + + + + ${name} + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/layout/tabBar.jelly b/core/src/main/resources/lib/layout/tabBar.jelly new file mode 100644 index 0000000000..04738870ec --- /dev/null +++ b/core/src/main/resources/lib/layout/tabBar.jelly @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/core/src/main/resources/lib/layout/taglib b/core/src/main/resources/lib/layout/taglib new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/src/main/resources/lib/layout/task.jelly b/core/src/main/resources/lib/layout/task.jelly new file mode 100644 index 0000000000..d87b78c43d --- /dev/null +++ b/core/src/main/resources/lib/layout/task.jelly @@ -0,0 +1,29 @@ + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/layout/tasks.jelly b/core/src/main/resources/lib/layout/tasks.jelly new file mode 100644 index 0000000000..e96035b6ed --- /dev/null +++ b/core/src/main/resources/lib/layout/tasks.jelly @@ -0,0 +1,15 @@ + + +
+ +
+
\ No newline at end of file diff --git a/core/src/main/resources/lib/taglib b/core/src/main/resources/lib/taglib new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/src/main/resources/lib/test/bar.jelly b/core/src/main/resources/lib/test/bar.jelly new file mode 100644 index 0000000000..ebfa989047 --- /dev/null +++ b/core/src/main/resources/lib/test/bar.jelly @@ -0,0 +1,37 @@ + + +
+ + + No tests + + +
+ ${it.failCount} failures + + (${h.getDiffString(it.failCount-prev.failCount)}) + +
+
+
+
+
+ ${it.totalCount} tests + + (${h.getDiffString(it.totalCount-prev.totalCount)}) + +
+
+
+
+
\ No newline at end of file diff --git a/core/src/main/resources/lib/test/taglib b/core/src/main/resources/lib/test/taglib new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/src/test/java/hudson/model/FingerprintTest.java b/core/src/test/java/hudson/model/FingerprintTest.java new file mode 100644 index 0000000000..36677a0462 --- /dev/null +++ b/core/src/test/java/hudson/model/FingerprintTest.java @@ -0,0 +1,93 @@ +package hudson.model; + +import junit.framework.TestCase; +import hudson.model.Fingerprint.RangeSet; + +/** + * @author Kohsuke Kawaguchi + */ +public class FingerprintTest extends TestCase { + public void test() { + RangeSet rs = new RangeSet(); + assertFalse(rs.includes(0)); + assertFalse(rs.includes(3)); + assertFalse(rs.includes(5)); + + rs.add(3); + assertFalse(rs.includes(2)); + assertTrue(rs.includes(3)); + assertFalse(rs.includes(4)); + assertEquals("[3,4)",rs.toString()); + + rs.add(4); + assertFalse(rs.includes(2)); + assertTrue(rs.includes(3)); + assertTrue(rs.includes(4)); + assertFalse(rs.includes(5)); + assertEquals("[3,5)",rs.toString()); + + rs.add(10); + assertEquals("[3,5),[10,11)",rs.toString()); + + rs.add(9); + assertEquals("[3,5),[9,11)",rs.toString()); + + rs.add(6); + assertEquals("[3,5),[6,7),[9,11)",rs.toString()); + + rs.add(5); + assertEquals("[3,7),[9,11)",rs.toString()); + } + + public void testMerge() { + RangeSet x = new RangeSet(); + x.add(1); + x.add(2); + x.add(3); + x.add(5); + x.add(6); + assertEquals("[1,4),[5,7)",x.toString()); + + RangeSet y = new RangeSet(); + y.add(3); + y.add(4); + y.add(5); + assertEquals("[3,6)",y.toString()); + + x.add(y); + assertEquals("[1,7)",x.toString()); + } + + public void testMerge2() { + RangeSet x = new RangeSet(); + x.add(1); + x.add(2); + x.add(5); + x.add(6); + assertEquals("[1,3),[5,7)",x.toString()); + + RangeSet y = new RangeSet(); + y.add(3); + y.add(4); + assertEquals("[3,5)",y.toString()); + + x.add(y); + assertEquals("[1,7)",x.toString()); + } + + public void testMerge3() { + RangeSet x = new RangeSet(); + x.add(1); + x.add(5); + assertEquals("[1,2),[5,6)",x.toString()); + + RangeSet y = new RangeSet(); + y.add(3); + y.add(5); + y.add(7); + assertEquals("[3,4),[5,6),[7,8)",y.toString()); + + x.add(y); + assertEquals("[1,2),[3,4),[5,6),[7,8)",x.toString()); + } +} diff --git a/core/src/test/java/hudson/scheduler/CrontabTest.java b/core/src/test/java/hudson/scheduler/CrontabTest.java new file mode 100644 index 0000000000..66f578324b --- /dev/null +++ b/core/src/test/java/hudson/scheduler/CrontabTest.java @@ -0,0 +1,15 @@ +package hudson.scheduler; + +import antlr.ANTLRException; + +/** + * @author Kohsuke Kawaguchi + */ +public class CrontabTest { + public static void main(String[] args) throws ANTLRException { + for (String arg : args) { + CronTab ct = new CronTab(arg); + System.out.println(ct.toString()); + } + } +} diff --git a/core/src/test/java/hudson/util/WriterOutputStreamTest.java b/core/src/test/java/hudson/util/WriterOutputStreamTest.java new file mode 100644 index 0000000000..f1f675c4cd --- /dev/null +++ b/core/src/test/java/hudson/util/WriterOutputStreamTest.java @@ -0,0 +1,22 @@ +package hudson.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; + +/** + * TODO: junit-ize + * + * @author jglick + */ +public class WriterOutputStreamTest { + public static void main(String[] args) throws IOException { + OutputStream os = new WriterOutputStream(new OutputStreamWriter(System.out)); + PrintStream ps = new PrintStream(os); + for (int i = 0; i < 200; i++) { + ps.println("#" + i + " blah blah blah"); + } + os.close(); + } +} diff --git a/lib/commons-jelly/commons-jelly-1.1-hudson-SNAPSHOT-sources.jar b/lib/commons-jelly/commons-jelly-1.1-hudson-SNAPSHOT-sources.jar new file mode 100644 index 0000000000..7d838f2d6f Binary files /dev/null and b/lib/commons-jelly/commons-jelly-1.1-hudson-SNAPSHOT-sources.jar differ diff --git a/lib/commons-jelly/commons-jelly-1.1-hudson-SNAPSHOT.jar b/lib/commons-jelly/commons-jelly-1.1-hudson-SNAPSHOT.jar new file mode 100644 index 0000000000..7509d03cd6 Binary files /dev/null and b/lib/commons-jelly/commons-jelly-1.1-hudson-SNAPSHOT.jar differ diff --git a/lib/commons-jelly/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT-sources.jar b/lib/commons-jelly/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT-sources.jar new file mode 100644 index 0000000000..bd8766d1c6 Binary files /dev/null and b/lib/commons-jelly/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT-sources.jar differ diff --git a/lib/commons-jelly/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.jar b/lib/commons-jelly/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.jar new file mode 100644 index 0000000000..6faa56c04e Binary files /dev/null and b/lib/commons-jelly/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.jar differ diff --git a/lib/commons-jelly/jars/commons-jelly-1.1-hudson-SNAPSHOT.jar b/lib/commons-jelly/jars/commons-jelly-1.1-hudson-SNAPSHOT.jar new file mode 100644 index 0000000000..ab86b31a63 Binary files /dev/null and b/lib/commons-jelly/jars/commons-jelly-1.1-hudson-SNAPSHOT.jar differ diff --git a/lib/commons-jelly/jars/commons-jelly-1.1-hudson-SNAPSHOT.jar.md5 b/lib/commons-jelly/jars/commons-jelly-1.1-hudson-SNAPSHOT.jar.md5 new file mode 100644 index 0000000000..4632f8041e --- /dev/null +++ b/lib/commons-jelly/jars/commons-jelly-1.1-hudson-SNAPSHOT.jar.md5 @@ -0,0 +1 @@ +66051665bef98850fee46b9f59281d45 \ No newline at end of file diff --git a/lib/commons-jelly/jars/commons-jelly-1.1-hudson-SNAPSHOT.jar.sha1 b/lib/commons-jelly/jars/commons-jelly-1.1-hudson-SNAPSHOT.jar.sha1 new file mode 100644 index 0000000000..8b51092b14 --- /dev/null +++ b/lib/commons-jelly/jars/commons-jelly-1.1-hudson-SNAPSHOT.jar.sha1 @@ -0,0 +1 @@ +4cefda30048bd7aaa0a294ede27a9d29e01244ed \ No newline at end of file diff --git a/lib/commons-jelly/jars/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.jar b/lib/commons-jelly/jars/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.jar new file mode 100644 index 0000000000..27f45c4b44 Binary files /dev/null and b/lib/commons-jelly/jars/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.jar differ diff --git a/lib/commons-jelly/jars/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.jar.md5 b/lib/commons-jelly/jars/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.jar.md5 new file mode 100644 index 0000000000..2ccd3ce76d --- /dev/null +++ b/lib/commons-jelly/jars/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.jar.md5 @@ -0,0 +1 @@ +42d614834378b1b2b64d166624777ae0 \ No newline at end of file diff --git a/lib/commons-jelly/jars/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.jar.sha1 b/lib/commons-jelly/jars/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.jar.sha1 new file mode 100644 index 0000000000..06558c4f11 --- /dev/null +++ b/lib/commons-jelly/jars/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.jar.sha1 @@ -0,0 +1 @@ +60a393b96a2dd6f53254d83343be6bfd09c3b071 \ No newline at end of file diff --git a/lib/commons-jelly/java-sources/commons-jelly-1.1-hudson-SNAPSHOT-sources.jar b/lib/commons-jelly/java-sources/commons-jelly-1.1-hudson-SNAPSHOT-sources.jar new file mode 100644 index 0000000000..0bba391c51 Binary files /dev/null and b/lib/commons-jelly/java-sources/commons-jelly-1.1-hudson-SNAPSHOT-sources.jar differ diff --git a/lib/commons-jelly/java-sources/commons-jelly-1.1-hudson-SNAPSHOT-sources.jar.md5 b/lib/commons-jelly/java-sources/commons-jelly-1.1-hudson-SNAPSHOT-sources.jar.md5 new file mode 100644 index 0000000000..0aead57474 --- /dev/null +++ b/lib/commons-jelly/java-sources/commons-jelly-1.1-hudson-SNAPSHOT-sources.jar.md5 @@ -0,0 +1 @@ +430d4af3cf10f0c713e327b2af8a1089 \ No newline at end of file diff --git a/lib/commons-jelly/java-sources/commons-jelly-1.1-hudson-SNAPSHOT-sources.jar.sha1 b/lib/commons-jelly/java-sources/commons-jelly-1.1-hudson-SNAPSHOT-sources.jar.sha1 new file mode 100644 index 0000000000..8e6ab23b8c --- /dev/null +++ b/lib/commons-jelly/java-sources/commons-jelly-1.1-hudson-SNAPSHOT-sources.jar.sha1 @@ -0,0 +1 @@ +259b64b3f5ed4bc9120fe5e3881bb66d1085578b \ No newline at end of file diff --git a/lib/commons-jelly/java-sources/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT-sources.jar b/lib/commons-jelly/java-sources/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT-sources.jar new file mode 100644 index 0000000000..9550677e70 Binary files /dev/null and b/lib/commons-jelly/java-sources/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT-sources.jar differ diff --git a/lib/commons-jelly/java-sources/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT-sources.jar.md5 b/lib/commons-jelly/java-sources/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT-sources.jar.md5 new file mode 100644 index 0000000000..53fa13d123 --- /dev/null +++ b/lib/commons-jelly/java-sources/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT-sources.jar.md5 @@ -0,0 +1 @@ +1eda460bd0518fd566723206536f3e59 \ No newline at end of file diff --git a/lib/commons-jelly/java-sources/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT-sources.jar.sha1 b/lib/commons-jelly/java-sources/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT-sources.jar.sha1 new file mode 100644 index 0000000000..ab856da2cc --- /dev/null +++ b/lib/commons-jelly/java-sources/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT-sources.jar.sha1 @@ -0,0 +1 @@ +4c27a12da25897539cbc86b2761f5e95268af093 \ No newline at end of file diff --git a/lib/commons-jelly/poms/commons-jelly-1.1-hudson-SNAPSHOT.pom b/lib/commons-jelly/poms/commons-jelly-1.1-hudson-SNAPSHOT.pom new file mode 100644 index 0000000000..82cbf62086 --- /dev/null +++ b/lib/commons-jelly/poms/commons-jelly-1.1-hudson-SNAPSHOT.pom @@ -0,0 +1,450 @@ + + 3 + commons-jelly + commons-jelly + commons-jelly + 1.1-hudson-SNAPSHOT + Commons Jelly + Jelly is a Java and XML based scripting engine. Jelly combines the best ideas from JSTL, Velocity, DVSL, Ant and Cocoon all together in a simple yet powerful scripting engine. + http://jakarta.apache.org/commons/jelly/ + /images/logo.jpg + http://issues.apache.org/jira/secure/BrowseProject.jspa?id=10012 + 2002 + jakarta + cvs.apache.org + /www/jakarta.apache.org/commons/jelly/ + cvs.apache.org + /www/jakarta.apache.org/builds/jakarta-commons/jelly/ + + + Commons Dev List + commons-dev-subscribe@jakarta.apache.org + commons-dev-unsubscribe@jakarta.apache.org + http://mail-archives.apache.org/eyebrowse/SummarizeList?listName=commons-dev@jakarta.apache.org + + + Commons User List + commons-user-subscribe@jakarta.apache.org + commons-user-unsubscribe@jakarta.apache.org + http://mail-archives.apache.org/eyebrowse/SummarizeList?listName=commons-user@jakarta.apache.org + + + + + jstrachan + James Strachan + jstrachan@apache.org + SpiritSoft, Inc. + + + geirm + Geir Magnusson Jr. + geirm@adeptra.com + Adeptra, Inc. + + + werken + Bob McWhirter + bob@eng.werken.com + The Werken Company + + + dion + dIon Gillard + dion@multitask.com.au + Multitask Consulting + + Interested party + + + + morgand + Morgan Delagrange + morgand@apache.org + + + rwaldhoff + Rodney Waldhoff + rwaldhoff@apache.org + + + proyal + Peter Royal + proyal@apache.org + + + mvdb + Martin van den Bemt + martin@mvdb.net + + + polx + Paul Libbrecht + paul@activemath.org + + + rdonkin + Robert Burrell Donkin + rdonkin@apache.org + + + dfs + Daniel F. Savarese + dfs -> apache.org + + + brett + Brett Porter + brett@apache.org + + + hgilde + Hans Gilde + hgilde@apache.org + + + + + Erik Fransen + erik167@xs4all.nl + + Logo designer + + + + Calvin Yu + + + Stephen Haberman + stephenh@chase3000.com + + + Vinay Chandran + sahilvinay@yahoo.com + + Developer + + + + Theo Niemeijer + + + Joe Walnes + joew@thoughtworks.com + ThoughtWorks, Inc. + + Inventor of Mock Tags + + + + Otto von Wachter + vonwao@yahoo.com + + + Author of the tutorials + Developer + + + + Robert Leftwich + robert@leftwich.info + + Developer + + + + Jim Birchfield + jim.birchfield@genscape.com + Genscape, Inc. + + Developer + + + + Jason Horman + jhorman@musicmatch.com + + Developer + + + + Tim Anderson + tima@intalio.com + Intalio, Inc. + + Developer + + + + Theo Niemeijer + theo.niemeijer@getthere.nl + + + Developer + + + + J. Matthew Pryor + matthew_pryor@versata.com + + + Developer + + + + Knut Wannheden + + + + Developer + + + + Kelvin Tan + + + + Developer + + + + Todd Jonker + + + + Developer + + + + Christiaan ten Klooster + + + + Developer + + + + Pete Kazmier + kaz@apache.org + + + Developer + + + + + + 1.0-beta-1 + 1.0-beta-1 + 1.0-beta-1 + + + 1.0-beta-4 + COMMONS-JELLY-1_0-beta-4 + 1.0-beta-4 + + + 1.0-RC1 + COMMONS_JELLY-1_0_RC1 + 1.0-RC1 + + + 1.0 + commons-jelly-1.0 + 1.0 + + + + + Core Public API + org.apache.commons.jelly + + + Utility classes for using Jelly from Ant + org.apache.commons.jelly.task + + + Utility classes for using Jelly from Servlets + org.apache.commons.jelly.servlet + + + Classes used by Tag Implementors + org.apache.commons.jelly.impl,org.apache.commons.jelly.tags,org.apache.commons.jelly.expression + + + Tag Implementations + org.apache.commons.jelly.tags.* + + + + maven-changelog-plugin + maven-changes-plugin + maven-checkstyle-plugin + maven-developer-activity-plugin + maven-file-activity-plugin + maven-javadoc-plugin + maven-jcoverage-plugin + maven-jdepend-plugin + maven-junit-report-plugin + maven-jxr-plugin + maven-license-plugin + maven-pmd-plugin + maven-tasklist-plugin + + + scm:svn:http://svn.apache.org/repos/asf/jakarta/commons/proper/jelly/trunk + scm:svn:https://svn.apache.org/repos/asf/jakarta/commons/proper/jelly/trunk + http://svn.apache.org/viewvc/jakarta/commons/proper/jelly/trunk + + + Apache Software Foundation + http://jakarta.apache.org + http://jakarta.apache.org/images/original-jakarta-logo.gif + + org.apache.commons.jelly + + commons-dev@jakarta.apache.org + c:\kohsuke\My Projects\hudson\jelly/src/java + c:\kohsuke\My Projects\hudson\jelly/src/test + + + + src/test + + META-INF/services/* + **/*.jelly + **/*.xml + **/*.xsl + **/*.rng + **/*.dtd + **/*.properties + **/*.html + + + + + **/Test*.java + + + **/TestCoreMemoryLeak.java + + + + + META-INF + c:\kohsuke\My Projects\hudson\jelly + + NOTICE.txt + + + + c:\kohsuke\My Projects\hudson\jelly/src/java + + **/*.properties + + + + + + + servletapi + servletapi + 2.3 + + jakarta-servletapi-5-servlet + + + + commons-cli + commons-cli + 1.0 + + + commons-lang + commons-lang + 2.0 + + true + + + + commons-discovery + commons-discovery + 20030211.213356 + + + forehead + forehead + 1.0-beta-5 + http://forehead.werken.com/ + + + jstl + jstl + 1.0.6 + http://jakarta.apache.org/taglibs/doc/standard-1.0-doc/intro.html + + jstl + jakarta-taglibs-standard + + + + junit + junit + 3.8.1 + http://junit.org + + + commons-jexl + commons-jexl + 1.1-hudson-SNAPSHOT + + + xml-apis + xml-apis + 1.0.b2 + + + commons-beanutils + commons-beanutils + 1.6 + + true + + + + commons-collections + commons-collections + 2.1 + + true + + + + commons-logging + commons-logging + 1.0.3 + + true + + + + dom4j + dom4j + 1.6.1 + + + jaxen + jaxen + 1.1-beta-8 + + + xerces + xerces + 2.2.1 + + xml-xerces + + + + \ No newline at end of file diff --git a/lib/commons-jelly/poms/commons-jelly-1.1-hudson-SNAPSHOT.pom.md5 b/lib/commons-jelly/poms/commons-jelly-1.1-hudson-SNAPSHOT.pom.md5 new file mode 100644 index 0000000000..8a84ae8489 --- /dev/null +++ b/lib/commons-jelly/poms/commons-jelly-1.1-hudson-SNAPSHOT.pom.md5 @@ -0,0 +1 @@ +e7ac3a31d9ce08199ffdd563a25c6f07 \ No newline at end of file diff --git a/lib/commons-jelly/poms/commons-jelly-1.1-hudson-SNAPSHOT.pom.sha1 b/lib/commons-jelly/poms/commons-jelly-1.1-hudson-SNAPSHOT.pom.sha1 new file mode 100644 index 0000000000..61034377aa --- /dev/null +++ b/lib/commons-jelly/poms/commons-jelly-1.1-hudson-SNAPSHOT.pom.sha1 @@ -0,0 +1 @@ +7edd1e001899458783a0c74a5f16f2b462ae8ca2 \ No newline at end of file diff --git a/lib/commons-jelly/poms/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.pom b/lib/commons-jelly/poms/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.pom new file mode 100644 index 0000000000..e7a1f52cbe --- /dev/null +++ b/lib/commons-jelly/poms/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.pom @@ -0,0 +1,372 @@ + + 3 + commons-jelly + commons-jelly-tags-define + commons-jelly-tags-define + 1.0.1-hudson-SNAPSHOT + Commons Jelly Define Tag Library + The Jelly Define Tag Library + http://jakarta.apache.org/commons/jelly/libs/define/index.html + /images/logo.gif + http://issues.apache.org/jira/browse/JELLY + 2002 + jakarta + cvs.apache.org + /www/jakarta.apache.org/commons/jelly/libs/define/ + /www/jakarta.apache.org/builds/jakarta-commons/jelly/jelly-tags/define/ + + + Commons Dev List + commons-dev-subscribe@jakarta.apache.org + commons-dev-unsubscribe@jakarta.apache.org + http://mail-archives.apache.org/eyebrowse/SummarizeList?listName=commons-dev@jakarta.apache.org + + + Commons User List + commons-user-subscribe@jakarta.apache.org + commons-user-unsubscribe@jakarta.apache.org + http://mail-archives.apache.org/eyebrowse/SummarizeList?listName=commons-user@jakarta.apache.org + + + + + jstrachan + James Strachan + jstrachan@apache.org + SpiritSoft, Inc. + + + geirm + Geir Magnusson Jr. + geirm@adeptra.com + Adeptra, Inc. + + + werken + Bob McWhirter + bob@eng.werken.com + The Werken Company + + + dion + dIon Gillard + dion@multitask.com.au + Multitask Consulting + + Interested party + + + + morgand + Morgan Delagrange + morgand@apache.org + + + rwaldhoff + Rodney Waldhoff + rwaldhoff@apache.org + + + rdonkin + Robert Burrell Donkin + rdonkin@apache.org + + + felipeal + Felipe Leme + jelly@felipeal.net + Falcon Informatica + + itch-scratcher Jakarta PMC + + -3 + + + Paul Libbrecht + paul@activemath.org + + Developer + + + + + + Martin van dem Bemt + mvdb@mvdb.com + + + Erik Fransen + erik167@xs4all.nl + + Logo designer + + + + Calvin Yu + + + Stephen Haberman + stephenh@chase3000.com + + + Vinay Chandran + sahilvinay@yahoo.com + + Developer + + + + Theo Niemeijer + + + Joe Walnes + joew@thoughtworks.com + ThoughtWorks, Inc. + + Inventor of Mock Tags + + + + Otto von Wachter + vonwao@yahoo.com + + + Author of the tutorials + Developer + + + + Robert Leftwich + robert@leftwich.info + + Developer + + + + Jim Birchfield + jim.birchfield@genscape.com + Genscape, Inc. + + Developer + + + + Jason Horman + jhorman@musicmatch.com + + Developer + + + + Tim Anderson + tima@intalio.com + Intalio, Inc. + + Developer + + + + Theo Niemeijer + theo.niemeijer@getthere.nl + + + Developer + + + + J. Matthew Pryor + matthew_pryor@versata.com + + + Developer + + + + Knut Wannheden + + + + Developer + + + + Kelvin Tan + + + + Developer + + + + Todd Jonker + + + + Developer + + + + + + 1.0 + COMMONS_JELLY_DEFINE-1_0 + 1.0 + + + + maven-changes-plugin + maven-checkstyle-plugin + maven-developer-activity-plugin + maven-file-activity-plugin + maven-javadoc-plugin + maven-jcoverage-plugin + maven-jdepend-plugin + maven-jellydoc-plugin + maven-junit-report-plugin + maven-jxr-plugin + maven-license-plugin + maven-pmd-plugin + maven-tasklist-plugin + + + scm:svn:http://svn.apache.org/repos/asf/jakarta/commons/proper/jelly/trunk/jelly-tags/define + scm:svn:https://svn.apache.org/repos/asf/jakarta/commons/proper/jelly/trunk/jelly-tags/define + http://svn.apache.org/viewcvs.cgi/jakarta/commons/proper/jelly/trunk/jelly-tags/define + + + Apache Software Foundation + http://jakarta.apache.org/ + http://jakarta.apache.org/images/jakarta-logo.gif + + org.apache.commons.jelly.tags.define + + commons-dev@jakarta.apache.org + src/java + src/test + + + + src/test + + **/*.jelly + **/*.xml + **/*.xsl + **/*.rng + **/*.dtd + **/*.properties + **/*.html + + + + + **/Test*.java + + + + + src/java + + **/*.properties + + + + META-INF + c:\kohsuke\My Projects\hudson\jelly\jelly-tags\define/.. + + NOTICE.txt + + + + + + + commons-jelly + commons-jelly-tags-dynabean + 1.0 + http://jakarta.apache.org/commons/jelly/tags/dynabean/ + + test + + + + commons-jelly + commons-jelly-tags-junit + 1.0 + http://jakarta.apache.org/commons/jelly/tags/junit/ + + test + + + + commons-jelly + commons-jelly-tags-log + 1.0 + http://jakarta.apache.org/commons/jelly/tags/log/ + + test + + + + commons-jelly + commons-jelly-tags-xml + 1.0 + http://jakarta.apache.org/commons/jelly/tags/xml/ + + test + + + + commons-cli + commons-cli + 1.0 + + + xml-apis + xml-apis + 1.0.b2 + + + commons-beanutils + commons-beanutils + 1.6 + + + commons-collections + commons-collections + 2.1 + + + commons-jexl + commons-jexl + 1.1 + + + commons-jelly + commons-jelly + 1.0 + + + commons-logging + commons-logging + 1.0.3 + + + dom4j + dom4j + 1.6.1 + + + jaxen + jaxen + 1.1-beta-8 + + + xerces + xerces + 2.2.1 + + xml-xerces + + + + \ No newline at end of file diff --git a/lib/commons-jelly/poms/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.pom.md5 b/lib/commons-jelly/poms/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.pom.md5 new file mode 100644 index 0000000000..727f35b41a --- /dev/null +++ b/lib/commons-jelly/poms/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.pom.md5 @@ -0,0 +1 @@ +a090902255dba86f350c7a46acf157c2 \ No newline at end of file diff --git a/lib/commons-jelly/poms/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.pom.sha1 b/lib/commons-jelly/poms/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.pom.sha1 new file mode 100644 index 0000000000..d64fd463fa --- /dev/null +++ b/lib/commons-jelly/poms/commons-jelly-tags-define-1.0.1-hudson-SNAPSHOT.pom.sha1 @@ -0,0 +1 @@ +d3954421ac0c4d755297cb66fb80d570d71bf966 \ No newline at end of file diff --git a/lib/commons-jexl/commons-jexl-1.1-hudson-SNAPSHOT-sources.jar b/lib/commons-jexl/commons-jexl-1.1-hudson-SNAPSHOT-sources.jar new file mode 100644 index 0000000000..d122b6a579 Binary files /dev/null and b/lib/commons-jexl/commons-jexl-1.1-hudson-SNAPSHOT-sources.jar differ diff --git a/lib/commons-jexl/commons-jexl-1.1-hudson-SNAPSHOT.jar b/lib/commons-jexl/commons-jexl-1.1-hudson-SNAPSHOT.jar new file mode 100644 index 0000000000..adfc445685 Binary files /dev/null and b/lib/commons-jexl/commons-jexl-1.1-hudson-SNAPSHOT.jar differ diff --git a/lib/commons-jexl/jars/commons-jexl-1.1-hudson-SNAPSHOT.jar b/lib/commons-jexl/jars/commons-jexl-1.1-hudson-SNAPSHOT.jar new file mode 100644 index 0000000000..adfc445685 Binary files /dev/null and b/lib/commons-jexl/jars/commons-jexl-1.1-hudson-SNAPSHOT.jar differ diff --git a/lib/commons-jexl/jars/commons-jexl-1.1-hudson-SNAPSHOT.jar.md5 b/lib/commons-jexl/jars/commons-jexl-1.1-hudson-SNAPSHOT.jar.md5 new file mode 100644 index 0000000000..a3809ba975 --- /dev/null +++ b/lib/commons-jexl/jars/commons-jexl-1.1-hudson-SNAPSHOT.jar.md5 @@ -0,0 +1 @@ +c982e5f72a0ad7768e92494bbcc1bcbe \ No newline at end of file diff --git a/lib/commons-jexl/jars/commons-jexl-1.1-hudson-SNAPSHOT.jar.sha1 b/lib/commons-jexl/jars/commons-jexl-1.1-hudson-SNAPSHOT.jar.sha1 new file mode 100644 index 0000000000..0285499023 --- /dev/null +++ b/lib/commons-jexl/jars/commons-jexl-1.1-hudson-SNAPSHOT.jar.sha1 @@ -0,0 +1 @@ +e04fa43b2dc07bc1df35b832feb311ae48ae8201 \ No newline at end of file diff --git a/lib/commons-jexl/java-sources/commons-jexl-1.1-hudson-SNAPSHOT-sources.jar b/lib/commons-jexl/java-sources/commons-jexl-1.1-hudson-SNAPSHOT-sources.jar new file mode 100644 index 0000000000..d122b6a579 Binary files /dev/null and b/lib/commons-jexl/java-sources/commons-jexl-1.1-hudson-SNAPSHOT-sources.jar differ diff --git a/lib/commons-jexl/java-sources/commons-jexl-1.1-hudson-SNAPSHOT-sources.jar.md5 b/lib/commons-jexl/java-sources/commons-jexl-1.1-hudson-SNAPSHOT-sources.jar.md5 new file mode 100644 index 0000000000..25e8405035 --- /dev/null +++ b/lib/commons-jexl/java-sources/commons-jexl-1.1-hudson-SNAPSHOT-sources.jar.md5 @@ -0,0 +1 @@ +79642c023954c197d16b449b5fe38cc8 \ No newline at end of file diff --git a/lib/commons-jexl/java-sources/commons-jexl-1.1-hudson-SNAPSHOT-sources.jar.sha1 b/lib/commons-jexl/java-sources/commons-jexl-1.1-hudson-SNAPSHOT-sources.jar.sha1 new file mode 100644 index 0000000000..ce8d67c19e --- /dev/null +++ b/lib/commons-jexl/java-sources/commons-jexl-1.1-hudson-SNAPSHOT-sources.jar.sha1 @@ -0,0 +1 @@ +eae91f887e8bfe3a5573ca26f0202ea8497e99f9 \ No newline at end of file diff --git a/lib/commons-jexl/poms/commons-jexl-1.1-hudson-SNAPSHOT.pom b/lib/commons-jexl/poms/commons-jexl-1.1-hudson-SNAPSHOT.pom new file mode 100644 index 0000000000..f3b5364992 --- /dev/null +++ b/lib/commons-jexl/poms/commons-jexl-1.1-hudson-SNAPSHOT.pom @@ -0,0 +1,163 @@ + + 3 + commons-jexl + commons-jexl + Commons JEXL + 1.1-hudson-SNAPSHOT + Commons JEXL Expression Language Engine + Jexl is an implementation of the JSTL Expression Language with extensions. + http://jakarta.apache.org/commons/jexl/ + /images/jexl-logo-white.png + http://issues.apache.org/jira/ + 2003 + jakarta + people.apache.org + /www/jakarta.apache.org/commons/jexl/ + /www/jakarta.apache.org/builds/jakarta-commons/jexl/ + + + Commons Dev List + commons-dev-subscribe@jakarta.apache.org + commons-dev-unsubscribe@jakarta.apache.org + http://mail-archives.apache.org/mod_mbox/jakarta-commons-dev/ + + + Commons User List + commons-user-subscribe@jakarta.apache.org + commons-user-unsubscribe@jakarta.apache.org + http://mail-archives.apache.org/mod_mbox/jakarta-commons-user/ + + + + + dion + dIon Gillard + dion@apache.org + Apache Software Foundation + + + geirm + Geir Magnusson Jr. + geirm@apache.org + independent + + + tobrien + Tim O'Brien + tobrien@apache.org + independent + + + proyal + Peter Royal + proyal@apache.org + Pace Systems Group, Inc. + + + jstrachan + James Strachan + jstrachan@apache.org + SpiritSoft, Inc. + + + rahul + Rahul Akolkar + rahul AT apache.org + Apache Software Foundation + + + + + The Apache Software License, Version 2.0 + /LICENSE.txt + repo + + + + + 1.1 + COMMONS_JEXL-1_1 + 1.1 + + + 1.0 + COMMONS_JEXL-1_0 + 1.0 + + + + maven-changes-plugin + maven-checkstyle-plugin + maven-javadoc-plugin + maven-jcoverage-plugin + maven-jdepend-plugin + maven-junit-report-plugin + maven-jxr-plugin + maven-license-plugin + maven-pmd-plugin + maven-tasklist-plugin + + + scm:svn:http://svn.apache.org/repos/asf/jakarta/commons/proper/jexl/trunk + scm:svn:https://svn.apache.org/repos/asf/jakarta/commons/proper/jexl/trunk + http://svn.apache.org/repos/asf/jakarta/commons/proper/jexl/trunk + + + The Apache Software Foundation + http://jakarta.apache.org + http://jakarta.apache.org/images/jakarta-logo.gif + + org.apache.commons.jexl + + commons-dev@jakarta.apache.org + src/java + src/test + + + + + **/*Test.java + + + + + + commons-logging + commons-logging + 1.0.3 + http://jakarta.apache.org/commons/logging/ + + <strong>Required</strong> + + + + junit + junit + 3.8.1 + http://www.junit.org/ + + <strong>Test Only</strong> + + + + maven + maven-xdoc-plugin + 1.9.2 + http://maven.apache.org/maven-1.x/reference/plugins/xdoc/ + plugin + + <strong>Site Only</strong> - v1.9.2 (minimum) + + + + maven-plugins + maven-findbugs-plugin + 1.1 + http://maven-plugins.sourceforge.net/maven-findbugs-plugin/ + plugin + + <strong>Site Only</strong> - v1.1 (minimum) + + + + \ No newline at end of file diff --git a/lib/commons-jexl/poms/commons-jexl-1.1-hudson-SNAPSHOT.pom.md5 b/lib/commons-jexl/poms/commons-jexl-1.1-hudson-SNAPSHOT.pom.md5 new file mode 100644 index 0000000000..fc39297e98 --- /dev/null +++ b/lib/commons-jexl/poms/commons-jexl-1.1-hudson-SNAPSHOT.pom.md5 @@ -0,0 +1 @@ +c4669dfcfac872236d940ba939280297 \ No newline at end of file diff --git a/lib/commons-jexl/poms/commons-jexl-1.1-hudson-SNAPSHOT.pom.sha1 b/lib/commons-jexl/poms/commons-jexl-1.1-hudson-SNAPSHOT.pom.sha1 new file mode 100644 index 0000000000..70b533ce3f --- /dev/null +++ b/lib/commons-jexl/poms/commons-jexl-1.1-hudson-SNAPSHOT.pom.sha1 @@ -0,0 +1 @@ +fb413fa9e888beadf6b2aa466fed366a09e9d7ca \ No newline at end of file diff --git a/lib/importDeps.sh b/lib/importDeps.sh new file mode 100644 index 0000000000..ea7adcf5b3 --- /dev/null +++ b/lib/importDeps.sh @@ -0,0 +1,50 @@ +#!/bin/zsh -ex + +function checkout() { + groupId=$1 + artifactId=$2 + version=$3 + + for i in jars poms java-sources + do + cp ~/.maven/repository/$groupId/$i/$artifactId-$version*.* $groupId/$i + done +} + +case $1 in +stapler) + tiger + pushd /kohsuke/projects/stapler/stapler + staplerDir=$PWD + maven jar source jar:install source:install + popd + checkout org.kohsuke.stapler stapler $(show-pom-version $staplerDir/project.xml) + ;; + +jelly) + # build with 1.4 + mantis + + pushd ../../../jelly/jelly-tags/define/ + jellyDir=$PWD + maven -Dmaven.test.skip=true clean jar source jar:install source:install + popd + checkout commons-jelly commons-jelly-tags-define $(show-pom-version $jellyDir/project.xml) + + pushd ../../../jelly + jellyDir=$PWD + maven -Dmaven.test.skip=true clean jar source jar:install source:install + popd + checkout commons-jelly commons-jelly $(show-pom-version $jellyDir/project.xml) + ;; + +jexl) + mantis + + pushd ../../../jexl + jexlDir=$PWD + maven -Dmaven.test.skip=true jar source jar:install source:install + popd + checkout commons-jexl commons-jexl $(show-pom-version $jexlDir/project.xml) + ;; +esac diff --git a/lib/org.kohsuke.stapler/jars/stapler-1.5.jar b/lib/org.kohsuke.stapler/jars/stapler-1.5.jar new file mode 100644 index 0000000000..5a8fe9f665 Binary files /dev/null and b/lib/org.kohsuke.stapler/jars/stapler-1.5.jar differ diff --git a/lib/org.kohsuke.stapler/jars/stapler-1.5.jar.md5 b/lib/org.kohsuke.stapler/jars/stapler-1.5.jar.md5 new file mode 100644 index 0000000000..1648dd0d81 --- /dev/null +++ b/lib/org.kohsuke.stapler/jars/stapler-1.5.jar.md5 @@ -0,0 +1 @@ +ba74a9b3fba4d57464b582f689488be8 \ No newline at end of file diff --git a/lib/org.kohsuke.stapler/jars/stapler-1.5.jar.sha1 b/lib/org.kohsuke.stapler/jars/stapler-1.5.jar.sha1 new file mode 100644 index 0000000000..b4ed8e9673 --- /dev/null +++ b/lib/org.kohsuke.stapler/jars/stapler-1.5.jar.sha1 @@ -0,0 +1 @@ +a8cefb508a99cd530db20c8462dcf4f9ea9b13b0 \ No newline at end of file diff --git a/lib/org.kohsuke.stapler/jars/stapler-1.9-SNAPSHOT.jar b/lib/org.kohsuke.stapler/jars/stapler-1.9-SNAPSHOT.jar new file mode 100644 index 0000000000..0dfaee1893 Binary files /dev/null and b/lib/org.kohsuke.stapler/jars/stapler-1.9-SNAPSHOT.jar differ diff --git a/lib/org.kohsuke.stapler/jars/stapler-1.9-SNAPSHOT.jar.md5 b/lib/org.kohsuke.stapler/jars/stapler-1.9-SNAPSHOT.jar.md5 new file mode 100644 index 0000000000..b6b57be42e --- /dev/null +++ b/lib/org.kohsuke.stapler/jars/stapler-1.9-SNAPSHOT.jar.md5 @@ -0,0 +1 @@ +67c3d187fa117586f5ac9ba33f818469 \ No newline at end of file diff --git a/lib/org.kohsuke.stapler/jars/stapler-1.9-SNAPSHOT.jar.sha1 b/lib/org.kohsuke.stapler/jars/stapler-1.9-SNAPSHOT.jar.sha1 new file mode 100644 index 0000000000..2acbb1fe7a --- /dev/null +++ b/lib/org.kohsuke.stapler/jars/stapler-1.9-SNAPSHOT.jar.sha1 @@ -0,0 +1 @@ +44fe90ab23f2dce105314f16eae018e8081512f6 \ No newline at end of file diff --git a/lib/org.kohsuke.stapler/java-sources/stapler-1.9-SNAPSHOT-sources.jar b/lib/org.kohsuke.stapler/java-sources/stapler-1.9-SNAPSHOT-sources.jar new file mode 100644 index 0000000000..061bd3d2e3 Binary files /dev/null and b/lib/org.kohsuke.stapler/java-sources/stapler-1.9-SNAPSHOT-sources.jar differ diff --git a/lib/org.kohsuke.stapler/java-sources/stapler-1.9-SNAPSHOT-sources.jar.md5 b/lib/org.kohsuke.stapler/java-sources/stapler-1.9-SNAPSHOT-sources.jar.md5 new file mode 100644 index 0000000000..9bd2da4041 --- /dev/null +++ b/lib/org.kohsuke.stapler/java-sources/stapler-1.9-SNAPSHOT-sources.jar.md5 @@ -0,0 +1 @@ +53cb0620af2e334796010ec43e448dc4 \ No newline at end of file diff --git a/lib/org.kohsuke.stapler/java-sources/stapler-1.9-SNAPSHOT-sources.jar.sha1 b/lib/org.kohsuke.stapler/java-sources/stapler-1.9-SNAPSHOT-sources.jar.sha1 new file mode 100644 index 0000000000..4d26b1dd75 --- /dev/null +++ b/lib/org.kohsuke.stapler/java-sources/stapler-1.9-SNAPSHOT-sources.jar.sha1 @@ -0,0 +1 @@ +1323ef6f38eb4f107dc67b1ed773102c3c810f61 \ No newline at end of file diff --git a/lib/org.kohsuke.stapler/poms/stapler-1.5.pom b/lib/org.kohsuke.stapler/poms/stapler-1.5.pom new file mode 100644 index 0000000000..c7e5b25cfb --- /dev/null +++ b/lib/org.kohsuke.stapler/poms/stapler-1.5.pom @@ -0,0 +1,75 @@ + + 3 + org.kohsuke.stapler + stapler + 1.5 + Stapler HTTP request handling engine + Stapler HTTP request handling engine + https://stapler.dev.java.net/servlets/ProjectIssues + + + Users List + users-subscribe@stapler.dev.java.net + users-unsubscribe@stapler.dev.java.net + https://stapler.dev.java.net/servlets/SummarizeList?listName=users + + + Issues List + issues-subscribe@stapler.dev.java.net + issues-unsubscribe@stapler.dev.java.net + https://stapler.dev.java.net/servlets/SummarizeList?listName=issues + + + CVS List + cvs-subscribe@stapler.dev.java.net + cvs-unsubscribe@stapler.dev.java.net + https://stapler.dev.java.net/servlets/SummarizeList?listName=cvs + + + + + kohsuke + Kohsuke Kawaguchi + kk@kohsuke.org + + + + maven-license-plugin + maven-changelog-plugin + maven-changes-plugin + maven-developer-activity-plugin + maven-file-activity-plugin + maven-javadoc-plugin + maven-junit-report-plugin + maven-linkcheck-plugin + + + http://www.java.net/ + https://dalma.dev.java.net/maven/images/java.net-logo.png + + org.kohsuke.stapler + + src + + + + src + + META-INF/taglib.tld + + + + + + + javax.servlet + servlet-api + 2.3 + + + javax.servlet + jsp-api + 2.0 + + + \ No newline at end of file diff --git a/lib/org.kohsuke.stapler/poms/stapler-1.5.pom.md5 b/lib/org.kohsuke.stapler/poms/stapler-1.5.pom.md5 new file mode 100644 index 0000000000..1d5d83146c --- /dev/null +++ b/lib/org.kohsuke.stapler/poms/stapler-1.5.pom.md5 @@ -0,0 +1 @@ +0fdeb5eb5a5c6bbc2843ec42d4546210 \ No newline at end of file diff --git a/lib/org.kohsuke.stapler/poms/stapler-1.5.pom.sha1 b/lib/org.kohsuke.stapler/poms/stapler-1.5.pom.sha1 new file mode 100644 index 0000000000..30cc83bdaa --- /dev/null +++ b/lib/org.kohsuke.stapler/poms/stapler-1.5.pom.sha1 @@ -0,0 +1 @@ +eb240d4793e8fc9a34c3d337e2de65512585609c \ No newline at end of file diff --git a/lib/org.kohsuke.stapler/poms/stapler-1.9-SNAPSHOT.pom b/lib/org.kohsuke.stapler/poms/stapler-1.9-SNAPSHOT.pom new file mode 100644 index 0000000000..3e0903e15f --- /dev/null +++ b/lib/org.kohsuke.stapler/poms/stapler-1.9-SNAPSHOT.pom @@ -0,0 +1,101 @@ + + 3 + org.kohsuke.stapler + stapler + 1.9-SNAPSHOT + Stapler HTTP request handling engine + Stapler HTTP request handling engine + https://stapler.dev.java.net/servlets/ProjectIssues + + + Users List + users-subscribe@stapler.dev.java.net + users-unsubscribe@stapler.dev.java.net + https://stapler.dev.java.net/servlets/SummarizeList?listName=users + + + Issues List + issues-subscribe@stapler.dev.java.net + issues-unsubscribe@stapler.dev.java.net + https://stapler.dev.java.net/servlets/SummarizeList?listName=issues + + + CVS List + cvs-subscribe@stapler.dev.java.net + cvs-unsubscribe@stapler.dev.java.net + https://stapler.dev.java.net/servlets/SummarizeList?listName=cvs + + + + + kohsuke + Kohsuke Kawaguchi + kk@kohsuke.org + + + + maven-license-plugin + maven-changelog-plugin + maven-changes-plugin + maven-developer-activity-plugin + maven-file-activity-plugin + maven-javadoc-plugin + maven-jellydoc-plugin + maven-junit-report-plugin + maven-linkcheck-plugin + + + http://www.java.net/ + https://dalma.dev.java.net/maven/images/java.net-logo.png + + org.kohsuke.stapler + + src + + + + src + + META-INF/taglib.tld + + + + + + + javax.servlet + servlet-api + 2.3 + + + javax.servlet + jsp-api + 2.0 + + + commons-jelly + commons-jelly + 1.0 + + + dom4j + dom4j + 1.6.1 + + + commons-jexl + commons-jexl + 1.0 + + + commons-beanutils + commons-beanutils + 1.6 + + + groovy + groovy-all + 1.0-jsr-06 + + + \ No newline at end of file diff --git a/lib/org.kohsuke.stapler/poms/stapler-1.9-SNAPSHOT.pom.md5 b/lib/org.kohsuke.stapler/poms/stapler-1.9-SNAPSHOT.pom.md5 new file mode 100644 index 0000000000..63c2e4e624 --- /dev/null +++ b/lib/org.kohsuke.stapler/poms/stapler-1.9-SNAPSHOT.pom.md5 @@ -0,0 +1 @@ +eb0eab1d1cd75a281fe6b690da8068c0 \ No newline at end of file diff --git a/lib/org.kohsuke.stapler/poms/stapler-1.9-SNAPSHOT.pom.sha1 b/lib/org.kohsuke.stapler/poms/stapler-1.9-SNAPSHOT.pom.sha1 new file mode 100644 index 0000000000..151a16a175 --- /dev/null +++ b/lib/org.kohsuke.stapler/poms/stapler-1.9-SNAPSHOT.pom.sha1 @@ -0,0 +1 @@ +a0770e690d998dbad2ec7372fa9123b2c4520067 \ No newline at end of file diff --git a/lib/retroweaver/Regex.jar b/lib/retroweaver/Regex.jar new file mode 100644 index 0000000000..713441c523 Binary files /dev/null and b/lib/retroweaver/Regex.jar differ diff --git a/lib/retroweaver/bcel-5.1.jar b/lib/retroweaver/bcel-5.1.jar new file mode 100644 index 0000000000..524e375cf3 Binary files /dev/null and b/lib/retroweaver/bcel-5.1.jar differ diff --git a/lib/retroweaver/jace.jar b/lib/retroweaver/jace.jar new file mode 100644 index 0000000000..963ff01e5e Binary files /dev/null and b/lib/retroweaver/jace.jar differ diff --git a/lib/retroweaver/retroweaver.jar b/lib/retroweaver/retroweaver.jar new file mode 100644 index 0000000000..38b9f55f81 Binary files /dev/null and b/lib/retroweaver/retroweaver.jar differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000..68f40dd642 --- /dev/null +++ b/pom.xml @@ -0,0 +1,22 @@ + + 4.0.0 + + org.jvnet.hudson + hudson + 1.0 + ../pom.xml + + + org.jvnet.hudson.main + pom + 1.60 + pom + + Hudson main module + The module that constitutes the main hudson.war + + + core + war + + \ No newline at end of file diff --git a/war/.cvsignore b/war/.cvsignore new file mode 100644 index 0000000000..eb5a316cbd --- /dev/null +++ b/war/.cvsignore @@ -0,0 +1 @@ +target diff --git a/war/images/.cvsignore b/war/images/.cvsignore new file mode 100644 index 0000000000..a78e386957 --- /dev/null +++ b/war/images/.cvsignore @@ -0,0 +1,5 @@ +16x16 +24x24 +32x32 +48x48 +Thumbs.db diff --git a/war/images/Tango-Palette.png b/war/images/Tango-Palette.png new file mode 100644 index 0000000000..9933717dd5 Binary files /dev/null and b/war/images/Tango-Palette.png differ diff --git a/war/images/TangoProject-License.url b/war/images/TangoProject-License.url new file mode 100644 index 0000000000..80c5fc2b84 --- /dev/null +++ b/war/images/TangoProject-License.url @@ -0,0 +1,2 @@ +[InternetShortcut] +URL=http://creativecommons.org/licenses/by-sa/2.5/ diff --git a/war/images/application-certificate.svg b/war/images/application-certificate.svg new file mode 100644 index 0000000000..086f55c4be --- /dev/null +++ b/war/images/application-certificate.svg @@ -0,0 +1,438 @@ + + + + + + image/svg+xml + + + + + + CertificateJakub Steinercertificate + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/war/images/blue.svg b/war/images/blue.svg new file mode 100644 index 0000000000..cd73d4b61f --- /dev/null +++ b/war/images/blue.svg @@ -0,0 +1,266 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Face - Plain + + + emoticon + emote + face + plain + :| + :-| + + + + + + Steven Garrity + + + http://www.tango-project.org + + + Based on face-smile by jimmac + + + + + + + + + + + + + + + + + + + diff --git a/war/images/bookmark-new.svg b/war/images/bookmark-new.svg new file mode 100644 index 0000000000..74e25693c1 --- /dev/null +++ b/war/images/bookmark-new.svg @@ -0,0 +1,600 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + New Bookmark + + + bookmark + remember + favorite + + + + + + Andreas Nilsson + + + + + + Jakub Steiner + + + create bookmark action + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/clipboard.svg b/war/images/clipboard.svg new file mode 100644 index 0000000000..489e3438b8 --- /dev/null +++ b/war/images/clipboard.svg @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Edit Paste + 2005-10-10 + + + Andreas Nilsson + + + + + edit + paste + + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/clock.svg b/war/images/clock.svg new file mode 100644 index 0000000000..b7a27f1640 --- /dev/null +++ b/war/images/clock.svg @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + New Appointment + + + appointment + new + meeting + rvsp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/computer-x.svg b/war/images/computer-x.svg new file mode 100644 index 0000000000..71c2c261fd --- /dev/null +++ b/war/images/computer-x.svg @@ -0,0 +1,1527 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Computer + 2005-03-08 + + + Jakub Steiner + + + + + workstation + computer + node + client + + + + http://jimmac.musichall.cz/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/computer.svg b/war/images/computer.svg new file mode 100644 index 0000000000..d6e0f6b4f7 --- /dev/null +++ b/war/images/computer.svg @@ -0,0 +1,738 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Computer + 2005-03-08 + + + Jakub Steiner + + + + + workstation + computer + node + client + + + + http://jimmac.musichall.cz/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/delete-document.svg b/war/images/delete-document.svg new file mode 100644 index 0000000000..fb4900fd62 --- /dev/null +++ b/war/images/delete-document.svg @@ -0,0 +1,1451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Generic Text + + + text + plaintext + regular + document + + + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/document.svg b/war/images/document.svg new file mode 100644 index 0000000000..8fbf4e7dc2 --- /dev/null +++ b/war/images/document.svg @@ -0,0 +1,476 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Generic Text + + + text + plaintext + regular + document + + + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/edit-delete.svg b/war/images/edit-delete.svg new file mode 100644 index 0000000000..7d1ada2b1f --- /dev/null +++ b/war/images/edit-delete.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Delete + 2005-12-28 + + + Andreas Nilsson + + + http://tango-project.org + + + delete + remove + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/error.svg b/war/images/error.svg new file mode 100644 index 0000000000..602fa79573 --- /dev/null +++ b/war/images/error.svg @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Rodney Dawes + + + + + Jakub Steiner, Garrett LeSage + + + + Dialog Error + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/fingerprint.svg b/war/images/fingerprint.svg new file mode 100644 index 0000000000..84b122a758 --- /dev/null +++ b/war/images/fingerprint.svg @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + New Contact + + + address + contact + e-mail + person + information + card + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/folder.svg b/war/images/folder.svg new file mode 100644 index 0000000000..effc00b478 --- /dev/null +++ b/war/images/folder.svg @@ -0,0 +1,486 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Folder Icon Accept + 2005-01-31 + + + Jakub Steiner + + + + http://jimmac.musichall.cz + Active state - when files are being dragged to. + + + Novell, Inc. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/gear.svg b/war/images/gear.svg new file mode 100644 index 0000000000..f5b116006a --- /dev/null +++ b/war/images/gear.svg @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Emblem System + + + emblem + system + library + crucial + base + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/gear2.svg b/war/images/gear2.svg new file mode 100644 index 0000000000..35e2ffa892 --- /dev/null +++ b/war/images/gear2.svg @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + System Applications + + + Jakub Steiner + + + http://jimmac.musichall.cz/ + + + system + applications + group + category + admin + root + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/graph.svg b/war/images/graph.svg new file mode 100644 index 0000000000..6bbc108013 --- /dev/null +++ b/war/images/graph.svg @@ -0,0 +1,631 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Spreadsheet + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + spreadheet + document + office + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/grey.svg b/war/images/grey.svg new file mode 100644 index 0000000000..9dabf8ce90 --- /dev/null +++ b/war/images/grey.svg @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Face - Plain + + + emoticon + emote + face + plain + :| + :-| + + + + + + Steven Garrity + + + http://www.tango-project.org + + + Based on face-smile by jimmac + + + + + + + + + + + + + + + + + + + diff --git a/war/images/headless.svg b/war/images/headless.svg new file mode 100644 index 0000000000..6ef0a7399b --- /dev/null +++ b/war/images/headless.svg @@ -0,0 +1,78 @@ + + + + + + + + + image/svg+xml + + + + + + Graphics N/A + Unable to access X. You need to run the web container in the headless mode. Add -Djava.awt.headless=true to VM option + diff --git a/war/images/help.svg b/war/images/help.svg new file mode 100644 index 0000000000..669dda345f --- /dev/null +++ b/war/images/help.svg @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Help Browser + 2005-11-06 + + + Tuomas Kuosmanen + + + + + help + browser + documentation + docs + man + info + + + + + + Jakub Steiner, Andreas Nilsson + + + http://tigert.com + + + + + + + + + + + + + + + + + + diff --git a/war/images/hudson-logo.vsd b/war/images/hudson-logo.vsd new file mode 100644 index 0000000000..80b02dc4e6 Binary files /dev/null and b/war/images/hudson-logo.vsd differ diff --git a/war/images/make.sh b/war/images/make.sh new file mode 100644 index 0000000000..eb54133e08 --- /dev/null +++ b/war/images/make.sh @@ -0,0 +1,19 @@ +#!/bin/sh -e +for src in *.svg +do + echo processing $src + e=$(echo $src | sed -e s/.svg/.gif/ ) + for sz in 16 24 32 48 + do + dst=${sz}x${sz}/$e + if [ ! -e $dst -o $src -nt $dst ]; + then + mkdir ${sz}x${sz} > /dev/null 2>&1 || true + svg2png -w $sz -h $sz < $src > t.png + #convert t.png \( +clone -fill white -draw 'color 0,0 reset' \) \ + # -compose Dst_Over $dst + composite -compose Dst_Over -tile xc:white t.png $dst + rm t.png + fi + done +done diff --git a/war/images/makeBalls.sh b/war/images/makeBalls.sh new file mode 100644 index 0000000000..fc723724d5 --- /dev/null +++ b/war/images/makeBalls.sh @@ -0,0 +1,19 @@ +#!/bin/sh -ex +# build flashing balls + +t=/tmp/makeBalls$$ + +for sz in 16x16 24x24 32x32 48x48 +do + for color in grey blue yellow red + do + cp $sz/$color.gif ../resources/images/$sz/$color.gif + convert $sz/$color.gif -fill white -colorize 20% $t.80.gif + convert $sz/$color.gif -fill white -colorize 40% $t.60.gif + convert $sz/$color.gif -fill white -colorize 60% $t.40.gif + convert $sz/$color.gif -fill white -colorize 80% $t.20.gif + convert -delay 10 $sz/$color.gif $t.80.gif $t.60.gif $t.40.gif $t.20.gif $sz/nothing.gif $t.20.gif $t.40.gif $t.60.gif $t.80.gif -loop 0 ../resources/images/$sz/${color}_anime.gif + done +done + +rm $t.*.gif diff --git a/war/images/new-document.svg b/war/images/new-document.svg new file mode 100644 index 0000000000..bfaa855350 --- /dev/null +++ b/war/images/new-document.svg @@ -0,0 +1,1001 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Generic Text + + + text + plaintext + regular + document + + + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/new-package.svg b/war/images/new-package.svg new file mode 100644 index 0000000000..3a6c74d3d7 --- /dev/null +++ b/war/images/new-package.svg @@ -0,0 +1,625 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Package + + + Jakub Steiner + + + http://jimmac.musichall.cz/ + + + package + archive + tarball + tar + bzip + gzip + zip + arj + tar + jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/next.svg b/war/images/next.svg new file mode 100644 index 0000000000..989bff5cf0 --- /dev/null +++ b/war/images/next.svg @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Go Next + + + go + next + right + arrow + pointer + > + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/notepad.svg b/war/images/notepad.svg new file mode 100644 index 0000000000..32c867102f --- /dev/null +++ b/war/images/notepad.svg @@ -0,0 +1,483 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Text Editor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/nothing.svg b/war/images/nothing.svg new file mode 100644 index 0000000000..c9fca93c9e --- /dev/null +++ b/war/images/nothing.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Delete + 2005-12-28 + + + Andreas Nilsson + + + http://tango-project.org + + + delete + remove + + + + + + + + + + + + + + + diff --git a/war/images/package.svg b/war/images/package.svg new file mode 100644 index 0000000000..022f7a0f3e --- /dev/null +++ b/war/images/package.svg @@ -0,0 +1,413 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Package + + + Jakub Steiner + + + http://jimmac.musichall.cz/ + + + package + archive + tarball + tar + bzip + gzip + zip + arj + tar + jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/png2gifanime.sh b/war/images/png2gifanime.sh new file mode 100644 index 0000000000..01edca9efd --- /dev/null +++ b/war/images/png2gifanime.sh @@ -0,0 +1,17 @@ +#!/bin/bash -e +# take multiple PNG files in the command line, and convert them to animation gif in the white background +# then send it to stdout +i=0 +tmpbase=/tmp/png2gifanime$$ + +for f in "$@" +do + convert $f \( +clone -fill white -draw 'color 0,0 reset' \) \ + -compose Dst_Over $tmpbase$i.gif + fileList[$i]=$tmpbase$i.gif + i=$((i+1)) +done + +convert -delay 10 ${fileList[@]} -loop 0 "${tmpbase}final.gif" +cat ${tmpbase}final.gif +rm ${fileList[@]} ${tmpbase}final.gif diff --git a/war/images/previous.svg b/war/images/previous.svg new file mode 100644 index 0000000000..f1eb97796d --- /dev/null +++ b/war/images/previous.svg @@ -0,0 +1,852 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Go Previous + + + go + previous + left + arrow + pointer + < + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/readme.txt b/war/images/readme.txt new file mode 100644 index 0000000000..9799add8cd --- /dev/null +++ b/war/images/readme.txt @@ -0,0 +1,2 @@ +This is the workshop to tweak with images. +These images are generated into PNGs and then copied manually over to the resouces/image directory diff --git a/war/images/red.svg b/war/images/red.svg new file mode 100644 index 0000000000..6ac0c77b27 --- /dev/null +++ b/war/images/red.svg @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Face - Plain + + + emoticon + emote + face + plain + :| + :-| + + + + + + Steven Garrity + + + http://www.tango-project.org + + + Based on face-smile by jimmac + + + + + + + + + + + + + + + + + + + diff --git a/war/images/refresh.svg b/war/images/refresh.svg new file mode 100644 index 0000000000..8b63d9c943 --- /dev/null +++ b/war/images/refresh.svg @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + View Refresh + + + reload + refresh + view + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/save.svg b/war/images/save.svg new file mode 100644 index 0000000000..b044e0f2af --- /dev/null +++ b/war/images/save.svg @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Media Floppy + + + Tuomas Kuosmanen + + + http://www.tango-project.org + + + save + document + store + file + io + floppy + media + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/scrollbar/.cvsignore b/war/images/scrollbar/.cvsignore new file mode 100644 index 0000000000..e736bc476d --- /dev/null +++ b/war/images/scrollbar/.cvsignore @@ -0,0 +1,2 @@ +*.gif +Thumbs.db diff --git a/war/images/scrollbar/make.sh b/war/images/scrollbar/make.sh new file mode 100644 index 0000000000..6dcba4d435 --- /dev/null +++ b/war/images/scrollbar/make.sh @@ -0,0 +1,24 @@ +#!/bin/zsh -ex +KSH_ARRAYS=off + +# width of the band +u=20 + +color="#3465a4" + +U=$(($u*2)) + + +make() { + convert -size ${U}x8 xc:white -fill $color -stroke $color -draw "polyline 0,$(($u-1)) $(($u-1)),0 $(($U-1)),0 $u,$(($u-1))" -roll +$1+0 screen.$1.gif +} + +set -A list + +for (( i=0 ; i<$U; i+=1 )) +do + make $i + list[$((${#list}+1))]=screen.$i.gif +done + +convert -delay 10 ${list[@]} -loop 0 progress-unknown.gif diff --git a/war/images/scrollbar/test.html b/war/images/scrollbar/test.html new file mode 100644 index 0000000000..217f7122a4 --- /dev/null +++ b/war/images/scrollbar/test.html @@ -0,0 +1,5 @@ + + +
+   +
\ No newline at end of file diff --git a/war/images/search.svg b/war/images/search.svg new file mode 100644 index 0000000000..f2573ffc07 --- /dev/null +++ b/war/images/search.svg @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/setting.svg b/war/images/setting.svg new file mode 100644 index 0000000000..a6183e8b0a --- /dev/null +++ b/war/images/setting.svg @@ -0,0 +1,396 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Preferences System + + + preferences + settings + control panel + tweaks + system + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/stop.svg b/war/images/stop.svg new file mode 100644 index 0000000000..8710ef751d --- /dev/null +++ b/war/images/stop.svg @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Unreadable + + + emblem + access + denied + unreadable + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/system-log-out.svg b/war/images/system-log-out.svg new file mode 100644 index 0000000000..c04625adaf --- /dev/null +++ b/war/images/system-log-out.svg @@ -0,0 +1,362 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + System Log Out + + + log out + logout + exit + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/terminal.svg b/war/images/terminal.svg new file mode 100644 index 0000000000..f926642443 --- /dev/null +++ b/war/images/terminal.svg @@ -0,0 +1,429 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Terminal + 2005-10-15 + + + Andreas Nilsson + + + + + terminal + emulator + term + command line + + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/undo.svg b/war/images/undo.svg new file mode 100644 index 0000000000..ba1d1f55b7 --- /dev/null +++ b/war/images/undo.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Edit Undo + + + edit + undo + revert + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/up.svg b/war/images/up.svg new file mode 100644 index 0000000000..0e3d01d172 --- /dev/null +++ b/war/images/up.svg @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Go Up + + + go + higher + up + arrow + pointer + > + + + + + Andreas Nilsson + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/user.svg b/war/images/user.svg new file mode 100644 index 0000000000..e0d05a105f --- /dev/null +++ b/war/images/user.svg @@ -0,0 +1,537 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + People + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + users + people + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/utilities-system-monitor.svg b/war/images/utilities-system-monitor.svg new file mode 100644 index 0000000000..bb07506e4e --- /dev/null +++ b/war/images/utilities-system-monitor.svg @@ -0,0 +1,363 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + System Monitor + 2005-10-10 + + + Andreas Nilsson + + + + + system + monitor + performance + + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/warning.svg b/war/images/warning.svg new file mode 100644 index 0000000000..51f7ff34ab --- /dev/null +++ b/war/images/warning.svg @@ -0,0 +1,359 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Dialog Warning + 2005-10-14 + + + Andreas Nilsson + + + + + Jakub Steiner, Garrett LeSage + + + + + dialog + warning + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/war/images/yellow.svg b/war/images/yellow.svg new file mode 100644 index 0000000000..de06f91888 --- /dev/null +++ b/war/images/yellow.svg @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Face - Plain + + + emoticon + emote + face + plain + :| + :-| + + + + + + Steven Garrity + + + http://www.tango-project.org + + + Based on face-smile by jimmac + + + + + + + + + + + + + + + + + + + diff --git a/war/pom.xml b/war/pom.xml new file mode 100644 index 0000000000..e59a0b0e4b --- /dev/null +++ b/war/pom.xml @@ -0,0 +1,114 @@ + + 4.0.0 + + org.jvnet.hudson.main + pom + 1.60 + ../pom.xml + + + hudson + war + Hudson war + + + target + hudson + + + + maven-war-plugin + 2.0 + + + + ${basedir}/resources + + WEB-INF/lib/* + + + + + + + + + + + org.jvnet.hudson.main + hudson-core + ${version} + + + + javax.servlet + servlet-api + + + xalan + xalan + + + xerces + xercesImpl + + + xml-apis + xml-apis + + + xerces + xmlParserAPIs + + + msv + xsdlib + + + msv + relaxngDatatype + + + forehead + forehead + + + jdom + jdom + + + xom + xom + + + commons-cli + commons-cli + + + commons-discovery + commons-discovery + + + commons-jelly + commons-jelly-tags-junit + + + jaxme + jaxme-api + + + junit + junit + + + stax + stax-api + + + pull-parser + pull-parser + + + + + \ No newline at end of file diff --git a/war/resources/WEB-INF/web.xml b/war/resources/WEB-INF/web.xml new file mode 100644 index 0000000000..01ca1370ee --- /dev/null +++ b/war/resources/WEB-INF/web.xml @@ -0,0 +1,65 @@ + + + Hudson + Build management system + + + Stapler + org.kohsuke.stapler.Stapler + + + + Stapler + / + + + + JellyServlet + org.apache.commons.jelly.servlet.JellyServlet + + + + JellyServlet + *.jelly + + + + + hudson.WebAppMain + + + + + admin + + + + + Hudson + /loginEntry + + + + admin + + + + + FORM + + /login + /loginError + + + + + + + HUDSON_HOME + java.lang.String + + + \ No newline at end of file diff --git a/war/resources/css/color.css b/war/resources/css/color.css new file mode 100644 index 0000000000..cd02110de1 --- /dev/null +++ b/war/resources/css/color.css @@ -0,0 +1,13 @@ +.greyed { + color: #999; +} + +.redbold { + color: darkred; + font-weight: bold; +} + +.greenbold { + color: #6c0; + font-weight: bold; +} diff --git a/war/resources/css/style.css b/war/resources/css/style.css new file mode 100644 index 0000000000..164b0194d0 --- /dev/null +++ b/war/resources/css/style.css @@ -0,0 +1,527 @@ +body { + margin: 0; + padding: 0; + background: white; +} + +body, table, form, input, td, th, p, textarea, select +{ + font-family: Verdana, Helvetica, sans serif; + font-size: 11px; +} + +td { + vertical-align: top; +} + +table.middle-align td { + vertical-align: middle; +} + +#main-table { + padding: 0; + border-collapse: collapse; +} + +#top-panel { + margin-bottom: 3pt; + height: 34px; + background: url(../images/topbar.png) repeat-x; +} +#top-panel a { + text-decoration: none; +} + +#left-top-nav { + text-align: left; + border-left: 10px solid #fff; + padding: 4px; + color: #222; +} + +#left-top-nav a, #right-top-nav a { + color: black; +} + +#right-top-nav { + text-align: right; + padding: 4px; +} + +#main-panel { + padding: 10px; +} + +#side-panel { + padding: 4px; + width: 220px; +} + +#footer { + text-align: right; + font-size: 8pt; + margin-top: 10em; + padding: 10px; + border-top: 1px solid #bbb; +} + +#tasks { + padding: 4px; +} + +a:link { + text-decoration: underline; + color: #204A87; +} + +a:visited { + text-decoration: underline; + color: #5c3566; +} + +/* tip - anchors of class info */ +a.tip { + position:relative; + z-index:24; + text-decoration: underline; +} + +a.tip:hover { + z-index:25; +} + +a.tip span { + display: none +} + +a.tip:hover span { + display:block; + position:absolute; + top:2em; + left:2em; + width:400px; + border:1px solid #bbbbbb; + background-color:#fffff0; + color:#000; + text-align: left +} + +#top-nav .a { + color: white; +} + +img { + vertical-align: middle; + border: 0; +} + +table.tab { + border-collapse: collapse; +} + +td.selected_tab { + vertical-align: middle; + border: 1px #090 solid; + background: #ffffff; +} + +td.tab_filler { + background: #ffffff; + border-bottom: 1px #090 solid; +} + +td.tab { + vertical-align: middle; + border: 1px #090 solid; + background: #f0f0f0; +} + +table.progress-bar { + border-collapse: collapse; + border: 1px solid #3465a4; + height: 6px; + width: 100px; + clear: none; +} + +td.progress-bar-done { + background-color: #3465a4; +} + +td.progress-bar-left { + background-color: #ffffff; +} + +.dashboard td { + padding: 4px 4px 4px 4px; +} + +pre.console { + overflow: auto; +} + +.setting-name { + white-space: nowrap; +} + +.setting-leftspace { + width: 2em; +} + +.setting-input { + width: 100%; +} + +.setting-description { + font-size: 0.8em; + margin-top: 0; + padding-top: 0; +} + +/* div that looks like a hyperlink */ +.pseudoLink { + cursor: pointer; +} + +.advancedLink { + text-align: right; +} + +.advancedBody { + display: none; +} + +.scm_info { + width: 480px; +} + +.build-row { + padding: 3px 4px 3px 4px; +} + +.task-header { + display: block; + border-bottom: 1px #090 solid; + font-weight: bold; + font-size: 12pt; +} + +.task { + white-space: nowrap; +} + +.main-table { +} + +table.dashboard { + width: 100%; +} + +.pane { + margin-top: 4px; + white-space: nowrap; +} +.pane td { + padding: 4px 4px 3px 4px; +} + +table.pane { + width: 100%; + border-collapse: collapse; + border: 1px #bbb solid; +} + +td.pane { + border: 1px #bbb solid; + padding: 3px 4px 3px 4px; + vertical-align: middle; +} + +td.pane-header { + border: 1px #bbb solid; + border-right: none; + border-left: none; + background-color: #f0f0f0; + font-weight: bold; +} + +th.pane { + border: 1px #bbb solid; + font-weight: bold; +} + +#projectstatus tr { + border: 1px solid #bbb; + padding: 3px 4px 3px 4px; +} + +#projectstatus th { + font-weight: bold; + border: none; + background-color: #f0f0f0; + padding: 3px 4px 3px 4px; +} + +#projectstatus td { + vertical-align: middle; + padding: 3px 4px 3px 4px; +} + +.smallfont { + font-size: 9px; +} + +#foldertab { + padding: 4px 0; + margin-left: 0; + border-bottom: 1px solid #090; + font: bold 12px Verdana, sans-serif; +} + +#foldertab li { + list-style: none; + margin: 0; + display: inline; +} + +#foldertab li a { + padding: 4px 0.5em; + margin-left: 3px; + border: 1px solid #090; + border-bottom: none; + background: #090; + text-decoration: none; +} + +#foldertab li a:link { color: white; } +#foldertab li a:visited { color: white; } + +#foldertab li a:hover { + color: white; + background: #6c0; + border-color: #6c0; +} + +#foldertab li a#current { + background: white; + border-bottom: 1px solid white; + color: black; +} + +.changeset-message { + border: 1px solid #ccb; + background: #eed; + padding: 4px; +} + +.error { + color: red; + font-weight: bold; +} + +.spinner { + padding-left: 32px; + padding-top: 0.5em; + padding-bottom: 0.5em; + background-image: url("../images/spinner.gif"); + background-repeat: no-repeat; + background-position: left; +} + +.spinner-right { + padding-right: 32px; + padding-top: 0.5em; + padding-bottom: 0.5em; + background-image: url("../images/spinner.gif"); + background-repeat: no-repeat; + background-position: right; +} +/* ====================== help ===================================== */ + +.help { + display: none; /* hidden until loaded */ + border: solid #bbb 1px; + background-color: #f0f0f0; + padding: 1em; + margin-bottom: 1em; +} + +.help-area { + /* this marker class is used by JavaScript to locate the area to display help text. */ +} + + +/* ====================== project view tab bar ===================================== */ +#viewList { + border: none; + margin-bottom: 0px; + width: 100%; + white-space: nowrap; +} +#viewList td { + padding: 0px; +} +#viewList td.inactive { + border: solid 1px #ccc; + border-bottom-color: #bbb; +} +#viewList td.inactive:hover { + background-color: #777; +} +#viewList td.inactive a { + text-decoration: none; + color: #444 +} +#viewList td.noleft { + border-left: none; +} +#viewList td.noright { + border-right: none; +} +#viewList td.active { + border: solid 1px #bbb; + padding: 0.5em; + border-bottom: none; + vertical-align:middle; + background-color: rgb(240,240,240); + font-weight: bold; +} +#viewList td.filler { + border: none; + border-bottom: solid 1px #bbb; + width: 100%; + text-align: right; +} +#viewList a { + display: block; + padding: 0.5em; + white-space: nowrap; +} + + +/* ========================= editable combobox style ========================= */ +.comboBoxList { + border: 1px solid #000; + overflow: visible; + color: MenuText; + background-color: Menu; +} +.comboBoxSelectedItem { + background-color: Highlight; + color: HighlightText; +} + + + +/* ========================= directory tree ========================= */ +.parentPath { + font-size: 1.2em; + font-weight: bold; +} + +.dirTree li { + list-style: none; +} + +.dirTree .rootIcon { + margin-right: 1em; +} + +TABLE.fileList { + margin-left: 2em; + padding: 0; +} + +TABLE.fileList TD { + padding: 0; +} + +TABLE.fileList TD.fileSize { + padding-left: 2em; + text-align: right; + color: #888; +} + + + +/* ========================= test result ========================= */ +.result-passed { + color: #3465a4; +} + +.result-fixed { + color: #3465a4; + font-weight: bold; +} + +.result-failed { + color: #ef2929; +} + +.result-regression { + color: #ef2929; + font-weight: bold; +} +.test-trend-caption { + text-align: center; + font-size: 1.2em; + font-weight: bold; +} + + + +/* ========================= sortable table ========================= */ +table.sortable a.sortheader { + text-decoration: none; + color: black; + display: block; +} +table.sortable span.sortarrow { + color: black; + text-decoration: none; +} + + + + +/* ========================= fingerprint ========================= */ +.md5sum { + text-align: right; +} + +.fingerprint-summary-header { + font-size: 1.2em; + vertical-align: middle; +} + +TABLE.fingerprint-in-build TD { + padding-left: 1em; + padding-right: 1em; +} + + + + +/* ========================= plugin manager ========================= */ +#pluginList { + width:100%; + border: 1px solid #bbb; + border-spacing: 0px; +} + +#pluginList TR:hover { + background-color: #eeeeec; +} + +#pluginList TD { + padding: 0.5em; + vertical-align: middle; +} + +/* Doesn't work because gear2.gif is not transparent +#pluginList TD.plugin-description { + background: url(../images/24x24/gear2.gif) no-repeat; + background-position: center left; + padding-left: 32px; +} +*/ \ No newline at end of file diff --git a/war/resources/dc-license.txt b/war/resources/dc-license.txt new file mode 100644 index 0000000000..d92973c037 --- /dev/null +++ b/war/resources/dc-license.txt @@ -0,0 +1,28 @@ +Copyright (c) 2004, DamageControl Organization, Jon Tirsen and Aslak Hellesoy +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + Neither the name of the DamageControl Organization nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/war/resources/env-vars.html b/war/resources/env-vars.html new file mode 100644 index 0000000000..444e1191c8 --- /dev/null +++ b/war/resources/env-vars.html @@ -0,0 +1,38 @@ + + + Available Environmental Variables + + + + The following variables are available to shell scripts + +
+
BUILD_NUMBER
+
The current build number, such as "153"
+ +
BUILD_ID
+
The current build id, such as "2005-08-22_23-59-59" (YYYY-MM-DD_hh-mm-ss)
+ +
JOB_NAME
+
Name of the project of this build, such as "foo"
+ +
BUILD_TAG
+
String of "hudson-${JOBNAME}-${BUILD_NUMBER}". Convenient to put into + a resource file, a jar file, etc for easier identification.
+
+ + +

+To understand how environmental variables provided by Hudson can be utilized by +Ant, study the following target: + +

<target name="printinfo">
+  <property environment="env" />
+  <echo message="${env.BUILD_TAG}"/>
+</target>
+ + + \ No newline at end of file diff --git a/war/resources/help/_cvs/branch.html b/war/resources/help/_cvs/branch.html new file mode 100644 index 0000000000..9209ceda44 --- /dev/null +++ b/war/resources/help/_cvs/branch.html @@ -0,0 +1,4 @@ +
+ If set, Hudson will run CVS with -r to build a particular branch. + If left empty, Hudson will build the trunk. +
\ No newline at end of file diff --git a/war/resources/help/_cvs/cvs-rsh.html b/war/resources/help/_cvs/cvs-rsh.html new file mode 100644 index 0000000000..de687150db --- /dev/null +++ b/war/resources/help/_cvs/cvs-rsh.html @@ -0,0 +1,5 @@ +
+ If set, Hudson will run CVS with the CVS_RSH environment variable set + to this value (If this value is not set, and the web container that Hudson runs in + has CVS_RSH set in its environment variable, then CVS will inherit it. +
\ No newline at end of file diff --git a/war/resources/help/_cvs/cvspass.jelly b/war/resources/help/_cvs/cvspass.jelly new file mode 100644 index 0000000000..d7e88b4940 --- /dev/null +++ b/war/resources/help/_cvs/cvspass.jelly @@ -0,0 +1,24 @@ + + +
+ CVS stores the login and password information in a file called ".cvspass". Normally + CVS loads this file in your home directory, but if you want to have Hudson load it + from a different place, specify the full path to the file. Otherwise leave it blank. +
+ + If you are using CVSNT (am I?), then this setting is + likely to be ignored. See + this note for more about why. In this case, you need to run "cvs login" command + manually to enter password. If Hudson runs under the same user as you do, then + this would be straight-forward. However, if Hudson runs as a service, you'd need to + first launch CMD.exe as the SYSTEM user, then you need to run cvs command + from there. See this document + for how to do this. +
+
+ +
+
\ No newline at end of file diff --git a/war/resources/help/_cvs/cvsroot.html b/war/resources/help/_cvs/cvsroot.html new file mode 100644 index 0000000000..1da6a2e909 --- /dev/null +++ b/war/resources/help/_cvs/cvsroot.html @@ -0,0 +1,4 @@ +
+ The CVS connection string Hudson uses to connect to the server. The format is + the same as $CVSROOT environment variable (:protocol:user@host:path) +
\ No newline at end of file diff --git a/war/resources/help/_cvs/legacy.html b/war/resources/help/_cvs/legacy.html new file mode 100644 index 0000000000..b33428ff36 --- /dev/null +++ b/war/resources/help/_cvs/legacy.html @@ -0,0 +1,10 @@ +
+ Hudson 1.20 and earlier used to create redudant directories inside the workspace. + For example, if the CVS module name is "foo/bar", it first created "foo/bar" and + then put everything below. With this option checked off, there will be no more + such unnecessary intermediate directories. + +

+ This affects other path specifiers, such as artifact archiverers --- you now specify + "build/foo.jar" instead of "foo/build/foo.jar". +

\ No newline at end of file diff --git a/war/resources/help/_cvs/modules.html b/war/resources/help/_cvs/modules.html new file mode 100644 index 0000000000..e71fa1087d --- /dev/null +++ b/war/resources/help/_cvs/modules.html @@ -0,0 +1,4 @@ +
+ The CVS module name(s) in the given CVSROOT to check out. + Multiple modules can be specified by separating them by a whitespace. +
\ No newline at end of file diff --git a/war/resources/help/_cvs/update.html b/war/resources/help/_cvs/update.html new file mode 100644 index 0000000000..9a3bfbb3cb --- /dev/null +++ b/war/resources/help/_cvs/update.html @@ -0,0 +1,6 @@ +
+ If checked, Hudson will use 'cvs update' whenever possible for builds. + This makes a build faster. But this also causes the artifacts from + the previous build to remain in the file system when a new build starts, + making it not a true clean build. +
\ No newline at end of file diff --git a/war/resources/help/project-config/ant.html b/war/resources/help/project-config/ant.html new file mode 100644 index 0000000000..861eef5ce4 --- /dev/null +++ b/war/resources/help/project-config/ant.html @@ -0,0 +1,10 @@ +
+ For projects that use Ant as the build system. This causes Hudson to + invoke Ant with the given targets and options. Non-0 exit code from + Ant makes Hudson to mark the build as a failure. + +

+ If your build script is located in somewhere other than the module top + build.xml, then you can use + Ant's -f option to specify where it is. +

\ No newline at end of file diff --git a/war/resources/help/project-config/archive-artifact.html b/war/resources/help/project-config/archive-artifact.html new file mode 100644 index 0000000000..f5d4ba92bf --- /dev/null +++ b/war/resources/help/project-config/archive-artifact.html @@ -0,0 +1,8 @@ +
+ Archives build artifacts (for example, distribution zip files or jar files) + so that they can be downloaded later. + Archived files will be accessible from Hudson webpage. +
+ Normally, Hudson keeps artifacts for a build as long as a build log itself is kept, + but if you don't need old artifacts and would rather save disk space, you can do so. +
\ No newline at end of file diff --git a/war/resources/help/project-config/batch.html b/war/resources/help/project-config/batch.html new file mode 100644 index 0000000000..e82a82bf2c --- /dev/null +++ b/war/resources/help/project-config/batch.html @@ -0,0 +1,11 @@ +
+ Runs Windows batch script for building the project. + The script will be run with the workspace as the current directory. + + The text you enter in the text box will be executed as a batch file, and a build + will be considered a failure if at the end of the execution %ERRORLEVEL% is not 0. + +

+ If you already have a batch file in SCM, you can just type in the path of that batch file + (again relative to the workspace directory), and simply execute that. +

\ No newline at end of file diff --git a/war/resources/help/project-config/description.html b/war/resources/help/project-config/description.html new file mode 100644 index 0000000000..f6072fdab7 --- /dev/null +++ b/war/resources/help/project-config/description.html @@ -0,0 +1,4 @@ +
+ This description is placed in the project top page so that visitors can + know what this job is about. You can use any HTML tag here. +
\ No newline at end of file diff --git a/war/resources/help/project-config/disable.html b/war/resources/help/project-config/disable.html new file mode 100644 index 0000000000..991075146f --- /dev/null +++ b/war/resources/help/project-config/disable.html @@ -0,0 +1,11 @@ +
+

+ Sometimes, you'd want to temporarily stop building a project. For example, + maybe you are in the middle of a large migration and you now new builds are + going to fail. Or maybe a project is built every hour but you know that the + CVS server will be down for the next 24 hours. +

+ When this option is set, no new build is performed on this project. This + allows you to disable new builds without changing any of the build dependency + chain nor change notification set up. +

\ No newline at end of file diff --git a/war/resources/help/project-config/fingerprint.html b/war/resources/help/project-config/fingerprint.html new file mode 100644 index 0000000000..3be86a9d2f --- /dev/null +++ b/war/resources/help/project-config/fingerprint.html @@ -0,0 +1,31 @@ +
+ Hudson can record the 'fingerprint' of files (most often jar files) to keep track + of where/when those files are produced and used. When you have inter-dependent + projects on Hudson, this allows you to quickly find out answers to questions like: + +
    +
  • + I have foo.jar on my HDD but which build number of FOO did it come from? +
  • +
  • + My BAR project depends on foo.jar from the FOO project. +
  • +
      +
    • + Which build of foo.jar is used in BAR #51? +
    • +
    • + Which build of BAR contains my bug fix to foo.jar #32? +
    • +
    +
+ +

+ To use this feature, all the involved projects (not just the project + in which a file is produced, but also the projects in which the file + is used) need to use this and record fingerprints. + +

+ See this document + fore more details. +

\ No newline at end of file diff --git a/war/resources/help/project-config/log-rotation.html b/war/resources/help/project-config/log-rotation.html new file mode 100644 index 0000000000..74f9ea450b --- /dev/null +++ b/war/resources/help/project-config/log-rotation.html @@ -0,0 +1,18 @@ +
+ This controls the disk consumption of Hudson by managing how long you'd like to keep + records of the builds (such as console output, build artifacts, and so on.) + Hudson offers two criteria: + +
    +
  1. + Driven by age. You can have Hudson delete records if it reaches certain age + (for example, 7 days old.) +
  2. + Driven by number. You can have Hudson make sure that it only maintains up to + N records of the builds. If a new build is started, the oldest record will + be simply removed. +
+ + Hudson also allows you to mark individual build as 'Keep this log forever', to + exclude certain important builds from being discarded automatically. +
\ No newline at end of file diff --git a/war/resources/help/project-config/mailer.html b/war/resources/help/project-config/mailer.html new file mode 100644 index 0000000000..248f50c4a2 --- /dev/null +++ b/war/resources/help/project-config/mailer.html @@ -0,0 +1,16 @@ +
+ If configured, Hudson will send out an e-mail to the specified recipients + when a certain important event occurs. + +
    +
  1. Every failed build triggers a new e-mail. +
  2. A successful build after a failed (or unstable) build triggers a new e-mail, + indicating that a crisis is over. +
  3. An unstable build after a successful build triggers a new e-mail, + indicating that there's a regression. +
  4. Unless configured, every unstable build triggers a new e-mail, + indicating that regression is still there. +
+ + For lazy projects where unstable builds are the norm, Check "Don't send e-mail for every unstable build". +
\ No newline at end of file diff --git a/war/resources/help/project-config/maven.html b/war/resources/help/project-config/maven.html new file mode 100644 index 0000000000..e294780037 --- /dev/null +++ b/war/resources/help/project-config/maven.html @@ -0,0 +1,6 @@ +
+ For projects that use Maven as the build system. This causes Hudson to + invoke Maven with the given goals and options. Non-0 exit code from + Maven makes Hudson to mark the build as a failure. + Some maven versions have a bug where it doesn't return the exit code correctly. +
\ No newline at end of file diff --git a/war/resources/help/project-config/poll-scm.html b/war/resources/help/project-config/poll-scm.html new file mode 100644 index 0000000000..0f877c77da --- /dev/null +++ b/war/resources/help/project-config/poll-scm.html @@ -0,0 +1,9 @@ +
+ Configure Hudson to poll changes in SCM. + +

+ Note that this is going to be an expensive operation for CVS, as every polling + requires Hudson to scan the entire workspace and verify it with the server. + Consider setting up a "push" trigger to avoid this overhead, as described in + this document +

\ No newline at end of file diff --git a/war/resources/help/project-config/quietPeriod.html b/war/resources/help/project-config/quietPeriod.html new file mode 100644 index 0000000000..0de6a39aa5 --- /dev/null +++ b/war/resources/help/project-config/quietPeriod.html @@ -0,0 +1,21 @@ +
+ If set, a newly scheduled build wait for this many seconds before actually built. + This is useful for: +
    +
  • + Collapsing multiple CVS change notification e-mails to one. (some CVS changelog + e-mail generation script generates multiple e-mails in quick succession when + a commit spans across directories.) +
  • +
  • + If your coding style is such that you commit one logical change in a few cvs/svn + operations, then setting a larger quiet period would prevent Hudson from building + it prematurely and report a failure. +
  • +
  • + Throttling builds. If your Hudson installation is too busy with too many builds, + setting a larger quiet period can reduce the number of builds. +
  • +
+ If not explicitly set at project-level, the system-wide default value is used. +
\ No newline at end of file diff --git a/war/resources/help/project-config/shell.html b/war/resources/help/project-config/shell.html new file mode 100644 index 0000000000..bfd3b411eb --- /dev/null +++ b/war/resources/help/project-config/shell.html @@ -0,0 +1,7 @@ +
+ Runs shell script (default to sh but configurable) for building the project. + The script will be run with the workspace as the current directory. + + shell will be invoked with "-ex" option. So all the commands are printed before executed, + and the build is considered a failure if any of the commands exit with a non-zero exit code. +
\ No newline at end of file diff --git a/war/resources/help/project-config/slave.html b/war/resources/help/project-config/slave.html new file mode 100644 index 0000000000..3220f66697 --- /dev/null +++ b/war/resources/help/project-config/slave.html @@ -0,0 +1,12 @@ +
+ Sometimes a project can be only successfully built on a particular slave + (or master.) If so, this option forces Hudson to always build this project + on a specific computer. + + Otherwise, uncheck the box so that Hudson can schedule builds on available + nodes, which results in faster turn-around time. + +

+ This option is also useful when you'd like to make sure that a project can + be built on a particular node. +

\ No newline at end of file diff --git a/war/resources/help/project-config/timer-format.html b/war/resources/help/project-config/timer-format.html new file mode 100644 index 0000000000..6e73335721 --- /dev/null +++ b/war/resources/help/project-config/timer-format.html @@ -0,0 +1,54 @@ +
+ This field follows the syntax of cron (with minor difference). + Specifically, each line consists of 5 fields separated by TAB or whitespace: +
MINUTE HOUR DOM MONTH DOW
+ + + + + + + + + + + + + + + + + + + + + +
MINUTEminutes with in the hour (0-59)
HOURThe hour of the day (0-23)
DOMTHe day of the month (1-31)
MONTHThe month (1-12)
DOWThe day of the week (0-7) where 0 and 7 are Sunday.
+

+ To specify multiple values for one field, following operators are + available. In the order of precedence, +

+
    +
  • '*' can be used to specify all valid values.
  • +
  • 'M-N' can be used to specify a range, such as "1-5"
  • +
  • 'M-N/X' or '*/X' can be used to specify skips of X's value through the range, + such as "*/15" in the MINUTE field for "0,15,30,45" and "1-6/2" for "1,3,5"
  • +
  • 'A,B,...,Z' can be used to specify multiple values, such as "0,30" or "1,3,5"
  • +
+

+ Empty lines and lines that start with '#' will be ignored as comments. +

+ + + + + +
Examples +
+# every minute
+* * * * *
+# every 5 mins past the hour 
+5 * * * *
+
+
+
\ No newline at end of file diff --git a/war/resources/help/project-config/timer.html b/war/resources/help/project-config/timer.html new file mode 100644 index 0000000000..e8ede11735 --- /dev/null +++ b/war/resources/help/project-config/timer.html @@ -0,0 +1,19 @@ +
+ Provides a cron-like feature + to periodically execute this project. + +

+ This feature is primarily for using Hudson as a cron replacement, + and it is not ideal for continuously building software project. + + When people first start continous integration, they are often so used to + the idea of regularly scheduled builds like nightly/weekly that they use + this feature. However, the point of continous integration is to start + a build as soon as a change is made, to provide a quick feedback to the change. + To do that you need to + hook up SCM change notification to Hudson.. + +

+ So, before using this feature, stop and ask yourself if this is really what you want. + +

\ No newline at end of file diff --git a/war/resources/help/project-config/upstream.html b/war/resources/help/project-config/upstream.html new file mode 100644 index 0000000000..e77df28516 --- /dev/null +++ b/war/resources/help/project-config/upstream.html @@ -0,0 +1,10 @@ +
+

+ Set up a trigger so that when some other projects finish building, + schedule a new build for this project. This is convenient for running + an extensive test after a build is complete, for example, +

+ This configuration is the opposite view of the "Build other projects" section + in the "Post-build Actions". Updating one will change the other automatically. +

+
\ No newline at end of file diff --git a/war/resources/help/subversion/executable.html b/war/resources/help/subversion/executable.html new file mode 100644 index 0000000000..4daaeeae7e --- /dev/null +++ b/war/resources/help/subversion/executable.html @@ -0,0 +1,4 @@ +
+ If 'svn' is not in your PATH, you can specify the full path + to the svn executable. If you do have svn in your path, leave it blank. +
\ No newline at end of file diff --git a/war/resources/help/system-config/cvs-browser.html b/war/resources/help/system-config/cvs-browser.html new file mode 100644 index 0000000000..d9f1d4ea2d --- /dev/null +++ b/war/resources/help/system-config/cvs-browser.html @@ -0,0 +1,36 @@ +
+

+ Specifies paths of the CVS repository browser web application. + Setting these values allow Hudson to generate links from changeset to those pages. + There are two different entries for each CVSROOT. + +

+
URL
+
+ URL of the page that displays details about a particular file + in the repository. +
+ +
Diff URL
+
+ URL of the page that displays details about difference between two revisions. +
+
+ +

+ The following macro characters are available. + + + + + + + + + + + + + +
%%PPath name of the file under CVS, like "foo/bar/Zot.java"
%%rRevision of the old version (for diff URLs)
%%rRevision of the new version (for diff URLs)
+

\ No newline at end of file diff --git a/war/resources/help/system-config/enableSecurity.html b/war/resources/help/system-config/enableSecurity.html new file mode 100644 index 0000000000..bfe6ccf860 --- /dev/null +++ b/war/resources/help/system-config/enableSecurity.html @@ -0,0 +1,16 @@ +
+ If enabled, you have to log with an username and a password that has "admin" role + in before changing the configuration or running a new build (look for the "login" link + at the top right portion of the page.) + Configuration of user accounts is specific to the web container you are using. + (For example, in Tomcat, by default, it looks for $TOMCAT_HOME/conf/tomcat-users.xml) + +

+ If you are using Hudson in an intranet (or other "trusted" environment), it's usually + desirable to leave this checkbox off, so that each project developer can configure its own + project without bothering you. + +

+ If you are exposing Hudson to the internet, you must turn this on. Hudson launches + processes, so unsecure Hudson is a sure way of being hacked. +

\ No newline at end of file diff --git a/war/resources/help/system-config/homeDirectory.html b/war/resources/help/system-config/homeDirectory.html new file mode 100644 index 0000000000..9e5ab78f89 --- /dev/null +++ b/war/resources/help/system-config/homeDirectory.html @@ -0,0 +1,17 @@ +
+ Hudson stores all the data files inside this directory in the file system. + You can change this by either: +
    +
  1. + Use your web container's admin tool to set the HUDSON_HOME + environment entry. +
  2. + Set the environment variable HUDSON_HOME before launching + your web container. +
  3. + (Not recommended) modify web.xml of hudson.war (or its expanded image + in your web container). +
+ This value cannot be changed while Hudson is running. + This entry is mostly for you to make sure that your configuration is taking effect. +
\ No newline at end of file diff --git a/war/resources/help/system-config/master-slave/clock.html b/war/resources/help/system-config/master-slave/clock.html new file mode 100644 index 0000000000..514d7aad59 --- /dev/null +++ b/war/resources/help/system-config/master-slave/clock.html @@ -0,0 +1,5 @@ +
+ Many aspects of a build are sensitive to clock, and therefore if the clock of the machine + that Hudson runs and that of the slave differs significantly, it causes mysterious problems. + Consider synchronizing clocks between machines by NTP. +
diff --git a/war/resources/help/system-config/master-slave/command.html b/war/resources/help/system-config/master-slave/command.html new file mode 100644 index 0000000000..1a0dbc1303 --- /dev/null +++ b/war/resources/help/system-config/master-slave/command.html @@ -0,0 +1,14 @@ +
+ Command to be used to execute a program on this slave, such as + 'ssh slave1' or 'rsh slave2'. Hudson appends the actual command it wants to run + after this and then execute it locally, and then expects the command you spplied + to do the remote job submission. + +

+ Normally, you'd want to use SSH and RSH for this, but + it can be any custom program as well. + +

+ Setting this to "ssh -v hostname" may be useful for debugging connectivity + issue. +

diff --git a/war/resources/help/system-config/master-slave/description.html b/war/resources/help/system-config/master-slave/description.html new file mode 100644 index 0000000000..bb63936258 --- /dev/null +++ b/war/resources/help/system-config/master-slave/description.html @@ -0,0 +1,11 @@ +
+ Optional human-readable description of this slave. This information + is exposed to project configuration screen. + +

+ When you have slaves that are different from others, it's often helpful + to use this field to explain what is different. For example, entering + "Windows slave" on this field would allow project owners to choose to + always build on a Windows machine (for example, if they need some Windows + specific build tool.) +

\ No newline at end of file diff --git a/war/resources/help/system-config/master-slave/localFS.html b/war/resources/help/system-config/master-slave/localFS.html new file mode 100644 index 0000000000..5d5c58a359 --- /dev/null +++ b/war/resources/help/system-config/master-slave/localFS.html @@ -0,0 +1,11 @@ +
+

+ A slave needs to have a directory dedicated for Hudson, and + it needs to be visible from the master Hudson. Specify + the path (from the viewpoint of the master Hudson) to this + work directory, such as '/net/slave1/var/hudson' + +

+ Master and slave needs to be able to read/write this directory + under the same user account. +

\ No newline at end of file diff --git a/war/resources/help/system-config/master-slave/name.html b/war/resources/help/system-config/master-slave/name.html new file mode 100644 index 0000000000..8aef863942 --- /dev/null +++ b/war/resources/help/system-config/master-slave/name.html @@ -0,0 +1,7 @@ +
+ Name that uniquely identifies a slave within this Hudson installation. + +

+ This value could be any string, and doesn't have to be the same with the slave host name, + but it's often convenient to make them the same. +

\ No newline at end of file diff --git a/war/resources/help/system-config/master-slave/numExecutors.html b/war/resources/help/system-config/master-slave/numExecutors.html new file mode 100644 index 0000000000..5141f26ba3 --- /dev/null +++ b/war/resources/help/system-config/master-slave/numExecutors.html @@ -0,0 +1,14 @@ +
+ This controls the number of concurrent builds that Hudson can perform on this slave. + So the value affects the overall system load Hudson may incur. + A good value to start with would be number of processors. + +

+ Increasing this value beyond that would cause each build to take longer, but it could increase + the overall throughput, because it allows CPU to build one project while another build is waiting + for I/O. + +

+ Setting this value to 0 is useful to remove a disabled slave from Hudson temporalily without + losing other configuration information. +

\ No newline at end of file diff --git a/war/resources/help/system-config/master-slave/remoteFS.html b/war/resources/help/system-config/master-slave/remoteFS.html new file mode 100644 index 0000000000..c10778fa73 --- /dev/null +++ b/war/resources/help/system-config/master-slave/remoteFS.html @@ -0,0 +1,5 @@ +
+ Specify the same path you specified above in 'local FS root', + but this time from the viewpoint of the slave node, such + as '/var/hudson' +
diff --git a/war/resources/help/system-config/master-slave/usage.html b/war/resources/help/system-config/master-slave/usage.html new file mode 100644 index 0000000000..3ae634342b --- /dev/null +++ b/war/resources/help/system-config/master-slave/usage.html @@ -0,0 +1,28 @@ +
+ Controls how Hudson schedules builds on this machine. + +
+
+ Utilize this slave as much as possible +
+
+ This is the default and normal setting. + In this mode, Hudson uses this slave freely. Whenever there is a build + that can be done by using this slave, Hudson will use it. +
+ +
+ Leave this machine for tied jobs only +
+
+ In this mode, Hudson will only build a project on this machine when + that project has specifically has this slave as the "assigned node". + + This allows a slave to be reserved for certain kinds of jobs. + For example, to run performance tests continuously from Hudson, + you can use this setting with # of executors as 1, so that only one performance + test runs at any given time, and that one executor won't be blocked + by other builds that can be done on other slaves. +
+
+
\ No newline at end of file diff --git a/war/resources/help/system-config/numExecutors.html b/war/resources/help/system-config/numExecutors.html new file mode 100644 index 0000000000..e7dfa27fac --- /dev/null +++ b/war/resources/help/system-config/numExecutors.html @@ -0,0 +1,14 @@ +
+ This controls the number of concurrent builds that Hudson can perform. So the value + affects the overall system load Hudson may incur. + A good value to start with would be number of processors on your system. + +

+ Increasing this value beyond that would cause each build to take longer, but it could increase + the overall throughput, because it allows CPU to build one project while another build is waiting + for I/O. + +

+ When using Hudson in the master/slave mode, setting this value to 0 would prevent the master + to do any build on its own. +

\ No newline at end of file diff --git a/war/resources/help/system-config/quietPeriod.html b/war/resources/help/system-config/quietPeriod.html new file mode 100644 index 0000000000..04f581577c --- /dev/null +++ b/war/resources/help/system-config/quietPeriod.html @@ -0,0 +1,6 @@ +
+

+ When set to >0, a newly scheduled build waits for this many seconds + before actually being built. This is useful for collapsing multiple CVS change notification + e-mails to one. +

\ No newline at end of file diff --git a/war/resources/help/system-config/systemMessage.html b/war/resources/help/system-config/systemMessage.html new file mode 100644 index 0000000000..34b430c884 --- /dev/null +++ b/war/resources/help/system-config/systemMessage.html @@ -0,0 +1,5 @@ +
+ This message will be displayed at the top page. + Useful for posting a system-wide notification to users. + Can contain HTML tags. +
\ No newline at end of file diff --git a/war/resources/help/tasks/fingerprint/keepDependencies.html b/war/resources/help/tasks/fingerprint/keepDependencies.html new file mode 100644 index 0000000000..fa0ab6f313 --- /dev/null +++ b/war/resources/help/tasks/fingerprint/keepDependencies.html @@ -0,0 +1,17 @@ +
+ If this option is enabled, all + the builds that are referenced from builds of this project (via fingerprint) + will be protected from log rotation. + +

+ When your job depends on other jobs on Hudson and you occasionally need + to tag your workspace, it's often convenient/necessary to also tag your + dependencies on Hudson. The problem is that the log rotation could + interface with this, since the build your project is using might be already + log rotated (if there has been a lot of builds in your dependency), and + if that happens you won't be able to reliably tag you dependencies. + +

+ This feature fixes that probelm by "locking" those builds that you depend on, + thereby guaranteeing that you can always tag your complete dependencies. +

\ No newline at end of file diff --git a/war/resources/help/tasks/mailer/admin-address.html b/war/resources/help/tasks/mailer/admin-address.html new file mode 100644 index 0000000000..a97a32c31c --- /dev/null +++ b/war/resources/help/tasks/mailer/admin-address.html @@ -0,0 +1,5 @@ +
+ Notification e-mails from Hudson to project owners will be sent + with this address in the from header. This can be just + "foo@acme.org" or it could be something like "Hudson Daemon <foo@acme.org>" +
\ No newline at end of file diff --git a/war/resources/help/tasks/mailer/default-suffix.html b/war/resources/help/tasks/mailer/default-suffix.html new file mode 100644 index 0000000000..c15ed259b3 --- /dev/null +++ b/war/resources/help/tasks/mailer/default-suffix.html @@ -0,0 +1,6 @@ +
+ If your users' e-mail addresses can be computed automatically by simply adding a suffix, + then specify that suffix. Otherwise leave it to empty. Note that users can always override + the e-mail address selectively. For example, if this field is set to @acme.org, + then user foo will by default get the e-mail address foo@acme.org +
\ No newline at end of file diff --git a/war/resources/help/tasks/mailer/sendToindividuals.html b/war/resources/help/tasks/mailer/sendToindividuals.html new file mode 100644 index 0000000000..8b508cb565 --- /dev/null +++ b/war/resources/help/tasks/mailer/sendToindividuals.html @@ -0,0 +1,8 @@ +
+ If this option is checked, the notification e-mail will be sent to individuals who have + committed changes for the broken build (by assuming that those changes broke the build.) +

+ If e-mail addresses are also specified in the recipient list, then both the individuals + as well as the specified addresses get the notification e-mail. If the recipient list + is empty, OTOH, then only the individuals will receive e-mails. +

\ No newline at end of file diff --git a/war/resources/help/tasks/mailer/smtp-server.html b/war/resources/help/tasks/mailer/smtp-server.html new file mode 100644 index 0000000000..f93215541b --- /dev/null +++ b/war/resources/help/tasks/mailer/smtp-server.html @@ -0,0 +1,10 @@ +
+ Name of the mail server. Leave it empty to use the default server + (which is normally the one running on localhost.) + +

+ Hudson uses JavaMail for sending out e-mails, and JavaMail allows + additional settings to be given as system properties to the container. + See + this document for possible values and effects. +

\ No newline at end of file diff --git a/war/resources/help/tasks/mailer/smtpAuth.html b/war/resources/help/tasks/mailer/smtpAuth.html new file mode 100644 index 0000000000..f16780ea8a --- /dev/null +++ b/war/resources/help/tasks/mailer/smtpAuth.html @@ -0,0 +1,4 @@ +
+ Use SMTP authentication when sending out e-mails. If your environment requires + the use of SMTP authentication, specify its user name and the password here. +
\ No newline at end of file diff --git a/war/resources/help/tasks/mailer/url.html b/war/resources/help/tasks/mailer/url.html new file mode 100644 index 0000000000..ee16cb9cc9 --- /dev/null +++ b/war/resources/help/tasks/mailer/url.html @@ -0,0 +1,9 @@ +
+ Optionally specify the HTTP address of the Hudson installation, such + as http://yourhost.yourdomain/hudson/. This value is used to + put links into e-mails generated by Hudson. + +

+ This is necessary because Hudson cannot reliably detect such an URL + from within itself. +

\ No newline at end of file diff --git a/war/resources/help/user/description.html b/war/resources/help/user/description.html new file mode 100644 index 0000000000..95b1c9b489 --- /dev/null +++ b/war/resources/help/user/description.html @@ -0,0 +1,5 @@ +
+ This description is placed in the user top page so that visitors can + know who you are. You can use any HTML tag here. Consider putting + some links to other pages related to you. +
\ No newline at end of file diff --git a/war/resources/help/user/fullName.html b/war/resources/help/user/fullName.html new file mode 100644 index 0000000000..0ba3271c07 --- /dev/null +++ b/war/resources/help/user/fullName.html @@ -0,0 +1,5 @@ +
+ Specify your name in more human friendly format, so that people can + see your real name as opposed to your ID. For example, "Jane Doe" + is usually easier for people to understand than IDs like "jd513". +
\ No newline at end of file diff --git a/war/resources/help/view-config/description.html b/war/resources/help/view-config/description.html new file mode 100644 index 0000000000..c034dae068 --- /dev/null +++ b/war/resources/help/view-config/description.html @@ -0,0 +1,5 @@ +
+ This message will be displayed at the view page. + Useful for describing what this view is about, or link to relevant resources. + Can contain HTML tags. +
\ No newline at end of file diff --git a/war/resources/images/.cvsignore b/war/resources/images/.cvsignore new file mode 100644 index 0000000000..085e8baf0c --- /dev/null +++ b/war/resources/images/.cvsignore @@ -0,0 +1 @@ +Thumbs.db diff --git a/war/resources/images/16x16/.cvsignore b/war/resources/images/16x16/.cvsignore new file mode 100644 index 0000000000..085e8baf0c --- /dev/null +++ b/war/resources/images/16x16/.cvsignore @@ -0,0 +1 @@ +Thumbs.db diff --git a/war/resources/images/16x16/Thumbs.db b/war/resources/images/16x16/Thumbs.db new file mode 100644 index 0000000000..5c035b1fdf Binary files /dev/null and b/war/resources/images/16x16/Thumbs.db differ diff --git a/war/resources/images/16x16/blue.gif b/war/resources/images/16x16/blue.gif new file mode 100644 index 0000000000..a05bc967f2 Binary files /dev/null and b/war/resources/images/16x16/blue.gif differ diff --git a/war/resources/images/16x16/blue_anime.gif b/war/resources/images/16x16/blue_anime.gif new file mode 100644 index 0000000000..f810db9fbc Binary files /dev/null and b/war/resources/images/16x16/blue_anime.gif differ diff --git a/war/resources/images/16x16/document_add.gif b/war/resources/images/16x16/document_add.gif new file mode 100644 index 0000000000..c983b5460c Binary files /dev/null and b/war/resources/images/16x16/document_add.gif differ diff --git a/war/resources/images/16x16/document_delete.gif b/war/resources/images/16x16/document_delete.gif new file mode 100644 index 0000000000..9effc22be6 Binary files /dev/null and b/war/resources/images/16x16/document_delete.gif differ diff --git a/war/resources/images/16x16/document_edit.gif b/war/resources/images/16x16/document_edit.gif new file mode 100644 index 0000000000..0b648b0d0c Binary files /dev/null and b/war/resources/images/16x16/document_edit.gif differ diff --git a/war/resources/images/16x16/edit-delete.gif b/war/resources/images/16x16/edit-delete.gif new file mode 100644 index 0000000000..1efbdc5b72 Binary files /dev/null and b/war/resources/images/16x16/edit-delete.gif differ diff --git a/war/resources/images/16x16/fingerprint.gif b/war/resources/images/16x16/fingerprint.gif new file mode 100644 index 0000000000..84fc2d4530 Binary files /dev/null and b/war/resources/images/16x16/fingerprint.gif differ diff --git a/war/resources/images/16x16/folder-open.gif b/war/resources/images/16x16/folder-open.gif new file mode 100644 index 0000000000..421a0e2659 Binary files /dev/null and b/war/resources/images/16x16/folder-open.gif differ diff --git a/war/resources/images/16x16/folder.gif b/war/resources/images/16x16/folder.gif new file mode 100644 index 0000000000..eb52127faa Binary files /dev/null and b/war/resources/images/16x16/folder.gif differ diff --git a/war/resources/images/16x16/go-next.gif b/war/resources/images/16x16/go-next.gif new file mode 100644 index 0000000000..32ecf1ff66 Binary files /dev/null and b/war/resources/images/16x16/go-next.gif differ diff --git a/war/resources/images/16x16/grey.gif b/war/resources/images/16x16/grey.gif new file mode 100644 index 0000000000..c899273355 Binary files /dev/null and b/war/resources/images/16x16/grey.gif differ diff --git a/war/resources/images/16x16/grey_anime.gif b/war/resources/images/16x16/grey_anime.gif new file mode 100644 index 0000000000..93dfa6cab4 Binary files /dev/null and b/war/resources/images/16x16/grey_anime.gif differ diff --git a/war/resources/images/16x16/help.gif b/war/resources/images/16x16/help.gif new file mode 100644 index 0000000000..dbf1c34fed Binary files /dev/null and b/war/resources/images/16x16/help.gif differ diff --git a/war/resources/images/16x16/notepad.gif b/war/resources/images/16x16/notepad.gif new file mode 100644 index 0000000000..ef0d988b28 Binary files /dev/null and b/war/resources/images/16x16/notepad.gif differ diff --git a/war/resources/images/16x16/red.gif b/war/resources/images/16x16/red.gif new file mode 100644 index 0000000000..651ad7bee5 Binary files /dev/null and b/war/resources/images/16x16/red.gif differ diff --git a/war/resources/images/16x16/red_anime.gif b/war/resources/images/16x16/red_anime.gif new file mode 100644 index 0000000000..98f7f902bf Binary files /dev/null and b/war/resources/images/16x16/red_anime.gif differ diff --git a/war/resources/images/16x16/search.gif b/war/resources/images/16x16/search.gif new file mode 100644 index 0000000000..62ecc8cff7 Binary files /dev/null and b/war/resources/images/16x16/search.gif differ diff --git a/war/resources/images/16x16/stop.gif b/war/resources/images/16x16/stop.gif new file mode 100644 index 0000000000..4dbd757ef1 Binary files /dev/null and b/war/resources/images/16x16/stop.gif differ diff --git a/war/resources/images/16x16/text.gif b/war/resources/images/16x16/text.gif new file mode 100644 index 0000000000..692456cfbc Binary files /dev/null and b/war/resources/images/16x16/text.gif differ diff --git a/war/resources/images/16x16/yellow.gif b/war/resources/images/16x16/yellow.gif new file mode 100644 index 0000000000..ca6aec6562 Binary files /dev/null and b/war/resources/images/16x16/yellow.gif differ diff --git a/war/resources/images/16x16/yellow_anime.gif b/war/resources/images/16x16/yellow_anime.gif new file mode 100644 index 0000000000..1562b27689 Binary files /dev/null and b/war/resources/images/16x16/yellow_anime.gif differ diff --git a/war/resources/images/24x24/.cvsignore b/war/resources/images/24x24/.cvsignore new file mode 100644 index 0000000000..085e8baf0c --- /dev/null +++ b/war/resources/images/24x24/.cvsignore @@ -0,0 +1 @@ +Thumbs.db diff --git a/war/resources/images/24x24/Thumbs.db b/war/resources/images/24x24/Thumbs.db new file mode 100644 index 0000000000..60c2d32147 Binary files /dev/null and b/war/resources/images/24x24/Thumbs.db differ diff --git a/war/resources/images/24x24/blue.gif b/war/resources/images/24x24/blue.gif new file mode 100644 index 0000000000..43f33a090c Binary files /dev/null and b/war/resources/images/24x24/blue.gif differ diff --git a/war/resources/images/24x24/blue_anime.gif b/war/resources/images/24x24/blue_anime.gif new file mode 100644 index 0000000000..67def04dc4 Binary files /dev/null and b/war/resources/images/24x24/blue_anime.gif differ diff --git a/war/resources/images/24x24/clipboard.gif b/war/resources/images/24x24/clipboard.gif new file mode 100644 index 0000000000..06befd05cf Binary files /dev/null and b/war/resources/images/24x24/clipboard.gif differ diff --git a/war/resources/images/24x24/clock.gif b/war/resources/images/24x24/clock.gif new file mode 100644 index 0000000000..37f33a5f2a Binary files /dev/null and b/war/resources/images/24x24/clock.gif differ diff --git a/war/resources/images/24x24/delete-document.gif b/war/resources/images/24x24/delete-document.gif new file mode 100644 index 0000000000..bae0d28d5c Binary files /dev/null and b/war/resources/images/24x24/delete-document.gif differ diff --git a/war/resources/images/24x24/document.gif b/war/resources/images/24x24/document.gif new file mode 100644 index 0000000000..6d0ec0bef6 Binary files /dev/null and b/war/resources/images/24x24/document.gif differ diff --git a/war/resources/images/24x24/edit-delete.gif b/war/resources/images/24x24/edit-delete.gif new file mode 100644 index 0000000000..dec87ec92a Binary files /dev/null and b/war/resources/images/24x24/edit-delete.gif differ diff --git a/war/resources/images/24x24/fingerprint.gif b/war/resources/images/24x24/fingerprint.gif new file mode 100644 index 0000000000..23b82a82fb Binary files /dev/null and b/war/resources/images/24x24/fingerprint.gif differ diff --git a/war/resources/images/24x24/folder.gif b/war/resources/images/24x24/folder.gif new file mode 100644 index 0000000000..b76b981682 Binary files /dev/null and b/war/resources/images/24x24/folder.gif differ diff --git a/war/resources/images/24x24/gear.gif b/war/resources/images/24x24/gear.gif new file mode 100644 index 0000000000..4823cf5582 Binary files /dev/null and b/war/resources/images/24x24/gear.gif differ diff --git a/war/resources/images/24x24/gear2.gif b/war/resources/images/24x24/gear2.gif new file mode 100644 index 0000000000..2a0d2cbe64 Binary files /dev/null and b/war/resources/images/24x24/gear2.gif differ diff --git a/war/resources/images/24x24/graph.gif b/war/resources/images/24x24/graph.gif new file mode 100644 index 0000000000..3369a864bd Binary files /dev/null and b/war/resources/images/24x24/graph.gif differ diff --git a/war/resources/images/24x24/grey.gif b/war/resources/images/24x24/grey.gif new file mode 100644 index 0000000000..f3636f9920 Binary files /dev/null and b/war/resources/images/24x24/grey.gif differ diff --git a/war/resources/images/24x24/grey_anime.gif b/war/resources/images/24x24/grey_anime.gif new file mode 100644 index 0000000000..9bf23489ba Binary files /dev/null and b/war/resources/images/24x24/grey_anime.gif differ diff --git a/war/resources/images/24x24/help.gif b/war/resources/images/24x24/help.gif new file mode 100644 index 0000000000..55a18c322b Binary files /dev/null and b/war/resources/images/24x24/help.gif differ diff --git a/war/resources/images/24x24/new-document.gif b/war/resources/images/24x24/new-document.gif new file mode 100644 index 0000000000..8b0141a03c Binary files /dev/null and b/war/resources/images/24x24/new-document.gif differ diff --git a/war/resources/images/24x24/new-package.gif b/war/resources/images/24x24/new-package.gif new file mode 100644 index 0000000000..dd06c7b2e9 Binary files /dev/null and b/war/resources/images/24x24/new-package.gif differ diff --git a/war/resources/images/24x24/next.gif b/war/resources/images/24x24/next.gif new file mode 100644 index 0000000000..f3633605e8 Binary files /dev/null and b/war/resources/images/24x24/next.gif differ diff --git a/war/resources/images/24x24/notepad.gif b/war/resources/images/24x24/notepad.gif new file mode 100644 index 0000000000..39e6924433 Binary files /dev/null and b/war/resources/images/24x24/notepad.gif differ diff --git a/war/resources/images/24x24/package.gif b/war/resources/images/24x24/package.gif new file mode 100644 index 0000000000..54e3402b07 Binary files /dev/null and b/war/resources/images/24x24/package.gif differ diff --git a/war/resources/images/24x24/previous.gif b/war/resources/images/24x24/previous.gif new file mode 100644 index 0000000000..b68baebb90 Binary files /dev/null and b/war/resources/images/24x24/previous.gif differ diff --git a/war/resources/images/24x24/red.gif b/war/resources/images/24x24/red.gif new file mode 100644 index 0000000000..9bd7122f81 Binary files /dev/null and b/war/resources/images/24x24/red.gif differ diff --git a/war/resources/images/24x24/red_anime.gif b/war/resources/images/24x24/red_anime.gif new file mode 100644 index 0000000000..04d6e3c082 Binary files /dev/null and b/war/resources/images/24x24/red_anime.gif differ diff --git a/war/resources/images/24x24/refresh.gif b/war/resources/images/24x24/refresh.gif new file mode 100644 index 0000000000..0224501a6d Binary files /dev/null and b/war/resources/images/24x24/refresh.gif differ diff --git a/war/resources/images/24x24/save.gif b/war/resources/images/24x24/save.gif new file mode 100644 index 0000000000..e2a0574bf7 Binary files /dev/null and b/war/resources/images/24x24/save.gif differ diff --git a/war/resources/images/24x24/search.gif b/war/resources/images/24x24/search.gif new file mode 100644 index 0000000000..756dbf1eb9 Binary files /dev/null and b/war/resources/images/24x24/search.gif differ diff --git a/war/resources/images/24x24/setting.gif b/war/resources/images/24x24/setting.gif new file mode 100644 index 0000000000..cbb526cd25 Binary files /dev/null and b/war/resources/images/24x24/setting.gif differ diff --git a/war/resources/images/24x24/terminal.gif b/war/resources/images/24x24/terminal.gif new file mode 100644 index 0000000000..7217511aa8 Binary files /dev/null and b/war/resources/images/24x24/terminal.gif differ diff --git a/war/resources/images/24x24/up.gif b/war/resources/images/24x24/up.gif new file mode 100644 index 0000000000..d35a00a8cb Binary files /dev/null and b/war/resources/images/24x24/up.gif differ diff --git a/war/resources/images/24x24/user.gif b/war/resources/images/24x24/user.gif new file mode 100644 index 0000000000..79b936dea9 Binary files /dev/null and b/war/resources/images/24x24/user.gif differ diff --git a/war/resources/images/24x24/yellow.gif b/war/resources/images/24x24/yellow.gif new file mode 100644 index 0000000000..f9198240b1 Binary files /dev/null and b/war/resources/images/24x24/yellow.gif differ diff --git a/war/resources/images/24x24/yellow_anime.gif b/war/resources/images/24x24/yellow_anime.gif new file mode 100644 index 0000000000..d8566764bd Binary files /dev/null and b/war/resources/images/24x24/yellow_anime.gif differ diff --git a/war/resources/images/32x32/.cvsignore b/war/resources/images/32x32/.cvsignore new file mode 100644 index 0000000000..085e8baf0c --- /dev/null +++ b/war/resources/images/32x32/.cvsignore @@ -0,0 +1 @@ +Thumbs.db diff --git a/war/resources/images/32x32/Thumbs.db b/war/resources/images/32x32/Thumbs.db new file mode 100644 index 0000000000..2d8e4f08d0 Binary files /dev/null and b/war/resources/images/32x32/Thumbs.db differ diff --git a/war/resources/images/32x32/blue.gif b/war/resources/images/32x32/blue.gif new file mode 100644 index 0000000000..da432a8356 Binary files /dev/null and b/war/resources/images/32x32/blue.gif differ diff --git a/war/resources/images/32x32/blue_anime.gif b/war/resources/images/32x32/blue_anime.gif new file mode 100644 index 0000000000..6c4a29c442 Binary files /dev/null and b/war/resources/images/32x32/blue_anime.gif differ diff --git a/war/resources/images/32x32/graph.gif b/war/resources/images/32x32/graph.gif new file mode 100644 index 0000000000..0733ddb6a9 Binary files /dev/null and b/war/resources/images/32x32/graph.gif differ diff --git a/war/resources/images/32x32/grey.gif b/war/resources/images/32x32/grey.gif new file mode 100644 index 0000000000..ae255d69e6 Binary files /dev/null and b/war/resources/images/32x32/grey.gif differ diff --git a/war/resources/images/32x32/grey_anime.gif b/war/resources/images/32x32/grey_anime.gif new file mode 100644 index 0000000000..3161e6212b Binary files /dev/null and b/war/resources/images/32x32/grey_anime.gif differ diff --git a/war/resources/images/32x32/red.gif b/war/resources/images/32x32/red.gif new file mode 100644 index 0000000000..1d99b75f82 Binary files /dev/null and b/war/resources/images/32x32/red.gif differ diff --git a/war/resources/images/32x32/red_anime.gif b/war/resources/images/32x32/red_anime.gif new file mode 100644 index 0000000000..84847eaacb Binary files /dev/null and b/war/resources/images/32x32/red_anime.gif differ diff --git a/war/resources/images/32x32/user.gif b/war/resources/images/32x32/user.gif new file mode 100644 index 0000000000..6657280363 Binary files /dev/null and b/war/resources/images/32x32/user.gif differ diff --git a/war/resources/images/32x32/yellow.gif b/war/resources/images/32x32/yellow.gif new file mode 100644 index 0000000000..d6f73803cb Binary files /dev/null and b/war/resources/images/32x32/yellow.gif differ diff --git a/war/resources/images/32x32/yellow_anime.gif b/war/resources/images/32x32/yellow_anime.gif new file mode 100644 index 0000000000..441e59150c Binary files /dev/null and b/war/resources/images/32x32/yellow_anime.gif differ diff --git a/war/resources/images/48x48/.cvsignore b/war/resources/images/48x48/.cvsignore new file mode 100644 index 0000000000..085e8baf0c --- /dev/null +++ b/war/resources/images/48x48/.cvsignore @@ -0,0 +1 @@ +Thumbs.db diff --git a/war/resources/images/48x48/Thumbs.db b/war/resources/images/48x48/Thumbs.db new file mode 100644 index 0000000000..9f092931ad Binary files /dev/null and b/war/resources/images/48x48/Thumbs.db differ diff --git a/war/resources/images/48x48/blue.gif b/war/resources/images/48x48/blue.gif new file mode 100644 index 0000000000..a0bcf4a8a4 Binary files /dev/null and b/war/resources/images/48x48/blue.gif differ diff --git a/war/resources/images/48x48/blue_anime.gif b/war/resources/images/48x48/blue_anime.gif new file mode 100644 index 0000000000..3bd71ecac9 Binary files /dev/null and b/war/resources/images/48x48/blue_anime.gif differ diff --git a/war/resources/images/48x48/clipboard.gif b/war/resources/images/48x48/clipboard.gif new file mode 100644 index 0000000000..d1bc59f73b Binary files /dev/null and b/war/resources/images/48x48/clipboard.gif differ diff --git a/war/resources/images/48x48/computer-x.gif b/war/resources/images/48x48/computer-x.gif new file mode 100644 index 0000000000..fb6805acbc Binary files /dev/null and b/war/resources/images/48x48/computer-x.gif differ diff --git a/war/resources/images/48x48/computer.gif b/war/resources/images/48x48/computer.gif new file mode 100644 index 0000000000..17b8afc983 Binary files /dev/null and b/war/resources/images/48x48/computer.gif differ diff --git a/war/resources/images/48x48/error.gif b/war/resources/images/48x48/error.gif new file mode 100644 index 0000000000..fb5be4db3f Binary files /dev/null and b/war/resources/images/48x48/error.gif differ diff --git a/war/resources/images/48x48/fingerprint.gif b/war/resources/images/48x48/fingerprint.gif new file mode 100644 index 0000000000..bfb9b1f1b1 Binary files /dev/null and b/war/resources/images/48x48/fingerprint.gif differ diff --git a/war/resources/images/48x48/folder.gif b/war/resources/images/48x48/folder.gif new file mode 100644 index 0000000000..da560b8f26 Binary files /dev/null and b/war/resources/images/48x48/folder.gif differ diff --git a/war/resources/images/48x48/gear2.gif b/war/resources/images/48x48/gear2.gif new file mode 100644 index 0000000000..2796f8e6fd Binary files /dev/null and b/war/resources/images/48x48/gear2.gif differ diff --git a/war/resources/images/48x48/graph.gif b/war/resources/images/48x48/graph.gif new file mode 100644 index 0000000000..ede343ee65 Binary files /dev/null and b/war/resources/images/48x48/graph.gif differ diff --git a/war/resources/images/48x48/grey.gif b/war/resources/images/48x48/grey.gif new file mode 100644 index 0000000000..7729d473a5 Binary files /dev/null and b/war/resources/images/48x48/grey.gif differ diff --git a/war/resources/images/48x48/grey_anime.gif b/war/resources/images/48x48/grey_anime.gif new file mode 100644 index 0000000000..1a89cfb55e Binary files /dev/null and b/war/resources/images/48x48/grey_anime.gif differ diff --git a/war/resources/images/48x48/help.gif b/war/resources/images/48x48/help.gif new file mode 100644 index 0000000000..d1e62cd134 Binary files /dev/null and b/war/resources/images/48x48/help.gif differ diff --git a/war/resources/images/48x48/notepad.gif b/war/resources/images/48x48/notepad.gif new file mode 100644 index 0000000000..509f1aa28d Binary files /dev/null and b/war/resources/images/48x48/notepad.gif differ diff --git a/war/resources/images/48x48/package.gif b/war/resources/images/48x48/package.gif new file mode 100644 index 0000000000..c333daa806 Binary files /dev/null and b/war/resources/images/48x48/package.gif differ diff --git a/war/resources/images/48x48/red.gif b/war/resources/images/48x48/red.gif new file mode 100644 index 0000000000..cfe0caff2b Binary files /dev/null and b/war/resources/images/48x48/red.gif differ diff --git a/war/resources/images/48x48/red_anime.gif b/war/resources/images/48x48/red_anime.gif new file mode 100644 index 0000000000..c8641d927b Binary files /dev/null and b/war/resources/images/48x48/red_anime.gif differ diff --git a/war/resources/images/48x48/refresh.gif b/war/resources/images/48x48/refresh.gif new file mode 100644 index 0000000000..34c55d8240 Binary files /dev/null and b/war/resources/images/48x48/refresh.gif differ diff --git a/war/resources/images/48x48/search.gif b/war/resources/images/48x48/search.gif new file mode 100644 index 0000000000..91ea35cfe1 Binary files /dev/null and b/war/resources/images/48x48/search.gif differ diff --git a/war/resources/images/48x48/setting.gif b/war/resources/images/48x48/setting.gif new file mode 100644 index 0000000000..04ad19a93c Binary files /dev/null and b/war/resources/images/48x48/setting.gif differ diff --git a/war/resources/images/48x48/system-log-out.gif b/war/resources/images/48x48/system-log-out.gif new file mode 100644 index 0000000000..ca2ac9c709 Binary files /dev/null and b/war/resources/images/48x48/system-log-out.gif differ diff --git a/war/resources/images/48x48/user.gif b/war/resources/images/48x48/user.gif new file mode 100644 index 0000000000..5e2e7b8498 Binary files /dev/null and b/war/resources/images/48x48/user.gif differ diff --git a/war/resources/images/48x48/yellow.gif b/war/resources/images/48x48/yellow.gif new file mode 100644 index 0000000000..5f471de019 Binary files /dev/null and b/war/resources/images/48x48/yellow.gif differ diff --git a/war/resources/images/48x48/yellow_anime.gif b/war/resources/images/48x48/yellow_anime.gif new file mode 100644 index 0000000000..8882285951 Binary files /dev/null and b/war/resources/images/48x48/yellow_anime.gif differ diff --git a/war/resources/images/TangoProject-License.url b/war/resources/images/TangoProject-License.url new file mode 100644 index 0000000000..80c5fc2b84 --- /dev/null +++ b/war/resources/images/TangoProject-License.url @@ -0,0 +1,2 @@ +[InternetShortcut] +URL=http://creativecommons.org/licenses/by-sa/2.5/ diff --git a/war/resources/images/Thumbs.db b/war/resources/images/Thumbs.db new file mode 100644 index 0000000000..db66238fc1 Binary files /dev/null and b/war/resources/images/Thumbs.db differ diff --git a/war/resources/images/atom-license.txt b/war/resources/images/atom-license.txt new file mode 100644 index 0000000000..fe8848ca1b --- /dev/null +++ b/war/resources/images/atom-license.txt @@ -0,0 +1 @@ +atom.gif is taken from http://www.feedicons.com/ diff --git a/war/resources/images/atom.gif b/war/resources/images/atom.gif new file mode 100644 index 0000000000..b0e4adf1d5 Binary files /dev/null and b/war/resources/images/atom.gif differ diff --git a/war/resources/images/headless.png b/war/resources/images/headless.png new file mode 100644 index 0000000000..475cda60ef Binary files /dev/null and b/war/resources/images/headless.png differ diff --git a/war/resources/images/hudson.png b/war/resources/images/hudson.png new file mode 100644 index 0000000000..3ae2411f15 Binary files /dev/null and b/war/resources/images/hudson.png differ diff --git a/war/resources/images/hudson.xcf b/war/resources/images/hudson.xcf new file mode 100644 index 0000000000..9ecba2c2af Binary files /dev/null and b/war/resources/images/hudson.xcf differ diff --git a/war/resources/images/none.gif b/war/resources/images/none.gif new file mode 100644 index 0000000000..0470a05e22 Binary files /dev/null and b/war/resources/images/none.gif differ diff --git a/war/resources/images/progress-unknown.gif b/war/resources/images/progress-unknown.gif new file mode 100644 index 0000000000..08f91bdb6a Binary files /dev/null and b/war/resources/images/progress-unknown.gif differ diff --git a/war/resources/images/spinner.gif b/war/resources/images/spinner.gif new file mode 100644 index 0000000000..529e72f45a Binary files /dev/null and b/war/resources/images/spinner.gif differ diff --git a/war/resources/images/title.png b/war/resources/images/title.png new file mode 100644 index 0000000000..8345374d6b Binary files /dev/null and b/war/resources/images/title.png differ diff --git a/war/resources/images/title.svg b/war/resources/images/title.svg new file mode 100644 index 0000000000..1dd24bf77a --- /dev/null +++ b/war/resources/images/title.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + Hudson + + diff --git a/war/resources/images/title.xcf b/war/resources/images/title.xcf new file mode 100644 index 0000000000..cd0388f116 Binary files /dev/null and b/war/resources/images/title.xcf differ diff --git a/war/resources/images/topbar.png b/war/resources/images/topbar.png new file mode 100644 index 0000000000..1b52dbd471 Binary files /dev/null and b/war/resources/images/topbar.png differ diff --git a/war/resources/robots.txt b/war/resources/robots.txt new file mode 100644 index 0000000000..41aebe0384 --- /dev/null +++ b/war/resources/robots.txt @@ -0,0 +1,3 @@ +# we don't want robots to click "build" links +User-agent: * +Disallow: / \ No newline at end of file diff --git a/war/resources/scripts/behavior.js b/war/resources/scripts/behavior.js new file mode 100644 index 0000000000..4a483fa1d7 --- /dev/null +++ b/war/resources/scripts/behavior.js @@ -0,0 +1,254 @@ +/* + Behaviour v1.1 by Ben Nolan, June 2005. Based largely on the work + of Simon Willison (see comments by Simon below). + + Description: + + Uses css selectors to apply javascript behaviours to enable + unobtrusive javascript in html documents. + + Usage: + + var myrules = { + 'b.someclass' : function(element){ + element.onclick = function(){ + alert(this.innerHTML); + } + }, + '#someid u' : function(element){ + element.onmouseover = function(){ + this.innerHTML = "BLAH!"; + } + } + }; + + Behaviour.register(myrules); + + // Call Behaviour.apply() to re-apply the rules (if you + // update the dom, etc). + + License: + + This file is entirely BSD licensed. + + More information: + + http://ripcord.co.nz/behaviour/ + +*/ + +var Behaviour = { + list : new Array, + + register : function(sheet){ + Behaviour.list.push(sheet); + }, + + start : function(){ + Behaviour.addLoadEvent(function(){ + Behaviour.apply(); + }); + }, + + apply : function(){ + for (h=0;sheet=Behaviour.list[h];h++){ + for (selector in sheet){ + list = document.getElementsBySelector(selector); + + if (!list){ + continue; + } + + for (i=0;element=list[i];i++){ + sheet[selector](element); + } + } + } + }, + + addLoadEvent : function(func){ + var oldonload = window.onload; + + if (typeof window.onload != 'function') { + window.onload = func; + } else { + window.onload = function() { + oldonload(); + func(); + } + } + } +} + +Behaviour.start(); + +/* + The following code is Copyright (C) Simon Willison 2004. + + document.getElementsBySelector(selector) + - returns an array of element objects from the current document + matching the CSS selector. Selectors can contain element names, + class names and ids and can be nested. For example: + + elements = document.getElementsBySelect('div#main p a.external') + + Will return an array of all 'a' elements with 'external' in their + class attribute that are contained inside 'p' elements that are + contained inside the 'div' element which has id="main" + + New in version 0.4: Support for CSS2 and CSS3 attribute selectors: + See http://www.w3.org/TR/css3-selectors/#attribute-selectors + + Version 0.4 - Simon Willison, March 25th 2003 + -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows + -- Opera 7 fails +*/ + +function getAllChildren(e) { + // Returns all children of element. Workaround required for IE5/Windows. Ugh. + return e.all ? e.all : e.getElementsByTagName('*'); +} + +document.getElementsBySelector = function(selector) { + // Attempt to fail gracefully in lesser browsers + if (!document.getElementsByTagName) { + return new Array(); + } + // Split selector in to tokens + var tokens = selector.split(' '); + var currentContext = new Array(document); + for (var i = 0; i < tokens.length; i++) { + token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');; + if (token.indexOf('#') > -1) { + // Token is an ID selector + var bits = token.split('#'); + var tagName = bits[0]; + var id = bits[1]; + var element = document.getElementById(id); + if (tagName && element.nodeName.toLowerCase() != tagName) { + // tag with that ID not found, return false + return new Array(); + } + // Set currentContext to contain just this element + currentContext = new Array(element); + continue; // Skip to next token + } + if (token.indexOf('.') > -1) { + // Token contains a class selector + var bits = token.split('.'); + var tagName = bits[0]; + var className = bits[1]; + if (!tagName) { + tagName = '*'; + } + // Get elements matching tag, filter them for class selector + var found = new Array; + var foundCount = 0; + for (var h = 0; h < currentContext.length; h++) { + var elements; + if (tagName == '*') { + elements = getAllChildren(currentContext[h]); + } else { + elements = currentContext[h].getElementsByTagName(tagName); + } + for (var j = 0; j < elements.length; j++) { + found[foundCount++] = elements[j]; + } + } + currentContext = new Array; + var currentContextIndex = 0; + for (var k = 0; k < found.length; k++) { + if (found[k].className && found[k].className.match(new RegExp('\\b'+className+'\\b'))) { + currentContext[currentContextIndex++] = found[k]; + } + } + continue; // Skip to next token + } + // Code to deal with attribute selectors + if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) { + var tagName = RegExp.$1; + var attrName = RegExp.$2; + var attrOperator = RegExp.$3; + var attrValue = RegExp.$4; + if (!tagName) { + tagName = '*'; + } + // Grab all of the tagName elements within current context + var found = new Array; + var foundCount = 0; + for (var h = 0; h < currentContext.length; h++) { + var elements; + if (tagName == '*') { + elements = getAllChildren(currentContext[h]); + } else { + elements = currentContext[h].getElementsByTagName(tagName); + } + for (var j = 0; j < elements.length; j++) { + found[foundCount++] = elements[j]; + } + } + currentContext = new Array; + var currentContextIndex = 0; + var checkFunction; // This function will be used to filter the elements + switch (attrOperator) { + case '=': // Equality + checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); }; + break; + case '~': // Match one of space seperated words + checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); }; + break; + case '|': // Match start with value followed by optional hyphen + checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); }; + break; + case '^': // Match starts with value + checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); }; + break; + case '$': // Match ends with value - fails with "Warning" in Opera 7 + checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); }; + break; + case '*': // Match ends with value + checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); }; + break; + default : + // Just test for existence of attribute + checkFunction = function(e) { return e.getAttribute(attrName); }; + } + currentContext = new Array; + var currentContextIndex = 0; + for (var k = 0; k < found.length; k++) { + if (checkFunction(found[k])) { + currentContext[currentContextIndex++] = found[k]; + } + } + // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); + continue; // Skip to next token + } + + if (!currentContext[0]){ + return; + } + + // If we get here, token is JUST an element (not a class or ID selector) + tagName = token; + var found = new Array; + var foundCount = 0; + for (var h = 0; h < currentContext.length; h++) { + var elements = currentContext[h].getElementsByTagName(tagName); + for (var j = 0; j < elements.length; j++) { + found[foundCount++] = elements[j]; + } + } + currentContext = found; + } + return currentContext; +} + +/* That revolting regular expression explained +/^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/ + \---/ \---/\-------------/ \-------/ + | | | | + | | | The value + | | ~,|,^,$,* or = + | Attribute + Tag +*/ diff --git a/war/resources/scripts/combobox-readme.txt b/war/resources/scripts/combobox-readme.txt new file mode 100644 index 0000000000..573869100e --- /dev/null +++ b/war/resources/scripts/combobox-readme.txt @@ -0,0 +1,31 @@ +DHTML Widgets +Copyright Barney Boisvert +bboisvert@gmail.com +Released under the MIT License (below) + +Installation: +Unzip the archive into a web-accessible directory, and point your +browser to index.html. Each widget has javadoc-style documentation +for it's use. + +---------------------------------------------------------------------- +Copyright (c) 2005 Barney Boisvert + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/war/resources/scripts/combobox.js b/war/resources/scripts/combobox.js new file mode 100644 index 0000000000..414daed71b --- /dev/null +++ b/war/resources/scripts/combobox.js @@ -0,0 +1,376 @@ + +if (typeof UTILITIES_VERSION == "undefined" || UTILITIES_VERSION < 0.1) { + alert("A suitable version of the Utilities class is not available"); +} + +COMBOBOX_VERSION = 0.1; + +/* +Sample CSS for the combobox + .comboBoxList { + padding: 0px; + border: 1px solid #999; + background-color: #f7f7ff; + overflow: visible; + } + .comboBoxItem { + margin: 0px; + padding: 2px 5px; + background-color: inherit; + cursor: default; + } + .comboBoxSelectedItem { + background-color: #ddf; + } +*/ + +/** + * I create a new combobox, using the specified text input field and + * population callback. The item list is styled with three CSS + * classes: comboBoxList, comboBoxItem, and comboBoxSelectedItem, which + * are for the containing DIV, the individial item DIVs, and for the + * currently selected item DIV. Note that the selected item has both + * the item and selectedItem classes applied. Sample CSS is available + * in a comment at the top of the implementation file. + * + * The 'config' argument allows passing of additional parameters that + * further govern the behaviour of the combo box. Supported parameters + * are listed here: + * allowMultipleValues - whether the form field should allow multiple + * values to be provided. Each individual value will get it's own + * separate dropdown with, so a field value such as "dog,ca" would + * operate as if the value were just "ca" (i.e. just "ca" would be + * passed to the callback, and a selection choice would only + * replace the "ca"). Defaults to false. + * valueDelimiter - if allowMultipleValues is set to true, this is the + * character used to delimit the values. Defaults to a comma. + * + * @param id The ID of the text field the combobox is based around. + * @param callback The function to call when the typed value changes. + * The function will be passed the current value of the field, and + * must return an array of values to display in the dropdown. + * @param config additional config parameters, as explained above. + * @param config An object containing configuration parameters for the + * instance. + */ +function ComboBox(id, callback, config) { + var self = this; + // instance variables + this.config = config || new Object(); + this.callback = callback; + this.availableItems = new Array(); + this.selectedItemIndex = -1; + this.id = id; + this.field = document.getElementById(id); + if (typeof this.field == "undefined") + alert("You have specified an invalid id for the field you want to turn into a combo box"); + this.dropdown = document.createElement("div"); + this.isDropdownShowing = false; + + // configure the dropdown div + this.dropdown.className = "comboBoxList"; + document.body.appendChild(this.dropdown); + this.dropdown.style.position = 'absolute'; + this.moveDropdown(); + this.hideDropdown(); + + // initialize the field + this.field.comboBox = this; + this.field.oldValue = this.field.value; + this.field.onkeyup = ComboBox.onKeyUp; + this.field.moveCaretToEnd = function() { + if (this.createTextRange) { + var range = this.createTextRange(); + range.collapse(false); + range.select(); + } else if (this.setSelectionRange) { + this.focus(); + var length = this.value.length; + this.setSelectionRange(length, length); + } + } + this.field.form.oldonsubmit = this.field.form.onsubmit; + this.field.onfocus = function() { + this.form.onsubmit = function() { + if (this.oldonsubmit) this.oldonsubmit(); + return ! self.isDropdownShowing; + }; + } + this.field.onblur = function() { + var cb = this.comboBox; + this.hideTimeout = setTimeout(function() { cb.hideDropdown(); }, 100); + this.form.onsubmit = function() { + if (this.oldonsubmit) this.oldonsubmit(); + return true; + }; + } + + // privileged methods + this.getConfigParam = function(name, defVal) { + return self.config[name] || defVal; + } +} + + + +/** + * I am the onKeyDown listener that gets installed on the input field + * that is the core of the ComboBox. I handle action operations. + * + * @param e The event object on Mozilla browsers, null on IE + */ +ComboBox.onKeyDown = function(e) { + if (!e) e = window.event; + var capture = function() { + e.cancelBubble = true; + if (e.stopPropagation) e.stopPropagation(); + } + switch (e.keyCode) { + case 13: // enter + case 9: // tab + this.comboBox.chooseSelection(); + capture(); + return false; + case 27: // escape + this.comboBox.hideDropdown(); + capture(); + case 38: // up arrow + this.comboBox.selectPrevious(); + capture(); + break; + case 40: // down arrow + this.comboBox.selectNext(); + capture(); + break; + } +} + + + +/** + * I am the onKeyUp listener that gets installed on the input field + * that is the core of the ComboBox. I handle value-change operations. + * + * @param e The event object on Mozilla browsers, null on IE + */ +ComboBox.onKeyUp = function(e) { + if (!e) e = window.event; + var capture = function() { + e.cancelBubble = true; + if (e.stopPropagation) e.stopPropagation(); + } + switch (e.keyCode) { + case 38: // up arrow + case 40: // down arrow + this.moveCaretToEnd(); + capture(); + break; + default: + if (this.value != this.oldValue) { + this.comboBox.valueChanged(); + this.oldValue = this.value; + } + capture(); + } +} + + + +/** + * I am called by the onKeyUp listener when the entered value changes, + * and am responsible for invoking the application callback function + * and repopulating the dropdown, if appropriate. + */ +ComboBox.prototype.valueChanged = function() { + var value = this.field.value; + if (this.getConfigParam("allowMultipleValues", false)) { + value = value.split(this.getConfigParam("valueDelimiter", ",")); + value = value[value.length - 1].replace(/^ +/, "").replace(/ +$/, ""); + } + var a = this.callback(value, this); + if (typeof a == "undefined") // to catch null returns + return; + this.setItems(a); +} + + + +/** + * I can be called at any time with a new set of items to display in + * the dropdown. + * + * @param items The array of items that should be used for the dropdown + * values. + */ +ComboBox.prototype.setItems = function(items) { + if (typeof items != "object") { + alert("setItems wasn't passed a valid array: " + typeof a); + return; + } + this.availableItems = items; + this.populateDropdown(); +} + + + +/** + * I am called to repopulate the dropdown. There should never be a + * need to invoke me externally. + */ +ComboBox.prototype.populateDropdown = function() { + if (this.availableItems.length > 0) { + Utilities.removeChildren(this.dropdown); + for (var i = 0; i < this.availableItems.length; i++) { + var item = document.createElement("div"); + item.className = "comboBoxItem"; + item.innerHTML = this.availableItems[i]; + item.id = "item_" + this.availableItems[i]; + item.comboBox = this; + item.comboBoxIndex = i; + item.onmouseover = function() {this.comboBox.select(this.comboBoxIndex);}; + item.onclick = function() {this.comboBox.choose(this.comboBoxIndex);}; + this.dropdown.appendChild(item); + } + this.selectedItemIndex = 0; + this.updateSelection(); + this.showDropdown(); + } else { + this.selectedItemIndex = -1; + this.hideDropdown(); + } +} + + + +/** + * I am called by a mouse listener on the dropdown items to choose a + * specific item straight away. + * + * @param index The index of the item to choose + */ +ComboBox.prototype.choose = function(index) { + if (this.select(index)) + this.chooseSelection(); +} + + + +/** + * I am called by the onKeyUp listener to indicate that the user wants + * to use the current selection as the new value of the field. + */ +ComboBox.prototype.chooseSelection = function() { + var i = this.selectedItemIndex; + var a = this.availableItems; + if (i >= 0 && i < a.length) { + var valueToAdd = a[i].replace(/<[^>]+>/g, ""); + if (this.getConfigParam("allowMultipleValues", false)) { + var currentValue = ""; + var delim = this.getConfigParam("valueDelimiter", ","); + values = this.field.value.split(delim); + for (var j = 0; j < values.length - 1; j++) { + currentValue = Utilities.listAppend(currentValue, values[j], delim); + } + this.field.value = Utilities.listAppend(currentValue, valueToAdd, delim); + } else { + this.field.value = valueToAdd; + } + + this.field.oldValue = this.field.value; + this.field.focus(); + this.field.moveCaretToEnd(); + this.hideDropdown(); + } +} + + + +/** + * I am called by a mouse listener on the dropdown items to select a + * specific item straight away. + * + * @param index The index of the item to select + * @return whether the selection happened (the index was valid) + */ +ComboBox.prototype.select = function(index) { + if (index < 0 || index >= this.availableItems.length) + return false; + this.selectedItemIndex = index; + this.updateSelection(); + return true; +} + + + +/** + * I am called by the onKeyUp listener to indicate that the user wants + * to select the next option in the dropdown. + */ +ComboBox.prototype.selectNext = function() { + if (this.selectedItemIndex >= this.availableItems.length - 1) + return false; + this.selectedItemIndex++; + this.updateSelection(); + return true; +} + + + +/** + * I am called by the onKeyUp listener to indicate that the user wants + * to select the previous option in the dropdown. + */ +ComboBox.prototype.selectPrevious = function() { + if (this.selectedItemIndex <= 0) + return false; + this.selectedItemIndex--; + this.updateSelection(); +} + + + +/** + * I show the dropdown DIV. + */ +ComboBox.prototype.showDropdown = function() { + this.moveDropdown(); + clearTimeout(this.field.hideTimeout); + this.dropdown.style.display = 'block'; + this.field.onkeydown = ComboBox.onKeyDown; + this.isDropdownShowing = true; +} + + + +/** + * I hide the dropdown DIV. + */ +ComboBox.prototype.hideDropdown = function() { + var self = this; + setTimeout(function() {self.isDropdownShowing = false;}, 100); + this.field.onkeydown = null; + this.dropdown.style.display = 'none'; +} + +ComboBox.prototype.moveDropdown = function() { + var offsets = Utilities.getOffsets(this.field); + this.dropdown.style.top = offsets.y + (this.field.offsetHeight ? this.field.offsetHeight : 22) + "px"; + this.dropdown.style.left = offsets.x + "px"; + this.dropdown.style.width = (this.field.offsetWidth ? this.field.offsetWidth : 100) + "px" +} + + + +/** + * I update the dropdown so that the display reflects the internally + * selected item, + */ +ComboBox.prototype.updateSelection = function() { + for (var i = 0; i < this.dropdown.childNodes.length; i++) { + if (i == this.selectedItemIndex) { + this.dropdown.childNodes[i].className += " comboBoxSelectedItem"; + } else { + this.dropdown.childNodes[i].className = this.dropdown.childNodes[i].className.replace(/ *comboBoxSelectedItem */g, ""); + } + } +} \ No newline at end of file diff --git a/war/resources/scripts/hudson-behavior.js b/war/resources/scripts/hudson-behavior.js new file mode 100644 index 0000000000..7e73a2fcb0 --- /dev/null +++ b/war/resources/scripts/hudson-behavior.js @@ -0,0 +1,153 @@ +// Form check code +//======================================================== +var Form = { + // pending requests + queue : [], + + inProgress : false, + + delayedCheck : function(checkUrl,targetElement) { + this.queue.push({url:checkUrl,target:targetElement}); + this.schedule(); + }, + + schedule : function() { + if(this.inProgress) return; + if(this.queue.length==0) return; + + next = this.queue.shift(); + this.inProgress = true; + + new Ajax.Request(next.url, { + method : 'get', + onComplete : function(x) { + next.target.innerHTML = x.responseText; + Form.inProgress = false; + Form.schedule(); + } + } + ); + } +} + +function findFollowingTR(input,className) { + // identify the parent TR + var tr = input; + while(tr.tagName!="TR") + tr = tr.parentNode; + + // then next TR that matches the CSS + do { + tr = tr.nextSibling; + } while(tr.tagName!="TR" || tr.className!=className); + + return tr; +} + + + + +// Behavior rules +//======================================================== + +var hudsonRules = { + ".advancedButton" : function(e) { + e.onclick = function() { + var link = this.parentNode; + link.style.display = "none"; // hide the button + + var container = link.nextSibling.firstChild; // TABLE -> TBODY + + var tr = link; + while(tr.tagName!="TR") + tr = tr.parentNode; + + // move the contents of the advanced portion into the main table + while(container.lastChild!=null) { + tr.parentNode.insertBefore(container.lastChild,tr.nextSibling); + } + } + }, + + ".pseudoLink" : function(e) { + e.onmouseover = function() { + this.style.textDecoration="underline"; + } + e.onmouseout = function() { + this.style.textDecoration="none"; + } + }, + + // form fields that are validated via AJAX call to the server + // elements with this class should have two attributes 'checkUrl' that evaluates to the server URL. + ".validated" : function(e) { + e.targetElement = findFollowingTR(e,"validation-error-area").firstChild.nextSibling; + e.targetUrl = function() {return eval(this.getAttribute("checkUrl"));}; + + Form.delayedCheck(e.targetUrl(),e.targetElement); + + e.onchange = function() { + new Ajax.Request(this.targetUrl(), { + method : 'get', + onComplete : function(x) {e.targetElement.innerHTML = x.responseText;} + } + ); + } + }, + + // validate form values to be a number + "input.number" : function(e) { + e.targetElement = findFollowingTR(e,"validation-error-area").firstChild.nextSibling; + e.onchange = function() { + if(this.value.match(/^\d+$/)) { + this.targetElement.innerHTML=""; + } else { + this.targetElement.innerHTML="
Not a number
"; + } + } + }, + + ".help-button" : function(e) { + e.onclick = function() { + tr = findFollowingTR(this,"help-area"); + div = tr.firstChild.nextSibling.firstChild; + + if(div.style.display!="block") { + div.style.display="block"; + // make it visible + new Ajax.Request( + this.getAttribute("helpURL"), + { + method : 'get', + onComplete : function(x) { + div.innerHTML = x.responseText; + } + } + ); + } else { + div.style.display = "none"; + } + + return false; + } + } +}; + +Behaviour.register(hudsonRules); + + +// used by editableDescription.jelly to replace the description field with a form +function replaceDescription() { + var d = document.getElementById("description"); + d.firstChild.nextSibling.innerHTML = "
loading...
"; + new Ajax.Request( + "./descriptionForm", + { + method : 'get', + onComplete : function(x) { + d.innerHTML = x.responseText; + } + } + ); + return false; +} \ No newline at end of file diff --git a/war/resources/scripts/prototype.js b/war/resources/scripts/prototype.js new file mode 100644 index 0000000000..d6c1bd9e60 --- /dev/null +++ b/war/resources/scripts/prototype.js @@ -0,0 +1,1790 @@ +/* Prototype JavaScript framework, version 1.4.0 + * (c) 2005 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.4.0', + ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', + + emptyFunction: function() {}, + K: function(x) {return x} +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +Object.extend = function(destination, source) { + for (property in source) { + destination[property] = source[property]; + } + return destination; +} + +Object.inspect = function(object) { + try { + if (object == undefined) return 'undefined'; + if (object == null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } +} + +Function.prototype.bind = function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this; + return function(event) { + return __method.call(object, event || window.event); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); + +var Try = { + these: function() { + var returnValue; + + for (var i = 0; i < arguments.length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) {} + } + + return returnValue; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(); + } finally { + this.currentlyExecuting = false; + } + } + } +} + +/*--------------------------------------------------------------------------*/ + +function $() { + var elements = new Array(); + + for (var i = 0; i < arguments.length; i++) { + var element = arguments[i]; + if (typeof element == 'string') + element = document.getElementById(element); + + if (arguments.length == 1) + return element; + + elements.push(element); + } + + return elements; +} +Object.extend(String.prototype, { + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(eval); + }, + + escapeHTML: function() { + var div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; + }, + + toQueryParams: function() { + var pairs = this.match(/^\??(.*)$/)[1].split('&'); + return pairs.inject({}, function(params, pairString) { + var pair = pairString.split('='); + params[pair[0]] = pair[1]; + return params; + }); + }, + + toArray: function() { + return this.split(''); + }, + + camelize: function() { + var oStringList = this.split('-'); + if (oStringList.length == 1) return oStringList[0]; + + var camelizedString = this.indexOf('-') == 0 + ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) + : oStringList[0]; + + for (var i = 1, len = oStringList.length; i < len; i++) { + var s = oStringList[i]; + camelizedString += s.charAt(0).toUpperCase() + s.substring(1); + } + + return camelizedString; + }, + + inspect: function() { + return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'"; + }, + + trim : function() { + var temp = this; + var obj = /^(\s*)([\W\w]*)(\b\s*$)/; + if (obj.test(temp)) { temp = temp.replace(obj, '$2'); } + obj = / /g; + while (temp.match(obj)) { temp = temp.replace(obj, " "); } + return temp; + } +}); + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var $break = new Object(); +var $continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != $continue) throw e; + } + }); + } catch (e) { + if (e != $break) throw e; + } + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + result = result && !!(iterator || Prototype.K)(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = true; + this.each(function(value, index) { + if (result = !!(iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function (iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.collect(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value >= (result || value)) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value <= (result || value)) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.collect(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.collect(Prototype.K); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (typeof args.last() == 'function') + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + iterator(value = collections.pluck(index)); + return value; + }); + }, + + inspect: function() { + return '#'; + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0; i < iterable.length; i++) + results.push(iterable[i]); + return results; + } +} + +Object.extend(Array.prototype, Enumerable); + +Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0; i < this.length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != undefined || value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0; i < this.length; i++) + if (this[i] == object) return i; + return -1; + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + shift: function() { + var result = this[0]; + for (var i = 0; i < this.length - 1; i++) + this[i] = this[i + 1]; + this.length--; + return result; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); +var Hash = { + _each: function(iterator) { + for (key in this) { + var value = this[key]; + if (typeof value == 'function') continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject($H(this), function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + return pair.map(encodeURIComponent).join('='); + }).join('&'); + }, + + inspect: function() { + return '#'; + } +} + +function $H(object) { + var hash = Object.extend({}, object || {}); + Object.extend(hash, Enumerable); + Object.extend(hash, Hash); + return hash; +} +ObjectRange = Class.create(); +Object.extend(ObjectRange.prototype, Enumerable); +Object.extend(ObjectRange.prototype, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + do { + iterator(value); + value = value.succ(); + } while (this.include(value)); + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +} + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')}, + function() {return new XMLHttpRequest()} + ) || false; + }, + + activeRequestCount: 0 +} + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responderToAdd) { + if (!this.include(responderToAdd)) + this.responders.push(responderToAdd); + }, + + unregister: function(responderToRemove) { + this.responders = this.responders.without(responderToRemove); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (responder[callback] && typeof responder[callback] == 'function') { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) {} + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { + Ajax.activeRequestCount++; + }, + + onComplete: function() { + Ajax.activeRequestCount--; + } +}); + +Ajax.Base = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + parameters: '' + } + Object.extend(this.options, options || {}); + }, + + responseIsSuccess: function() { + return this.transport.status == undefined + || this.transport.status == 0 + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + responseIsFailure: function() { + return !this.responseIsSuccess(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + var parameters = this.options.parameters || ''; + if (parameters.length > 0) parameters += '&_='; + + try { + this.url = url; + if (this.options.method == 'get' && parameters.length > 0) + this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; + + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.options.method, this.url, + this.options.asynchronous); + + if (this.options.asynchronous) { + this.transport.onreadystatechange = this.onStateChange.bind(this); + setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); + } + + this.setRequestHeaders(); + + var body = this.options.postBody ? this.options.postBody : parameters; + this.transport.send(this.options.method == 'post' ? body : null); + + } catch (e) { + this.dispatchException(e); + } + }, + + setRequestHeaders: function() { + var requestHeaders = + ['X-Requested-With', 'XMLHttpRequest', + 'X-Prototype-Version', Prototype.Version]; + + if (this.options.method == 'post') { + requestHeaders.push('Content-type', + 'application/x-www-form-urlencoded'); + + /* Force "Connection: close" for Mozilla browsers to work around + * a bug where XMLHttpReqeuest sends an incorrect Content-length + * header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType) + requestHeaders.push('Connection', 'close'); + } + + if (this.options.requestHeaders) + requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + + for (var i = 0; i < requestHeaders.length; i += 2) + this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState != 1) + this.respondToReadyState(this.transport.readyState); + }, + + header: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) {} + }, + + evalJSON: function() { + try { + return eval(this.header('X-JSON')); + } catch (e) {} + }, + + evalResponse: function() { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } + }, + + respondToReadyState: function(readyState) { + var event = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (event == 'Complete') { + try { + (this.options['on' + this.transport.status] + || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.header('Content-type') || '').match(/^text\/javascript/i)) + this.evalResponse(); + } + + try { + (this.options['on' + event] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + event, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ + if (event == 'Complete') + this.transport.onreadystatechange = Prototype.emptyFunction; + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Updater = Class.create(); + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.containers = { + success: container.success ? $(container.success) : $(container), + failure: container.failure ? $(container.failure) : + (container.success ? null : $(container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, object) { + this.updateContent(); + onComplete(transport, object); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.responseIsSuccess() ? + this.containers.success : this.containers.failure; + var response = this.transport.responseText; + + if (!this.options.evalScripts) + response = response.stripScripts(); + + if (receiver) { + if (this.options.insertion) { + new this.options.insertion(receiver, response); + } else { + Element.update(receiver, response); + } + } + + if (this.responseIsSuccess()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = {}; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +document.getElementsByClassName = function(className, parentElement) { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + return $A(children).inject([], function(elements, child) { + if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + elements.push(child); + return elements; + }); +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) { + var Element = new Object(); +} + +Object.extend(Element, { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + Element[Element.visible(element) ? 'hide' : 'show'](element); + } + }, + + hide: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = 'none'; + } + }, + + show: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = ''; + } + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + }, + + update: function(element, html) { + $(element).innerHTML = html.stripScripts(); + setTimeout(function() {html.evalScripts()}, 10); + }, + + getHeight: function(element) { + element = $(element); + return element.offsetHeight; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).include(className); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).add(className); + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).remove(className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + for (var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + Element.remove(node); + } + }, + + empty: function(element) { + return $(element).innerHTML.match(/^\s*$/); + }, + + scrollTo: function(element) { + element = $(element); + var x = element.x ? element.x : element.offsetLeft, + y = element.y ? element.y : element.offsetTop; + window.scrollTo(x, y); + }, + + getStyle: function(element, style) { + element = $(element); + var value = element.style[style.camelize()]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css.getPropertyValue(style) : null; + } else if (element.currentStyle) { + value = element.currentStyle[style.camelize()]; + } + } + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + + return value == 'auto' ? null : value; + }, + + setStyle: function(element, style) { + element = $(element); + for (name in style) + element.style[name.camelize()] = style[name]; + }, + + getDimensions: function(element) { + element = $(element); + if (Element.getStyle(element, 'display') != 'none') + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = ''; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = 'none'; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return; + element._overflow = element.style.overflow; + if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element.style.overflow = 'hidden'; + }, + + undoClipping: function(element) { + element = $(element); + if (element._overflow) return; + element.style.overflow = element._overflow; + element._overflow = undefined; + } +}); + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content.stripScripts(); + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + if (this.element.tagName.toLowerCase() == 'tbody') { + this.insertContent(this.contentFromAnonymousTable()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + + setTimeout(function() {content.evalScripts()}, 10); + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '' + this.content + '
'; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse(false).each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set(this.toArray().concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set(this.select(function(className) { + return className != classNameToRemove; + }).join(' ')); + }, + + toString: function() { + return this.toArray().join(' '); + } +} + +Object.extend(Element.ClassNames.prototype, Enumerable); +var Field = { + clear: function() { + for (var i = 0; i < arguments.length; i++) + $(arguments[i]).value = ''; + }, + + focus: function(element) { + $(element).focus(); + }, + + present: function() { + for (var i = 0; i < arguments.length; i++) + if ($(arguments[i]).value == '') return false; + return true; + }, + + select: function(element) { + $(element).select(); + }, + + activate: function(element) { + element = $(element); + element.focus(); + if (element.select) + element.select(); + } +} + +/*--------------------------------------------------------------------------*/ + +var Form = { + serialize: function(form) { + var elements = Form.getElements($(form)); + var queryComponents = new Array(); + + for (var i = 0; i < elements.length; i++) { + var queryComponent = Form.Element.serialize(elements[i]); + if (queryComponent) + queryComponents.push(queryComponent); + } + + return queryComponents.join('&'); + }, + + getElements: function(form) { + form = $(form); + var elements = new Array(); + + for (tagName in Form.Element.Serializers) { + var tagElements = form.getElementsByTagName(tagName); + for (var j = 0; j < tagElements.length; j++) + elements.push(tagElements[j]); + } + return elements; + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) + return inputs; + + var matchingInputs = new Array(); + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || + (name && input.name != name)) + continue; + matchingInputs.push(input); + } + + return matchingInputs; + }, + + disable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.blur(); + element.disabled = 'true'; + } + }, + + enable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.disabled = ''; + } + }, + + findFirstElement: function(form) { + return Form.getElements(form).find(function(element) { + return element.type != 'hidden' && !element.disabled && + ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + Field.activate(Form.findFirstElement(form)); + }, + + reset: function(form) { + $(form).reset(); + } +} + +Form.Element = { + serialize: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) { + var key = encodeURIComponent(parameter[0]); + if (key.length == 0) return; + + if (parameter[1].constructor != Array) + parameter[1] = [parameter[1]]; + + return parameter[1].map(function(value) { + return key + '=' + encodeURIComponent(value); + }).join('&'); + } + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return parameter[1]; + } +} + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'submit': + case 'hidden': + case 'password': + case 'text': + return Form.Element.Serializers.textarea(element); + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + } + return false; + }, + + inputSelector: function(element) { + if (element.checked) + return [element.name, element.value]; + }, + + textarea: function(element) { + return [element.name, element.value]; + }, + + select: function(element) { + return Form.Element.Serializers[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var value = '', opt, index = element.selectedIndex; + if (index >= 0) { + opt = element.options[index]; + value = opt.value; + if (!value && !('value' in opt)) + value = opt.text; + } + return [element.name, value]; + }, + + selectMany: function(element) { + var value = new Array(); + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) { + var optValue = opt.value; + if (!optValue && !('value' in opt)) + optValue = opt.text; + value.push(optValue); + } + } + return [element.name, value]; + } +} + +/*--------------------------------------------------------------------------*/ + +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + var elements = Form.getElements(this.element); + for (var i = 0; i < elements.length; i++) + this.registerCallback(elements[i]); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + case 'password': + case 'text': + case 'textarea': + case 'select-one': + case 'select-multiple': + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0; i < Event.observers.length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + this._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + element.detachEvent('on' + name, observer); + } + } +}); + +/* prevent memory leaks in IE */ +Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: function(element) { + if (element.offsetParent) return element.offsetParent; + if (element == document.body) return element; + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return element; + + return document.body; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + clone: function(source, target) { + source = $(source); + target = $(target); + target.style.position = 'absolute'; + var offsets = this.cumulativeOffset(source); + target.style.top = offsets[1] + 'px'; + target.style.left = offsets[0] + 'px'; + target.style.width = source.offsetWidth + 'px'; + target.style.height = source.offsetHeight + 'px'; + }, + + page: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent==document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || {}) + + // find page position of source + source = $(source); + var p = Position.page(source); + + // find coordinate system to use + target = $(target); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px';; + element.style.left = left + 'px';; + element.style.width = width + 'px';; + element.style.height = height + 'px';; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return [valueL, valueT]; + } +} \ No newline at end of file diff --git a/war/resources/scripts/sortable.js b/war/resources/scripts/sortable.js new file mode 100644 index 0000000000..70858e84fd --- /dev/null +++ b/war/resources/scripts/sortable.js @@ -0,0 +1,245 @@ +/* +The MIT Licence, for code from kryogenix.org + +Code downloaded from the Browser Experiments section +of kryogenix.org is licenced under the so-called MIT +licence. The licence is below. + +Copyright (c) 1997-date Stuart Langridge + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. +*/ +/* +Usage +===== + +Add the "sortable" CSS class to a table to make it sortable. +The first column must be always table header, and the rest must be table data. +(the script seems to support rows to be fixed at the bottom, but haven't figured out how to use it.) + +If the table data is sorted to begin with, you can add 'initialSortDir="up|down"' to the +corresponding column in the header row to display the direction icon from the beginning. +This is recommended to provide a visual cue that the table can be sorted. + +The script guesses the table data, and try to use the right sorting algorithm. +But you can override this behavior by having 'data="..."' attribute on each row, +in which case the sort will be done on that field. +*/ +addEvent(window, "load", sortables_init); + +function sortables_init() { + document.getElementsByClassName("sortable")._each(ts_makeSortable) +} + +function ts_makeSortable(table) { + var firstRow; + if (table.rows && table.rows.length > 0) { + firstRow = table.rows[0]; + } + if (!firstRow) return; + + // We have a first row: assume it's the header, and make its contents clickable links + for (var i=0;i'+initialSortDir.text+''; + + if(initialSortDir!=arrowTable.none) + cell.firstChild.lastChild.sortdir = initialSortDir; + } +} + +function ts_getInnerText(el) { + if (typeof el == "string") return el; + if (typeof el == "undefined") { return el }; + if (el.innerText) return el.innerText; //Not needed but it is faster + var str = ""; + + var cs = el.childNodes; + var l = cs.length; + for (var i = 0; i < l; i++) { + switch (cs[i].nodeType) { + case 1: //ELEMENT_NODE + str += ts_getInnerText(cs[i]); + break; + case 3: //TEXT_NODE + str += cs[i].nodeValue; + break; + } + } + return str; +} + +// extract data for sorting from a cell +function extractData(x) { + var data = x.getAttribute("data"); + if(data!=null) + return data; + return ts_getInnerText(x); +} + +var arrowTable = { + up: { + text: "  ↑", + reorder: function(rows) { rows.reverse(); } + }, + down: { + text: "  ↓", + reorder: function() {} + }, + none: { + text: "   " + } +} + +arrowTable.up.next = arrowTable.down; +arrowTable.down.next = arrowTable.up; + +function ts_resortTable(lnk) { + // get the span + var span = lnk.lastChild; + var spantext = ts_getInnerText(span); + var th = lnk.parentNode; + var column = th.cellIndex; + var table = getParent(th,'TABLE'); + + // Work out a type for the column + if (table.rows.length <= 1) return; + var itm = extractData(table.rows[1].cells[column]).trim(); + var sortfn = ts_sort_caseinsensitive; + if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/)) sortfn = ts_sort_date; + if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)) sortfn = ts_sort_date; + if (itm.match(/^[£$]/)) sortfn = ts_sort_currency; + if (itm.match(/^[\d\.]+$/)) sortfn = ts_sort_numeric; + var firstRow = new Array(); + var newRows = new Array(); + for (i=0;i= 0; i--) { + parent.removeChild(parent.childNodes[i]); + } +} + + + +/** + * I return an object with an x and y fields, indicating the object's + * offset from the top left corner of the document. The 'offsets' + * argument is optional; if not provided, one will be initialized and + * used. It is exposed as a parameter because it can be useful for + * computing deltas. The 'object' parameter can be either an actual + * document element, or the ID of one. + * + * @param object The object to compute the offset of. May be an object + * or the ID of one. + * @param offsets The starting offsets to calculate from. In almost + * all cases, this should be omitted. + * @return An offsets object with x and y fields, indicating the + * computed offsets for the object. If an offsets object is + * passed, that will be the object returned, though the values + * will have been changed. + */ +Utilities.getOffsets = function(object, offsets) { + if (! offsets) { + offsets = new Object(); + offsets.x = offsets.y = 0; + } + if (typeof object == "string") + object = document.getElementById(object); + offsets.x += object.offsetLeft; + offsets.y += object.offsetTop; + do { + object = object.offsetParent; + if (! object) + break; + offsets.x += object.offsetLeft; + offsets.y += object.offsetTop; + } while(object.tagName.toUpperCase() != "BODY"); + return offsets; +} + + + +/** + * + */ +Utilities.listAppend = function(list, value, delimiter) { + if (typeof delimiter == "undefined") + delimiter = ","; + if (list == "") + return value; + else + return list + delimiter + value; +} \ No newline at end of file