11"""
22MarkItDown Local Frontend
33=========================
4- Version: v0.42
4+ Version: v0.42.1
55
66A self-contained Flask web application that provides a browser-based UI
77for Microsoft's MarkItDown library (https://github.com/microsoft/markitdown).
135135
136136from flask import Flask , request , jsonify , render_template_string
137137from markitdown import MarkItDown
138- import io , os , signal , traceback , threading , webbrowser
138+ import io , os , signal , time , traceback , threading , webbrowser
139139
140140# ---------------------------------------------------------------------------
141141# App Initialisation
598598 <button id="quit-btn" onclick="quitApp()">Quit</button>
599599 <h1>⚡ MarkItDown</h1>
600600 <p>Convert documents, PDFs, Office files & more to Markdown — locally.</p>
601- <p id="version">v0.42</p>
601+ <p id="version">v0.42.1 </p>
602602</header>
603603
604604<!-- ═══════════════════════════════════════════════════
954954
955955 /** Sends a shutdown request to the server then closes the tab. */
956956 function quitApp() {
957- fetch("/quit", { method: "POST" }).finally(() => window.close());
957+ // Stop the heartbeat so the watchdog doesn't fire before the quit response
958+ clearInterval(heartbeatTimer);
959+ fetch("/quit", { method: "POST" }).finally(() => {
960+ // Redirect to the stopped page — works even when window.close() is blocked
961+ window.location.href = "/stopped";
962+ });
958963 }
964+
965+ // ── Heartbeat ─────────────────────────────────────────────────────────────
966+ // Pings the server every 5 s so the watchdog knows the tab is still open.
967+ // When the tab is closed the pings stop; the watchdog shuts the server down
968+ // after a 12 s timeout (2× the interval + grace period).
969+ const heartbeatTimer = setInterval(() => {
970+ fetch("/heartbeat", { method: "POST" }).catch(() => {});
971+ }, 5000);
959972</script>
960973</body>
961974</html>
@@ -1057,6 +1070,55 @@ def _shutdown():
10571070 return jsonify ({"status" : "bye" })
10581071
10591072
1073+ @app .route ("/stopped" )
1074+ def stopped ():
1075+ """Shown after the server has received a quit request and the browser redirects here."""
1076+ return """<!DOCTYPE html>
1077+ <html lang="en">
1078+ <head>
1079+ <meta charset="UTF-8"/>
1080+ <title>MarkItDown stopped</title>
1081+ <style>
1082+ body { background:#0f0f13; color:#a0a0b0; font-family: system-ui, sans-serif;
1083+ display:flex; align-items:center; justify-content:center; height:100vh; margin:0; }
1084+ p { font-size:1rem; opacity:.7; }
1085+ </style>
1086+ </head>
1087+ <body><p>MarkItDown has stopped. You can close this tab.</p></body>
1088+ </html>"""
1089+
1090+
1091+ # Timestamp of the most recent heartbeat from the browser tab.
1092+ _last_heartbeat = None
1093+
1094+
1095+ @app .route ("/heartbeat" , methods = ["POST" ])
1096+ def heartbeat ():
1097+ """Receives a periodic ping from the browser tab. Resets the watchdog timer."""
1098+ global _last_heartbeat
1099+ _last_heartbeat = time .monotonic ()
1100+ return jsonify ({"status" : "ok" })
1101+
1102+
1103+ def _watchdog ():
1104+ """Background thread: shuts the server down if no heartbeat arrives within 12 s.
1105+
1106+ The browser sends a heartbeat every 5 s. 12 s gives two missed pings plus a
1107+ grace period, covering page reloads and brief network hiccups without
1108+ triggering a false shutdown.
1109+ """
1110+ import time as _time
1111+ # Wait for the first heartbeat before starting to enforce the timeout,
1112+ # so a slow browser open doesn't trigger a premature shutdown.
1113+ while _last_heartbeat is None :
1114+ _time .sleep (1 )
1115+ while True :
1116+ _time .sleep (3 )
1117+ if _time .monotonic () - _last_heartbeat > 12 :
1118+ os .kill (os .getpid (), signal .SIGTERM )
1119+ break
1120+
1121+
10601122# ---------------------------------------------------------------------------
10611123# Entry point
10621124# ---------------------------------------------------------------------------
@@ -1073,6 +1135,7 @@ def open_browser():
10731135 webbrowser .open (f"http://127.0.0.1:{ port } " )
10741136
10751137 threading .Thread (target = open_browser , daemon = True ).start ()
1138+ threading .Thread (target = _watchdog , daemon = True ).start ()
10761139
10771140 # debug=False is required when packaging as a Mac app — debug mode uses
10781141 # a reloader that spawns a second process, which breaks PyInstaller bundles.
0 commit comments