Skip to content

Commit e15d417

Browse files
kasey6801claude
andcommitted
Release v0.42.1: tab/server lifecycle improvements
- Close tab on quit: Quit button now redirects to /stopped page instead of using window.close() (which browsers block) - Close server on tab close: heartbeat watchdog shuts server down 12s after the last browser ping, so closing the tab exits the app cleanly - Add /heartbeat and /stopped endpoints - Add _watchdog background thread Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c4d0194 commit e15d417

File tree

2 files changed

+70
-7
lines changed

2 files changed

+70
-7
lines changed

MarkItDown.spec

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,14 @@ app = BUNDLE(
103103
name='MarkItDown.app',
104104
icon=None,
105105
bundle_identifier='com.markitdown.app',
106-
version='0.42',
106+
version='0.42.1',
107107
info_plist={
108108
'NSPrincipalClass': 'NSApplication',
109109
'NSHighResolutionCapable': True,
110110
'LSBackgroundOnly': False,
111111
'NSHumanReadableCopyright': 'MarkItDown Local — MIT License',
112-
'CFBundleShortVersionString': '0.42',
113-
'CFBundleVersion': '42',
112+
'CFBundleShortVersionString': '0.42.1',
113+
'CFBundleVersion': '421',
114114
'LSMinimumSystemVersion': '12.0',
115115
},
116116
)

app.py

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
MarkItDown Local Frontend
33
=========================
4-
Version: v0.42
4+
Version: v0.42.1
55
66
A self-contained Flask web application that provides a browser-based UI
77
for Microsoft's MarkItDown library (https://github.com/microsoft/markitdown).
@@ -135,7 +135,7 @@
135135

136136
from flask import Flask, request, jsonify, render_template_string
137137
from 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
@@ -598,7 +598,7 @@
598598
<button id="quit-btn" onclick="quitApp()">Quit</button>
599599
<h1>⚡ MarkItDown</h1>
600600
<p>Convert documents, PDFs, Office files &amp; more to Markdown — locally.</p>
601-
<p id="version">v0.42</p>
601+
<p id="version">v0.42.1</p>
602602
</header>
603603
604604
<!-- ═══════════════════════════════════════════════════
@@ -954,8 +954,21 @@
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

Comments
 (0)