Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
Python http.server how can I programatically refresh the browser page?
I'm currently using a static site generator (SSG) to convert a large collection of Markdown notes to a personal HTML/CSS wiki, but I decided to replace it with a Python script so I can fully customize every aspect of the site generation and my workflow. It's also something of a programming challenge/learning exercise for me.
I've already figured out how to create a local HTTP server and simultaneously watch the input files for changes (the actual site construction isn't implemented yet). However, my SSG also refreshes the browser page whenever the content updates so I don't have to do so manually, a convenience I'd like to include in my script. But I'm at a bit of a loss on how that can be done.
Here's my code so far:
# Import dependencies
import sys, signal
import _thread as thread
from http.server import HTTPServer, SimpleHTTPRequestHandler
from PyQt6.QtCore import QFileSystemWatcher, QTimer
from PyQt6.QtWidgets import QApplication
# Define server address and important directories
host = 'localhost'
port = 8000
contentDirectory = '/path/to/content'
siteDirectory = '/path/to/site'
# Define input paths for the filesystem watcher to periodically check for changes
paths = [
'/path/1',
'/path/2',
'/path/3',
]
# Define some colors with ANSI escape sequences
class color():
red = '\033[91m'
yellow = '\033[93m'
green = '\033[92m'
cyan = '\033[96m'
blue = '\033[94m'
magenta = '\033[95m'
white = '\033[97m'
end = '\033[0m'
# Function to be called when the input changes
def changeDetected(path):
print('Directory changed: ' + path)
# Handler for the SIGINT signal
def sigintHandler(*args):
QApplication.quit()
print(color.red + "\nServer stopped" + color.end)
# Function for closing the server on demand
def serverStopped():
server.server_close()
# Subclass SimpleHTTPRequestHandler so we can serve a specific directory
class Handler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=siteDirectory, **kwargs)
# Final setup
if __name__ == "__main__":
# Create the core app and the filesystem watcher
app = QApplication(sys.argv)
filesystemWatcher = QFileSystemWatcher(paths)
filesystemWatcher.directoryChanged.connect(changeDetected) # Run some code whenever the input changes
# Listen for signals - particularly Ctrl+C - in the terminal (needed for quitting the QApplication on demand)
signal.signal(signal.SIGINT, sigintHandler) # Implement custom handling for the SIGINT signal
timer = QTimer()
timer.start(500) # NOTE: Change time as needed
timer.timeout.connect(lambda: None) # Let the interpreter run every 500 milliseconds so signals come through
# Create and start the server
def startServer(): # Setup stuff here...
server = HTTPServer((host, port), Handler) # Create the server object
try: # Run the server until...
server.serve_forever()
except KeyboardInterrupt: # ...someone presses Ctrl+C
serverStopped()
thread.start_new_thread(startServer, ()) # Run it in a separate thread for concurrency with other code (particularly the filesystem watcher)
print(color.blue + f'Server started at http://{host}:{port} (Ctrl+C to stop)' + color.end)
# Run the event loop for the watcher
app.exec()
2 answers
Two basic approaches to this, both require having your server inject a <script> tag into your page...
-
The old-school "simple" script, which uses
setInterval()to periodically poll your server usingXMLHttpRequest.open()orfetch()to query for changes, either by ETag header or 304 Not Modified status code (generation of which you must build into your HTTP server). If the response from the server is not the same ETag as the last time you requested it, or the server sends back something other than 304, callwindow.location.reload(). -
The newer-style "complex" script, which establishes either a
WebSocketor anEventSourceto your HTTP server, and waits for messages -- the event handler function for that would callwindow.location.reload(). In your HTTP server code, you send a message on that channel whenever you want the client to reload (i.e. when you detect the change to your content).
EventSource is possibly the cleanest approach (see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events or https://dev.to/philip_zhang_854092d88473/mastering-server-sent-events-sse-with-python-and-go-for-real-time-data-streaming-38bf for ideas), but the old-school approach is probably fine for a local dev instance, where the load can be controlled.
In any case, if you intend to expose your HTTP server to the public, you should make it configurable on the server-side, whether the client script is actually injected. You probably don't want hundreds of pings to your server from the outside world, every time someone browses your pages.
1 comment thread
The typical solution is to inject some JavaScript so that the client can be notified of changes. Nowadays, you'd typically use Web Sockets for this so that the server can actively notify the client when changes are made. This leads to the quickest reaction to changes of the source files. The other alternative is to have the client poll for changes.
Once you have a mechanism for the client-side to notice changes from the server, there are a variety of ways of actually implementing those changes. By far the simplest thing to do, which is probably completely adequate for your purposes, is to just have the page to a reload with window.location.reload() in JavaScript. That means all that needs to be sent over a Web Socket or received via polling is a notification that something has changed. At the other end of the spectrum is stuff like React's Fast Refresh which can make incremental changes and maintain client-side (React) state. Obviously, this is in the context of a React web app. Intermediate (and used by Fast Refresh) is webpack's Hot Module Replacement which allows replacing JavaScript modules on the fly, but doesn't, by itself, maintain state.
It probably wouldn't be hard to implement something that works a bit differently but still serves your purposes well. At the most basic level, you could just send the full generated HTML of the actual content and then just do mainContainer.innerHTML = receivedHTML. This is tantamount to refreshing the page but will probably look more seamless. If you wanted to go further, you could diff the generated HTML and then send (logical) updates instead of the full generated output. This would take much more development effort to implement (from scratch at least) and likely would be a marginal improvement in responsiveness.

0 comment threads