如何实现暂停(及更多)功能?
首先为问题的长度表示歉意,我不想遗漏任何细节。
一些背景信息
我正在尝试通过编写一个Python应用程序来自动化数据录入过程,这个程序使用Windows API来模拟键盘输入、鼠标移动和窗口/控件的操作。我不得不使用这种方法,因为我还没有获得直接访问数据存储/数据库的安全权限(例如,使用SQL)或通过更合适的API间接访问。官僚主义真让人头疼;-)
数据录入过程涉及到由于商品可用性变化而需要修正销售订单。不可用的商品要么从订单中删除,要么用其他合适的商品替代。
最开始,我希望人类能够监控这个自动化的数据录入过程,以确保一切正常进行。为此,我一方面减慢操作速度,另一方面通过一个固定窗口告知用户当前正在发生什么。
实际问题
为了让用户能够暂停自动化过程,我将暂停/中断键注册为热键,并希望在处理程序中暂停自动化功能。然而,我目前在找到合适的方法来正确暂停自动化功能的执行上遇到了困难。当调用暂停功能时,我希望自动化过程能够立即停止,无论它正在做什么。我甚至不希望它执行另一个键盘输入。
更新 [23/01]:实际上,我想做的不仅仅是暂停,我希望能够在自动化过程运行时与其进行通信,请求它暂停、跳过当前的销售订单、完全放弃,甚至更多。
有没有人能告诉我实现我想要的正确方法?
更多信息
以下是自动化工作原理的一个示例(我使用pywinauto库):
from pywinauto import application
app = application.Application()
app.start_("notepad")
app.Notepad.TypeKeys("abcdef")
更新 [25/01]:经过几天的工作,我发现我并不太依赖pywinauto,现在我只用它来查找窗口,然后直接使用SendKeysCtypes.SendKeys
来模拟键盘输入,以及使用win32api
函数来模拟鼠标输入。
我目前发现的内容
以下是我在寻找答案过程中遇到的一些方法:
我可以将自动化功能和界面 + 热键监听器分成两个独立的进程。我们称前者为“自动化器”,后者为“管理器”。管理器可以通过向自动化器进程发送SIGSTOP信号来暂停其执行,并通过SIGCONT信号(或通过SuspendThread/ResumeThread)来恢复。
为了能够更新用户界面,自动化器需要通过某种IPC机制通知管理器其进展。
缺点:
使用SIGSTOP会不会太过严厉?它能正常工作吗?很多人似乎都建议不要这样做,甚至称其为“危险”。
我担心实现IPC机制会有点复杂。另一方面,我曾经使用过DBus,这个实现起来不会太难。
第二种方法是很多人似乎在建议的,涉及使用线程,基本上可以简化为以下内容:
while True: if self.pause: # pause # Do the work...
然而,以这种方式做似乎只有在没有更多工作要做时才会暂停。我认为这种方法能工作的唯一方式是将工作(整个自动化过程)分成更小的工作段(即任务)。在开始新任务之前,工作线程会检查是否应该暂停并等待。
缺点:
似乎将工作分成更小的段的实现会在代码上显得很丑(美观上)。
我想象中的样子是,所有语句都会变成类似于:
queue.put((function, args))
(例如queue.put((app.Notepad.TypeKeys, "abcdef"))
),然后自动化过程线程会遍历任务,并在开始任务之前不断检查暂停状态。这显然不对……程序实际上不会立即停止,而是会先完成一个任务(无论多小),然后才会暂停。
取得的进展
更新 [23/01]:我已经通过提到的SuspendThread/ResumeThread
功能实现了我的应用程序的一个版本。到目前为止,这似乎工作得很好,也让我能够像写其他脚本一样编写自动化的内容。我遇到的唯一小问题是,暂停时键盘修饰键(CTRL、ALT、SHIFT)会“卡住”。这个问题我应该可以轻松解决。
我还使用第二种方法(线程和信号/消息传递)编写了一个测试,并实现了暂停功能。然而,这看起来真的很丑(无论是检查暂停标志还是与“执行工作”相关的所有内容)。所以如果有人能给我一个类似于第二种方法的正确示例,我将不胜感激。
相关问题
- 如何暂停一个进程?
-
没有其他线程可以强制暂停一个线程的方法(就像没有其他线程可以杀死那个线程一样)——目标线程必须通过偶尔检查适当的“标志”来配合(一个threading.Condition可能适合暂停/恢复的情况)。
他还提到了多进程模块和SIGSTOP/SIGCONT。
- 有没有办法无限期暂停一个线程?
-
对此问题的一个回答引用了MSDN文档关于SuspendThread的内容:
此功能主要设计用于调试器使用。并不打算用于线程同步。在拥有同步对象(如互斥锁或临界区)的线程上调用SuspendThread可能会导致死锁,如果调用线程尝试获取由被挂起线程拥有的同步对象。为了避免这种情况,应用程序中的线程(而不是调试器)应该向另一个线程发出挂起自己的信号。目标线程必须设计为监视此信号并做出适当响应。
- 有没有办法在Python中杀死一个线程?
- 如何在Python中在线程之间传递异常
4 个回答
我觉得把功能和界面分开处理是最好的选择。虽然第二种方案更快更简单,但绝对不如第一种好。
也许用多个线程和异常处理会比用多个进程更好。不过如果你选择用多个进程的话,SIGSTOP
可能是让它正常工作的唯一办法。
使用两个线程来处理这个问题有什么不好的地方吗?
- 一个线程负责实际执行任务
- 一个线程负责读取用户输入
要记住,虽然在你理解的层面上,“执行一个按键”看起来像是一个简单的操作,但在机器内部,它实际上是由一系列复杂的机器指令组成的。所以,如果在任意时刻暂停一个线程,可能会导致一些事情处于不确定的状态。发送SIGSTOP信号和在任意时刻暂停线程的危险性是一样的。不过,具体情况还得看你在某个步骤的哪个位置,这样你的自动化可能会出现问题。例如,如果你在一个依赖时间的步骤中途暂停,就可能会出错。
我觉得这个问题最好在自动化库的层面上解决。我对你使用的自动化库不是很熟悉。或许可以联系一下这个库的开发者,看看他们是否有建议,能让你在安全的子步骤级别上暂停自动化步骤的执行。
我对pywinauto不太了解。不过我猜想你应该有一个类似于Application的类,里面有一些方法,比如SendKeys、SendMouseEvent等等,用来执行各种操作。
你可以创建一个自己的MyApplication类,这个类里面保存了对pywinauto的应用类的引用。然后提供相同的方法,但在每个方法执行之前,先检查一下是否发生了暂停事件。如果发生了暂停事件,你就可以跳转到处理暂停事件的代码。这样每次你触发一个事件的时候,都能检查一下是否需要暂停,而这一切都由这个类来处理,避免在代码中到处插入暂停的检查。
一旦你检测到暂停事件,你可以根据自己的需要来处理它。例如,你可以抛出一个异常,强制放弃当前的任务。