Package buildbot :: Package clients :: Module tryclient
[frames] | no frames]

Source Code for Module buildbot.clients.tryclient

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Copyright Buildbot Team Members 
 15   
 16  from __future__ import with_statement 
 17   
 18   
 19  import os 
 20  import random 
 21  import re 
 22  import sys 
 23  import time 
 24   
 25  from twisted.cred import credentials 
 26  from twisted.internet import defer 
 27  from twisted.internet import protocol 
 28  from twisted.internet import reactor 
 29  from twisted.internet import task 
 30  from twisted.internet import utils 
 31  from twisted.python import log 
 32  from twisted.python.procutils import which 
 33  from twisted.spread import pb 
 34   
 35  from buildbot.sourcestamp import SourceStamp 
 36  from buildbot.status import builder 
 37  from buildbot.util import json 
 38  from buildbot.util import now 
 39  from buildbot.util.eventual import fireEventually 
40 41 42 -class SourceStampExtractor:
43
44 - def __init__(self, treetop, branch, repository):
45 self.treetop = treetop 46 self.repository = repository 47 self.branch = branch 48 exes = which(self.vcexe) 49 if not exes: 50 print "Could not find executable '%s'." % self.vcexe 51 sys.exit(1) 52 self.exe = exes[0]
53
54 - def dovc(self, cmd):
55 """This accepts the arguments of a command, without the actual 56 command itself.""" 57 env = os.environ.copy() 58 env['LC_ALL'] = "C" 59 d = utils.getProcessOutputAndValue(self.exe, cmd, env=env, 60 path=self.treetop) 61 d.addCallback(self._didvc, cmd) 62 return d
63
64 - def _didvc(self, res, cmd):
65 (stdout, stderr, code) = res 66 # 'bzr diff' sets rc=1 if there were any differences. 67 # cvs does something similar, so don't bother requring rc=0. 68 return stdout
69
70 - def get(self):
71 """Return a Deferred that fires with a SourceStamp instance.""" 72 d = self.getBaseRevision() 73 d.addCallback(self.getPatch) 74 d.addCallback(self.done) 75 return d
76
77 - def readPatch(self, diff, patchlevel):
78 if not diff: 79 diff = None 80 self.patch = (patchlevel, diff)
81
82 - def done(self, res):
83 if not self.repository: 84 self.repository = self.treetop 85 # TODO: figure out the branch and project too 86 ss = SourceStamp(self.branch, self.baserev, self.patch, 87 repository=self.repository) 88 return ss
89
90 91 -class CVSExtractor(SourceStampExtractor):
92 patchlevel = 0 93 vcexe = "cvs" 94
95 - def getBaseRevision(self):
96 # this depends upon our local clock and the repository's clock being 97 # reasonably synchronized with each other. We express everything in 98 # UTC because the '%z' format specifier for strftime doesn't always 99 # work. 100 self.baserev = time.strftime("%Y-%m-%d %H:%M:%S +0000", 101 time.gmtime(now())) 102 return defer.succeed(None)
103
104 - def getPatch(self, res):
105 # the -q tells CVS to not announce each directory as it works 106 if self.branch is not None: 107 # 'cvs diff' won't take both -r and -D at the same time (it 108 # ignores the -r). As best I can tell, there is no way to make 109 # cvs give you a diff relative to a timestamp on the non-trunk 110 # branch. A bare 'cvs diff' will tell you about the changes 111 # relative to your checked-out versions, but I know of no way to 112 # find out what those checked-out versions are. 113 print "Sorry, CVS 'try' builds don't work with branches" 114 sys.exit(1) 115 args = ['-q', 'diff', '-u', '-D', self.baserev] 116 d = self.dovc(args) 117 d.addCallback(self.readPatch, self.patchlevel) 118 return d
119
120 121 -class SVNExtractor(SourceStampExtractor):
122 patchlevel = 0 123 vcexe = "svn" 124
125 - def getBaseRevision(self):
126 d = self.dovc(["status", "-u"]) 127 d.addCallback(self.parseStatus) 128 return d
129
130 - def parseStatus(self, res):
131 # svn shows the base revision for each file that has been modified or 132 # which needs an update. You can update each file to a different 133 # version, so each file is displayed with its individual base 134 # revision. It also shows the repository-wide latest revision number 135 # on the last line ("Status against revision: \d+"). 136 137 # for our purposes, we use the latest revision number as the "base" 138 # revision, and get a diff against that. This means we will get 139 # reverse-diffs for local files that need updating, but the resulting 140 # tree will still be correct. The only weirdness is that the baserev 141 # that we emit may be different than the version of the tree that we 142 # first checked out. 143 144 # to do this differently would probably involve scanning the revision 145 # numbers to find the max (or perhaps the min) revision, and then 146 # using that as a base. 147 148 for line in res.split("\n"): 149 m = re.search(r'^Status against revision:\s+(\d+)', line) 150 if m: 151 self.baserev = int(m.group(1)) 152 return 153 print "Could not find 'Status against revision' in SVN output: %s" % res 154 sys.exit(1)
155
156 - def getPatch(self, res):
157 d = self.dovc(["diff", "-r%d" % self.baserev]) 158 d.addCallback(self.readPatch, self.patchlevel) 159 return d
160
161 162 -class BzrExtractor(SourceStampExtractor):
163 patchlevel = 0 164 vcexe = "bzr" 165
166 - def getBaseRevision(self):
167 d = self.dovc(["revision-info", "-rsubmit:"]) 168 d.addCallback(self.get_revision_number) 169 return d
170
171 - def get_revision_number(self, out):
172 revno, revid = out.split() 173 self.baserev = 'revid:' + revid 174 return
175
176 - def getPatch(self, res):
177 d = self.dovc(["diff", "-r%s.." % self.baserev]) 178 d.addCallback(self.readPatch, self.patchlevel) 179 return d
180
181 182 -class MercurialExtractor(SourceStampExtractor):
183 patchlevel = 1 184 vcexe = "hg" 185
186 - def _didvc(self, res, cmd):
187 (stdout, stderr, code) = res 188 189 if code: 190 cs = ' '.join(['hg'] + cmd) 191 if stderr: 192 stderr = '\n' + stderr.rstrip() 193 raise RuntimeError("%s returned %d%s" % (cs, code, stderr)) 194 195 return stdout
196 197 @defer.inlineCallbacks
198 - def getBaseRevision(self):
199 upstream = "" 200 if self.repository: 201 upstream = "r'%s'" % self.repository 202 output = '' 203 try: 204 output = yield self.dovc(["log", "--template", "{node}\\n", "-r", 205 "max(::. - outgoing(%s))" % upstream]) 206 except RuntimeError: 207 # outgoing() will abort if no default-push/default path is configured 208 if upstream: 209 raise 210 # fall back to current working directory parent 211 output = yield self.dovc(["log", "--template", "{node}\\n", "-r", "p1()"]) 212 m = re.search(r'^(\w+)', output) 213 if not m: 214 raise RuntimeError("Revision %r is not in the right format" % (output,)) 215 self.baserev = m.group(0)
216
217 - def getPatch(self, res):
218 d = self.dovc(["diff", "-r", self.baserev]) 219 d.addCallback(self.readPatch, self.patchlevel) 220 return d
221
222 223 -class PerforceExtractor(SourceStampExtractor):
224 patchlevel = 0 225 vcexe = "p4" 226
227 - def getBaseRevision(self):
228 d = self.dovc(["changes", "-m1", "..."]) 229 d.addCallback(self.parseStatus) 230 return d
231
232 - def parseStatus(self, res):
233 # 234 # extract the base change number 235 # 236 m = re.search(r'Change (\d+)', res) 237 if m: 238 self.baserev = m.group(1) 239 return 240 241 print "Could not find change number in output: %s" % res 242 sys.exit(1)
243
244 - def readPatch(self, res, patchlevel):
245 # 246 # extract the actual patch from "res" 247 # 248 if not self.branch: 249 print "you must specify a branch" 250 sys.exit(1) 251 mpatch = "" 252 found = False 253 for line in res.split("\n"): 254 m = re.search('==== //depot/' + self.branch 255 + r'/([\w\/\.\d\-\_]+)#(\d+) -', line) 256 if m: 257 mpatch += "--- %s#%s\n" % (m.group(1), m.group(2)) 258 mpatch += "+++ %s\n" % (m.group(1)) 259 found = True 260 else: 261 mpatch += line 262 mpatch += "\n" 263 if not found: 264 print "could not parse patch file" 265 sys.exit(1) 266 self.patch = (patchlevel, mpatch)
267
268 - def getPatch(self, res):
269 d = self.dovc(["diff"]) 270 d.addCallback(self.readPatch, self.patchlevel) 271 return d
272
273 274 -class DarcsExtractor(SourceStampExtractor):
275 patchlevel = 1 276 vcexe = "darcs" 277
278 - def getBaseRevision(self):
279 d = self.dovc(["changes", "--context"]) 280 d.addCallback(self.parseStatus) 281 return d
282
283 - def parseStatus(self, res):
284 self.baserev = res # the whole context file
285
286 - def getPatch(self, res):
287 d = self.dovc(["diff", "-u"]) 288 d.addCallback(self.readPatch, self.patchlevel) 289 return d
290
291 292 -class GitExtractor(SourceStampExtractor):
293 patchlevel = 1 294 vcexe = "git" 295 config = None 296
297 - def getBaseRevision(self):
298 # If a branch is specified, parse out the rev it points to 299 # and extract the local name (assuming it has a slash). 300 # This may break if someone specifies the name of a local 301 # branch that has a slash in it and has no corresponding 302 # remote branch (or something similarly contrived). 303 if self.branch: 304 d = self.dovc(["rev-parse", self.branch]) 305 if '/' in self.branch: 306 self.branch = self.branch.split('/', 1)[1] 307 d.addCallback(self.override_baserev) 308 return d 309 d = self.dovc(["branch", "--no-color", "-v", "--no-abbrev"]) 310 d.addCallback(self.parseStatus) 311 return d
312
313 - def readConfig(self):
314 if self.config: 315 return defer.succeed(self.config) 316 d = self.dovc(["config", "-l"]) 317 d.addCallback(self.parseConfig) 318 return d
319
320 - def parseConfig(self, res):
321 self.config = {} 322 for l in res.split("\n"): 323 if l.strip(): 324 parts = l.strip().split("=", 2) 325 self.config[parts[0]] = parts[1] 326 return self.config
327
328 - def parseTrackingBranch(self, res):
329 # If we're tracking a remote, consider that the base. 330 remote = self.config.get("branch." + self.branch + ".remote") 331 ref = self.config.get("branch." + self.branch + ".merge") 332 if remote and ref: 333 remote_branch = ref.split("/", 3)[-1] 334 d = self.dovc(["rev-parse", remote + "/" + remote_branch]) 335 d.addCallback(self.override_baserev) 336 return d
337
338 - def override_baserev(self, res):
339 self.baserev = res.strip()
340
341 - def parseStatus(self, res):
342 # The current branch is marked by '*' at the start of the 343 # line, followed by the branch name and the SHA1. 344 # 345 # Branch names may contain pretty much anything but whitespace. 346 m = re.search(r'^\* (\S+)\s+([0-9a-f]{40})', res, re.MULTILINE) 347 if m: 348 self.baserev = m.group(2) 349 self.branch = m.group(1) 350 d = self.readConfig() 351 d.addCallback(self.parseTrackingBranch) 352 return d 353 print "Could not find current GIT branch: %s" % res 354 sys.exit(1)
355
356 - def getPatch(self, res):
357 d = self.dovc(["diff", self.baserev]) 358 d.addCallback(self.readPatch, self.patchlevel) 359 return d
360
361 362 -class MonotoneExtractor(SourceStampExtractor):
363 patchlevel = 0 364 vcexe = "mtn" 365
366 - def getBaseRevision(self):
367 d = self.dovc(["automate", "get_base_revision_id"]) 368 d.addCallback(self.parseStatus) 369 return d
370
371 - def parseStatus(self, output):
372 hash = output.strip() 373 if len(hash) != 40: 374 self.baserev = None 375 self.baserev = hash
376
377 - def getPatch(self, res):
378 d = self.dovc(["diff"]) 379 d.addCallback(self.readPatch, self.patchlevel) 380 return d
381
382 383 -def getSourceStamp(vctype, treetop, branch=None, repository=None):
384 if vctype == "cvs": 385 cls = CVSExtractor 386 elif vctype == "svn": 387 cls = SVNExtractor 388 elif vctype == "bzr": 389 cls = BzrExtractor 390 elif vctype == "hg": 391 cls = MercurialExtractor 392 elif vctype == "p4": 393 cls = PerforceExtractor 394 elif vctype == "darcs": 395 cls = DarcsExtractor 396 elif vctype == "git": 397 cls = GitExtractor 398 elif vctype == "mtn": 399 cls = MonotoneExtractor 400 else: 401 print "unknown vctype '%s'" % vctype 402 sys.exit(1) 403 return cls(treetop, branch, repository).get()
404
405 406 -def ns(s):
407 return "%d:%s," % (len(s), s)
408
409 410 -def createJobfile(jobid, branch, baserev, patch_level, patch_body, repository, 411 project, who, comment, builderNames, properties):
412 #Determine job file version from provided arguments 413 if properties: 414 version = 5 415 elif comment: 416 version = 4 417 elif who: 418 version = 3 419 else: 420 version = 2 421 job = "" 422 job += ns(str(version)) 423 if version < 5: 424 job += ns(jobid) 425 job += ns(branch) 426 job += ns(str(baserev)) 427 job += ns("%d" % patch_level) 428 job += ns(patch_body) 429 job += ns(repository) 430 job += ns(project) 431 if (version >= 3): 432 job += ns(who) 433 if (version >= 4): 434 job += ns(comment) 435 for bn in builderNames: 436 job += ns(bn) 437 else: 438 job += ns( 439 json.dumps({ 440 'jobid': jobid, 'branch': branch, 'baserev': str(baserev), 441 'patch_level': patch_level, 'patch_body': patch_body, 442 'repository': repository, 'project': project, 'who': who, 443 'comment': comment, 'builderNames': builderNames, 444 'properties': properties, 445 })) 446 return job
447
448 449 -def getTopdir(topfile, start=None):
450 """walk upwards from the current directory until we find this topfile""" 451 if not start: 452 start = os.getcwd() 453 here = start 454 toomany = 20 455 while toomany > 0: 456 if os.path.exists(os.path.join(here, topfile)): 457 return here 458 next = os.path.dirname(here) 459 if next == here: 460 break # we've hit the root 461 here = next 462 toomany -= 1 463 print ("Unable to find topfile '%s' anywhere from %s upwards" 464 % (topfile, start)) 465 sys.exit(1)
466
467 468 -class RemoteTryPP(protocol.ProcessProtocol):
469 - def __init__(self, job):
470 self.job = job 471 self.d = defer.Deferred()
472
473 - def connectionMade(self):
474 self.transport.write(self.job) 475 self.transport.closeStdin()
476
477 - def outReceived(self, data):
478 sys.stdout.write(data)
479
480 - def errReceived(self, data):
481 sys.stderr.write(data)
482
483 - def processEnded(self, status_object):
484 sig = status_object.value.signal 485 rc = status_object.value.exitCode 486 if sig != None or rc != 0: 487 self.d.errback(RuntimeError("remote 'buildbot tryserver' failed" 488 ": sig=%s, rc=%s" % (sig, rc))) 489 return 490 self.d.callback((sig, rc))
491
492 493 -class BuildSetStatusGrabber:
494 retryCount = 5 # how many times to we try to grab the BuildSetStatus? 495 retryDelay = 3 # seconds to wait between attempts 496
497 - def __init__(self, status, bsid):
498 self.status = status 499 self.bsid = bsid
500
501 - def grab(self):
502 # return a Deferred that either fires with the BuildSetStatus 503 # reference or errbacks because we were unable to grab it 504 self.d = defer.Deferred() 505 # wait a second before querying to give the master's maildir watcher 506 # a chance to see the job 507 reactor.callLater(1, self.go) 508 return self.d
509
510 - def go(self, dummy=None):
511 if self.retryCount == 0: 512 print "couldn't find matching buildset" 513 sys.exit(1) 514 self.retryCount -= 1 515 d = self.status.callRemote("getBuildSets") 516 d.addCallback(self._gotSets)
517
518 - def _gotSets(self, buildsets):
519 for bs, bsid in buildsets: 520 if bsid == self.bsid: 521 # got it 522 self.d.callback(bs) 523 return 524 d = defer.Deferred() 525 d.addCallback(self.go) 526 reactor.callLater(self.retryDelay, d.callback, None)
527
528 529 -class Try(pb.Referenceable):
530 buildsetStatus = None 531 quiet = False 532 printloop = False 533
534 - def __init__(self, config):
535 self.config = config 536 self.connect = self.getopt('connect') 537 if self.connect not in ['ssh', 'pb']: 538 print "you must specify a connect style: ssh or pb" 539 sys.exit(1) 540 self.builderNames = self.getopt('builders') 541 self.project = self.getopt('project', '') 542 self.who = self.getopt('who') 543 self.comment = self.getopt('comment')
544
545 - def getopt(self, config_name, default=None):
546 value = self.config.get(config_name) 547 if value is None or value == []: 548 value = default 549 return value
550
551 - def createJob(self):
552 # returns a Deferred which fires when the job parameters have been 553 # created 554 555 # generate a random (unique) string. It would make sense to add a 556 # hostname and process ID here, but a) I suspect that would cause 557 # windows portability problems, and b) really this is good enough 558 self.bsid = "%d-%s" % (time.time(), random.randint(0, 1000000)) 559 560 # common options 561 branch = self.getopt("branch") 562 563 difffile = self.config.get("diff") 564 if difffile: 565 baserev = self.config.get("baserev") 566 if difffile == "-": 567 diff = sys.stdin.read() 568 else: 569 with open(difffile, "r") as f: 570 diff = f.read() 571 if not diff: 572 diff = None 573 patch = (self.config['patchlevel'], diff) 574 ss = SourceStamp( 575 branch, baserev, patch, repository=self.getopt("repository")) 576 d = defer.succeed(ss) 577 else: 578 vc = self.getopt("vc") 579 if vc in ("cvs", "svn"): 580 # we need to find the tree-top 581 topdir = self.getopt("topdir") 582 if topdir: 583 treedir = os.path.expanduser(topdir) 584 else: 585 topfile = self.getopt("topfile") 586 if topfile: 587 treedir = getTopdir(topfile) 588 else: 589 print "Must specify topdir or topfile." 590 sys.exit(1) 591 else: 592 treedir = os.getcwd() 593 d = getSourceStamp(vc, treedir, branch, self.getopt("repository")) 594 d.addCallback(self._createJob_1) 595 return d
596
597 - def _createJob_1(self, ss):
598 self.sourcestamp = ss 599 if self.connect == "ssh": 600 patchlevel, diff = ss.patch 601 revspec = ss.revision 602 if revspec is None: 603 revspec = "" 604 self.jobfile = createJobfile( 605 self.bsid, ss.branch or "", revspec, patchlevel, diff, 606 ss.repository, self.project, self.who, self.comment, 607 self.builderNames, self.config.get('properties', {}))
608
609 - def fakeDeliverJob(self):
610 # Display the job to be delivered, but don't perform delivery. 611 ss = self.sourcestamp 612 print ("Job:\n\tRepository: %s\n\tProject: %s\n\tBranch: %s\n\t" 613 "Revision: %s\n\tBuilders: %s\n%s" 614 % (ss.repository, self.project, ss.branch, 615 ss.revision, 616 self.builderNames, 617 ss.patch[1])) 618 d = defer.Deferred() 619 d.callback(True) 620 return d
621
622 - def deliverJob(self):
623 # returns a Deferred that fires when the job has been delivered 624 if self.connect == "ssh": 625 tryhost = self.getopt("host") 626 tryuser = self.getopt("username") 627 trydir = self.getopt("jobdir") 628 buildbotbin = self.getopt("buildbotbin") 629 if tryuser: 630 argv = ["ssh", "-l", tryuser, tryhost, 631 buildbotbin, "tryserver", "--jobdir", trydir] 632 else: 633 argv = ["ssh", tryhost, 634 buildbotbin, "tryserver", "--jobdir", trydir] 635 pp = RemoteTryPP(self.jobfile) 636 reactor.spawnProcess(pp, argv[0], argv, os.environ) 637 d = pp.d 638 return d 639 if self.connect == "pb": 640 user = self.getopt("username") 641 passwd = self.getopt("passwd") 642 master = self.getopt("master") 643 tryhost, tryport = master.split(":") 644 tryport = int(tryport) 645 f = pb.PBClientFactory() 646 d = f.login(credentials.UsernamePassword(user, passwd)) 647 reactor.connectTCP(tryhost, tryport, f) 648 d.addCallback(self._deliverJob_pb) 649 return d 650 raise RuntimeError("unknown connecttype '%s', should be 'ssh' or 'pb'" 651 % self.connect)
652
653 - def _deliverJob_pb(self, remote):
654 ss = self.sourcestamp 655 print "Delivering job; comment=", self.comment 656 657 d = remote.callRemote("try", 658 ss.branch, 659 ss.revision, 660 ss.patch, 661 ss.repository, 662 self.project, 663 self.builderNames, 664 self.who, 665 self.comment, 666 self.config.get('properties', {})) 667 d.addCallback(self._deliverJob_pb2) 668 return d
669
670 - def _deliverJob_pb2(self, status):
671 self.buildsetStatus = status 672 return status
673
674 - def getStatus(self):
675 # returns a Deferred that fires when the builds have finished, and 676 # may emit status messages while we wait 677 wait = bool(self.getopt("wait")) 678 if not wait: 679 # TODO: emit the URL where they can follow the builds. This 680 # requires contacting the Status server over PB and doing 681 # getURLForThing() on the BuildSetStatus. To get URLs for 682 # individual builds would require we wait for the builds to 683 # start. 684 print "not waiting for builds to finish" 685 return 686 d = self.running = defer.Deferred() 687 if self.buildsetStatus: 688 self._getStatus_1() 689 return self.running 690 # contact the status port 691 # we're probably using the ssh style 692 master = self.getopt("master") 693 host, port = master.split(":") 694 port = int(port) 695 self.announce("contacting the status port at %s:%d" % (host, port)) 696 f = pb.PBClientFactory() 697 creds = credentials.UsernamePassword("statusClient", "clientpw") 698 d = f.login(creds) 699 reactor.connectTCP(host, port, f) 700 d.addCallback(self._getStatus_ssh_1) 701 return self.running
702
703 - def _getStatus_ssh_1(self, remote):
704 # find a remotereference to the corresponding BuildSetStatus object 705 self.announce("waiting for job to be accepted") 706 g = BuildSetStatusGrabber(remote, self.bsid) 707 d = g.grab() 708 d.addCallback(self._getStatus_1) 709 return d
710
711 - def _getStatus_1(self, res=None):
712 if res: 713 self.buildsetStatus = res 714 # gather the set of BuildRequests 715 d = self.buildsetStatus.callRemote("getBuildRequests") 716 d.addCallback(self._getStatus_2)
717
718 - def _getStatus_2(self, brs):
719 self.builderNames = [] 720 self.buildRequests = {} 721 722 # self.builds holds the current BuildStatus object for each one 723 self.builds = {} 724 725 # self.outstanding holds the list of builderNames which haven't 726 # finished yet 727 self.outstanding = [] 728 729 # self.results holds the list of build results. It holds a tuple of 730 # (result, text) 731 self.results = {} 732 733 # self.currentStep holds the name of the Step that each build is 734 # currently running 735 self.currentStep = {} 736 737 # self.ETA holds the expected finishing time (absolute time since 738 # epoch) 739 self.ETA = {} 740 741 for n, br in brs: 742 self.builderNames.append(n) 743 self.buildRequests[n] = br 744 self.builds[n] = None 745 self.outstanding.append(n) 746 self.results[n] = [None, None] 747 self.currentStep[n] = None 748 self.ETA[n] = None 749 # get new Builds for this buildrequest. We follow each one until 750 # it finishes or is interrupted. 751 br.callRemote("subscribe", self) 752 753 # now that those queries are in transit, we can start the 754 # display-status-every-30-seconds loop 755 if not self.getopt("quiet"): 756 self.printloop = task.LoopingCall(self.printStatus) 757 self.printloop.start(3, now=False)
758 759 # these methods are invoked by the status objects we've subscribed to 760
761 - def remote_newbuild(self, bs, builderName):
762 if self.builds[builderName]: 763 self.builds[builderName].callRemote("unsubscribe", self) 764 self.builds[builderName] = bs 765 bs.callRemote("subscribe", self, 20) 766 d = bs.callRemote("waitUntilFinished") 767 d.addCallback(self._build_finished, builderName)
768
769 - def remote_stepStarted(self, buildername, build, stepname, step):
770 self.currentStep[buildername] = stepname
771
772 - def remote_stepFinished(self, buildername, build, stepname, step, results):
773 pass
774
775 - def remote_buildETAUpdate(self, buildername, build, eta):
776 self.ETA[buildername] = now() + eta
777
778 - def _build_finished(self, bs, builderName):
779 # we need to collect status from the newly-finished build. We don't 780 # remove the build from self.outstanding until we've collected 781 # everything we want. 782 self.builds[builderName] = None 783 self.ETA[builderName] = None 784 self.currentStep[builderName] = "finished" 785 d = bs.callRemote("getResults") 786 d.addCallback(self._build_finished_2, bs, builderName) 787 return d
788
789 - def _build_finished_2(self, results, bs, builderName):
790 self.results[builderName][0] = results 791 d = bs.callRemote("getText") 792 d.addCallback(self._build_finished_3, builderName) 793 return d
794
795 - def _build_finished_3(self, text, builderName):
796 self.results[builderName][1] = text 797 798 self.outstanding.remove(builderName) 799 if not self.outstanding: 800 # all done 801 return self.statusDone()
802
803 - def printStatus(self):
804 try: 805 names = self.buildRequests.keys() 806 names.sort() 807 for n in names: 808 if n not in self.outstanding: 809 # the build is finished, and we have results 810 code, text = self.results[n] 811 t = builder.Results[code] 812 if text: 813 t += " (%s)" % " ".join(text) 814 elif self.builds[n]: 815 t = self.currentStep[n] or "building" 816 if self.ETA[n]: 817 t += " [ETA %ds]" % (self.ETA[n] - now()) 818 else: 819 t = "no build" 820 self.announce("%s: %s" % (n, t)) 821 self.announce("") 822 except Exception: 823 log.err(None, "printing status")
824
825 - def statusDone(self):
826 if self.printloop: 827 self.printloop.stop() 828 print "All Builds Complete" 829 # TODO: include a URL for all failing builds 830 names = self.buildRequests.keys() 831 names.sort() 832 happy = True 833 for n in names: 834 code, text = self.results[n] 835 t = "%s: %s" % (n, builder.Results[code]) 836 if text: 837 t += " (%s)" % " ".join(text) 838 print t 839 if code != builder.SUCCESS: 840 happy = False 841 842 if happy: 843 self.exitcode = 0 844 else: 845 self.exitcode = 1 846 self.running.callback(self.exitcode)
847
848 - def getAvailableBuilderNames(self):
849 # This logs into the master using the PB protocol to 850 # get the names of the configured builders that can 851 # be used for the --builder argument 852 if self.connect == "pb": 853 user = self.getopt("username") 854 passwd = self.getopt("passwd") 855 master = self.getopt("master") 856 tryhost, tryport = master.split(":") 857 tryport = int(tryport) 858 f = pb.PBClientFactory() 859 d = f.login(credentials.UsernamePassword(user, passwd)) 860 reactor.connectTCP(tryhost, tryport, f) 861 d.addCallback(self._getBuilderNames, self._getBuilderNames2) 862 return d 863 if self.connect == "ssh": 864 print "Cannot get availble builders over ssh." 865 sys.exit(1) 866 raise RuntimeError( 867 "unknown connecttype '%s', should be 'pb'" % self.connect)
868
869 - def _getBuilderNames(self, remote, output):
870 # Older schedulers won't support the properties argument, so only 871 # attempt to send them when necessary. 872 properties = self.config.get('properties', {}) 873 if properties: 874 d = remote.callRemote("getAvailableBuilderNames", properties) 875 else: 876 d = remote.callRemote("getAvailableBuilderNames") 877 d.addCallback(self._getBuilderNames2) 878 return d
879
880 - def _getBuilderNames2(self, buildernames):
881 print "The following builders are available for the try scheduler: " 882 for buildername in buildernames: 883 print buildername
884
885 - def announce(self, message):
886 if not self.quiet: 887 print message
888
889 - def run(self):
890 # we can't do spawnProcess until we're inside reactor.run(), so get 891 # funky 892 print "using '%s' connect method" % self.connect 893 self.exitcode = 0 894 d = fireEventually(None) 895 if bool(self.config.get("get-builder-names")): 896 d.addCallback(lambda res: self.getAvailableBuilderNames()) 897 else: 898 d.addCallback(lambda res: self.createJob()) 899 d.addCallback(lambda res: self.announce("job created")) 900 deliver = self.deliverJob 901 if bool(self.config.get("dryrun")): 902 deliver = self.fakeDeliverJob 903 d.addCallback(lambda res: deliver()) 904 d.addCallback(lambda res: self.announce("job has been delivered")) 905 d.addCallback(lambda res: self.getStatus()) 906 d.addErrback(self.trapSystemExit) 907 d.addErrback(log.err) 908 d.addCallback(self.cleanup) 909 d.addCallback(lambda res: reactor.stop()) 910 911 reactor.run() 912 sys.exit(self.exitcode)
913
914 - def trapSystemExit(self, why):
915 why.trap(SystemExit) 916 self.exitcode = why.value.code
917
918 - def cleanup(self, res=None):
919 if self.buildsetStatus: 920 self.buildsetStatus.broker.transport.loseConnection()
921