13

My apologies beforehand for the length of the question, I didn't want to leave anything out.

Some background information

I'm trying to automate a data entry process by writing a Python application that uses the Windows API to simulate keystrokes, mouse movement and window/control manipulation. I have to resort to this method because I do not (yet) have the security clearance required to access the datastore/database directly (e.g. using SQL) or indirectly through a better suited API. Bureaucracy, it's a pain ;-)

The data entry process involves the correction of sales orders due to changes in article availability. The unavailable articles are either removed from the order or replaced by another suitable article.

Initially I want a human to be able to monitor the automatic data entry process to make sure everything goes right. To achieve this I slow down the actions on the one hand but also inform the user of what is currently going on through a pinned window.

The actual question

To allow the user to halt the automation process I'm registering the Pause/Break key as a hotkey and in the handler I want to pause the automation functionality. However, I'm currently struggling to figure out a way to properly pause the execution of the automation functionality. When the pause function is invoked I want the automation process to stop dead in its tracks, no matter what it is doing. I don't want it to even execute another keystroke.

UPDATE [23/01]: I actually want to do more than just pause, I want to be able to communicate with the automation process while it is running and request it to pause, skip the current sales order, give up completely and perhaps even more.

Can anybody show me The Right Way (TM) to achieve what I want?

Some more information

Here's an example of how the automation works (I'm using the pywinauto library):

from pywinauto import application
app = application.Application()
app.start_("notepad")
app.Notepad.TypeKeys("abcdef")

UPDATE [25/01]: After a few days of working on my application I've noticed I don't really use pywinauto that much, right now I'm only using it for finding window and then I directly use SendKeysCtypes.SendKeys to simulate keyboard input and win32api functions to simulate mouse input.

What I've found out so far

Here are a few methods I've come across so far in my search for an answer:

  1. I could separate the automation functionality and the interface + hotkey listener in two separate processes. Let's refer to the former as "automator" and the latter as "manager". The manager can then pause the execution of the automator by sending the process a SIGSTOP signal and unpause it using the SIGCONT signal (or the Windows equivalents through SuspendThread/ResumeThread).

    To be able to update the user interface the automator will need to inform the manager of its progression through some sort of an IPC mechanism.

    Cons:

    • Would using SIGSTOP not be a little harsh? Would it even work properly? Lots of people seem to be advising against it and even calling it "dangerous".

    • I am worried that implementing the IPC mechanism is going to be a bit complicated. On the other hand, I have worked with DBus which wouldn't be too hard to implement.

  2. The second method and one that lots of people seem to be suggesting involves using threads and essentially boils down to the following (simplified):

    while True:
        if self.pause: # pause
        # Do the work...
    

    However, doing it this way it seems it will only pause after there is no more work to do. The only way I see this method would work would be to divide the work (the entire automation process) into smaller work segments (i.e. tasks). Before starting on a new task the worker thread would check if it should pause and wait.

    Cons:

    • Seems like an implementation to divide the work into smaller segments, such as the one above, would be very ugly code wise (aesthetically).

      The way I imagine it, all statements would be transformed to look something like: queue.put((function, args)) (e.g. queue.put((app.Notepad.TypeKeys, "abcdef"))) and you'd have the automating process thread running through the tasks and continuously checking for the pause state before starting a task. That just can't be right...

    • The program would not actually stop dead in its tracks, but would first finish a task (however small) before actually pausing.

Progress made

UPDATE [23/01]: I've implemented a version of my application using the first method through the mentioned SuspendThread/ResumeThread functionality. So far this seems to work very nicely and also allows me to write the automation stuff just like you'd write any other script. The only quirk I've come across is that keyboard modifiers (CTRL, ALT, SHIFT) get "stuck" while paused. Something I can probably easily work around.

I've also written a test using the second method (threads and signals/message passing) and implemented the pause functionality. However, it looks really ugly (both checking for the pause flag and everything related to the "doing the work"). So if anybody can show me a proper example of something similar to the second method I'd appreciate it.

Related questions

Community
  • 1
  • 1
Bruce van der Kooij
  • 2,192
  • 1
  • 18
  • 29
  • What toolkit are you using for the GUI? You mentioned a pinned window. – Cristian Ciupitu Jan 23 '11 at 03:52
  • @Cristian: I decided to give Qt a shot, but am thinking about switching back to GTK. – Bruce van der Kooij Jan 23 '11 at 09:31
  • 1
    You may want to look into the bdb module http://docs.python.org/library/bdb.html. It would allow you to run your script programmaticly and step through it like a debuger. At least I think, I've never used it. – Ben Jan 25 '11 at 06:15

