Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I'm using the Bottle web app framework for Python (pip install bottle) and want to run a web app that will just be accessed from the local machine (it's essentially a desktop app that uses the browser for the GUI). To start the bottle web app, I have to call bottle.run() but this blocks for as long as the script is running. You stop it by pressing Ctrl-C.

However, I also want this app to open a web browser to localhost by calling webbrowser.open(). The problem is, I can't call webbrowser.open() first because the web app won't be running, but if I call bottle.run() first it won't return as long as the web app is running and I can't continue on to call webbrowser.open().

My solution was to put the call to webbrowser.open() inside of a thread:

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)

The problem with this is now pressing Ctrl-C in the terminal doesn't seem to work, so I have no way of stopping the web app other than closing the terminal entirely. I'm not sure why this is: 'Browser opened' gets printed to the screen so I know webbrowser.open() is returning.

I'm on Windows 7.

I've tried the solution from how to terminate a thread which calls the webbrowser in python of setting self._running = False but that doesn't change anything. There's also no place outside the thread I can call join() from.

Even if I get rid of the separate thread and use os.system('python openbrowser.py') to run a script that waits a second and opens the webbrowser, this still prevents Ctrl-C from working.

I also tried launching the browser using threading.Timer(1, webbrowser.open, ['http://localhost:8042']).start() but this still prevents Ctrl-C from working too.

Is there a solution I'm not seeing?

like image 209
Al Sweigart Avatar asked Apr 13 '17 18:04

Al Sweigart


1 Answers

Two immediate caveats with this answer:

  1. There may be a way of accomplishing what you would like that is much closer to your original design. If you'd prefer not deviating as much from your original idea, perhaps another answerer can provide a better solution.
  2. This solution has not been tested on Windows and therefore may run up against the same or similar issues with it failing to recognize the Ctrl-C signal. Unfortunately, I do not have a Windows machine with a Python interpreter on hand to try it out first.

With that out of the way:

You may find that you have an easier time by placing the server in a seperate thread and then controlling it from the main (non-blocked) thread via some simple signals. I've created a toy example below to demonstrate what I mean. You may prefer to put the class in a file by itself and then simply import it and instantiate a new instance of the class into your other scripts.

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()

You'll likely need to modify this fairly heavily for the purposes of your actual application, but it should hopefully get you started. Good luck!

like image 190
user3351605 Avatar answered Oct 21 '22 03:10

user3351605