如果线程 运行 webbrowser.open() 无法使用 Ctrl-C 退出 Python 脚本

Can't quit Python script with Ctrl-C if a thread ran webbrowser.open()

我正在为 Python (pip install bottle) 使用 Bottle 网络应用程序框架,并希望 运行 一个只能从本地计算机访问的网络应用程序(它本质上是使用浏览器作为 GUI 的桌面应用程序)。要启动 Bottle Web 应用程序,我必须调用 bottle.run(),但只要脚本为 运行ning,就会一直阻塞。您可以按 Ctrl-C 停止它。

但是,我还希望此应用程序通过调用 webbrowser.open() 打开一个网络浏览器到本地主机。问题是,我不能先调用 webbrowser.open(),因为 Web 应用程序不会 运行ning,但是如果我先调用 bottle.run(),它就不会 return只要 Web 应用程序处于 运行ning 状态,我就无法继续调用 webbrowser.open().

我的解决方案是将对 webbrowser.open() 的调用放在一个线程中:

import bottle
import threading
import webbrowser
import time

class BrowserOpener(threading.Thread):
  def run(self):
    time.sleep(1) # waiting 1 sec is a hack, but it works
    webbrowser.open('http://localhost:8042')
    print('Browser opened')

@bottle.route('/')
def index():
  return 'hello world!'

BrowserOpener().start()
bottle.run(host='localhost', port=8042)

现在的问题是在终端中按 Ctrl-C 似乎不起作用,所以除了完全关闭终端之外我没有办法停止网络应用程序。我不确定这是为什么:'Browser opened' 被打印到屏幕上所以我知道 webbrowser.open() 是 returning.

我在 Windows 7.

我已经尝试了 how to terminate a thread which calls the webbrowser in python 中设置 self._running = False 的解决方案,但这并没有改变任何东西。在我可以从中调用 join() 的线程之外也没有地方。

即使我摆脱了单独的线程并使用 os.system('python openbrowser.py') 到 运行 等待一秒钟并打开网络浏览器的脚本,这仍然会阻止 Ctrl-C 工作。

我也尝试使用 threading.Timer(1, webbrowser.open, ['http://localhost:8042']).start() 启动浏览器,但这仍然会阻止 Ctrl-C 工作。

是否有我没有看到的解决方案?

关于此答案的两个直接警告:

  1. 可能有一种方法可以实现您想要的效果,它更接近您的原始设计。如果您不想偏离您最初的想法那么多,也许另一个回答者可以提供更好的解决方案。
  2. 此解决方案尚未在 Windows 上进行测试,因此可能 运行 遇到无法识别 Ctrl-C 信号的相同或类似问题。不幸的是,我手头没有带 Python 解释器的 Windows 机器,无法先试用一下。

除此之外:

您可能会发现将服务器放在一个单独的线程中,然后通过一些简单的信号从主(非阻塞)线程控制它会更轻松。 我在下面创建了一个玩具示例来演示我的意思。您可能更愿意将 class 单独放入一个文件中,然后简单地将其导入并将 class 的一个新实例实例化到您的其他脚本中。

import ctypes
import threading
import webbrowser
import bottle


class SimpleExampleApp():
    def __init__(self):
        self.app = bottle.Bottle()

        #define all of the routes for your app inside the init method
        @self.app.get('/')
        def index():
            return 'It works!'

        @self.app.get('/other_route')
        def alternative():
            return 'This Works Too'

    def run(self):
        self.start_server_thread()
        #depending upon how much configuration you are doing
        #when you start the server you may need to add a brief
        #delay before opening the browser to make sure that it
        #is ready to receive the initial request
        webbrowser.open('http://localhost:8042')

    def start_server_thread(self):
        self.server_thread = threading.Thread(
            target = self.app.run, 
            kwargs = {'host': 'localhost', 'port': 8042}
        )
        self.server_thread.start()

    def stop_server_thread(self):
        stop = ctypes.pythonapi.PyThreadState_SetAsyncExc(
            ctypes.c_long(self.server_thread.get_ident()), 
            ctypes.py_object(KeyboardInterrupt)
        )
        #adding a print statement for debugging purposes since I
        #do not know how well this will work on Windows platform
        print('Return value of stop was {0}'.format(stop))
        self.server_thread.join()


my_app = SimpleExampleApp()
my_app.run()

#when you're ready to stop the app, simply call
#my_app.stop_server_thread()

出于实际应用的目的,您可能需要对它进行相当大的修改,但它应该能让您入门。 祝你好运!