Search code examples
pythontkinterhttpserver

Why httpserver won't on separate thread with tkinter


First of all, I'm relatively new to python so in case I'm doing something incredibly stupid please let me know.

I have this desktop app to be run on a raspberyy pi and developed with python and tkinter for the GUI and due to new requirements in the project, it needs to be able to receive a command remotely to do some action.

To achieve this i wanted to add a http server to the project

In my main script i have this:

from tkinter import *  
from http.server import BaseHTTPRequestHandler, HTTPServer
import threading

# Create GUI app and define general properties
window = Tk()
window.attributes("-fullscreen", True)
window.config(cursor="none")

winWidth = int(window.winfo_screenwidth() * 1)
winHeight = int(window.winfo_screenheight() * 1)

window.geometry(f"{winWidth}x{winHeight}")

class HttpHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()
        self.wfile.write(bytes("<html><head><title>https://pythonbasics.org</title></head>", "utf-8"))
        self.wfile.write(bytes("<p>Request: %s</p>" % self.path, "utf-8"))
        self.wfile.write(bytes("<body>", "utf-8"))
        self.wfile.write(bytes("<p>This is an example web server.</p>", "utf-8"))
        self.wfile.write(bytes("</body></html>", "utf-8"))

 
#Start HTTP Server
webServer = HTTPServer(('localhost', 9080), HttpHandler)
print('running http server on port: ', 9080)
threading.Thread(target=webServer.serve_forever, daemon=True) 
    
window.mainloop()

Right now the code is very basic just to get it running but the idea was to later on, do an IO operation on a http request. For example /api/lights/on would trigger a GPIO.

I had to use threading.Thread because otherwise the script would block on webServer.serve_forever()

By using threading it no longer blocks and shows the GUI properly. Also with a 'netstat -lnt' I can tell that the http server is listening on the specified port.

When I open the browser on http://127.0.0.1:9080/ the browser never gets a response.

Am I doing something wrong here?


Solution

  • I found two mistakes

    1. you forgot import threading so it gives error message

      import threading
      
    2. you created thread but you forgot to start it

      t = threading.Thread(target=webServer.serve_forever, daemon=True)
      t.start()
      

    BTW:

    You could better organize code.
    See: PEP 8 --Style Guide for Python Code

    import threading
    import tkinter as tk  # PEP8: `import *` is not preferred
    from http.server import BaseHTTPRequestHandler, HTTPServer
    
    # --- classes ---
    
    class HttpHandler(BaseHTTPRequestHandler):
        def do_GET(self):
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(bytes("<html><head><title>https://pythonbasics.org</title></head>", "utf-8"))
            self.wfile.write(bytes("<p>Request: %s</p>" % self.path, "utf-8"))
            self.wfile.write(bytes("<body>", "utf-8"))
            self.wfile.write(bytes("<p>This is an example web server.</p>", "utf-8"))
            self.wfile.write(bytes("</body></html>", "utf-8"))
    
    # --- functions ---
    
    # empty
    
    # --- main ---
    
    webServer = HTTPServer(('localhost', 9080), HttpHandler)
    print('running http server: http://localhost:9080')  # some consoles will display URL as clickable so it is easier to run browser
    t = threading.Thread(target=webServer.serve_forever, daemon=True)
    t.start()
    
    window = tk.Tk()
    
    #window.attributes("-fullscreen", True)
    window.config(cursor="none")
    
    #winWidth = int(window.winfo_screenwidth() * 1)
    #winHeight = int(window.winfo_screenheight() * 1)
    #window.geometry(f"{winWidth}x{winHeight}")
        
    window.mainloop()
    

    I only wondering if using http.server is good idea. If you want to access by web page then it can be simpler to create pages with Flask. If you want to send small commands then maybe it would be simpler use server MQTT instead of HTTP. Some IoT devices my already use MQTT


    Other problem can make communications between threads. Tkinter doesn't like to run in subthreads so you can't access widget directly in thread with server and it will need queue to send values to main thread and tkinter will need after(millisecond, function) to check queue periodically to get command.


    EDIT:

    Version which uses queue to send information from http server to tkinter and it displays it in widget Text

    import threading
    import tkinter as tk  # PEP8: `import *` is not preferred
    from http.server import BaseHTTPRequestHandler, HTTPServer
    import queue
    
    # --- classes ---
    
    class HttpHandler(BaseHTTPRequestHandler):
        def do_GET(self):
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(bytes("<html><head><title>https://pythonbasics.org</title></head>", "utf-8"))
            self.wfile.write(bytes("<p>Request: %s</p>" % self.path, "utf-8"))
            self.wfile.write(bytes("<body>", "utf-8"))
            self.wfile.write(bytes("<p>This is an example web server.</p>", "utf-8"))
            self.wfile.write(bytes("</body></html>", "utf-8"))
    
            q.put("Request: %s" % self.path)  # put in `queue`
            
    # --- functions ---
    
    def check_queue():
        if not q.empty():
            text.insert('end', q.get()+'\n') # get from `queue` and put in `Text`
        window.after(100, check_queue)  # check again after 100ms
        
    # --- main ---
    
    q = queue.Queue()
    
    webServer = HTTPServer(('localhost', 9080), HttpHandler)
    print('running http server: http://localhost:9080')  # some consoles will display URL as clickable so it is easier to run browser
    t = threading.Thread(target=webServer.serve_forever, daemon=True)
    t.start()
    
    window = tk.Tk()
    
    text = tk.Text(window)
    text.pack()
    
    check_queue()
    
    window.mainloop()
    

    enter image description here


    EDIT:

    The same with Flask. It can get data: args, json, form, files, etc.

    import queue
    import threading
    import tkinter as tk  # PEP8: `import *` is not preferred
    from flask import Flask, request, render_template_string
    
    # --- classes ---
    
        
    # --- functions ---
    
    app = Flask(__name__)
    
    #@app.route('/')
    @app.route('/', defaults={'path': ''})
    @app.route('/<path:path>')
    def index(path):
    
        print('path:', path)
        print(f'Request: {request.method} {request.url}')
        print('args:', request.args)
        print('form:', request.form)
        print('data:', request.data)
        print('json:', request.json)
        print('files:', request.files)
        
        q.put(f'Request: {request.method} {request.url}')  # put in `queue`
    
        return render_template_string('''<!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8">
    <title>https://pythonbasics.org</title>
    </head>
    <body>
    <p>Request: {{ request.method }} {{ request.url }}</p>
    <p>This is an example web server.</p>
    </body>
    </html>''', request=request)
    
    def check_queue():
        if not q.empty():
            text.insert('end', q.get()+'\n') # get from `queue` and put in `Text`
        window.after(100, check_queue)  # check again after 100ms
        
    # --- main ---
    
    q = queue.Queue()
    
    print('running http server: http://localhost:9080')  # some consoles will display URL as clickable so it is easier to run browser
    t = threading.Thread(target=app.run, args=('localhost', 9080), daemon=True)
    t.start()
    
    window = tk.Tk()
    
    text = tk.Text(window)
    text.pack()
    
    check_queue()
    
    window.mainloop()
    

    But now question is: why to use tkinter if you may do all in flask