AbstractProject.java 15.9 KB
Newer Older
1 2 3
package hudson.model;

import hudson.FilePath;
4 5
import hudson.Launcher;
import hudson.Launcher.LocalLauncher;
6
import hudson.maven.MavenModule;
7 8 9
import hudson.model.Descriptor.FormException;
import hudson.model.Fingerprint.RangeSet;
import hudson.model.RunMap.Constructor;
10
import hudson.scm.NullSCM;
11
import hudson.scm.SCM;
K
kohsuke 已提交
12
import hudson.scm.SCMS;
13 14
import hudson.triggers.Trigger;
import hudson.triggers.Triggers;
15
import hudson.triggers.TriggerDescriptor;
16
import hudson.util.EditDistance;
17 18 19 20 21
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

import javax.servlet.ServletException;
import java.io.File;
22
import java.io.IOException;
23 24
import java.util.Collection;
import java.util.Comparator;
25 26
import java.util.List;
import java.util.Map;
27 28 29
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Vector;
30 31 32 33

/**
 * Base implementation of {@link Job}s that build software.
 *
34
 * For now this is primarily the common part of {@link Project} and {@link MavenModule}.
35 36 37 38
 *
 * @author Kohsuke Kawaguchi
 * @see AbstractBuild
 */
39
public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends AbstractBuild<P,R>> extends Job<P,R> implements BuildableItem {
40 41 42

    private SCM scm = new NullSCM();

43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
    /**
     * All the builds keyed by their build number.
     */
    protected transient /*almost final*/ RunMap<R> builds = new RunMap<R>();

    /**
     * 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.
     *
     * <p>
     * This somewhat ugly flag combination is so that we can migrate
     * existing Hudson installations nicely.
     */
    private boolean canRoam;

    /**
     * True to suspend new builds.
     */
74
    protected boolean disabled;
75 76 77 78 79 80 81 82 83 84 85 86 87

    /**
     * Identifies {@link JDK} to be used.
     * Null if no explicit configuration is required.
     *
     * <p>
     * Can't store {@link JDK} directly because {@link Hudson} and {@link Project}
     * are saved independently.
     *
     * @see Hudson#getJDK(String)
     */
    private String jdk;

88 89 90 91
    /**
     * @deprecated
     */
    private transient boolean enableRemoteTrigger;
92

93
    private BuildAuthorizationToken authToken = null;
94

95 96 97
    /**
     * List of all {@link Trigger}s for this project.
     */
98
    protected List<Trigger<?>> triggers = new Vector<Trigger<?>>();
99

100 101
    protected AbstractProject(ItemGroup parent, String name) {
        super(parent,name);
102

103
        if(!Hudson.getInstance().getSlaves().isEmpty()) {
104 105 106 107 108 109
            // if a new job is configured with Hudson that already has slave nodes
            // make it roamable by default
            canRoam = true;
        }
    }

110
    @Override
111 112
    public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
        super.onLoad(parent, name);
113 114 115 116 117 118 119 120 121 122

        this.builds = new RunMap<R>();
        this.builds.load(this,new Constructor<R>() {
            public R create(File dir) throws IOException {
                return loadBuild(dir);
            }
        });

        if(triggers==null)
            // it didn't exist in < 1.28
123
            triggers = new Vector<Trigger<?>>();
124 125 126 127
        for (Trigger t : triggers)
            t.start(this,false);
    }

128 129 130 131 132 133 134 135 136 137
    /**
     * 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();
138
        return Hudson.getInstance().getSlave(assignedNode);
139 140 141 142 143
    }

    /**
     * Gets the directory where the module is checked out.
     */
144
    public abstract FilePath getWorkspace();
145

146 147 148 149 150 151 152 153 154 155
    /**
     * Returns the root directory of the checked-out module.
     * <p>
     * This is usually where <tt>pom.xml</tt>, <tt>build.xml</tt>
     * and so on exists. 
     */
    public FilePath getModuleRoot() {
        return getScm().getModuleRoot(getWorkspace());
    }

156
    public int getQuietPeriod() {
157
        return quietPeriod!=null ? quietPeriod : Hudson.getInstance().getQuietPeriod();
158 159 160 161 162 163 164 165
    }

    // ugly name because of EL
    public boolean getHasCustomQuietPeriod() {
        return quietPeriod!=null;
    }

    public final boolean isBuildable() {
K
kohsuke 已提交
166
        return !isDisabled();
167 168 169 170 171 172
    }

    public boolean isDisabled() {
        return disabled;
    }

