fix(close): Edge fallback + better tryClose + honest hint
There is no JavaScript override for browser tab-close security:
``window.close()`` only succeeds on windows JS opened (Chrome --app
windows qualify; a regular browser tab does not). What we can do is
make the --app path easier to hit and the failure case more
actionable.
Three changes:
1. ``src/gui/__main__.py`` — extend browser detection. PATH lookup
now also looks for ``msedge`` / ``microsoft-edge``; Windows install
candidates include the Edge install path; macOS candidates include
Edge and Chromium. Edge is Chromium-based, supports ``--app``, and
ships on every Windows 10+ machine — so users without Chrome no
longer fall through to the regular browser tab. When the fallback
IS hit, print a warning to stderr explaining why Close-from-page
will require Ctrl+W. Renamed ``_find_chrome`` to
``_find_app_browser`` to reflect the broader scope.
2. ``_FAREWELL_SCRIPT_TEMPLATE`` in ``components/_legacy.py`` —
factor close attempts into a ``tryClose`` helper that runs three
escalating tries: standard ``win.close()``, the
``win.open('', '_self')`` history-rewrite trick (no-op in modern
Chrome but free), and ``win.top.close()``. Auto-close on paint AND
the manual button now both call this helper. Skip the manual hint
if the close eventually succeeded between the click and the 250 ms
timeout.
3. ``quit.close_hint`` in en/es i18n packs — rewrite the message to
tell the user honestly that this is a browser security restriction,
tell them the Ctrl+W keystroke that works, and point them at
``python -m src.gui`` for the auto-closing app-mode experience.
2008 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,23 +16,42 @@ from pathlib import Path
|
||||
_URL = "http://localhost:8501"
|
||||
|
||||
|
||||
def _find_chrome() -> str | None:
|
||||
"""Return the path to a Chrome/Chromium executable, or None."""
|
||||
# Check PATH first
|
||||
for name in ("chrome", "google-chrome", "google-chrome-stable", "chromium", "chromium-browser"):
|
||||
def _find_app_browser() -> str | None:
|
||||
"""Return the path to a Chromium-based browser that supports --app mode.
|
||||
|
||||
Chrome is preferred. Edge (Chromium-based, ships on every Windows
|
||||
10+ install) is the fallback that lets ``window.close()`` actually
|
||||
close the tab on the Close page — without it Windows users who
|
||||
don't have Chrome end up in a regular browser tab where the close
|
||||
button is silently blocked by browser security.
|
||||
"""
|
||||
# PATH first — covers Linux package installs and devs who put their
|
||||
# preferred browser on PATH explicitly.
|
||||
for name in (
|
||||
"chrome", "google-chrome", "google-chrome-stable",
|
||||
"chromium", "chromium-browser",
|
||||
"msedge", "microsoft-edge",
|
||||
):
|
||||
found = shutil.which(name)
|
||||
if found:
|
||||
return found
|
||||
|
||||
# Windows common install locations
|
||||
# Windows common install locations.
|
||||
if sys.platform == "win32":
|
||||
candidates = [
|
||||
# Chrome — preferred when present.
|
||||
Path(os.environ.get("PROGRAMFILES", r"C:\Program Files"))
|
||||
/ "Google" / "Chrome" / "Application" / "chrome.exe",
|
||||
Path(os.environ.get("PROGRAMFILES(X86)", r"C:\Program Files (x86)"))
|
||||
/ "Google" / "Chrome" / "Application" / "chrome.exe",
|
||||
Path(os.environ.get("LOCALAPPDATA", ""))
|
||||
/ "Google" / "Chrome" / "Application" / "chrome.exe",
|
||||
# Edge — ships with Windows 10+, Chromium-based, also
|
||||
# supports --app and the window.close() it enables.
|
||||
Path(os.environ.get("PROGRAMFILES(X86)", r"C:\Program Files (x86)"))
|
||||
/ "Microsoft" / "Edge" / "Application" / "msedge.exe",
|
||||
Path(os.environ.get("PROGRAMFILES", r"C:\Program Files"))
|
||||
/ "Microsoft" / "Edge" / "Application" / "msedge.exe",
|
||||
]
|
||||
for c in candidates:
|
||||
if c.exists():
|
||||
@@ -40,23 +59,42 @@ def _find_chrome() -> str | None:
|
||||
|
||||
# macOS
|
||||
if sys.platform == "darwin":
|
||||
mac_chrome = Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
|
||||
if mac_chrome.exists():
|
||||
return str(mac_chrome)
|
||||
candidates = [
|
||||
Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
|
||||
Path("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"),
|
||||
Path("/Applications/Chromium.app/Contents/MacOS/Chromium"),
|
||||
]
|
||||
for c in candidates:
|
||||
if c.exists():
|
||||
return str(c)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _open_app_window() -> None:
|
||||
"""Wait for the server, then open Chrome in app mode (no address bar)."""
|
||||
"""Wait for the server, then open the URL in an --app-mode window.
|
||||
|
||||
--app mode makes the window look like a desktop app (no address bar,
|
||||
no tabs) AND — crucially — makes ``window.close()`` work, because
|
||||
the browser treats --app windows as programmatically closable. The
|
||||
regular-browser fallback prints a warning so the user knows why the
|
||||
Close button on the closing screen will require a manual Ctrl+W.
|
||||
"""
|
||||
time.sleep(2)
|
||||
chrome = _find_chrome()
|
||||
if chrome:
|
||||
subprocess.Popen([chrome, f"--app={_URL}"])
|
||||
else:
|
||||
# Fallback: open in default browser (will have address bar)
|
||||
import webbrowser
|
||||
webbrowser.open(_URL)
|
||||
browser = _find_app_browser()
|
||||
if browser:
|
||||
subprocess.Popen([browser, f"--app={_URL}"])
|
||||
return
|
||||
print(
|
||||
"DataTools: Chrome and Edge were not found in their standard "
|
||||
"locations. Falling back to the default browser. The Close "
|
||||
"button on the shutdown screen will likely require a manual "
|
||||
"Ctrl+W to close the tab (a browser security restriction "
|
||||
"prevents JavaScript from closing user-opened tabs).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
import webbrowser
|
||||
webbrowser.open(_URL)
|
||||
|
||||
|
||||
app_path = Path(__file__).parent / "app.py"
|
||||
|
||||
@@ -159,15 +159,33 @@ _FAREWELL_SCRIPT_TEMPLATE = """
|
||||
'</div>';
|
||||
return overlay;
|
||||
}
|
||||
function tryClose(win) {
|
||||
// Three escalating attempts. Modern browsers only honour close()
|
||||
// for windows that JS opened (Chrome --app windows qualify; a
|
||||
// regular browser tab does not) — there is no JS override for
|
||||
// that policy. The window.open('', '_self') trick used to rewrite
|
||||
// a window's "opener" lineage and let close() through in older
|
||||
// Chrome; modern Chrome patched it but it costs nothing to try.
|
||||
try { win.close(); } catch (e) {}
|
||||
if (win.closed) return;
|
||||
try {
|
||||
var w = win.open('', '_self', '');
|
||||
if (w) {
|
||||
try { w.close(); } catch (e) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
if (win.closed) return;
|
||||
try { win.top.close(); } catch (e) {}
|
||||
}
|
||||
function wireClose(doc, win) {
|
||||
var btn = doc.getElementById('datatools-close-btn');
|
||||
if (!btn) return;
|
||||
btn.onclick = function () {
|
||||
try { win.close(); } catch (e) {}
|
||||
try { win.top.close(); } catch (e) {}
|
||||
tryClose(win);
|
||||
// If after 250ms the window is still here, the browser
|
||||
// blocked the close — show the manual-close hint.
|
||||
setTimeout(function () {
|
||||
if (win.closed) return;
|
||||
var hint = doc.getElementById('datatools-close-hint');
|
||||
if (hint) hint.style.display = 'block';
|
||||
}, 250);
|
||||
@@ -180,9 +198,9 @@ _FAREWELL_SCRIPT_TEMPLATE = """
|
||||
doc.body.appendChild(buildOverlay(doc));
|
||||
}
|
||||
wireClose(doc, win);
|
||||
// Try to close the tab outright too — succeeds in Chrome --app
|
||||
// Auto-close attempt on first paint — succeeds in Chrome --app
|
||||
// windows, no-ops on regular tabs.
|
||||
try { win.close(); } catch (e) {}
|
||||
tryClose(win);
|
||||
} catch (e) {
|
||||
// Cross-origin access denied (shouldn't happen given Streamlit's
|
||||
// sandbox flags, but fall back gracefully): cover this iframe.
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
"farewell_title": "DataTools has shut down",
|
||||
"farewell_subtitle": "You can close this window.",
|
||||
"close_window_button": "Close this window",
|
||||
"close_hint": "Your browser blocked the close. Press Ctrl+W (or ⌘W on Mac) to close this tab manually."
|
||||
"close_hint": "Your browser is blocking the close — this is a security restriction on tabs you opened yourself, not something we can override. Press Ctrl+W (or ⌘W on Mac) to close this tab. Launch DataTools with python -m src.gui to get an auto-closing app window."
|
||||
},
|
||||
"close_page": {
|
||||
"page_title": "DataTools — Close",
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
"farewell_title": "DataTools se ha cerrado",
|
||||
"farewell_subtitle": "Ya puedes cerrar esta ventana.",
|
||||
"close_window_button": "Cerrar esta ventana",
|
||||
"close_hint": "Tu navegador bloqueó el cierre. Presiona Ctrl+W (o ⌘W en Mac) para cerrar esta pestaña manualmente."
|
||||
"close_hint": "Tu navegador está bloqueando el cierre — es una restricción de seguridad para pestañas que abriste tú mismo, no algo que podamos anular. Presiona Ctrl+W (o ⌘W en Mac) para cerrar esta pestaña. Ejecuta DataTools con python -m src.gui para tener una ventana de aplicación que se cierra sola."
|
||||
},
|
||||
"close_page": {
|
||||
"page_title": "DataTools — Cerrar",
|
||||
|
||||
Reference in New Issue
Block a user