Skip to content

Fix app hang on startup/shutdown with no internet connection - #541

Merged
FrankFaulstich merged 2 commits into
mainfrom
claude/festive-montalcini-3fda59
Aug 7, 2026
Merged

Fix app hang on startup/shutdown with no internet connection#541
FrankFaulstich merged 2 commits into
mainfrom
claude/festive-montalcini-3fda59

Conversation

@FrankFaulstich

Copy link
Copy Markdown
Owner

Summary

Two independent fixes, both around code hanging indefinitely with no clear failure:

  • SOAP server import hang: spyne 2.14.0 (the only PyPI release) vendors an old copy of six whose meta path importer lacks Python 3.12's required find_spec(), so import spyne itself fails with ModuleNotFoundError: No module named 'spyne.util.six.moves'. On Python 3.13+, a second break surfaces (spyne's SOAP11/WSGI code still does import cgi, removed from the stdlib in 3.13). Both were causing every test in tests/test_TimeTrackerSOAP_Server.py to silently skip via a broad except SystemExit — the SOAP interface had effectively zero real test coverage.
  • App hangs with no internet connection: requests'/subprocess's own timeout= only bounds the connect()/read() phases of a socket that already knows its target — it never covers DNS resolution, which runs first and can hang far longer than any such timeout with no network route at all. Two call sites were affected: tt/TimeTracker.py's _check_and_install_dependencies() (a bare pip install subprocess with no timeout, running before any UI in every entry point) and update.py's GitHub release check (called once at CLI/GUI exit, and — after finding my branch was 14+ PRs behind main and merging up — once per Streamlit session on sl/SL_Menu.py's "new version available" banner).

Changes

  • TimeTrackerSOAP_Server.py: two import-time shims — one loads spyne's vendored six.py directly and patches find_spec() onto its importer; the other registers a minimal cgi stand-in (parse_header only, via email.message.EmailMessage) when the real module is absent. Both no-ops if spyne is ever fixed upstream, no new dependencies.
  • update.py: new _call_with_deadline() helper runs the actual network call in a daemon thread with a bounded join() — deliberately not concurrent.futures.ThreadPoolExecutor, which registers worker threads to be joined at interpreter exit and would just relocate a genuine hang to app shutdown. check_for_updates()/download_update() now catch the resulting TimeoutError alongside their existing requests.exceptions.RequestException handling. Also adds should_check_for_updates(session_state, current_menu), a small testable helper for the throttling decision below.
  • tt/TimeTracker.py: _check_and_install_dependencies()'s pip install subprocess now has a PIP_INSTALL_TIMEOUT (120s); a timeout is treated like a failed install (skip the package, keep going) instead of hanging startup indefinitely.
  • install.py: same timeout fix applied to the standalone setup script for consistency.
  • sl/SL_Menu.py: the "new version available" banner now re-checks on every view change (not just once per session) while still skipping repeated reruns of the same view (a keystroke, the 5s auto-refresh tick), via should_check_for_updates().
  • tests/test_update.py (new) and tests/test_TimeTracker.py: regression tests simulating network/subprocess calls that never return, asserting bounded return times; tests for the view-change throttling decision.

Test plan

  • Full suite passes: 236 tests, 0 failures (python -m unittest discover tests)
  • SOAP fix verified in fresh venvs across Python 3.10–3.14 (pip install spyne lxml), confirming tests/test_TimeTrackerSOAP_Server.py executes and passes instead of skipping
  • Update-hang fix verified with simulated hung network calls (deadline patched low in tests) — bounded return confirmed
  • Daemon-thread-vs-ThreadPoolExecutor design claim verified empirically: a control test using ThreadPoolExecutor for the same scenario genuinely hung process exit, confirming the daemon-thread approach is necessary, not just theoretically nicer
  • Live-tested in a browser: started the Streamlit app, navigated across multiple views, confirmed the version banner appears/persists correctly with no console errors

Notes for reviewer

  • No new dependencies added anywhere.
  • German-locale translations for the two new user-facing warning strings in update.py were not added (they fall back to English, same as one pre-existing untranslated string already does) — this repo's translations are historically done in dedicated follow-up commits with proper msgfmt tooling, not alongside the code change.
  • A related, lower-priority gap was found but intentionally left out of this PR: fetch_emails_to_tasks()'s IMAP connection has the same zero-timeout pattern, but it's opt-in (disabled by default) and only reached via explicit user action, so it doesn't block startup the way the fixes above did.

spyne 2.14.0 (the only PyPI release) vendors a 2020-era copy of `six`
whose meta path importer only implements the legacy PEP 302
find_module()/load_module() protocol. Python 3.12 dropped the
compatibility shim for finders lacking PEP 451's find_spec(), so
`spyne.util.six.moves.*` imports fail during `import spyne` itself
with "ModuleNotFoundError: No module named 'spyne.util.six.moves'"
(see arskom/spyne#711, open/unreleased).

On Python 3.13+, a second break surfaces once the first is patched:
spyne's SOAP11 protocol and WSGI transport still `import cgi`, which
was removed from the stdlib in 3.13 (PEP 594).

Two import-time shims in TimeTrackerSOAP_Server.py fix both without
new dependencies: one loads spyne's vendored six.py directly and adds
find_spec() to its importer; the other registers a minimal `cgi` stub
(parse_header only, reimplemented via email.message.EmailMessage) when
the real module is absent. Both are inert if spyne is ever fixed
upstream. Verified across Python 3.10-3.14 in fresh venvs - all SOAP
tests execute and pass instead of being silently skipped.
Two independent unbounded network calls could hang the app
indefinitely with no internet connection, both hitting the classic
Python gotcha: a `requests`/`subprocess` `timeout=` only bounds the
connect()/read() phases of a socket that already knows its target -
it never covers DNS resolution, which runs first and can hang far
longer than any such timeout when there's no network route at all
(especially on Windows, or whenever DNS packets are silently dropped).

- tt/TimeTracker.py's _check_and_install_dependencies() runs a bare
  `pip install` subprocess with no timeout at all, before any UI, in
  every entry point. Now bounded by PIP_INSTALL_TIMEOUT (120s); a
  timeout is treated like a failed install (skip the package, keep
  going) instead of hanging app startup indefinitely. Same fix applied
  to install.py's standalone setup script for consistency.

- update.py's check_for_updates()/download_update() (the GitHub
  release check) now run under a hard wall-clock deadline via a new
  _call_with_deadline() helper: the actual network call runs in a
  daemon thread with a bounded join(); if it's still running past the
  deadline, the caller gives up instead of waiting forever. A daemon
  thread (not concurrent.futures.ThreadPoolExecutor) is deliberate -
  ThreadPoolExecutor registers its worker threads to be joined at
  interpreter exit, which would just relocate a genuine hang from
  "before the check" to "at app shutdown".

- sl/SL_Menu.py's "new version available" banner used to check GitHub
  exactly once per session; it now re-checks on every view change
  (via the new update.should_check_for_updates() helper) while still
  skipping repeated reruns of the same view (a keystroke, the 5s
  auto-refresh tick). Combined with the deadline above, a dead network
  now means the check is silently skipped rather than blocking the
  view change.

Verified: new regression tests simulate network calls that never
return and assert bounded return times; live-tested in a browser
across multiple view changes with no errors. Full suite: 236 tests
passing.
@FrankFaulstich
FrankFaulstich merged commit 002cdb9 into main Aug 7, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant