command.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. """
  2. BitBake 'Command' module
  3. Provide an interface to interact with the bitbake server through 'commands'
  4. """
  5. # Copyright (C) 2006-2007 Richard Purdie
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License version 2 as
  9. # published by the Free Software Foundation.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License along
  17. # with this program; if not, write to the Free Software Foundation, Inc.,
  18. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  19. """
  20. The bitbake server takes 'commands' from its UI/commandline.
  21. Commands are either synchronous or asynchronous.
  22. Async commands return data to the client in the form of events.
  23. Sync commands must only return data through the function return value
  24. and must not trigger events, directly or indirectly.
  25. Commands are queued in a CommandQueue
  26. """
  27. from collections import OrderedDict, defaultdict
  28. import bb.event
  29. import bb.cooker
  30. import bb.remotedata
  31. class DataStoreConnectionHandle(object):
  32. def __init__(self, dsindex=0):
  33. self.dsindex = dsindex
  34. class CommandCompleted(bb.event.Event):
  35. pass
  36. class CommandExit(bb.event.Event):
  37. def __init__(self, exitcode):
  38. bb.event.Event.__init__(self)
  39. self.exitcode = int(exitcode)
  40. class CommandFailed(CommandExit):
  41. def __init__(self, message):
  42. self.error = message
  43. CommandExit.__init__(self, 1)
  44. class CommandError(Exception):
  45. pass
  46. class Command:
  47. """
  48. A queue of asynchronous commands for bitbake
  49. """
  50. def __init__(self, cooker):
  51. self.cooker = cooker
  52. self.cmds_sync = CommandsSync()
  53. self.cmds_async = CommandsAsync()
  54. self.remotedatastores = bb.remotedata.RemoteDatastores(cooker)
  55. # FIXME Add lock for this
  56. self.currentAsyncCommand = None
  57. def runCommand(self, commandline, ro_only = False):
  58. command = commandline.pop(0)
  59. if hasattr(CommandsSync, command):
  60. # Can run synchronous commands straight away
  61. command_method = getattr(self.cmds_sync, command)
  62. if ro_only:
  63. if not hasattr(command_method, 'readonly') or False == getattr(command_method, 'readonly'):
  64. return None, "Not able to execute not readonly commands in readonly mode"
  65. try:
  66. if getattr(command_method, 'needconfig', False):
  67. self.cooker.updateCacheSync()
  68. result = command_method(self, commandline)
  69. except CommandError as exc:
  70. return None, exc.args[0]
  71. except (Exception, SystemExit):
  72. import traceback
  73. return None, traceback.format_exc()
  74. else:
  75. return result, None
  76. if self.currentAsyncCommand is not None:
  77. return None, "Busy (%s in progress)" % self.currentAsyncCommand[0]
  78. if command not in CommandsAsync.__dict__:
  79. return None, "No such command"
  80. self.currentAsyncCommand = (command, commandline)
  81. self.cooker.configuration.server_register_idlecallback(self.cooker.runCommands, self.cooker)
  82. return True, None
  83. def runAsyncCommand(self):
  84. try:
  85. if self.cooker.state in (bb.cooker.state.error, bb.cooker.state.shutdown, bb.cooker.state.forceshutdown):
  86. # updateCache will trigger a shutdown of the parser
  87. # and then raise BBHandledException triggering an exit
  88. self.cooker.updateCache()
  89. return False
  90. if self.currentAsyncCommand is not None:
  91. (command, options) = self.currentAsyncCommand
  92. commandmethod = getattr(CommandsAsync, command)
  93. needcache = getattr( commandmethod, "needcache" )
  94. if needcache and self.cooker.state != bb.cooker.state.running:
  95. self.cooker.updateCache()
  96. return True
  97. else:
  98. commandmethod(self.cmds_async, self, options)
  99. return False
  100. else:
  101. return False
  102. except KeyboardInterrupt as exc:
  103. self.finishAsyncCommand("Interrupted")
  104. return False
  105. except SystemExit as exc:
  106. arg = exc.args[0]
  107. if isinstance(arg, str):
  108. self.finishAsyncCommand(arg)
  109. else:
  110. self.finishAsyncCommand("Exited with %s" % arg)
  111. return False
  112. except Exception as exc:
  113. import traceback
  114. if isinstance(exc, bb.BBHandledException):
  115. self.finishAsyncCommand("")
  116. else:
  117. self.finishAsyncCommand(traceback.format_exc())
  118. return False
  119. def finishAsyncCommand(self, msg=None, code=None):
  120. if msg or msg == "":
  121. bb.event.fire(CommandFailed(msg), self.cooker.data)
  122. elif code:
  123. bb.event.fire(CommandExit(code), self.cooker.data)
  124. else:
  125. bb.event.fire(CommandCompleted(), self.cooker.data)
  126. self.currentAsyncCommand = None
  127. self.cooker.finishcommand()
  128. def split_mc_pn(pn):
  129. if pn.startswith("multiconfig:"):
  130. _, mc, pn = pn.split(":", 2)
  131. return (mc, pn)
  132. return ('', pn)
  133. class CommandsSync:
  134. """
  135. A class of synchronous commands
  136. These should run quickly so as not to hurt interactive performance.
  137. These must not influence any running synchronous command.
  138. """
  139. def stateShutdown(self, command, params):
  140. """
  141. Trigger cooker 'shutdown' mode
  142. """
  143. command.cooker.shutdown(False)
  144. def stateForceShutdown(self, command, params):
  145. """
  146. Stop the cooker
  147. """
  148. command.cooker.shutdown(True)
  149. def getAllKeysWithFlags(self, command, params):
  150. """
  151. Returns a dump of the global state. Call with
  152. variable flags to be retrieved as params.
  153. """
  154. flaglist = params[0]
  155. return command.cooker.getAllKeysWithFlags(flaglist)
  156. getAllKeysWithFlags.readonly = True
  157. def getVariable(self, command, params):
  158. """
  159. Read the value of a variable from data
  160. """
  161. varname = params[0]
  162. expand = True
  163. if len(params) > 1:
  164. expand = (params[1] == "True")
  165. return command.cooker.data.getVar(varname, expand)
  166. getVariable.readonly = True
  167. def setVariable(self, command, params):
  168. """
  169. Set the value of variable in data
  170. """
  171. varname = params[0]
  172. value = str(params[1])
  173. command.cooker.extraconfigdata[varname] = value
  174. command.cooker.data.setVar(varname, value)
  175. def getSetVariable(self, command, params):
  176. """
  177. Read the value of a variable from data and set it into the datastore
  178. which effectively expands and locks the value.
  179. """
  180. varname = params[0]
  181. result = self.getVariable(command, params)
  182. command.cooker.data.setVar(varname, result)
  183. return result
  184. def setConfig(self, command, params):
  185. """
  186. Set the value of variable in configuration
  187. """
  188. varname = params[0]
  189. value = str(params[1])
  190. setattr(command.cooker.configuration, varname, value)
  191. def enableDataTracking(self, command, params):
  192. """
  193. Enable history tracking for variables
  194. """
  195. command.cooker.enableDataTracking()
  196. def disableDataTracking(self, command, params):
  197. """
  198. Disable history tracking for variables
  199. """
  200. command.cooker.disableDataTracking()
  201. def setPrePostConfFiles(self, command, params):
  202. prefiles = params[0].split()
  203. postfiles = params[1].split()
  204. command.cooker.configuration.prefile = prefiles
  205. command.cooker.configuration.postfile = postfiles
  206. setPrePostConfFiles.needconfig = False
  207. def matchFile(self, command, params):
  208. fMatch = params[0]
  209. return command.cooker.matchFile(fMatch)
  210. matchFile.needconfig = False
  211. def getUIHandlerNum(self, command, params):
  212. return bb.event.get_uihandler()
  213. getUIHandlerNum.needconfig = False
  214. getUIHandlerNum.readonly = True
  215. def setEventMask(self, command, params):
  216. handlerNum = params[0]
  217. llevel = params[1]
  218. debug_domains = params[2]
  219. mask = params[3]
  220. return bb.event.set_UIHmask(handlerNum, llevel, debug_domains, mask)
  221. setEventMask.needconfig = False
  222. setEventMask.readonly = True
  223. def setFeatures(self, command, params):
  224. """
  225. Set the cooker features to include the passed list of features
  226. """
  227. features = params[0]
  228. command.cooker.setFeatures(features)
  229. setFeatures.needconfig = False
  230. # although we change the internal state of the cooker, this is transparent since
  231. # we always take and leave the cooker in state.initial
  232. setFeatures.readonly = True
  233. def updateConfig(self, command, params):
  234. options = params[0]
  235. environment = params[1]
  236. cmdline = params[2]
  237. command.cooker.updateConfigOpts(options, environment, cmdline)
  238. updateConfig.needconfig = False
  239. def parseConfiguration(self, command, params):
  240. """Instruct bitbake to parse its configuration
  241. NOTE: it is only necessary to call this if you aren't calling any normal action
  242. (otherwise parsing is taken care of automatically)
  243. """
  244. command.cooker.parseConfiguration()
  245. parseConfiguration.needconfig = False
  246. def getLayerPriorities(self, command, params):
  247. ret = []
  248. # regex objects cannot be marshalled by xmlrpc
  249. for collection, pattern, regex, pri in command.cooker.bbfile_config_priorities:
  250. ret.append((collection, pattern, regex.pattern, pri))
  251. return ret
  252. getLayerPriorities.readonly = True
  253. def getRecipes(self, command, params):
  254. try:
  255. mc = params[0]
  256. except IndexError:
  257. mc = ''
  258. return list(command.cooker.recipecaches[mc].pkg_pn.items())
  259. getRecipes.readonly = True
  260. def getRecipeDepends(self, command, params):
  261. try:
  262. mc = params[0]
  263. except IndexError:
  264. mc = ''
  265. return list(command.cooker.recipecaches[mc].deps.items())
  266. getRecipeDepends.readonly = True
  267. def getRecipeVersions(self, command, params):
  268. try:
  269. mc = params[0]
  270. except IndexError:
  271. mc = ''
  272. return command.cooker.recipecaches[mc].pkg_pepvpr
  273. getRecipeVersions.readonly = True
  274. def getRuntimeDepends(self, command, params):
  275. ret = []
  276. try:
  277. mc = params[0]
  278. except IndexError:
  279. mc = ''
  280. rundeps = command.cooker.recipecaches[mc].rundeps
  281. for key, value in rundeps.items():
  282. if isinstance(value, defaultdict):
  283. value = dict(value)
  284. ret.append((key, value))
  285. return ret
  286. getRuntimeDepends.readonly = True
  287. def getRuntimeRecommends(self, command, params):
  288. ret = []
  289. try:
  290. mc = params[0]
  291. except IndexError:
  292. mc = ''
  293. runrecs = command.cooker.recipecaches[mc].runrecs
  294. for key, value in runrecs.items():
  295. if isinstance(value, defaultdict):
  296. value = dict(value)
  297. ret.append((key, value))
  298. return ret
  299. getRuntimeRecommends.readonly = True
  300. def getRecipeInherits(self, command, params):
  301. try:
  302. mc = params[0]
  303. except IndexError:
  304. mc = ''
  305. return command.cooker.recipecaches[mc].inherits
  306. getRecipeInherits.readonly = True
  307. def getBbFilePriority(self, command, params):
  308. try:
  309. mc = params[0]
  310. except IndexError:
  311. mc = ''
  312. return command.cooker.recipecaches[mc].bbfile_priority
  313. getBbFilePriority.readonly = True
  314. def getDefaultPreference(self, command, params):
  315. try:
  316. mc = params[0]
  317. except IndexError:
  318. mc = ''
  319. return command.cooker.recipecaches[mc].pkg_dp
  320. getDefaultPreference.readonly = True
  321. def getSkippedRecipes(self, command, params):
  322. # Return list sorted by reverse priority order
  323. import bb.cache
  324. skipdict = OrderedDict(sorted(command.cooker.skiplist.items(),
  325. key=lambda x: (-command.cooker.collection.calc_bbfile_priority(bb.cache.virtualfn2realfn(x[0])[0]), x[0])))
  326. return list(skipdict.items())
  327. getSkippedRecipes.readonly = True
  328. def getOverlayedRecipes(self, command, params):
  329. return list(command.cooker.collection.overlayed.items())
  330. getOverlayedRecipes.readonly = True
  331. def getFileAppends(self, command, params):
  332. fn = params[0]
  333. return command.cooker.collection.get_file_appends(fn)
  334. getFileAppends.readonly = True
  335. def getAllAppends(self, command, params):
  336. return command.cooker.collection.bbappends
  337. getAllAppends.readonly = True
  338. def findProviders(self, command, params):
  339. return command.cooker.findProviders()
  340. findProviders.readonly = True
  341. def findBestProvider(self, command, params):
  342. (mc, pn) = split_mc_pn(params[0])
  343. return command.cooker.findBestProvider(pn, mc)
  344. findBestProvider.readonly = True
  345. def allProviders(self, command, params):
  346. try:
  347. mc = params[0]
  348. except IndexError:
  349. mc = ''
  350. return list(bb.providers.allProviders(command.cooker.recipecaches[mc]).items())
  351. allProviders.readonly = True
  352. def getRuntimeProviders(self, command, params):
  353. rprovide = params[0]
  354. try:
  355. mc = params[1]
  356. except IndexError:
  357. mc = ''
  358. all_p = bb.providers.getRuntimeProviders(command.cooker.recipecaches[mc], rprovide)
  359. if all_p:
  360. best = bb.providers.filterProvidersRunTime(all_p, rprovide,
  361. command.cooker.data,
  362. command.cooker.recipecaches[mc])[0][0]
  363. else:
  364. best = None
  365. return all_p, best
  366. getRuntimeProviders.readonly = True
  367. def dataStoreConnectorFindVar(self, command, params):
  368. dsindex = params[0]
  369. name = params[1]
  370. datastore = command.remotedatastores[dsindex]
  371. value, overridedata = datastore._findVar(name)
  372. if value:
  373. content = value.get('_content', None)
  374. if isinstance(content, bb.data_smart.DataSmart):
  375. # Value is a datastore (e.g. BB_ORIGENV) - need to handle this carefully
  376. idx = command.remotedatastores.check_store(content, True)
  377. return {'_content': DataStoreConnectionHandle(idx),
  378. '_connector_origtype': 'DataStoreConnectionHandle',
  379. '_connector_overrides': overridedata}
  380. elif isinstance(content, set):
  381. return {'_content': list(content),
  382. '_connector_origtype': 'set',
  383. '_connector_overrides': overridedata}
  384. else:
  385. value['_connector_overrides'] = overridedata
  386. else:
  387. value = {}
  388. value['_connector_overrides'] = overridedata
  389. return value
  390. dataStoreConnectorFindVar.readonly = True
  391. def dataStoreConnectorGetKeys(self, command, params):
  392. dsindex = params[0]
  393. datastore = command.remotedatastores[dsindex]
  394. return list(datastore.keys())
  395. dataStoreConnectorGetKeys.readonly = True
  396. def dataStoreConnectorGetVarHistory(self, command, params):
  397. dsindex = params[0]
  398. name = params[1]
  399. datastore = command.remotedatastores[dsindex]
  400. return datastore.varhistory.variable(name)
  401. dataStoreConnectorGetVarHistory.readonly = True
  402. def dataStoreConnectorExpandPythonRef(self, command, params):
  403. config_data_dict = params[0]
  404. varname = params[1]
  405. expr = params[2]
  406. config_data = command.remotedatastores.receive_datastore(config_data_dict)
  407. varparse = bb.data_smart.VariableParse(varname, config_data)
  408. return varparse.python_sub(expr)
  409. def dataStoreConnectorRelease(self, command, params):
  410. dsindex = params[0]
  411. if dsindex <= 0:
  412. raise CommandError('dataStoreConnectorRelease: invalid index %d' % dsindex)
  413. command.remotedatastores.release(dsindex)
  414. def dataStoreConnectorSetVarFlag(self, command, params):
  415. dsindex = params[0]
  416. name = params[1]
  417. flag = params[2]
  418. value = params[3]
  419. datastore = command.remotedatastores[dsindex]
  420. datastore.setVarFlag(name, flag, value)
  421. def dataStoreConnectorDelVar(self, command, params):
  422. dsindex = params[0]
  423. name = params[1]
  424. datastore = command.remotedatastores[dsindex]
  425. if len(params) > 2:
  426. flag = params[2]
  427. datastore.delVarFlag(name, flag)
  428. else:
  429. datastore.delVar(name)
  430. def dataStoreConnectorRenameVar(self, command, params):
  431. dsindex = params[0]
  432. name = params[1]
  433. newname = params[2]
  434. datastore = command.remotedatastores[dsindex]
  435. datastore.renameVar(name, newname)
  436. def parseRecipeFile(self, command, params):
  437. """
  438. Parse the specified recipe file (with or without bbappends)
  439. and return a datastore object representing the environment
  440. for the recipe.
  441. """
  442. fn = params[0]
  443. appends = params[1]
  444. appendlist = params[2]
  445. if len(params) > 3:
  446. config_data_dict = params[3]
  447. config_data = command.remotedatastores.receive_datastore(config_data_dict)
  448. else:
  449. config_data = None
  450. if appends:
  451. if appendlist is not None:
  452. appendfiles = appendlist
  453. else:
  454. appendfiles = command.cooker.collection.get_file_appends(fn)
  455. else:
  456. appendfiles = []
  457. # We are calling bb.cache locally here rather than on the server,
  458. # but that's OK because it doesn't actually need anything from
  459. # the server barring the global datastore (which we have a remote
  460. # version of)
  461. if config_data:
  462. # We have to use a different function here if we're passing in a datastore
  463. # NOTE: we took a copy above, so we don't do it here again
  464. envdata = bb.cache.parse_recipe(config_data, fn, appendfiles)['']
  465. else:
  466. # Use the standard path
  467. parser = bb.cache.NoCache(command.cooker.databuilder)
  468. envdata = parser.loadDataFull(fn, appendfiles)
  469. idx = command.remotedatastores.store(envdata)
  470. return DataStoreConnectionHandle(idx)
  471. parseRecipeFile.readonly = True
  472. class CommandsAsync:
  473. """
  474. A class of asynchronous commands
  475. These functions communicate via generated events.
  476. Any function that requires metadata parsing should be here.
  477. """
  478. def buildFile(self, command, params):
  479. """
  480. Build a single specified .bb file
  481. """
  482. bfile = params[0]
  483. task = params[1]
  484. if len(params) > 2:
  485. internal = params[2]
  486. else:
  487. internal = False
  488. if internal:
  489. command.cooker.buildFileInternal(bfile, task, fireevents=False, quietlog=True)
  490. else:
  491. command.cooker.buildFile(bfile, task)
  492. buildFile.needcache = False
  493. def buildTargets(self, command, params):
  494. """
  495. Build a set of targets
  496. """
  497. pkgs_to_build = params[0]
  498. task = params[1]
  499. command.cooker.buildTargets(pkgs_to_build, task)
  500. buildTargets.needcache = True
  501. def generateDepTreeEvent(self, command, params):
  502. """
  503. Generate an event containing the dependency information
  504. """
  505. pkgs_to_build = params[0]
  506. task = params[1]
  507. command.cooker.generateDepTreeEvent(pkgs_to_build, task)
  508. command.finishAsyncCommand()
  509. generateDepTreeEvent.needcache = True
  510. def generateDotGraph(self, command, params):
  511. """
  512. Dump dependency information to disk as .dot files
  513. """
  514. pkgs_to_build = params[0]
  515. task = params[1]
  516. command.cooker.generateDotGraphFiles(pkgs_to_build, task)
  517. command.finishAsyncCommand()
  518. generateDotGraph.needcache = True
  519. def generateTargetsTree(self, command, params):
  520. """
  521. Generate a tree of buildable targets.
  522. If klass is provided ensure all recipes that inherit the class are
  523. included in the package list.
  524. If pkg_list provided use that list (plus any extras brought in by
  525. klass) rather than generating a tree for all packages.
  526. """
  527. klass = params[0]
  528. pkg_list = params[1]
  529. command.cooker.generateTargetsTree(klass, pkg_list)
  530. command.finishAsyncCommand()
  531. generateTargetsTree.needcache = True
  532. def findConfigFiles(self, command, params):
  533. """
  534. Find config files which provide appropriate values
  535. for the passed configuration variable. i.e. MACHINE
  536. """
  537. varname = params[0]
  538. command.cooker.findConfigFiles(varname)
  539. command.finishAsyncCommand()
  540. findConfigFiles.needcache = False
  541. def findFilesMatchingInDir(self, command, params):
  542. """
  543. Find implementation files matching the specified pattern
  544. in the requested subdirectory of a BBPATH
  545. """
  546. pattern = params[0]
  547. directory = params[1]
  548. command.cooker.findFilesMatchingInDir(pattern, directory)
  549. command.finishAsyncCommand()
  550. findFilesMatchingInDir.needcache = False
  551. def findConfigFilePath(self, command, params):
  552. """
  553. Find the path of the requested configuration file
  554. """
  555. configfile = params[0]
  556. command.cooker.findConfigFilePath(configfile)
  557. command.finishAsyncCommand()
  558. findConfigFilePath.needcache = False
  559. def showVersions(self, command, params):
  560. """
  561. Show the currently selected versions
  562. """
  563. command.cooker.showVersions()
  564. command.finishAsyncCommand()
  565. showVersions.needcache = True
  566. def showEnvironmentTarget(self, command, params):
  567. """
  568. Print the environment of a target recipe
  569. (needs the cache to work out which recipe to use)
  570. """
  571. pkg = params[0]
  572. command.cooker.showEnvironment(None, pkg)
  573. command.finishAsyncCommand()
  574. showEnvironmentTarget.needcache = True
  575. def showEnvironment(self, command, params):
  576. """
  577. Print the standard environment
  578. or if specified the environment for a specified recipe
  579. """
  580. bfile = params[0]
  581. command.cooker.showEnvironment(bfile)
  582. command.finishAsyncCommand()
  583. showEnvironment.needcache = False
  584. def parseFiles(self, command, params):
  585. """
  586. Parse the .bb files
  587. """
  588. command.cooker.updateCache()
  589. command.finishAsyncCommand()
  590. parseFiles.needcache = True
  591. def compareRevisions(self, command, params):
  592. """
  593. Parse the .bb files
  594. """
  595. if bb.fetch.fetcher_compare_revisions(command.cooker.data):
  596. command.finishAsyncCommand(code=1)
  597. else:
  598. command.finishAsyncCommand()
  599. compareRevisions.needcache = True
  600. def triggerEvent(self, command, params):
  601. """
  602. Trigger a certain event
  603. """
  604. event = params[0]
  605. bb.event.fire(eval(event), command.cooker.data)
  606. command.currentAsyncCommand = None
  607. triggerEvent.needcache = False
  608. def resetCooker(self, command, params):
  609. """
  610. Reset the cooker to its initial state, thus forcing a reparse for
  611. any async command that has the needcache property set to True
  612. """
  613. command.cooker.reset()
  614. command.finishAsyncCommand()
  615. resetCooker.needcache = False
  616. def clientComplete(self, command, params):
  617. """
  618. Do the right thing when the controlling client exits
  619. """
  620. command.cooker.clientComplete()
  621. command.finishAsyncCommand()
  622. clientComplete.needcache = False