xref: /openbmc/openbmc/poky/bitbake/lib/bb/command.py (revision 169d7bcc)
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