如何检测Python脚本被用户关闭?适配Mac/Windows终端退出场景
嘿,刚好我之前处理过类似的需求——atexit确实只能覆盖正常退出、Ctrl+C这类“温和”的终止场景,遇到用户直接关终端、或者用任务管理器结束进程这种情况就抓瞎了。下面给你一套跨Mac和Windows的完整解决方案:
跨平台监测脚本退出的实现方案
1. Mac(类Unix系统):捕获系统信号
在Mac上,当用户关闭终端窗口时,系统会给终端内的进程发送SIGHUP信号;如果是通过kill命令正常终止进程(非强制的kill -9),进程会收到SIGTERM信号。我们可以用Python内置的signal模块来拦截这些信号,先执行清理逻辑再退出。
示例代码:
import signal import atexit import sys def cleanup(): print("脚本正在执行清理工作...") # 这里写你的具体清理逻辑:关闭文件句柄、释放网络连接、保存临时数据等 def signal_handler(signum, frame): # 收到信号后触发清理,然后优雅退出 cleanup() sys.exit(0) # 注册需要监听的信号 signal.signal(signal.SIGHUP, signal_handler) # 捕获终端关闭事件 signal.signal(signal.SIGTERM, signal_handler) # 捕获正常终止命令 signal.signal(signal.SIGINT, signal_handler) # 捕获Ctrl+C(可选,替代atexit的部分场景) # 保留atexit作为兜底,处理正常退出的情况 atexit.register(cleanup) # 模拟你的主业务逻辑 if __name__ == "__main__": print("脚本运行中...可以试试关闭终端或按Ctrl+C测试") while True: pass # 这里替换成你的长时间运行任务
2. Windows系统:处理控制台关闭事件
Windows没有SIGHUP这类信号,当用户关闭控制台窗口、或者通过任务管理器结束控制台进程时,系统会发送CTRL_CLOSE_EVENT(还有登出、关机时的CTRL_LOGOFF_EVENT、CTRL_SHUTDOWN_EVENT)。我们需要用ctypes调用Windows系统API来捕获这些事件。
示例代码:
import ctypes import atexit import sys def cleanup(): print("脚本正在执行清理工作...") # 你的清理逻辑 # Windows控制台事件的回调函数 def console_handler(event): # 0=Ctrl+C事件,2=控制台关闭事件 if event in (0, 2): cleanup() return True # 告诉系统我们已经处理了这个事件 return False # 注册控制台事件处理器 handler = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_uint)(console_handler) ctypes.windll.kernel32.SetConsoleCtrlHandler(handler, True) # 兜底的atexit注册 atexit.register(cleanup) # 主业务逻辑 if __name__ == "__main__": print("脚本运行中...试试关闭控制台窗口测试") while True: pass
3. 一键兼容跨平台的整合版本
把上面的逻辑结合起来,写一个自动适配Mac和Windows的脚本,不用手动区分平台:
import atexit import sys def cleanup(): print("执行清理操作...") # 此处添加你的清理代码:比如保存程序状态、关闭数据库连接等 # 根据平台选择对应的监听方式 if sys.platform == "win32": import ctypes def console_handler(event): if event in (0, 2): # 处理Ctrl+C和控制台关闭事件 cleanup() return True return False # 注册Windows控制台事件 handler = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_uint)(console_handler) ctypes.windll.kernel32.SetConsoleCtrlHandler(handler, True) else: import signal def signal_handler(signum, frame): cleanup() sys.exit(0) # 注册类Unix系统的信号监听 signal.signal(signal.SIGHUP, signal_handler) signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) # 兜底的正常退出处理 atexit.register(cleanup) # 主程序逻辑 if __name__ == "__main__": print(f"脚本在{sys.platform}平台上运行中...") try: while True: # 你的业务代码写在这里 pass except Exception as e: print(f"程序发生异常: {str(e)}") cleanup() sys.exit(1)
几个关键注意点
- 无法处理强制终止:不管是Mac上的
kill -9还是Windows上的“结束进程树”,这类强制终止是系统直接干掉进程,根本不给执行清理的机会——这是系统级的限制,没有任何办法绕过。 - 清理逻辑要轻量化:处理信号或控制台事件时,系统留给进程的执行时间非常有限,所以清理代码要尽量简洁,避免复杂的IO操作或者耗时任务,否则可能还没执行完就被系统强制终止了。
内容的提问来源于stack exchange,提问作者Tron




