fix(garmin): make wrapper.py's startup tokenstore login lazy
_startup_login() used to run unconditionally at process boot, before main()'s stdin dispatch loop started. Whenever the very first real command turned out to be an explicit authenticate, this was actively counterproductive: on success it duplicated a login authenticate was about to redo anyway, and on failure it was a wasted, unauthenticated hit against Garmin's servers moments before the real attempt -- exactly the kind of extra load that worsens rate-limiting risk. It also never handled MFA, so it couldn't stand in for authenticate regardless. Make it lazy instead: only _handle_call falls back to it, at most once per subprocess lifetime, and only if authenticate was never explicitly attempted first. This is what it was actually for -- silently resuming a cached tokenstore session for a data call that never goes through the explicit authenticate command (e.g. "Sync now" reaching an already-connected user's client right after a backend restart cleared the in-memory client cache).
This commit is contained in:
@@ -11,12 +11,17 @@ import traceback
|
||||
|
||||
from garminconnect import Garmin
|
||||
|
||||
TOKENSTORE = os.path.expanduser(os.environ.get("GARMIN_TOKENSTORE", "~/.garth"))
|
||||
TOKENSTORE = os.path.expanduser(os.environ.get("GARMIN_TOKENSTORE", "~/.garmin"))
|
||||
|
||||
_client = None
|
||||
_auth_state = "unauthenticated"
|
||||
_mfa_input_queue = queue.Queue()
|
||||
_login_result_queue = queue.Queue()
|
||||
# Set the first time this subprocess attempts any login, whichever path
|
||||
# gets there first (see _handle_call's lazy call and _handle_authenticate) --
|
||||
# guarantees _startup_login runs at most once per subprocess lifetime, and
|
||||
# never at all once an explicit authenticate has been attempted.
|
||||
_startup_login_attempted = False
|
||||
|
||||
|
||||
def _debug(msg):
|
||||
@@ -31,18 +36,28 @@ def _prompt_mfa():
|
||||
|
||||
|
||||
def _startup_login():
|
||||
"""Silently resume a cached tokenstore session at process start, so a
|
||||
freshly (re)spawned subprocess is already authenticated for background
|
||||
syncs that never call the explicit authenticate command.
|
||||
"""Silently resume a cached tokenstore session, so a freshly
|
||||
(re)spawned subprocess can already be authenticated for a data 'call'
|
||||
that never goes through the explicit authenticate command -- e.g.
|
||||
"Sync now" reaching an already-connected user's client right after a
|
||||
backend restart cleared the in-memory cache.
|
||||
|
||||
Called lazily (see _handle_call), at most once per subprocess lifetime,
|
||||
and only if nothing has explicitly called authenticate first. Running
|
||||
it unconditionally at process start used to be actively counterproductive
|
||||
whenever the very first command actually was authenticate: on success it
|
||||
just duplicated a login _handle_authenticate was about to redo anyway
|
||||
(it always rebuilds _client from scratch), and on failure it was a
|
||||
wasted, unauthenticated hit against Garmin's servers moments before the
|
||||
real attempt -- extra load that only makes rate-limiting worse.
|
||||
|
||||
Bounded to the same 10s timeout as _handle_authenticate: the actual
|
||||
login runs on a background daemon thread, and this function waits up
|
||||
to 10s for it before returning either way. This runs before main()
|
||||
enters its stdin dispatch loop, so a slow/rate-limited Garmin login
|
||||
must never block indefinitely here -- if it times out, the thread
|
||||
keeps running and will update _auth_state whenever it eventually
|
||||
finishes (success or failure), same as today, just without wedging
|
||||
the whole subprocess unresponsive in the meantime.
|
||||
to 10s for it before returning either way, so a slow/rate-limited
|
||||
Garmin login can never block the caller indefinitely -- if it times
|
||||
out, the thread keeps running and will update _auth_state whenever it
|
||||
eventually finishes (success or failure), just without wedging the
|
||||
whole subprocess unresponsive in the meantime.
|
||||
|
||||
Deliberately uses its own private, function-local result queue rather
|
||||
than the module-level _login_result_queue that _handle_authenticate and
|
||||
@@ -87,7 +102,12 @@ def _startup_login():
|
||||
|
||||
|
||||
def _handle_authenticate(_params):
|
||||
global _client, _auth_state
|
||||
global _client, _auth_state, _startup_login_attempted
|
||||
# An explicit authenticate is happening (successful or not) -- the lazy
|
||||
# startup-login fallback in _handle_call must never fire after this, it
|
||||
# would be redundant at best and a wasted extra hit against Garmin at
|
||||
# worst.
|
||||
_startup_login_attempted = True
|
||||
|
||||
email = os.environ.get("GARMIN_EMAIL", "")
|
||||
password = os.environ.get("GARMIN_PASSWORD", "")
|
||||
@@ -159,6 +179,10 @@ def _handle_complete_mfa(params):
|
||||
|
||||
|
||||
def _handle_call(params):
|
||||
global _startup_login_attempted
|
||||
if _auth_state != "authenticated" and not _startup_login_attempted:
|
||||
_startup_login_attempted = True
|
||||
_startup_login()
|
||||
if _auth_state != "authenticated":
|
||||
raise RuntimeError("Not authenticated. Call authenticate first.")
|
||||
method = params["method"]
|
||||
@@ -188,7 +212,6 @@ def dispatch(req):
|
||||
|
||||
|
||||
def main():
|
||||
_startup_login()
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
|
||||
Reference in New Issue
Block a user