1#!/usr/bin/env python 2from __future__ import division, print_function 3"""Code to run scripts that can wait for various things without messing up the main event loop 4(and thus starving the rest of your program). 5 6ScriptRunner allows your script to wait for the following: 7- wait for a given time interval using: yield waitMS(...) 8- run a slow computation as a background thread using waitThread 9- run a command via the keyword dispatcher using waitCmd 10- run multiple commands at the same time: 11 - start each command with startCmd, 12 - wait for one or more commands to finish using waitCmdVars 13- wait for a keyword variable to be set using waitKeyVar 14- wait for a sub-script by yielding it (i.e. yield subscript(...)); 15 the sub-script must contain a yield for this to work; if it has no yield then just call it directly 16 17An example is given as the test code at the end. 18 19Code comments: 20- Wait functions use a class to do all the work. This standardizes 21 some tricky internals (such as registering and deregistering 22 cancel functions) and allows the class to easily keep track 23 of private internal data. 24- The wait class is created but is not explicitly kept around. 25 Why doesn't it immediately vanish? Because the wait class registers a method as a completion callback. 26 As long as somebody has a pointer to that method then the instance is kept alive. 27- waitThread originally relied on generating an event when the script ended. 28 Unfortunately, that proved unreliable; if the thread was very short, 29 it could actually start trying to continue before the current 30 iteration of the generator was finished! I'm surprised that was 31 possible (I expected the event to get queued), but in any case 32 it was bad news. The current scheme is a kludge -- poll the thread. 33 I hope I can figure out something better. 34 35History: 362004-08-12 ROwen 372004-09-10 ROwen Modified for RO.Wdg.Constants->RO.Constants. 38 Bug fix: _WaitMS cancel used afterID instead of self.afterID. 39 Bug fix: test for resume while wait callback pending was broken, 40 leading to false "You forgot the 'yield'" errors. 412004-10-01 ROwen Bug fix: waitKeyVar was totally broken. 422004-10-08 ROwen Bug fix: waitThread could fail if the thread was too short. 432004-12-16 ROwen Added a debug mode that prints diagnostics to stdout 44 and does not wait for commands or keyword variables. 452005-01-05 ROwen showMsg: changed level to severity. 462005-06-16 ROwen Changed default cmdStatusBar from statusBar to no bar. 472005-06-24 ROwen Changed to use new CmdVar.lastReply instead of .replies. 482005-08-22 ROwen Clarified _WaitCmdVars.getState() doc string. 492006-03-09 ROwen Added scriptClass argument to ScriptRunner. 502006-03-28 ROwen Modified to allow scripts to call subscripts. 512006-04-24 ROwen Improved error handling in _continue. 52 Bug fixes to debug mode: 53 - waitCmd miscomputed iterID 54 - startCmd dispatched commands 552006-11-02 ROwen Added checkFail argument to waitCmd and waitCmdVars methods. 56 waitCmd now returns the cmdVar in sr.value. 57 Added keyVars argument to startCmd and waitCmd. 582006-11-13 ROwen Added waitUser and resumeUser methods. 592006-12-12 ROwen Bug fix: start did not initialize waitUser instance vars. 60 Added initVars method to centralize initialization. 612008-04-21 ROwen Improved debug mode output: 62 - showMsg prints messages 63 - _setState prints requested state 64 - _end prints the end function 65 Added debugPrint method to simplify handling unicode errors. 662008-04-24 ROwen Bug fix: waitKeyVar referenced a nonexistent variable in non-debug mode. 672008-04-29 ROwen Fixed reporting of exceptions that contain unicode arguments. 682008-06-26 ROwen Renamed isAborting method to didFail (to be more consistent with isDone). 69 Added isPaused method. 70 Improved documentation for didFail and isDone methods. 71 Improved documentation for abortCmdStr and keyVars arguments to waitCmd. 722010-05-26 ROwen Tweaked to use _removeAllCallbacks() instead of nulling _callbacks. 732010-06-28 ROwen Made _WaitBase a modern class (thanks to pychecker). 74 Removed unused and broken internal method _waitEndFunc (thanks to pychecker). 752010-10-20 ROwen Tweaked waitCmd doc string. 762011-06-17 ROwen Changed "type" to "msgType" in parsed message dictionaries to avoid conflict with builtin. 772011-08-16 ROwen Commented out a diagnostic print statement. 782012-01-26 ROwen Write full state to stderr on unexpected errors. 792012-06-01 ROwen Use best effort to remove callbacks during cleanup, instead of raising an exception on failure. 80 Modified _WaitCmdVars to not try to register callbacks on commands that are finished, 81 and to not try to remove callbacks from CmdVars that are done. 822012-07-09 ROwen Made ScriptRunner argument "master" optional and moved later in argument list. 83 Modified to use RO.Comm.Generic.Timer. 842014-03-14 ROwen Changed default abortCmdStr from None to "". 852014-04-29 ROwen Bug fix: pause followed by resume lost the value returned by whatever was being paused. 862014-07-21 ROwen Added waitPause and waitSec. 872015-09-24 ROwen Replace "== None" with "is None" to modernize the code. 882015-11-03 ROwen Replace "!= None" with "is not None" to modernize the code. 892015-11-05 ROwen Changed ==/!= True/False to is/is not True/False to modernize the code. 90""" 91__all__ = ["ScriptError", "ScriptRunner"] 92 93import sys 94import threading 95import Queue 96import traceback 97import RO.AddCallback 98import RO.Constants 99import RO.KeyVariable 100import RO.SeqUtil 101import RO.StringUtil 102from RO.Comm.Generic import Timer 103 104# state constants 105Ready = 2 106Paused = 1 107Running = 0 108Done = -1 109Cancelled = -2 110Failed = -3 111 112_DebugState = False 113 114# a dictionary that describes the various values for the connection state 115_StateDict = { 116 Ready: "Ready", 117 Paused: "Paused", 118 Running: "Running", 119 Done: "Done", 120 Cancelled: "Cancelled", 121 Failed: "Failed", 122} 123 124# internal constants 125_PollDelaySec = 0.1 # polling interval for threads (sec) 126 127# a list of possible keywords that hold reasons for a command failure 128# in the order in which they are checked 129_ErrKeys = ("text", "txt", "exposetxt") 130 131class _Blank(object): 132 def __init__(self): 133 object.__init__(self) 134 135class ScriptError (RuntimeError): 136 """Use to raise exceptions in your script 137 when you don't want a traceback. 138 """ 139 pass 140 141class ScriptRunner(RO.AddCallback.BaseMixin): 142 """Execute a script. 143 144 Allows waiting for various things without messing up the main event loop. 145 """ 146 def __init__(self, 147 name, 148 runFunc = None, 149 scriptClass = None, 150 dispatcher = None, 151 master = None, 152 initFunc = None, 153 endFunc = None, 154 stateFunc = None, 155 startNow = False, 156 statusBar = None, 157 cmdStatusBar = None, 158 debug = False, 159 ): 160 """Create a ScriptRunner 161 162 Inputs: 163 - name script name; used to report status 164 - runFunc the main script function; executed whenever 165 the start button is pressed 166 - scriptClass a class with a run method and an optional end method; 167 if specified, runFunc, initFunc and endFunc may not be specified. 168 - dispatcher keyword dispatcher (opscore.actor.CmdKeyVarDispatcher); 169 required to use wait methods and startCmd. 170 - master master Tk widget; your script may grid or pack objects into this; 171 may be None for scripts that do not have widgets. 172 - initFunc function to call ONCE when the ScriptRunner is constructed 173 - endFunc function to call when runFunc ends for any reason 174 (finishes, fails or is cancelled); used for cleanup 175 - stateFunc function to call when the ScriptRunner changes state 176 - startNow if True, starts executing the script immediately 177 instead of waiting for user to call start. 178 - statusBar status bar, if available. Used by showMsg 179 - cmdStatusBar command status bar, if available. 180 Used to show the status of executing commands. 181 May be the same as statusBar. 182 - debug if True, startCmd and wait... print diagnostic messages to stdout 183 and there is no waiting for commands or keyword variables. Thus: 184 - waitCmd and waitCmdVars return success immediately 185 - waitKeyVar returns defVal (or None if not specified) immediately 186 187 All functions (runFunc, initFunc, endFunc and stateFunc) receive one argument: sr, 188 this ScriptRunner object. The functions can pass information using sr.globals, 189 an initially empty object (to which you can add instance variables and set or read them). 190 191 Only runFunc is allowed to call sr methods that wait. 192 The other functions may only run non-waiting code. 193 194 WARNING: when runFunc calls any of the ScriptRunner methods that wait, 195 IT MUST YIELD THE RESULT, as in: 196 def runFunc(sr): 197 ... 198 yield sr.waitMS(500) 199 ... 200 All such methods are marked "yield required". 201 202 If you forget to yield, your script will not wait. Your script will then halt 203 with an error message when it calls the next ScriptRunner method that involves waiting 204 (but by the time it gets that far it may have done some strange things). 205 206 If your script yields when it should not, it will simply halt. 207 """ 208 if scriptClass: 209 if runFunc or initFunc or endFunc: 210 raise ValueError("Cannot specify runFunc, initFunc or endFunc with scriptClass") 211 if not hasattr(scriptClass, "run"): 212 raise ValueError("scriptClass=%r has no run method" % scriptClass) 213 elif runFunc is None: 214 raise ValueError("Must specify runFunc or scriptClass") 215 elif not callable(runFunc): 216 raise ValueError("runFunc=%r not callable" % (runFunc,)) 217 218 self.runFunc = runFunc 219 self.name = name 220 self.dispatcher = dispatcher 221 self.master = master 222 self.initFunc = initFunc 223 self.endFunc = endFunc 224 self.debug = bool(debug) 225 self._statusBar = statusBar 226 self._cmdStatusBar = cmdStatusBar 227 228 # useful constant for script writers 229 self.ScriptError = ScriptError 230 231 RO.AddCallback.BaseMixin.__init__(self) 232 233 self.globals = _Blank() 234 235 self.initVars() 236 237 """create a private widget and bind <Delete> to it 238 to kill the script when the master widget is destroyed. 239 This makes sure the script halts when the master toplevel closes 240 and avoids wait_variable hanging forever when the application is killed. 241 Binding <Destroy> to a special widget instead of master avoids two problems: 242 - If the user creates a widget and then destroys it 243 <Delete> would be called, mysteriously halting the script. 244 - When the master is deleted it also gets a <Delete> event for every 245 child widget. Thus the <Delete> binding would be called repeatedly, 246 which is needlessly inefficient. 247 """ 248 if self.master: 249 import Tkinter 250 self._privateWdg = Tkinter.Frame(self.master) 251 self._privateWdg.bind("<Destroy>", self.__del__) 252 253 if stateFunc: 254 self.addCallback(stateFunc) 255 256 # initialize, as appropriate 257 if scriptClass: 258 self.scriptObj = scriptClass(self) 259 self.runFunc = self.scriptObj.run 260 self.endFunc = getattr(self.scriptObj, "end", None) 261 elif self.initFunc: 262 res = self.initFunc(self) 263 if hasattr(res, "next"): 264 raise RuntimeError("init function tried to wait") 265 266 if startNow: 267 self.start() 268 269 # methods for starting, pausing and aborting script 270 # and for getting the current state of execution. 271 272 def cancel(self): 273 """Cancel the script. 274 275 The script will not actually halt until the next 276 waitXXX or doXXX method is called, but this should 277 occur quickly. 278 """ 279 if self.isExecuting(): 280 self._setState(Cancelled, "") 281 282 def debugPrint(self, msgStr): 283 """Print the message to stdout if in debug mode. 284 Handles unicode as best it can. 285 """ 286 if not self.debug: 287 return 288 try: 289 print(msgStr) 290 except (TypeError, ValueError): 291 print(repr(msgStr)) 292 293 def getFullState(self): 294 """Returns the current state as a tuple: 295 - state: a numeric value; named constants are available 296 - stateStr: a short string describing the state 297 - reason: the reason for the state ("" if none) 298 """ 299 state, reason = self._state, self._reason 300 try: 301 stateStr = _StateDict[state] 302 except KeyError: 303 stateStr = "Unknown (%r)" % (state,) 304 return (state, stateStr, reason) 305 306 def getState(self): 307 """Return the current state as a numeric value. 308 See the state constants defined in RO.ScriptRunner. 309 See also getFullState. 310 """ 311 return self._state 312 313 def initVars(self): 314 """Initialize variables. 315 Call at construction and when starting a new run. 316 """ 317 self._cancelFuncs = [] 318 self._endingState = None 319 self._state = Ready 320 self._reason = "" 321 self._iterID = [0] 322 self._iterStack = [] 323 self._waiting = False # set when waiting for a callback 324 self._userWaitID = None 325 self.value = None 326 327 def didFail(self): 328 """Return True if script aborted or failed. 329 330 Note: may not be fully ended (there may be cleanup to do and callbacks to call). 331 """ 332 return self._endingState in (Cancelled, Failed) 333 334 def isDone(self): 335 """Return True if script is finished, successfully or otherwise. 336 337 Note: may not be fully ended (there may be cleanup to do and callbacks to call). 338 """ 339 return self._state <= Done 340 341 def isExecuting(self): 342 """Returns True if script is running or paused.""" 343 return self._state in (Running, Paused) 344 345 def isPaused(self): 346 """Return True if script is paused 347 """ 348 return self._state == Paused 349 350 def pause(self): 351 """Pause execution. 352 353 Note that the script must be waiting for something when the pause occurs 354 (because that's when the GUI will be freed up to get the request to pause). 355 If the thing being waited for fails then the script will fail (thus going 356 from Paused to Failed with no user interation). 357 358 Has no effect unless the script is running. 359 """ 360 self._printState("pause") 361 if not self._state == Running: 362 return 363 364 self._setState(Paused) 365 366 def resume(self): 367 """Resume execution after a pause. 368 369 Has no effect if not paused. 370 """ 371 self._printState("resume") 372 if not self._state == Paused: 373 return 374 375 self._setState(Running) 376 if not self._waiting: 377 self._continue(self._iterID, val=self.value) 378 379 def resumeUser(self): 380 """Resume execution from waitUser 381 """ 382 if self._userWaitID is None: 383 raise RuntimeError("Not in user wait mode") 384 385 iterID = self._userWaitID 386 self._userWaitID = None 387 self._continue(iterID) 388 389 def start(self): 390 """Start executing runFunc. 391 392 If already running, raises RuntimeError 393 """ 394 if self.isExecuting(): 395 raise RuntimeError("already executing") 396 397 if self._statusBar: 398 self._statusBar.setMsg("") 399 if self._cmdStatusBar: 400 self._cmdStatusBar.setMsg("") 401 402 self.initVars() 403 404 self._iterID = [0] 405 self._iterStack = [] 406 self._setState(Running) 407 self._continue(self._iterID) 408 409 # methods for use in scripts 410 # with few exceptions all wait for something 411 # and thus require a "yield" 412 413 def getKeyVar(self, 414 keyVar, 415 ind=0, 416 defVal=Exception, 417 ): 418 """Return the current value of keyVar. 419 See also waitKeyVar, which can wait for a value. 420 421 Note: if you want to be sure the keyword data was in response to a particular command 422 that you sent, then use the keyVars argument of startCmd or waitCmd instead. 423 424 Do not use yield because it does not wait for anything. 425 426 Inputs: 427 - keyVar keyword variable 428 - ind which value is wanted? (None for all values) 429 - defVal value to return if value cannot be determined 430 (if omitted, the script halts) 431 """ 432 if self.debug: 433 argList = ["keyVar=%s" % (keyVar,)] 434 if ind != 0: 435 argList.append("ind=%s" % (ind,)) 436 if defVal != Exception: 437 argList.append("defVal=%r" % (defVal,)) 438 if defVal == Exception: 439 defVal = None 440 441 currVal, isCurrent = keyVar.get() 442 if isCurrent: 443 if ind is not None: 444 retVal = currVal[ind] 445 else: 446 retVal = currVal 447 else: 448 if defVal==Exception: 449 raise ScriptError("Value of %s invalid" % (keyVar,)) 450 else: 451 retVal = defVal 452 453 if self.debug: # else argList does not exist 454 self.debugPrint("getKeyVar(%s); returning %r" % (", ".join(argList), retVal)) 455 return retVal 456 457 def showMsg(self, msg, severity=RO.Constants.sevNormal): 458 """Display a message--on the status bar, if available, 459 else sys.stdout. 460 461 Do not use yield because it does not wait for anything. 462 463 Inputs: 464 - msg: string to display, without a final \n 465 - severity: one of RO.Constants.sevNormal (default), sevWarning or sevError 466 """ 467 if self._statusBar: 468 self._statusBar.setMsg(msg, severity) 469 self.debugPrint(msg) 470 else: 471 print(msg) 472 473 def startCmd(self, 474 actor="", 475 cmdStr = "", 476 timeLim = 0, 477 callFunc = None, 478 callTypes = RO.KeyVariable.DoneTypes, 479 timeLimKeyword = None, 480 abortCmdStr = "", 481 keyVars = None, 482 checkFail = True, 483 ): 484 """Start a command using the same arguments as waitCmd. 485 486 Inputs: same as waitCmd, which see. 487 488 Returns a command variable that you can wait for using waitCmdVars. 489 490 Do not use yield because it does not wait for anything. 491 """ 492 cmdVar = RO.KeyVariable.CmdVar( 493 actor=actor, 494 cmdStr = cmdStr, 495 timeLim = timeLim, 496 callFunc = callFunc, 497 callTypes = callTypes, 498 timeLimKeyword = timeLimKeyword, 499 abortCmdStr = abortCmdStr, 500 keyVars = keyVars, 501 ) 502 if checkFail: 503 cmdVar.addCallback( 504 callFunc = self._cmdFailCallback, 505 callTypes = RO.KeyVariable.FailTypes, 506 ) 507 if self.debug: 508 argList = ["actor=%r, cmdStr=%r" % (actor, cmdStr)] 509 if timeLim != 0: 510 argList.append("timeLim=%s" % (timeLim,)) 511 if callFunc is not None: 512 argList.append("callFunc=%r" % (callFunc,)) 513 if callTypes != RO.KeyVariable.DoneTypes: 514 argList.append("callTypes=%r" % (callTypes,)) 515 if timeLimKeyword is not None: 516 argList.append("timeLimKeyword=%r" % (timeLimKeyword,)) 517 if abortCmdStr: 518 argList.append("abortCmdStr=%r" % (abortCmdStr,)) 519 if checkFail is not True: 520 argList.append("checkFail=%r" % (checkFail,)) 521 self.debugPrint("startCmd(%s)" % (", ".join(argList),)) 522 523 self._showCmdMsg("%s started" % cmdStr) 524 525 526 # set up command completion callback 527 def endCmd(self=self, cmdVar=cmdVar): 528 endMsgDict = self.dispatcher.makeMsgDict( 529 cmdr = None, 530 actor = cmdVar.actor, 531 msgType = ":", 532 533 ) 534 cmdVar.reply(endMsgDict) 535 msgStr = "%s finished" % cmdVar.cmdStr 536 self._showCmdMsg(msgStr) 537 Timer(1.0, endCmd) 538 539 else: 540 if self._cmdStatusBar: 541 self._cmdStatusBar.doCmd(cmdVar) 542 else: 543 self.dispatcher.executeCmd(cmdVar) 544 545 return cmdVar 546 547 def waitCmd(self, 548 actor="", 549 cmdStr = "", 550 timeLim = 0, 551 callFunc=None, 552 callTypes = RO.KeyVariable.DoneTypes, 553 timeLimKeyword = None, 554 abortCmdStr = "", 555 keyVars = None, 556 checkFail = True, 557 ): 558 """Start a command and wait for it to finish. 559 Returns the cmdVar in sr.value. 560 561 A yield is required. 562 563 Inputs: 564 - actor: the name of the device to command 565 - cmdStr: the command (without a terminating \n) 566 - timeLim: maximum time before command expires, in sec; 0 for no limit 567 - callFunc: a function to call when the command changes state; 568 see below for details. 569 - callTypes: the message types for which to call the callback; 570 a string of one or more choices; see RO.KeyVariable.TypeDict for the choices; 571 useful constants include DoneTypes (command finished or failed) 572 and AllTypes (all message types, thus any reply). 573 Not case sensitive (the string you supply will be lowercased). 574 - timeLimKeyword: a keyword specifying a delta-time by which the command must finish 575 - abortCmdStr: a command string that will abort the command. This string is sent to the actor 576 if the command is aborted, e.g. if the script is cancelled while the command is executing. 577 - keyVars: a sequence of 0 or more keyword variables to monitor. 578 Any data for those variables that arrives IN RESPONSE TO THIS COMMAND is saved 579 and can be retrieved using cmdVar.getKeyVarData or cmdVar.getLastKeyVarData, 580 where cmdVar is returned in sr.value. 581 - checkFail: check for command failure? 582 if True (the default) command failure will halt your script 583 584 Callback arguments: 585 msgType: the message type, a character (e.g. "i", "w" or ":"); 586 see RO.KeyVariable.TypeDict for the various types. 587 msgDict: the entire message dictionary 588 cmdVar (by name): the key command object 589 see RO.KeyVariable.CmdVar for details 590 591 Note: timeLim and timeLimKeyword work together as follows: 592 - The initial time limit for the command is timeLim 593 - If timeLimKeyword is seen before timeLim seconds have passed 594 then self.maxEndTime is updated with the new value 595 596 Also the time limit is a lower limit. The command is guaranteed to 597 expire no sooner than this but it may take a second longer. 598 """ 599 if isinstance(actor, RO.KeyVariable.CmdVar): 600 raise RuntimeError("waitCmd error: actor must be a string; did you mean to call waitCmdVars? actor=%s" % (actor,)) 601 self._waitCheck(setWait = False) 602 603 self.debugPrint("waitCmd calling startCmd") 604 605 cmdVar = self.startCmd ( 606 actor = actor, 607 cmdStr = cmdStr, 608 timeLim = timeLim, 609 callFunc = callFunc, 610 callTypes = callTypes, 611 timeLimKeyword = timeLimKeyword, 612 abortCmdStr = abortCmdStr, 613 keyVars = keyVars, 614 checkFail = False, 615 ) 616 617 self.waitCmdVars(cmdVar, checkFail=checkFail, retVal=cmdVar) 618 619 def waitCmdVars(self, cmdVars, checkFail=True, retVal=None): 620 """Wait for one or more command variables to finish. 621 Command variables are the objects returned by startCmd. 622 623 A yield is required. 624 625 Returns successfully if all commands succeed. 626 Fails as soon as any command fails. 627 628 Inputs: 629 - one or more command variables (RO.KeyVariable.CmdVar objects) 630 - checkFail: check for command failure? 631 if True (the default) command failure will halt your script 632 - retVal: value to return at the end; defaults to None 633 """ 634 _WaitCmdVars(self, cmdVars, checkFail=checkFail, retVal=retVal) 635 636 def waitKeyVar(self, 637 keyVar, 638 ind=0, 639 defVal=Exception, 640 waitNext=False, 641 ): 642 """Get the value of keyVar in self.value. 643 If it is currently unknown or if waitNext is true, 644 wait for the variable to be updated. 645 See also getKeyVar (which does not wait). 646 647 A yield is required. 648 649 Inputs: 650 - keyVar keyword variable 651 - ind which value is wanted? (None for all values) 652 - defVal value to return if value cannot be determined 653 (if omitted, the script halts) 654 - waitNext if True, ignores the current value and waits 655 for the next transition. 656 """ 657 _WaitKeyVar( 658 scriptRunner = self, 659 keyVar = keyVar, 660 ind = ind, 661 defVal = defVal, 662 waitNext = waitNext, 663 ) 664 665 def waitMS(self, msec): 666 """Wait for msec milliseconds. 667 668 A yield is required. 669 670 Inputs: 671 - msec number of milliseconds to pause 672 """ 673 self.debugPrint("waitMS(msec=%s)" % (msec,)) 674 675 _WaitMS(self, msec) 676 677 def waitSec(self, sec): 678 """Wait for sec seconds. 679 680 A yield is required. 681 682 Inputs: 683 - sec number of seconds to pause 684 """ 685 self.debugPrint("waitSec(sec=%s)" % (sec,)) 686 687 _WaitMS(self, sec * 1000) 688 689 def waitPause(self, msgStr="Paused", severity=RO.Constants.sevNormal): 690 """Pause execution and wait 691 692 A no-op if not running 693 """ 694 Timer(0, self.showMsg, msgStr, severity=severity) 695 self.pause() 696 697 def waitThread(self, func, *args, **kargs): 698 """Run func as a background thread, waits for completion 699 and sets self.value = the result of that function call. 700 701 A yield is required. 702 703 Warning: func must NOT interact with Tkinter widgets or variables 704 (not even reading them) because Tkinter is not thread-safe. 705 (The only thing I'm sure a background thread can safely do with Tkinter 706 is generate an event, a technique that is used to detect end of thread). 707 """ 708 self.debugPrint("waitThread(func=%r, args=%s, keyArgs=%s)" % (func, args, kargs)) 709 710 _WaitThread(self, func, *args, **kargs) 711 712 def waitUser(self): 713 """Wait until resumeUser called. 714 715 Typically used if waiting for user input 716 but can be used for any external trigger. 717 """ 718 self._waitCheck(setWait=True) 719 720 if self._userWaitID is not None: 721 raise RuntimeError("Already in user wait mode") 722 723 self._userWaitID = self._getNextID() 724 725 # private methods 726 727 def _cmdFailCallback(self, msgType, msgDict, cmdVar): 728 """Use as a callback for when an asynchronous command fails. 729 """ 730# print "ScriptRunner._cmdFailCallback(%r, %r, %r)" % (msgType, msgDict, cmdVar) 731 if not msgType in RO.KeyVariable.FailTypes: 732 errMsg = "Bug! RO.ScriptRunner._cmdFail(%r, %r, %r) called for non-failed command" % (msgType, msgDict, cmdVar) 733 raise RuntimeError(errMsg) 734 MaxLen = 10 735 if len(cmdVar.cmdStr) > MaxLen: 736 cmdDescr = "%s %s..." % (cmdVar.actor, cmdVar.cmdStr[0:MaxLen]) 737 else: 738 cmdDescr = "%s %s" % (cmdVar.actor, cmdVar.cmdStr) 739 for key, values in msgDict.get("data", {}).iteritems(): 740 if key.lower() in _ErrKeys: 741 reason = values[0] 742 break 743 else: 744 reason = msgDict.get("data") 745 if not reason: 746 reason = str(msgDict) 747 self._setState(Failed, reason="%s failed: %s" % (cmdDescr, reason)) 748 749 def _continue(self, iterID, val=None): 750 """Continue executing the script. 751 752 Inputs: 753 - iterID: ID of iterator that is continuing 754 - val: self.value is set to val 755 """ 756 self._printState("_continue(%r, %r)" % (iterID, val)) 757 if not self.isExecuting(): 758 raise RuntimeError('%s: bug! _continue called but script not executing' % (self,)) 759 760 try: 761 if iterID != self._iterID: 762 #print "Warning: _continue called with iterID=%s; expected %s" % (iterID, self._iterID) 763 raise RuntimeError("%s: bug! _continue called with bad id; got %r, expected %r" % (self, iterID, self._iterID)) 764 765 self.value = val 766 767 self._waiting = False 768 769 if self._state == Paused: 770 #print "_continue: still paused" 771 return 772 773 if not self._iterStack: 774 # just started; call run function, 775 # and if it's an iterator, put it on the stack 776 res = self.runFunc(self) 777 if not hasattr(res, "next"): 778 # function was a function, not a generator; all done 779 self._setState(Done) 780 return 781 782 self._iterStack = [res] 783 784 self._printState("_continue: before iteration") 785 self._state = 0 786 possIter = next(self._iterStack[-1]) 787 if hasattr(possIter, "next"): 788 self._iterStack.append(possIter) 789 self._iterID = self._getNextID(addLevel = True) 790# print "Iteration yielded an iterator" 791 self._continue(self._iterID) 792 else: 793 self._iterID = self._getNextID() 794 795 self._printState("_continue: after iteration") 796 797 except StopIteration: 798# print "StopIteration seen in _continue" 799 self._iterStack.pop(-1) 800 if not self._iterStack: 801 self._setState(Done) 802 else: 803 self._continue(self._iterID, val=self.value) 804 except KeyboardInterrupt: 805 self._setState(Cancelled, "keyboard interrupt") 806 except SystemExit: 807 self.__del__() 808 sys.exit(0) 809 except ScriptError as e: 810 self._setState(Failed, RO.StringUtil.strFromException(e)) 811 except Exception as e: 812 traceback.print_exc(file=sys.stderr) 813 self._printFullState() 814 self._setState(Failed, RO.StringUtil.strFromException(e)) 815 816 def _printState(self, prefix): 817 """Print the state at various times. 818 Ignored unless _DebugState or self.debug true. 819 """ 820 if _DebugState: 821 print("Script %s: %s: state=%s, iterID=%s, waiting=%s, iterStack depth=%s" % \ 822 (self.name, prefix, self._state, self._iterID, self._waiting, len(self._iterStack))) 823 824 def _printFullState(self): 825 """Print the full state to stderr 826 """ 827 sys.stderr.write("self.name=%s, self._state=%s, self._iterID=%r, self._waiting=%r, self._userWaitID=%r, self.value=%r\n" % \ 828 (self.name, self._state, self._iterID, self._waiting, self._userWaitID, self.value)) 829 sys.stderr.write("self._iterStack=%r\n" % (self._iterStack,)) 830 831 def _showCmdMsg(self, msg, severity=RO.Constants.sevNormal): 832 """Display a message--on the command status bar, if available, 833 else sys.stdout. 834 835 Do not use yield because it does not wait for anything. 836 837 Inputs: 838 - msg: string to display, without a final \n 839 - severity: one of RO.Constants.sevNormal (default), sevWarning or sevError 840 """ 841 if self._cmdStatusBar: 842 self._cmdStatusBar.setMsg(msg, severity) 843 else: 844 print(msg) 845 846 def __del__(self, evt=None): 847 """Called just before the object is deleted. 848 Deletes any state callbacks and then cancels script execution. 849 The evt argument is ignored, but allows __del__ to be 850 called from a Tk event binding. 851 """ 852 self._removeAllCallbacks() 853 self.cancel() 854 855 def _end(self): 856 """Call the end function (if any). 857 """ 858 # Warning: this code must not execute _setState or __del__ 859 # to avoid infinite loops. It also need not execute _cancelFuncs. 860 if self.endFunc: 861 self.debugPrint("ScriptRunner._end: calling end function") 862 try: 863 res = self.endFunc(self) 864 if hasattr(res, "next"): 865 self._state = Failed 866 self._reason = "endFunc tried to wait" 867 except KeyboardInterrupt: 868 self._state = Cancelled 869 self._reason = "keyboard interrupt" 870 except SystemExit: 871 raise 872 except Exception as e: 873 self._state = Failed 874 self._reason = "endFunc failed: %s" % (RO.StringUtil.strFromException(e),) 875 traceback.print_exc(file=sys.stderr) 876 else: 877 self.debugPrint("ScriptRunner._end: no end function to call") 878 879 def _getNextID(self, addLevel=False): 880 """Return the next iterator ID""" 881 self._printState("_getNextID(addLevel=%s)" % (addLevel,)) 882 newID = self._iterID[:] 883 if addLevel: 884 newID += [0] 885 else: 886 newID[-1] = (newID[-1] + 1) % 10000 887 return newID 888 889 def _setState(self, newState, reason=None): 890 """Update the state of the script runner. 891 892 If the new state is Cancelled or Failed 893 then any existing cancel function is called 894 to abort outstanding callbacks. 895 """ 896 self._printState("_setState(%r, %r)" % (newState, reason)) 897 if self.debug: 898 newStateName = _StateDict.get(newState, "?") 899 self.debugPrint("ScriptRunner._setState(newState=%s=%s, reason=%r)" % (newState, newStateName, reason)) 900 901 # if ending, clean up appropriately 902 if self.isExecuting() and newState <= Done: 903 self._endingState = newState 904 # if aborting and a cancel function exists, call it 905 if newState < Done: 906 for func in self._cancelFuncs: 907# print "%s _setState calling cancel function %r" % (self, func) 908 func() 909 self._cancelFuncs = [] 910 self._end() 911 912 self._state = newState 913 if reason is not None: 914 self._reason = reason 915 self._doCallbacks() 916 917 def __str__(self): 918 """String representation of script""" 919 return "script %s" % (self.name,) 920 921 def _waitCheck(self, setWait=False): 922 """Verifies that the script runner is running and not already waiting 923 (as can easily happen if the script is missing a "yield"). 924 925 Call at the beginning of every waitXXX method. 926 927 Inputs: 928 - setWait: if True, sets the _waiting flag True 929 """ 930 if self._state != Running: 931 raise RuntimeError("Tried to wait when not running") 932 933 if self._waiting: 934 raise RuntimeError("Already waiting; did you forget the 'yield' when calling a ScriptRunner method?") 935 936 if setWait: 937 self._waiting = True 938 939 940class _WaitBase(object): 941 """Base class for waiting. 942 Handles verifying iterID, registering the termination function, 943 registering and unregistering the cancel function, etc. 944 """ 945 def __init__(self, scriptRunner): 946 scriptRunner._printState("%s init" % (self.__class__.__name__)) 947 scriptRunner._waitCheck(setWait = True) 948 self.scriptRunner = scriptRunner 949 self.master = scriptRunner.master 950 self._iterID = scriptRunner._getNextID() 951 self.scriptRunner._cancelFuncs.append(self.cancelWait) 952 953 def cancelWait(self): 954 """Call to cancel waiting. 955 Perform necessary cleanup but do not set state. 956 Subclasses can override and should usually call cleanup. 957 """ 958 self.cleanup() 959 960 def fail(self, reason): 961 """Call if waiting fails. 962 """ 963 # report failure; this causes the scriptRunner to call 964 # all pending cancelWait functions, so don't do that here 965 self.scriptRunner._setState(Failed, reason) 966 967 def cleanup(self): 968 """Called when ending for any reason 969 (unless overridden cancelWait does not call cleanup). 970 """ 971 pass 972 973 def _continue(self, val=None): 974 """Call to resume execution.""" 975 self.cleanup() 976 try: 977 self.scriptRunner._cancelFuncs.remove(self.cancelWait) 978 except ValueError: 979 raise RuntimeError("Cancel function missing; did you forgot the 'yield' when calling a ScriptRunner method?") 980 if self.scriptRunner.debug and val is not None: 981 print("wait returns %r" % (val,)) 982 self.scriptRunner._continue(self._iterID, val) 983 984 985class _WaitMS(_WaitBase): 986 def __init__(self, scriptRunner, msec): 987 self._waitTimer = Timer() 988 _WaitBase.__init__(self, scriptRunner) 989 self._waitTimer.start(msec / 1000.0, self._continue) 990 991 def cancelWait(self): 992 self._waitTimer.cancel() 993 994 995class _WaitCmdVars(_WaitBase): 996 """Wait for one or more command variables to finish. 997 998 Inputs: 999 - scriptRunner: the script runner 1000 - one or more command variables (RO.KeyVariable.CmdVar objects) 1001 - checkFail: check for command failure? 1002 if True (the default) command failure will halt your script 1003 - retVal: the value to return at the end (in scriptRunner.value) 1004 """ 1005 def __init__(self, scriptRunner, cmdVars, checkFail=True, retVal=None): 1006 self.cmdVars = RO.SeqUtil.asSequence(cmdVars) 1007 self.checkFail = bool(checkFail) 1008 self.retVal = retVal 1009 self.addedCallback = False 1010 _WaitBase.__init__(self, scriptRunner) 1011 1012 if self.getState()[0] != 0: 1013 # no need to wait; commands are already done or one has failed 1014 # schedule a callback for asap 1015# print "_WaitCmdVars: no need to wait" 1016 Timer(0, self.varCallback) 1017 else: 1018 # need to wait; add self as callback to each cmdVar 1019 # and remove self.scriptRunner._cmdFailCallback if present 1020 for cmdVar in self.cmdVars: 1021 if not cmdVar.isDone(): 1022 cmdVar.removeCallback(self.scriptRunner._cmdFailCallback, doRaise=False) 1023 cmdVar.addCallback(self.varCallback) 1024 self.addedCallback = True 1025 1026 def getState(self): 1027 """Return one of: 1028 - (-1, failedCmdVar) if a command has failed and checkFail True 1029 - (1, None) if all commands are done (and possibly failed if checkFail False) 1030 - (0, None) not finished yet 1031 Note that getState()[0] is logically True if done waiting. 1032 """ 1033 allDone = 1 1034 for var in self.cmdVars: 1035 if var.isDone(): 1036 if var.lastType != ":" and self.checkFail: 1037 return (-1, var) 1038 else: 1039 allDone = 0 1040 return (allDone, None) 1041 1042 def varCallback(self, *args, **kargs): 1043 """Check state of script runner and fail or continue if appropriate 1044 """ 1045 currState, cmdVar = self.getState() 1046 if currState < 0: 1047 self.fail(cmdVar) 1048 elif currState > 0: 1049 self._continue(self.retVal) 1050 1051 def cancelWait(self): 1052 """Call when aborting early. 1053 """ 1054# print "_WaitCmdVars.cancelWait" 1055 self.cleanup() 1056 for cmdVar in self.cmdVars: 1057 cmdVar.abort() 1058 1059 def cleanup(self): 1060 """Called when ending for any reason. 1061 """ 1062# print "_WaitCmdVars.cleanup" 1063 if self.addedCallback: 1064 for cmdVar in self.cmdVars: 1065 if not cmdVar.isDone(): 1066 didRemove = cmdVar.removeCallback(self.varCallback, doRaise=False) 1067 if not didRemove: 1068 sys.stderr.write("_WaitCmdVar cleanup could not remove callback from %s\n" % (cmdVar,)) 1069 1070 def fail(self, cmdVar): 1071 """A command var failed. 1072 """ 1073# print "_WaitCmdVars.fail(%s)" % (cmdVar,) 1074 msgDict = cmdVar.lastReply 1075 msgType = msgDict["msgType"] 1076 self.scriptRunner._cmdFailCallback(msgType, msgDict, cmdVar) 1077 1078 1079class _WaitKeyVar(_WaitBase): 1080 """Wait for one keyword variable, returning the value in scriptRunner.value. 1081 """ 1082 def __init__(self, 1083 scriptRunner, 1084 keyVar, 1085 ind, 1086 defVal, 1087 waitNext, 1088 ): 1089 """ 1090 Inputs: 1091 - scriptRunner: a ScriptRunner instance 1092 - keyVar keyword variable 1093 - ind index of desired value (None for all values) 1094 - defVal value to return if value cannot be determined; if Exception, the script halts 1095 - waitNext if True, ignore the current value and wait for the next transition. 1096 """ 1097 self.keyVar = keyVar 1098 self.ind = ind 1099 self.defVal = defVal 1100 self.waitNext = bool(waitNext) 1101 self.addedCallback = False 1102 _WaitBase.__init__(self, scriptRunner) 1103 1104 if self.keyVar.isCurrent() and not self.waitNext: 1105 # no need to wait; value already known 1106 # schedule a wakeup for asap 1107 Timer(0, self.varCallback) 1108 elif self.scriptRunner.debug: 1109 # display message 1110 argList = ["keyVar=%s" % (keyVar,)] 1111 if ind != 0: 1112 argList.append("ind=%s" % (ind,)) 1113 if defVal != Exception: 1114 argList.append("defVal=%r" % (defVal,)) 1115 if waitNext: 1116 argList.append("waitNext=%r" % (waitNext,)) 1117 print("waitKeyVar(%s)" % ", ".join(argList)) 1118 1119 # prevent the call from failing by using None instead of Exception 1120 if self.defVal == Exception: 1121 self.defVal = None 1122 1123 Timer(0, self.varCallback) 1124 else: 1125 # need to wait; set self as a callback 1126# print "_WaitKeyVar adding callback" 1127 self.keyVar.addCallback(self.varCallback, callNow=False) 1128 self.addedCallback = True 1129 1130 def varCallback(self, *args, **kargs): 1131 """Set scriptRunner.value to value. If value is invalid, 1132 use defVal (if specified) else cancel the wait and fail. 1133 """ 1134 currVal, isCurrent = self.getVal() 1135# print "_WaitKeyVar.varCallback; currVal=%r; isCurrent=%r" % (currVal, isCurrent) 1136 if isCurrent: 1137 self._continue(currVal) 1138 elif self.defVal != Exception: 1139 self._continue(self.defVal) 1140 else: 1141 self.fail("Value of %s invalid" % (self.keyVar,)) 1142 1143 def cleanup(self): 1144 """Called when ending for any reason. 1145 """ 1146# print "_WaitKeyVar.cleanup" 1147 if self.addedCallback: 1148 self.keyVar.removeCallback(self.varCallback, doRaise=False) 1149 1150 def getVal(self): 1151 """Return isCurrent, currVal, where currVal 1152 is the current value[ind] or value tuple (if ind=None). 1153 Ignores defVal. 1154 """ 1155 currVal, isCurrent = self.keyVar.get() 1156 if self.ind is not None: 1157 return currVal[self.ind], isCurrent 1158 else: 1159 return currVal, isCurrent 1160 1161 1162class _WaitThread(_WaitBase): 1163 def __init__(self, scriptRunner, func, *args, **kargs): 1164# print "_WaitThread.__init__(%r, *%r, **%r)" % (func, args, kargs) 1165 self._pollTimer = Timer() 1166 _WaitBase.__init__(self, scriptRunner) 1167 1168 if not callable(func): 1169 raise ValueError("%r is not callable" % func) 1170 1171 self.queue = Queue.Queue() 1172 self.func = func 1173 1174 self.threadObj = threading.Thread(target=self.threadFunc, args=args, kwargs=kargs) 1175 self.threadObj.setDaemon(True) 1176 self.threadObj.start() 1177 self._pollTimer.start(_PollDelaySec, self.checkEnd) 1178# print "_WaitThread__init__(%r) done" % self.func 1179 1180 def checkEnd(self): 1181 if self.threadObj.isAlive(): 1182 self._pollTimer.start(_PollDelaySec, self.checkEnd) 1183 return 1184# print "_WaitThread(%r).checkEnd: thread done" % self.func 1185 1186 retVal = self.queue.get() 1187# print "_WaitThread(%r).checkEnd; retVal=%r" % (self.func, retVal) 1188 self._continue(val=retVal) 1189 1190 def cleanup(self): 1191# print "_WaitThread(%r).cleanup" % self.func 1192 self._pollTimer.cancel() 1193 self.threadObj = None 1194 1195 def threadFunc(self, *args, **kargs): 1196 retVal = self.func(*args, **kargs) 1197 self.queue.put(retVal) 1198 1199 1200if __name__ == "__main__": 1201 import Tkinter 1202 import RO.KeyDispatcher 1203 import time 1204 1205 root = Tkinter.Tk() 1206 1207 dispatcher = RO.KeyDispatcher.KeyDispatcher() 1208 1209 scriptList = [] 1210 1211 def initFunc(sr): 1212 global scriptList 1213 print("%s init function called" % (sr,)) 1214 scriptList.append(sr) 1215 1216 def endFunc(sr): 1217 print("%s end function called" % (sr,)) 1218 1219 def script(sr): 1220 def threadFunc(nSec): 1221 time.sleep(nSec) 1222 nSec = 1.0 1223 sr.showMsg("%s waiting in a thread for %s sec" % (sr, nSec)) 1224 yield sr.waitThread(threadFunc, 1.0) 1225 1226 for val in range(5): 1227 sr.showMsg("%s value = %s" % (sr, val)) 1228 yield sr.waitMS(1000) 1229 1230 def stateFunc(sr): 1231 state, stateStr, reason = sr.getFullState() 1232 if reason: 1233 msgStr = "%s state=%s: %s" % (sr, stateStr, reason) 1234 else: 1235 msgStr = "%s state=%s" % (sr, stateStr) 1236 sr.showMsg(msgStr) 1237 for sr in scriptList: 1238 if not sr.isDone(): 1239 return 1240 root.quit() 1241 1242 sr1 = ScriptRunner( 1243 master = root, 1244 runFunc = script, 1245 name = "Script 1", 1246 dispatcher = dispatcher, 1247 initFunc = initFunc, 1248 endFunc = endFunc, 1249 stateFunc = stateFunc, 1250 ) 1251 1252 sr2 = ScriptRunner( 1253 master = root, 1254 runFunc = script, 1255 name = "Script 2", 1256 dispatcher = dispatcher, 1257 initFunc = initFunc, 1258 endFunc = endFunc, 1259 stateFunc = stateFunc, 1260 ) 1261 1262 # start the scripts in a staggared fashion 1263 sr1.start() 1264 Timer(1.5, sr1.pause) 1265 Timer(3.0, sr1.resume) 1266 Timer(2.5, sr2.start) 1267 1268 root.mainloop() 1269