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