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