4 Answers4

4

Keep in mind that although in your level of abstraction, "executing a keystroke" is a single atomic operation, it's implemented on the machine as a rather complicated sequence of machine instructions. So, pausing a thread at arbitrary points could lead to things being in an indeterminate state. Sending SIGSTOP is the same level of dangerous as pausing a thread at an arbitrary point. Depending on where you are in a particular step, though, your automation could potentially be broken. For example, if you pause in the middle of a timing-dependent step.

It seems to me that this problem would be best solved at the level of the automation library. I'm not very familiar with the automation library that you're using. It might be worth contacting the developers of the library to see if they have any suggestions for pausing the execution of automation steps at safe sub-step levels.

Jason LeBrun
  • 13,037
  • 3
  • 46
  • 42
  • 1
    Good points. The only quirk I've found so far with suspending the process is that keyboard modifiers (CTRL, ALT, SHIFT) get "stuck" while paused. I can work around that though. Luckily other keys don't get stuck in down state :-) – Bruce van der Kooij Jan 23 '11 at 03:07
2

I don't know pywinauto. But I'll assume that you have something like an Application class which you obtain and have methods like SendKeys/SendMouseEvent/etc to do things.

Create your own MyApplication class which holds a reference to pywinauto's application class. Provide the same methods but before each method check whether a pause event has occurred. If it has, you can jump into code which handles the pause event. That way you are checking for a pause every time you cause an event, but this all is handled by the one class without putting pause all over your code.

Once you've detected the pause you can handle it any way you like. For example, you can throw an exception to force giving up on the current task.

Winston Ewert
  • 44,070
  • 10
  • 68
  • 83
  • +1 You might be on to something there... The automation stuff I'm working on is a little bit more complicated than just simulating keyboard input (there's text selection, parsing text, text comparison etc.) but wrapping a bunch of functions/classes like this might just work out very nicely. I'm going to be looking into it and I'll get back to you. – Bruce van der Kooij Jan 25 '11 at 21:02
1

Separating the functionality and the interface thread/process is definately the best option imho, the second solution is quicker and easier but definately not better.

Perhaps using multiple threads and an exception would be a better idea than using multiple processes. But if you're using multiple processes than SIGSTOP might be your only way to get it to work.

Is there anything against using 2 threads for this?

  • 1 thread for actually executing
  • 1 thread for reading the user input
Wolph
  • 78,177
  • 11
  • 137
  • 148
  • The second method actually involves using threads (I've updated the answer to make this clearer). People are saying you should not force threads to pause from the outside and recommend signaling/messaging between threads instead. I understand how to implement the latter, just not how to actually do the "work" in a esthetically pleasing manner. – Bruce van der Kooij Jan 23 '11 at 02:19
  • @Bruce van der Kooij: I am aware of that. It is indeed not recommended, but imho the best way to do it. There are 3 ways of doing this, the `while true` option (which doesn't really work that great because you have to check the condition yourself). The thread option (indeed, not pretty). And externals signals which are usually more of a bother than the threaded option is. – Wolph Jan 23 '11 at 03:23
  • I added another update because I'm quite stuck right now. Would you be willing to elaborate as to what you meant? Perhaps even provide a code sample? I would surely appreciate it. – Bruce van der Kooij Jan 23 '11 at 19:55
  • 1
    @Bruce van der Kooij: not sure if I can help you much further than that. My suggestion was communication through exceptions, but that's already mentioned in one of your links: http://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread-in-python/325528#325528 – Wolph Jan 24 '11 at 17:33
0

I use Python but not pywinauto; for this sort of tasks I use AutoHotKey . One way to implement a simple pause in an AutoHotkey script may be using a "toggle" key like ScrollLock and testing the key state in the script. Also, the script can restore the key state after switching the internal pause setting on / off.

PabloG
  • 25,761
  • 10
  • 46
  • 59
  • Thanks for your answer. I've worked with AutoHotKey before (some simple script) but this time I decided to go with Python because I had my doubts whether I could do some of the complicated things I'm looking to do with the same ease as I can in Python. – Bruce van der Kooij Jan 25 '11 at 20:53
  • @Bruce: I also prefer Python over AutoHotKey for complicated tasks. But the "toggle" key method isn't available in pywinauto? Maybe you can use sys.setprofile or sys.settrace to check the toggle key state and do the pause / resume without modifying the actual code. (just my two cents) – PabloG Jan 25 '11 at 22:03