K
kohsuke 已提交
173 174 175 176 177 178 179 180 181
    @Override
    public BallColor getIconColor() {
        if(isDisabled())
            // use grey to indicate that the build is disabled
            return BallColor.GREY;
        else
            return super.getIconColor();
    }
    
182 183
    /**
     * Schedules a build of this project.
184 185 186 187 188
     *
     * @return
     *      true if the project is actually added to the queue.
     *      false if the queue contained it and therefore the add()
     *      was noop
189
     */
190 191 192
    public boolean scheduleBuild() {
        if(isDisabled())    return false;
        return Hudson.getInstance().getQueue().add(this);
193 194 195 196 197 198 199
    }

    /**
     * Returns true if the build is in the queue.
     */
    @Override
    public boolean isInQueue() {
200
        return Hudson.getInstance().getQueue().contains(this);
201 202
    }

203 204 205 206 207 208 209 210
    /**
     * Returns true if a build of this project is in progress.
     */
    public boolean isBuilding() {
        R b = getLastBuild();
        return b!=null && b.isBuilding();
    }

211
    public JDK getJDK() {
212
        return Hudson.getInstance().getJDK(jdk);
213 214 215 216 217 218 219 220 221 222
    }

    /**
     * Overwrites the JDK setting.
     */
    public synchronized void setJDK(JDK jdk) throws IOException {
        this.jdk = jdk.getName();
        save();
    }

223 224
    public BuildAuthorizationToken getAuthToken() {
        return authToken;
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
    }

    public SortedMap<Integer, ? extends R> _getRuns() {
        return builds.getView();
    }

    public void removeRun(R run) {
        this.builds.remove(run);
    }

    /**
     * Creates a new build of this project for immediate execution.
     */
    protected abstract R newBuild() throws IOException;

240 241 242 243 244
    /**
     * Loads an existing build record from disk.
     */
    protected abstract R loadBuild(File dir) throws IOException;

245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
    /**
     * 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?
        AbstractBuild b = getLastBuild();
        if(b==null)
            return null;
        else
            return b.getBuiltOn();
    }

261 262 263 264 265 266 267 268 269 270 271 272
    /**
     * Returns true if this project's build execution should be blocked
     * for temporary reasons. This method is used by {@link Queue}.
     *
     * <p>
     * A project must be blocked if its own previous build is in progress,
     * but derived classes can also check other conditions.
     */
    protected boolean isBuildBlocked() {
        return isBuilding();
    }

273
    public boolean checkout(AbstractBuild build, Launcher launcher, BuildListener listener, File changelogFile) throws IOException {
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
        if(scm==null)
            return true;    // no SCM

        try {
            FilePath workspace = getWorkspace();
            workspace.mkdirs();

            return scm.checkout(build, launcher, workspace, listener, changelogFile);
        } catch (InterruptedException e) {
            e.printStackTrace(listener.fatalError("SCM check out aborted"));
            return false;
        }
    }

    /**
     * Checks if there's any update in SCM, and returns true if any is found.
     *
     * <p>
     * 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");
298 299 300 301 302
            return false;
        }
        if(isDisabled()) {
            listener.getLogger().println("Build disabled");
            return false;
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
        }

        try {
            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;
            }

            // TODO: do this by using the right slave
            return scm.pollChanges(this, new LocalLauncher(listener), workspace, listener );
        } catch (IOException e) {
            e.printStackTrace(listener.fatalError(e.getMessage()));
            return false;
        } catch (InterruptedException e) {
            e.printStackTrace(listener.fatalError("SCM polling aborted"));
            return false;
        }
    }

    public SCM getScm() {
        return scm;
    }

    public void setScm(SCM scm) {
        this.scm = scm;
    }

333 334 335
    /**
     * Adds a new {@link Trigger} to this {@link Project} if not active yet.
     */
336
    public void addTrigger(Trigger<?> trigger) throws IOException {
337 338 339
        addToList(trigger,triggers);
    }

340
    public void removeTrigger(TriggerDescriptor trigger) throws IOException {
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
        removeFromList(trigger,triggers);
    }

    protected final synchronized <T extends Describable<T>>
    void addToList( T item, List<T> collection ) throws IOException {
        for( int i=0; i<collection.size(); i++ ) {
            if(collection.get(i).getDescriptor()==item.getDescriptor()) {
                // replace
                collection.set(i,item);
                save();
                return;
            }
        }
        // add
        collection.add(item);
        save();
    }

    protected final synchronized <T extends Describable<T>>
    void removeFromList(Descriptor<T> item, List<T> collection) throws IOException {
        for( int i=0; i< collection.size(); i++ ) {
            if(collection.get(i).getDescriptor()==item) {
                // found it
                collection.remove(i);
                save();
                return;
            }
        }
    }

