1""" 2BitBake 'Command' module 3 4Provide an interface to interact with the bitbake server through 'commands' 5""" 6 7# Copyright (C) 2006-2007 Richard Purdie 8# 9# SPDX-License-Identifier: GPL-2.0-only 10# 11 12""" 13The bitbake server takes 'commands' from its UI/commandline. 14Commands are either synchronous or asynchronous. 15Async commands return data to the client in the form of events. 16Sync commands must only return data through the function return value 17and must not trigger events, directly or indirectly. 18Commands are queued in a CommandQueue 19""" 20 21from collections import OrderedDict, defaultdict 22 23import io 24import bb.event 25import bb.cooker 26import bb.remotedata 27 28class DataStoreConnectionHandle(object): 29 def __init__(self, dsindex=0): 30 self.dsindex = dsindex 31 32class CommandCompleted(bb.event.Event): 33 pass 34 35class CommandExit(bb.event.Event): 36 def __init__(self, exitcode): 37 bb.event.Event.__init__(self) 38 self.exitcode = int(exitcode) 39 40class CommandFailed(CommandExit): 41 def __init__(self, message): 42 self.error = message 43 CommandExit.__init__(self, 1) 44 def __str__(self): 45 return "Command execution failed: %s" % self.error 46 47class CommandError(Exception): 48 pass 49 50class Command: 51 """ 52 A queue of asynchronous commands for bitbake 53 """ 54 def __init__(self, cooker, process_server): 55 self.cooker = cooker 56 self.cmds_sync = CommandsSync() 57 self.cmds_async = CommandsAsync() 58 self.remotedatastores = None 59 60 self.process_server = process_server 61 # Access with locking using process_server.{get/set/clear}_async_cmd() 62 self.currentAsyncCommand = None 63 64 def runCommand(self, commandline, process_server, ro_only=False): 65 command = commandline.pop(0) 66 67 # Ensure cooker is ready for commands 68 if command not in ["updateConfig", "setFeatures", "ping"]: 69 try: 70 self.cooker.init_configdata() 71 if not self.remotedatastores: 72 self.remotedatastores = bb.remotedata.RemoteDatastores(self.cooker) 73 except (Exception, SystemExit) as exc: 74 import traceback 75 if isinstance(exc, bb.BBHandledException): 76 # We need to start returning real exceptions here. Until we do, we can't 77 # tell if an exception is an instance of bb.BBHandledException 78 return None, "bb.BBHandledException()\n" + traceback.format_exc() 79 return None, traceback.format_exc() 80 81 if hasattr(CommandsSync, command): 82 # Can run synchronous commands straight away 83 command_method = getattr(self.cmds_sync, command) 84 if ro_only: 85 if not hasattr(command_method, 'readonly') or not getattr(command_method, 'readonly'): 86 return None, "Not able to execute not readonly commands in readonly mode" 87 try: 88 if getattr(command_method, 'needconfig', True): 89 self.cooker.updateCacheSync() 90 result = command_method(self, commandline) 91 except CommandError as exc: 92 return None, exc.args[0] 93 except (Exception, SystemExit) as exc: 94 import traceback 95 if isinstance(exc, bb.BBHandledException): 96 # We need to start returning real exceptions here. Until we do, we can't 97 # tell if an exception is an instance of bb.BBHandledException 98 return None, "bb.BBHandledException()\n" + traceback.format_exc() 99 return None, traceback.format_exc() 100 else: 101 return result, None 102 if command not in CommandsAsync.__dict__: 103 return None, "No such command" 104 if not process_server.set_async_cmd((command, commandline)): 105 return None, "Busy (%s in progress)" % self.process_server.get_async_cmd()[0] 106 self.cooker.idleCallBackRegister(self.runAsyncCommand, process_server) 107 return True, None 108 109 def runAsyncCommand(self, _, process_server, halt): 110 try: 111 if self.cooker.state in (bb.cooker.state.error, bb.cooker.state.shutdown, bb.cooker.state.forceshutdown): 112 # updateCache will trigger a shutdown of the parser 113 # and then raise BBHandledException triggering an exit 114 self.cooker.updateCache() 115 return bb.server.process.idleFinish("Cooker in error state") 116 cmd = process_server.get_async_cmd() 117 if cmd is not None: 118 (command, options) = cmd 119 commandmethod = getattr(CommandsAsync, command) 120 needcache = getattr( commandmethod, "needcache" ) 121 if needcache and self.cooker.state != bb.cooker.state.running: 122 self.cooker.updateCache() 123 return True 124 else: 125 commandmethod(self.cmds_async, self, options) 126 return False 127 else: 128 return bb.server.process.idleFinish("Nothing to do, no async command?") 129 except KeyboardInterrupt as exc: 130 return bb.server.process.idleFinish("Interrupted") 131 except SystemExit as exc: 132 arg = exc.args[0] 133 if isinstance(arg, str): 134 return bb.server.process.idleFinish(arg) 135 else: 136 return bb.server.process.idleFinish("Exited with %s" % arg) 137 except Exception as exc: 138 import traceback 139 if isinstance(exc, bb.BBHandledException): 140 return bb.server.process.idleFinish("") 141 else: 142 return bb.server.process.idleFinish(traceback.format_exc()) 143 144 def finishAsyncCommand(self, msg=None, code=None): 145 if msg or msg == "": 146 bb.event.fire(CommandFailed(msg), self.cooker.data) 147 elif code: 148 bb.event.fire(CommandExit(code), self.cooker.data) 149 else: 150 bb.event.fire(CommandCompleted(), self.cooker.data) 151 self.cooker.finishcommand() 152 self.process_server.clear_async_cmd() 153 154 def reset(self): 155 if self.remotedatastores: 156 self.remotedatastores = bb.remotedata.RemoteDatastores(self.cooker) 157 158class CommandsSync: 159 """ 160 A class of synchronous commands 161 These should run quickly so as not to hurt interactive performance. 162 These must not influence any running synchronous command. 163 """ 164 165 def ping(self, command, params): 166 """ 167 Allow a UI to check the server is still alive 168 """ 169 return "Still alive!" 170 ping.needconfig = False 171 ping.readonly = True 172 173 def stateShutdown(self, command, params): 174 """ 175 Trigger cooker 'shutdown' mode 176 """ 177 command.cooker.shutdown(False) 178 179 def stateForceShutdown(self, command, params): 180 """ 181 Stop the cooker 182 """ 183 command.cooker.shutdown(True) 184 185 def getAllKeysWithFlags(self, command, params): 186 """ 187 Returns a dump of the global state. Call with 188 variable flags to be retrieved as params. 189 """ 190 flaglist = params[0] 191 return command.cooker.getAllKeysWithFlags(flaglist) 192 getAllKeysWithFlags.readonly = True 193 194 def getVariable(self, command, params): 195 """ 196 Read the value of a variable from data 197 """ 198 varname = params[0] 199 expand = True 200 if len(params) > 1: 201 expand = (params[1] == "True") 202 203 return command.cooker.data.getVar(varname, expand) 204 getVariable.readonly = True 205 206 def setVariable(self, command, params): 207 """ 208 Set the value of variable in data 209 """ 210 varname = params[0] 211 value = str(params[1]) 212 command.cooker.extraconfigdata[varname] = value 213 command.cooker.data.setVar(varname, value) 214 215 def getSetVariable(self, command, params): 216 """ 217 Read the value of a variable from data and set it into the datastore 218 which effectively expands and locks the value. 219 """ 220 varname = params[0] 221 result = self.getVariable(command, params) 222 command.cooker.data.setVar(varname, result) 223 return result 224 225 def setConfig(self, command, params): 226 """ 227 Set the value of variable in configuration 228 """ 229 varname = params[0] 230 value = str(params[1]) 231 setattr(command.cooker.configuration, varname, value) 232 233 def enableDataTracking(self, command, params): 234 """ 235 Enable history tracking for variables 236 """ 237 command.cooker.enableDataTracking() 238 239 def disableDataTracking(self, command, params): 240 """ 241 Disable history tracking for variables 242 """ 243 command.cooker.disableDataTracking() 244 245 def setPrePostConfFiles(self, command, params): 246 prefiles = params[0].split() 247 postfiles = params[1].split() 248 command.cooker.configuration.prefile = prefiles 249 command.cooker.configuration.postfile = postfiles 250 setPrePostConfFiles.needconfig = False 251 252 def matchFile(self, command, params): 253 fMatch = params[0] 254 try: 255 mc = params[0] 256 except IndexError: 257 mc = '' 258 return command.cooker.matchFile(fMatch, mc) 259 matchFile.needconfig = False 260 261 def getUIHandlerNum(self, command, params): 262 return bb.event.get_uihandler() 263 getUIHandlerNum.needconfig = False 264 getUIHandlerNum.readonly = True 265 266 def setEventMask(self, command, params): 267 handlerNum = params[0] 268 llevel = params[1] 269 debug_domains = params[2] 270 mask = params[3] 271 return bb.event.set_UIHmask(handlerNum, llevel, debug_domains, mask) 272 setEventMask.needconfig = False 273 setEventMask.readonly = True 274 275 def setFeatures(self, command, params): 276 """ 277 Set the cooker features to include the passed list of features 278 """ 279 features = params[0] 280 command.cooker.setFeatures(features) 281 setFeatures.needconfig = False 282 # although we change the internal state of the cooker, this is transparent since 283 # we always take and leave the cooker in state.initial 284 setFeatures.readonly = True 285 286 def updateConfig(self, command, params): 287 options = params[0] 288 environment = params[1] 289 cmdline = params[2] 290 command.cooker.updateConfigOpts(options, environment, cmdline) 291 updateConfig.needconfig = False 292 293 def parseConfiguration(self, command, params): 294 """Instruct bitbake to parse its configuration 295 NOTE: it is only necessary to call this if you aren't calling any normal action 296 (otherwise parsing is taken care of automatically) 297 """ 298 command.cooker.parseConfiguration() 299 parseConfiguration.needconfig = False 300 301 def getLayerPriorities(self, command, params): 302 command.cooker.parseConfiguration() 303 ret = [] 304 # regex objects cannot be marshalled by xmlrpc 305 for collection, pattern, regex, pri in command.cooker.bbfile_config_priorities: 306 ret.append((collection, pattern, regex.pattern, pri)) 307 return ret 308 getLayerPriorities.readonly = True 309 310 def revalidateCaches(self, command, params): 311 """Called by UI clients when metadata may have changed""" 312 command.cooker.revalidateCaches() 313 parseConfiguration.needconfig = False 314 315 def getRecipes(self, command, params): 316 try: 317 mc = params[0] 318 except IndexError: 319 mc = '' 320 return list(command.cooker.recipecaches[mc].pkg_pn.items()) 321 getRecipes.readonly = True 322 323 def getRecipeDepends(self, command, params): 324 try: 325 mc = params[0] 326 except IndexError: 327 mc = '' 328 return list(command.cooker.recipecaches[mc].deps.items()) 329 getRecipeDepends.readonly = True 330 331 def getRecipeVersions(self, command, params): 332 try: 333 mc = params[0] 334 except IndexError: 335 mc = '' 336 return command.cooker.recipecaches[mc].pkg_pepvpr 337 getRecipeVersions.readonly = True 338 339 def getRecipeProvides(self, command, params): 340 try: 341 mc = params[0] 342 except IndexError: 343 mc = '' 344 return command.cooker.recipecaches[mc].fn_provides 345 getRecipeProvides.readonly = True 346 347 def getRecipePackages(self, command, params): 348 try: 349 mc = params[0] 350 except IndexError: 351 mc = '' 352 return command.cooker.recipecaches[mc].packages 353 getRecipePackages.readonly = True 354 355 def getRecipePackagesDynamic(self, command, params): 356 try: 357 mc = params[0] 358 except IndexError: 359 mc = '' 360 return command.cooker.recipecaches[mc].packages_dynamic 361 getRecipePackagesDynamic.readonly = True 362 363 def getRProviders(self, command, params): 364 try: 365 mc = params[0] 366 except IndexError: 367 mc = '' 368 return command.cooker.recipecaches[mc].rproviders 369 getRProviders.readonly = True 370 371 def getRuntimeDepends(self, command, params): 372 ret = [] 373 try: 374 mc = params[0] 375 except IndexError: 376 mc = '' 377 rundeps = command.cooker.recipecaches[mc].rundeps 378 for key, value in rundeps.items(): 379 if isinstance(value, defaultdict): 380 value = dict(value) 381 ret.append((key, value)) 382 return ret 383 getRuntimeDepends.readonly = True 384 385 def getRuntimeRecommends(self, command, params): 386 ret = [] 387 try: 388 mc = params[0] 389 except IndexError: 390 mc = '' 391 runrecs = command.cooker.recipecaches[mc].runrecs 392 for key, value in runrecs.items(): 393 if isinstance(value, defaultdict): 394 value = dict(value) 395 ret.append((key, value)) 396 return ret 397 getRuntimeRecommends.readonly = True 398 399 def getRecipeInherits(self, command, params): 400 try: 401 mc = params[0] 402 except IndexError: 403 mc = '' 404 return command.cooker.recipecaches[mc].inherits 405 getRecipeInherits.readonly = True 406 407 def getBbFilePriority(self, command, params): 408 try: 409 mc = params[0] 410 except IndexError: 411 mc = '' 412 return command.cooker.recipecaches[mc].bbfile_priority 413 getBbFilePriority.readonly = True 414 415 def getDefaultPreference(self, command, params): 416 try: 417 mc = params[0] 418 except IndexError: 419 mc = '' 420 return command.cooker.recipecaches[mc].pkg_dp 421 getDefaultPreference.readonly = True 422 423 def getSkippedRecipes(self, command, params): 424 # Return list sorted by reverse priority order 425 import bb.cache 426 def sortkey(x): 427 vfn, _ = x 428 realfn, _, mc = bb.cache.virtualfn2realfn(vfn) 429 return (-command.cooker.collections[mc].calc_bbfile_priority(realfn)[0], vfn) 430 431 skipdict = OrderedDict(sorted(command.cooker.skiplist.items(), key=sortkey)) 432 return list(skipdict.items()) 433 getSkippedRecipes.readonly = True 434 435 def getOverlayedRecipes(self, command, params): 436 try: 437 mc = params[0] 438 except IndexError: 439 mc = '' 440 return list(command.cooker.collections[mc].overlayed.items()) 441 getOverlayedRecipes.readonly = True 442 443 def getFileAppends(self, command, params): 444 fn = params[0] 445 try: 446 mc = params[1] 447 except IndexError: 448 mc = '' 449 return command.cooker.collections[mc].get_file_appends(fn) 450 getFileAppends.readonly = True 451 452 def getAllAppends(self, command, params): 453 try: 454 mc = params[0] 455 except IndexError: 456 mc = '' 457 return command.cooker.collections[mc].bbappends 458 getAllAppends.readonly = True 459 460 def findProviders(self, command, params): 461 try: 462 mc = params[0] 463 except IndexError: 464 mc = '' 465 return command.cooker.findProviders(mc) 466 findProviders.readonly = True 467 468 def findBestProvider(self, command, params): 469 (mc, pn) = bb.runqueue.split_mc(params[0]) 470 return command.cooker.findBestProvider(pn, mc) 471 findBestProvider.readonly = True 472 473 def allProviders(self, command, params): 474 try: 475 mc = params[0] 476 except IndexError: 477 mc = '' 478 return list(bb.providers.allProviders(command.cooker.recipecaches[mc]).items()) 479 allProviders.readonly = True 480 481 def getRuntimeProviders(self, command, params): 482 rprovide = params[0] 483 try: 484 mc = params[1] 485 except IndexError: 486 mc = '' 487 all_p = bb.providers.getRuntimeProviders(command.cooker.recipecaches[mc], rprovide) 488 if all_p: 489 best = bb.providers.filterProvidersRunTime(all_p, rprovide, 490 command.cooker.data, 491 command.cooker.recipecaches[mc])[0][0] 492 else: 493 best = None 494 return all_p, best 495 getRuntimeProviders.readonly = True 496 497 def dataStoreConnectorCmd(self, command, params): 498 dsindex = params[0] 499 method = params[1] 500 args = params[2] 501 kwargs = params[3] 502 503 d = command.remotedatastores[dsindex] 504 ret = getattr(d, method)(*args, **kwargs) 505 506 if isinstance(ret, bb.data_smart.DataSmart): 507 idx = command.remotedatastores.store(ret) 508 return DataStoreConnectionHandle(idx) 509 510 return ret 511 512 def dataStoreConnectorVarHistCmd(self, command, params): 513 dsindex = params[0] 514 method = params[1] 515 args = params[2] 516 kwargs = params[3] 517 518 d = command.remotedatastores[dsindex].varhistory 519 return getattr(d, method)(*args, **kwargs) 520 521 def dataStoreConnectorVarHistCmdEmit(self, command, params): 522 dsindex = params[0] 523 var = params[1] 524 oval = params[2] 525 val = params[3] 526 d = command.remotedatastores[params[4]] 527 528 o = io.StringIO() 529 command.remotedatastores[dsindex].varhistory.emit(var, oval, val, o, d) 530 return o.getvalue() 531 532 def dataStoreConnectorIncHistCmd(self, command, params): 533 dsindex = params[0] 534 method = params[1] 535 args = params[2] 536 kwargs = params[3] 537 538 d = command.remotedatastores[dsindex].inchistory 539 return getattr(d, method)(*args, **kwargs) 540 541 def dataStoreConnectorRelease(self, command, params): 542 dsindex = params[0] 543 if dsindex <= 0: 544 raise CommandError('dataStoreConnectorRelease: invalid index %d' % dsindex) 545 command.remotedatastores.release(dsindex) 546 547 def parseRecipeFile(self, command, params): 548 """ 549 Parse the specified recipe file (with or without bbappends) 550 and return a datastore object representing the environment 551 for the recipe. 552 """ 553 virtualfn = params[0] 554 (fn, cls, mc) = bb.cache.virtualfn2realfn(virtualfn) 555 appends = params[1] 556 appendlist = params[2] 557 if len(params) > 3: 558 config_data = command.remotedatastores[params[3]] 559 else: 560 config_data = None 561 562 if appends: 563 if appendlist is not None: 564 appendfiles = appendlist 565 else: 566 appendfiles = command.cooker.collections[mc].get_file_appends(fn) 567 else: 568 appendfiles = [] 569 layername = command.cooker.collections[mc].calc_bbfile_priority(fn)[2] 570 # We are calling bb.cache locally here rather than on the server, 571 # but that's OK because it doesn't actually need anything from 572 # the server barring the global datastore (which we have a remote 573 # version of) 574 if config_data: 575 # We have to use a different function here if we're passing in a datastore 576 # NOTE: we took a copy above, so we don't do it here again 577 envdata = command.cooker.databuilder._parse_recipe(config_data, fn, appendfiles, mc, layername)[cls] 578 else: 579 # Use the standard path 580 envdata = command.cooker.databuilder.parseRecipe(virtualfn, appendfiles, layername) 581 idx = command.remotedatastores.store(envdata) 582 return DataStoreConnectionHandle(idx) 583 parseRecipeFile.readonly = True 584 585class CommandsAsync: 586 """ 587 A class of asynchronous commands 588 These functions communicate via generated events. 589 Any function that requires metadata parsing should be here. 590 """ 591 592 def buildFile(self, command, params): 593 """ 594 Build a single specified .bb file 595 """ 596 bfile = params[0] 597 task = params[1] 598 if len(params) > 2: 599 internal = params[2] 600 else: 601 internal = False 602 603 if internal: 604 command.cooker.buildFileInternal(bfile, task, fireevents=False, quietlog=True) 605 else: 606 command.cooker.buildFile(bfile, task) 607 buildFile.needcache = False 608 609 def buildTargets(self, command, params): 610 """ 611 Build a set of targets 612 """ 613 pkgs_to_build = params[0] 614 task = params[1] 615 616 command.cooker.buildTargets(pkgs_to_build, task) 617 buildTargets.needcache = True 618 619 def generateDepTreeEvent(self, command, params): 620 """ 621 Generate an event containing the dependency information 622 """ 623 pkgs_to_build = params[0] 624 task = params[1] 625 626 command.cooker.generateDepTreeEvent(pkgs_to_build, task) 627 command.finishAsyncCommand() 628 generateDepTreeEvent.needcache = True 629 630 def generateDotGraph(self, command, params): 631 """ 632 Dump dependency information to disk as .dot files 633 """ 634 pkgs_to_build = params[0] 635 task = params[1] 636 637 command.cooker.generateDotGraphFiles(pkgs_to_build, task) 638 command.finishAsyncCommand() 639 generateDotGraph.needcache = True 640 641 def generateTargetsTree(self, command, params): 642 """ 643 Generate a tree of buildable targets. 644 If klass is provided ensure all recipes that inherit the class are 645 included in the package list. 646 If pkg_list provided use that list (plus any extras brought in by 647 klass) rather than generating a tree for all packages. 648 """ 649 klass = params[0] 650 pkg_list = params[1] 651 652 command.cooker.generateTargetsTree(klass, pkg_list) 653 command.finishAsyncCommand() 654 generateTargetsTree.needcache = True 655 656 def findConfigFiles(self, command, params): 657 """ 658 Find config files which provide appropriate values 659 for the passed configuration variable. i.e. MACHINE 660 """ 661 varname = params[0] 662 663 command.cooker.findConfigFiles(varname) 664 command.finishAsyncCommand() 665 findConfigFiles.needcache = False 666 667 def findFilesMatchingInDir(self, command, params): 668 """ 669 Find implementation files matching the specified pattern 670 in the requested subdirectory of a BBPATH 671 """ 672 pattern = params[0] 673 directory = params[1] 674 675 command.cooker.findFilesMatchingInDir(pattern, directory) 676 command.finishAsyncCommand() 677 findFilesMatchingInDir.needcache = False 678 679 def testCookerCommandEvent(self, command, params): 680 """ 681 Dummy command used by OEQA selftest to test tinfoil without IO 682 """ 683 pattern = params[0] 684 685 command.cooker.testCookerCommandEvent(pattern) 686 command.finishAsyncCommand() 687 testCookerCommandEvent.needcache = False 688 689 def findConfigFilePath(self, command, params): 690 """ 691 Find the path of the requested configuration file 692 """ 693 configfile = params[0] 694 695 command.cooker.findConfigFilePath(configfile) 696 command.finishAsyncCommand() 697 findConfigFilePath.needcache = False 698 699 def showVersions(self, command, params): 700 """ 701 Show the currently selected versions 702 """ 703 command.cooker.showVersions() 704 command.finishAsyncCommand() 705 showVersions.needcache = True 706 707 def showEnvironmentTarget(self, command, params): 708 """ 709 Print the environment of a target recipe 710 (needs the cache to work out which recipe to use) 711 """ 712 pkg = params[0] 713 714 command.cooker.showEnvironment(None, pkg) 715 command.finishAsyncCommand() 716 showEnvironmentTarget.needcache = True 717 718 def showEnvironment(self, command, params): 719 """ 720 Print the standard environment 721 or if specified the environment for a specified recipe 722 """ 723 bfile = params[0] 724 725 command.cooker.showEnvironment(bfile) 726 command.finishAsyncCommand() 727 showEnvironment.needcache = False 728 729 def parseFiles(self, command, params): 730 """ 731 Parse the .bb files 732 """ 733 command.cooker.updateCache() 734 command.finishAsyncCommand() 735 parseFiles.needcache = True 736 737 def compareRevisions(self, command, params): 738 """ 739 Parse the .bb files 740 """ 741 if bb.fetch.fetcher_compare_revisions(command.cooker.data): 742 command.finishAsyncCommand(code=1) 743 else: 744 command.finishAsyncCommand() 745 compareRevisions.needcache = True 746 747 def triggerEvent(self, command, params): 748 """ 749 Trigger a certain event 750 """ 751 event = params[0] 752 bb.event.fire(eval(event), command.cooker.data) 753 process_server.clear_async_cmd() 754 triggerEvent.needcache = False 755 756 def resetCooker(self, command, params): 757 """ 758 Reset the cooker to its initial state, thus forcing a reparse for 759 any async command that has the needcache property set to True 760 """ 761 command.cooker.reset() 762 command.finishAsyncCommand() 763 resetCooker.needcache = False 764 765 def clientComplete(self, command, params): 766 """ 767 Do the right thing when the controlling client exits 768 """ 769 command.cooker.clientComplete() 770 command.finishAsyncCommand() 771 clientComplete.needcache = False 772 773 def findSigInfo(self, command, params): 774 """ 775 Find signature info files via the signature generator 776 """ 777 (mc, pn) = bb.runqueue.split_mc(params[0]) 778 taskname = params[1] 779 sigs = params[2] 780 bb.siggen.check_siggen_version(bb.siggen) 781 res = bb.siggen.find_siginfo(pn, taskname, sigs, command.cooker.databuilder.mcdata[mc]) 782 bb.event.fire(bb.event.FindSigInfoResult(res), command.cooker.databuilder.mcdata[mc]) 783 command.finishAsyncCommand() 784 findSigInfo.needcache = False 785 786 def getTaskSignatures(self, command, params): 787 res = command.cooker.getTaskSignatures(params[0], params[1]) 788 bb.event.fire(bb.event.GetTaskSignatureResult(res), command.cooker.data) 789 command.finishAsyncCommand() 790 getTaskSignatures.needcache = True 791