1 package hudson.model; 2 3 import com.thoughtworks.xstream.XStream; 4 import hudson.CloseProofOutputStream; 5 import hudson.ExtensionPoint; 6 import hudson.FeedAdapter; 7 import hudson.FilePath; 8 import hudson.Util; 9 import static hudson.Util.combine; 10 import hudson.XmlFile; 11 import hudson.tasks.BuildStep; 12 import hudson.tasks.LogRotator; 13 import hudson.tasks.test.AbstractTestResultAction; 14 import hudson.util.IOException2; 15 import hudson.util.XStream2; 16 import org.kohsuke.stapler.StaplerRequest; 17 import org.kohsuke.stapler.StaplerResponse; 18 19 import javax.servlet.ServletException ; 20 import javax.servlet.http.HttpServletResponse ; 21 import java.io.File ; 22 import java.io.FileOutputStream ; 23 import java.io.IOException ; 24 import java.io.PrintStream ; 25 import java.io.PrintWriter ; 26 import java.io.Writer ; 27 import java.text.ParseException ; 28 import java.text.SimpleDateFormat ; 29 import java.util.ArrayList ; 30 import java.util.Calendar ; 31 import java.util.Comparator ; 32 import java.util.GregorianCalendar ; 33 import java.util.HashMap ; 34 import java.util.List ; 35 import java.util.Map ; 36 import java.util.logging.Logger ; 37 38 48 public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,RunT>> 49 extends Actionable implements ExtensionPoint, Comparable <RunT> { 50 51 protected transient final JobT project; 52 53 60 public int number; 61 62 66 protected volatile transient RunT previousBuild; 67 70 protected volatile transient RunT nextBuild; 71 72 75 protected transient final Calendar timestamp; 76 77 81 protected volatile Result result; 82 83 86 protected volatile String description; 87 88 91 protected volatile transient State state; 92 93 private static enum State { 94 NOT_STARTED, 95 BUILDING, 96 COMPLETED 97 } 98 99 102 protected long duration; 103 104 107 private boolean keepLog; 108 109 protected static final SimpleDateFormat ID_FORMATTER = new SimpleDateFormat ("yyyy-MM-dd_HH-mm-ss"); 110 111 114 protected Run(JobT job) throws IOException { 115 this(job, new GregorianCalendar ()); 116 this.number = project.assignBuildNumber(); 117 } 118 119 123 protected Run(JobT job, Calendar timestamp) { 124 this.project = job; 125 this.timestamp = timestamp; 126 this.state = State.NOT_STARTED; 127 } 128 129 132 protected Run(JobT project, File buildDir) throws IOException { 133 this(project, new GregorianCalendar ()); 134 try { 135 this.timestamp.setTime(ID_FORMATTER.parse(buildDir.getName())); 136 } catch (ParseException e) { 137 throw new IOException2("Invalid directory name "+buildDir,e); 138 } catch (NumberFormatException e) { 139 throw new IOException2("Invalid directory name "+buildDir,e); 140 } 141 this.state = State.COMPLETED; 142 this.result = Result.FAILURE; getDataFile().unmarshal(this); } 145 146 147 150 public int compareTo(RunT that) { 151 return this.number - that.number; 152 } 153 154 161 public final Result getResult() { 162 return result; 163 } 164 165 public void setResult(Result r) { 166 assert state==State.BUILDING; 168 169 StackTraceElement caller = findCaller(Thread.currentThread().getStackTrace(),"setResult"); 170 171 172 if(result==null) { 174 result = r; 175 LOGGER.info(toString()+" : result is set to "+r+" by "+caller); 176 } else { 177 if(r.isWorseThan(result)) { 178 LOGGER.info(toString()+" : result is set to "+r+" by "+caller); 179 result = r; 180 } 181 } 182 } 183 184 private StackTraceElement findCaller(StackTraceElement [] stackTrace, String callee) { 185 for(int i=0; i<stackTrace.length-1; i++) { 186 StackTraceElement e = stackTrace[i]; 187 if(e.getMethodName().equals(callee)) 188 return stackTrace[i+1]; 189 } 190 return null; } 192 193 196 public boolean isBuilding() { 197 return state!=State.COMPLETED; 198 } 199 200 204 public Executor getExecutor() { 205 for( Computer c : Hudson.getInstance().getComputers() ) { 206 for (Executor e : c.getExecutors()) { 207 if(e.getCurrentBuild()==(Object )this) 208 return e; 209 } 210 } 211 return null; 212 } 213 214 219 public final boolean isKeepLog() { 220 return getWhyKeepLog()!=null; 221 } 222 223 227 public String getWhyKeepLog() { 228 if(keepLog) 229 return "explicitly marked to keep the record"; 230 return null; } 232 233 236 public JobT getParent() { 237 return project; 238 } 239 240 243 public Calendar getTimestamp() { 244 return timestamp; 245 } 246 247 public String getDescription() { 248 return description; 249 } 250 251 255 public String getTruncatedDescription() { 256 final int maxDescrLength = 100; 257 if (description == null || description.length() < maxDescrLength) { 258 return description; 259 } 260 261 final String ending = "..."; 262 263 String truncDescr = description.substring( 265 0, maxDescrLength - ending.length()); 266 267 int lastSpace = truncDescr.lastIndexOf(" "); 269 if (lastSpace != -1) { 270 truncDescr = truncDescr.substring(0, lastSpace); 271 } 272 273 return truncDescr + ending; 274 } 275 276 282 public String getTimestampString() { 283 long duration = new GregorianCalendar ().getTimeInMillis()-timestamp.getTimeInMillis(); 284 return Util.getTimeSpanString(duration); 285 } 286 287 290 public String getTimestampString2() { 291 return Util.XS_DATETIME_FORMATTER.format(timestamp.getTime()); 292 } 293 294 297 public String getDurationString() { 298 return Util.getTimeSpanString(duration); 299 } 300 301 304 public long getDuration() { 305 return duration; 306 } 307 308 311 public BallColor getIconColor() { 312 if(!isBuilding()) { 313 if(result==Result.SUCCESS) 315 return BallColor.BLUE; 316 if(result== Result.UNSTABLE) 317 return BallColor.YELLOW; 318 else 319 return BallColor.RED; 320 } 321 322 BallColor baseColor; 324 if(previousBuild==null) 325 baseColor = BallColor.GREY; 326 else 327 baseColor = previousBuild.getIconColor(); 328 329 return baseColor.anime(); 330 } 331 332 335 public boolean hasntStartedYet() { 336 return state ==State.NOT_STARTED; 337 } 338 339 public String toString() { 340 return project.getName()+" #"+number; 341 } 342 343 public String getDisplayName() { 344 return "#"+number; 345 } 346 347 public int getNumber() { 348 return number; 349 } 350 351 public RunT getPreviousBuild() { 352 return previousBuild; 353 } 354 355 358 public RunT getPreviousNotFailedBuild() { 359 RunT r=previousBuild; 360 while( r!=null && r.getResult()==Result.FAILURE ) 361 r=r.previousBuild; 362 return r; 363 } 364 365 368 public RunT getPreviousFailedBuild() { 369 RunT r=previousBuild; 370 while( r!=null && r.getResult()!=Result.FAILURE ) 371 r=r.previousBuild; 372 return r; 373 } 374 375 public RunT getNextBuild() { 376 return nextBuild; 377 } 378 379 public String getUrl() { 382 return project.getUrl()+getNumber()+'/'; 383 } 384 385 388 public String getId() { 389 return ID_FORMATTER.format(timestamp.getTime()); 390 } 391 392 397 public File getRootDir() { 398 File f = new File(project.getBuildDir(),getId()); 399 f.mkdirs(); 400 return f; 401 } 402 403 406 public File getArtifactsDir() { 407 return new File(getRootDir(),"archive"); 408 } 409 410 413 public List <Artifact> getArtifacts() { 414 List <Artifact> r = new ArrayList <Artifact>(); 415 addArtifacts(getArtifactsDir(),"",r); 416 return r; 417 } 418 419 425 public boolean getHasArtifacts() { 426 return !getArtifacts().isEmpty(); 427 } 428 429 private void addArtifacts( File dir, String path, List <Artifact> r ) { 430 String [] children = dir.list(); 431 if(children==null) return; 432 for (String child : children) { 433 if(r.size()>CUTOFF) 434 return; 435 File sub = new File(dir, child); 436 if (sub.isDirectory()) { 437 addArtifacts(sub, path + child + '/', r); 438 } else { 439 r.add(new Artifact(path + child)); 440 } 441 } 442 } 443 444 private static final int CUTOFF = 17; 446 449 public class Artifact { 450 453 private final String relativePath; 454 455 private Artifact(String relativePath) { 456 this.relativePath = relativePath; 457 } 458 459 462 public File getFile() { 463 return new File(getArtifactsDir(),relativePath); 464 } 465 466 469 public String getFileName() { 470 return getFile().getName(); 471 } 472 473 public String toString() { 474 return relativePath; 475 } 476 } 477 478 481 public File getLogFile() { 482 return new File(getRootDir(),"log"); 483 } 484 485 491 public synchronized void delete() throws IOException { 492 File rootDir = getRootDir(); 493 File tmp = new File(rootDir.getParentFile(),'.'+rootDir.getName()); 494 495 if(!rootDir.renameTo(tmp)) 496 throw new IOException (rootDir+" is in use"); 497 498 Util.deleteRecursive(tmp); 499 500 removeRunFromParent(); 501 } 502 @SuppressWarnings ("unchecked") private void removeRunFromParent() { 504 getParent().removeRun((RunT)this); 505 } 506 507 protected static interface Runner { 508 514 Result run( BuildListener listener ) throws Exception , RunnerAbortedException; 515 516 519 void post( BuildListener listener ); 520 } 521 522 527 public static final class RunnerAbortedException extends RuntimeException {} 528 529 protected final void run(Runner job) { 530 if(result!=null) 531 return; 533 onStartBuilding(); 534 try { 535 538 long start = System.currentTimeMillis(); 539 BuildListener listener=null; 540 PrintStream log = null; 541 542 try { 543 try { 544 log = new PrintStream (new FileOutputStream (getLogFile())); 545 listener = new StreamBuildListener(new CloseProofOutputStream(log)); 546 547 listener.started(); 548 549 setResult(job.run(listener)); 550 551 LOGGER.info(toString()+" main build action completed: "+result); 552 } catch (ThreadDeath t) { 553 throw t; 554 } catch( RunnerAbortedException e ) { 555 result = Result.FAILURE; 556 } catch( Throwable e ) { 557 handleFatalBuildProblem(listener,e); 558 result = Result.FAILURE; 559 } 560 561 job.post(listener); 563 564 } catch (ThreadDeath t) { 565 throw t; 566 } catch( Throwable e ) { 567 handleFatalBuildProblem(listener,e); 568 result = Result.FAILURE; 569 } 570 571 long end = System.currentTimeMillis(); 572 duration = end-start; 573 574 if(listener!=null) 575 listener.finished(result); 576 if(log!=null) 577 log.close(); 578 579 try { 580 save(); 581 } catch (IOException e) { 582 e.printStackTrace(); 583 } 584 585 try { 586 LogRotator lr = getParent().getLogRotator(); 587 if(lr!=null) 588 lr.perform(getParent()); 589 } catch (IOException e) { 590 e.printStackTrace(); 591 } 592 } finally { 593 onEndBuilding(); 594 } 595 } 596 597 600 private void handleFatalBuildProblem(BuildListener listener, Throwable e) { 601 if(listener!=null) { 602 if(e instanceof IOException ) 603 Util.displayIOException((IOException )e,listener); 604 605 Writer w = listener.fatalError(e.getMessage()); 606 if(w!=null) { 607 try { 608 e.printStackTrace(new PrintWriter (w)); 609 w.close(); 610 } catch (IOException e1) { 611 } 613 } 614 } 615 } 616 617 620 protected void onStartBuilding() { 621 state = State.BUILDING; 622 } 623 624 627 protected void onEndBuilding() { 628 state = State.COMPLETED; 629 if(result==null) { 630 result = Result.FAILURE; 632 LOGGER.warning(toString()+": No build result is set, so marking as failure. This shouldn't happen"); 633 } 634 } 635 636 639 public synchronized void save() throws IOException { 640 getDataFile().write(this); 641 } 642 643 private XmlFile getDataFile() { 644 return new XmlFile(XSTREAM,new File(getRootDir(),"build.xml")); 645 } 646 647 652 public String getLog() throws IOException { 653 return Util.loadFile(getLogFile()); 654 } 655 656 public void doBuildStatus( StaplerRequest req, StaplerResponse rsp ) throws IOException { 657 rsp.sendRedirect2(req.getContextPath()+"/nocacheImages/48x48/"+getBuildStatusUrl()); 659 } 660 661 public String getBuildStatusUrl() { 662 return getIconColor()+".gif"; 663 } 664 665 public static class Summary { 666 669 public boolean isWorse; 670 public String message; 671 672 public Summary(boolean worse, String message) { 673 this.isWorse = worse; 674 this.message = message; 675 } 676 } 677 678 681 public Summary getBuildStatusSummary() { 682 Run prev = getPreviousBuild(); 683 684 if(getResult()==Result.SUCCESS) { 685 if(prev==null || prev.getResult()== Result.SUCCESS) 686 return new Summary(false,"stable"); 687 else 688 return new Summary(false,"back to normal"); 689 } 690 691 if(getResult()==Result.FAILURE) { 692 RunT since = getPreviousNotFailedBuild(); 693 if(since==null) 694 return new Summary(false,"broken for a long time"); 695 if(since==prev) 696 return new Summary(true,"broken since this build"); 697 return new Summary(false,"broken since "+since.getDisplayName()); 698 } 699 700 if(getResult()==Result.ABORTED) 701 return new Summary(false,"aborted"); 702 703 if(getResult()==Result.UNSTABLE) { 704 if(((Run)this) instanceof Build) { 705 AbstractTestResultAction trN = ((Build)(Run)this).getTestResultAction(); 706 AbstractTestResultAction trP = prev==null ? null : ((Build) prev).getTestResultAction(); 707 if(trP==null) { 708 if(trN!=null && trN.getFailCount()>0) 709 return new Summary(false,combine(trN.getFailCount(),"test failure")); 710 else return new Summary(false,"unstable"); 712 } 713 if(trP.getFailCount()==0) 714 return new Summary(true,combine(trP.getFailCount(),"test")+" started to fail"); 715 if(trP.getFailCount() < trN.getFailCount()) 716 return new Summary(true,combine(trN.getFailCount()-trP.getFailCount(),"more test") 717 +" are failing ("+trN.getFailCount()+" total)"); 718 if(trP.getFailCount() > trN.getFailCount()) 719 return new Summary(false,combine(trP.getFailCount()-trN.getFailCount(),"less test") 720 +" are failing ("+trN.getFailCount()+" total)"); 721 722 return new Summary(false,combine(trN.getFailCount(),"test")+" are still failing"); 723 } 724 } 725 726 return new Summary(false,"?"); 727 } 728 729 732 public void doArtifact( StaplerRequest req, StaplerResponse rsp ) throws IOException , ServletException , InterruptedException { 733 new DirectoryBrowserSupport(this).serveFile(req, rsp, new FilePath(getArtifactsDir()), "package.gif", true); 734 } 735 736 739 public void doBuildNumber( StaplerRequest req, StaplerResponse rsp ) throws IOException { 740 rsp.setContentType("text/plain"); 741 rsp.setCharacterEncoding("US-ASCII"); 742 rsp.setStatus(HttpServletResponse.SC_OK); 743 rsp.getWriter().print(number); 744 } 745 746 749 public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException { 750 new LargeText(getLogFile(),!isBuilding()).doProgressText(req,rsp); 751 } 752 753 public void doToggleLogKeep( StaplerRequest req, StaplerResponse rsp ) throws IOException , ServletException { 754 if(!Hudson.adminCheck(req,rsp)) 755 return; 756 757 keepLog = !keepLog; 758 save(); 759 rsp.forwardToPreviousPage(req); 760 } 761 762 765 public void keepLog() throws IOException { 766 keepLog = true; 767 save(); 768 } 769 770 773 public void doDoDelete( StaplerRequest req, StaplerResponse rsp ) throws IOException , ServletException { 774 if(!Hudson.adminCheck(req,rsp)) 775 return; 776 777 String why = getWhyKeepLog(); 781 if (why!=null) { 782 sendError("Unable to delete "+toString()+": "+why,req,rsp); 783 return; 784 } 785 786 delete(); 787 rsp.sendRedirect2(req.getContextPath()+'/' + getParent().getUrl()); 788 } 789 790 793 public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException , ServletException { 794 if(!Hudson.adminCheck(req,rsp)) 795 return; 796 797 req.setCharacterEncoding("UTF-8"); 798 description = req.getParameter("description"); 799 save(); 800 rsp.sendRedirect("."); } 802 803 808 public Map <String ,String > getEnvVars() { 809 Map <String ,String > env = new HashMap <String ,String >(); 810 env.put("BUILD_NUMBER",String.valueOf(number)); 811 env.put("BUILD_ID",getId()); 812 env.put("BUILD_TAG","hudson-"+getParent().getName()+"-"+number); 813 env.put("JOB_NAME",getParent().getName()); 814 815 Thread t = Thread.currentThread(); 816 if (t instanceof Executor) { 817 Executor e = (Executor) t; 818 env.put("EXECUTOR_NUMBER",String.valueOf(e.getNumber())); 819 } 820 821 return env; 822 } 823 824 private static final XStream XSTREAM = new XStream2(); 825 static { 826 XSTREAM.alias("build",Build.class); 827 XSTREAM.registerConverter(Result.conv); 828 } 829 830 private static final Logger LOGGER = Logger.getLogger(Run.class.getName()); 831 832 835 public static final Comparator <Run> ORDER_BY_DATE = new Comparator <Run>() { 836 public int compare(Run lhs, Run rhs) { 837 long lt = lhs.getTimestamp().getTimeInMillis(); 838 long rt = rhs.getTimestamp().getTimeInMillis(); 839 if(lt>rt) return -1; 840 if(lt<rt) return 1; 841 return 0; 842 } 843 }; 844 845 848 public static final FeedAdapter<Run> FEED_ADAPTER = new FeedAdapter<Run>() { 849 public String getEntryTitle(Run entry) { 850 return entry+" ("+entry.getResult()+")"; 851 } 852 853 public String getEntryUrl(Run entry) { 854 return entry.getUrl(); 855 } 856 857 public String getEntryID(Run entry) { 859 return "tag:" + "hudson.dev.java.net," 860 + entry.getTimestamp().get(Calendar.YEAR) + ":" 861 + entry.getParent().getName()+':'+entry.getId(); 862 } 863 864 public Calendar getEntryTimestamp(Run entry) { 865 return entry.getTimestamp(); 866 } 867 }; 868 } 869 | Popular Tags |