371 372
    public synchronized Map<TriggerDescriptor,Trigger> getTriggers() {
        return (Map)Descriptor.toMap(triggers);
373 374
    }

375 376 377 378 379 380 381 382 383 384 385 386 387 388
//
//
// fingerprint related
//
//
    /**
     * True if the builds of this project produces {@link Fingerprint} records.
     */
    public abstract boolean isFingerprintConfigured();

    /**
     * Gets the other {@link AbstractProject}s that should be built
     * when a build of this project is completed.
     */
389 390 391
    public final List<AbstractProject> getDownstreamProjects() {
        return Hudson.getInstance().getDependencyGraph().getDownstream(this);
    }
392

393 394
    public final List<AbstractProject> getUpstreamProjects() {
        return Hudson.getInstance().getDependencyGraph().getUpstream(this);
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
    }

    /**
     * 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<Integer, RangeSet> getRelationship(AbstractProject that) {
        TreeMap<Integer,RangeSet> r = new TreeMap<Integer,RangeSet>(REVERSE_INTEGER_COMPARATOR);

        checkAndRecord(that, r, this.getBuilds());
        // checkAndRecord(that, r, that.getBuilds());

        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(AbstractProject that, TreeMap<Integer, RangeSet> r, Collection<R> builds) {
        for (R 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);
        }
    }

435 436 437 438 439 440
    /**
     * Builds the dependency graph.
     * @see DependencyGraph
     */
    protected abstract void buildDependencyGraph(DependencyGraph graph);

441 442 443 444 445 446 447 448 449
//
//
// actions
//
//
    /**
     * Schedules a new build command.
     */
    public void doBuild( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
450
        BuildAuthorizationToken.startBuildIfAuthorized(authToken,this,req,rsp);
451 452 453 454 455 456 457 458 459
    }

    /**
     * Cancels a scheduled build.
     */
    public void doCancelQueue( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
        if(!Hudson.adminCheck(req,rsp))
            return;

460
        Hudson.getInstance().getQueue().cancel(this);
461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
        rsp.forwardToPreviousPage(req);
    }

    public synchronized void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
        super.doConfigSubmit(req, rsp);

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

489
        authToken = BuildAuthorizationToken.create(req);
490 491

        try {
492
            setScm(SCMS.parseSCM(req));
K
kohsuke 已提交
493

494 495
            for (Trigger t : triggers)
                t.stop();
496
            buildDescribable(req, Triggers.getApplicableTriggers(this), triggers, "trigger");
497 498 499 500 501 502 503
            for (Trigger t : triggers)
                t.start(this,true);
        } catch (FormException e) {
            throw new ServletException(e);
        }
    }

504
    protected final <T extends Describable<T>> void buildDescribable(StaplerRequest req, List<? extends Descriptor<T>> descriptors, List<T> result, String prefix)
505 506 507 508 509 510 511 512 513
        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);
            }
        }
514 515 516 517 518 519 520 521 522 523 524
    }

    /**
     * Serves the workspace files.
     */
    public void doWs( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, InterruptedException {
        FilePath ws = getWorkspace();
        if(!ws.exists()) {
            // if there's no workspace, report a nice error message
            rsp.forward(this,"noWorkspace",req);
        } else {
525
            new DirectoryBrowserSupport(this).serveFile(req, rsp, ws, "folder.gif", true);
526 527
        }
    }
528 529 530 531 532 533 534 535 536 537 538 539 540

    /**
     * Finds a {@link AbstractProject} that has the name closest to the given name.
     */
    public static AbstractProject findNearest(String name) {
        List<AbstractProject> projects = Hudson.getInstance().getAllItems(AbstractProject.class);
        String[] names = new String[projects.size()];
        for( int i=0; i<projects.size(); i++ )
            names[i] = projects.get(i).getName();

        String nearest = EditDistance.findNearest(name, names);
        return (AbstractProject)Hudson.getInstance().getItem(nearest);
    }
541 542 543 544 545 546

    private static final Comparator<Integer> REVERSE_INTEGER_COMPARATOR = new Comparator<Integer>() {
        public int compare(Integer o1, Integer o2) {
            return o2-o1;
        }
    };
547
}