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