gcal/gmail: update MCP servers + SKALD.md

This commit is contained in:
2026-08-07 12:22:24 +01:00
parent c49c3fa74d
commit 371b7f75b5
3 changed files with 146 additions and 46 deletions
+41 -16
View File
@@ -36,6 +36,24 @@ def log(msg: str) -> None:
# Protects all stdout writes (main thread + poll thread).
_stdout_lock = threading.Lock()
# Serializes every Calendar API call and the service/credential build.
#
# googleapiclient is built on httplib2, which is NOT thread-safe: a service object
# owns a single Http holding one reused TLS connection. The poll thread and the
# request thread sharing that object means two threads can drive the same OpenSSL
# socket at once — which does not raise, it segfaults (this exact race killed the
# Gmail connector in production with SIGSEGV, no traceback). `_creds` has the same
# problem: google.auth refreshes it in place, so two concurrent refreshes corrupt
# the token.
#
# Every API call in this file goes through `_call`, and the service and creds are
# built in `_get_service`, so serializing those two entry points covers the whole
# surface — including any call site added later. Reentrant because the two can nest.
#
# Held per-call, never per-batch: correctness needs non-concurrency on the socket,
# not atomicity, and a poll tick must not wait for a long listing to finish.
_api_lock = threading.RLock()
# ── Push notifications ─────────────────────────────────────────────────────────
def _emit_notification(method: str, params: dict) -> None:
@@ -215,9 +233,12 @@ def _build_service() -> Any:
def _get_service() -> Any:
global _service
if _service is None:
_service = _build_service()
return _service
# Locked: two threads racing here would build (and refresh) two services, and
# the loser's would be dropped while its connection was still in flight.
with _api_lock:
if _service is None:
_service = _build_service()
return _service
# ── Error mapping & refresh-on-auth-error ──────────────────────────────────────
@@ -245,21 +266,25 @@ def _call(fn: Callable[[], Any], api_label: str) -> Any:
or a RefreshError. We refresh once, persist the new token, and retry the call.
Anything else (or a second failure) is re-raised so the caller can format it
via _format_google_error.
Holds `_api_lock` for the whole body — see the lock's comment. This is the one
choke point every `.execute()` in this file passes through.
"""
try:
return fn()
except Exception as e:
if not _is_auth_error(e) or _creds is None or not getattr(_creds, "refresh_token", None):
raise
with _api_lock:
try:
from google.auth.transport.requests import Request
_creds.refresh(Request())
_persist_creds()
log("Access token refreshed mid-session after auth error; retrying the call.")
except Exception as refresh_err:
log(f"Mid-session token refresh failed: {refresh_err}")
raise
return fn()
return fn()
except Exception as e:
if not _is_auth_error(e) or _creds is None or not getattr(_creds, "refresh_token", None):
raise
try:
from google.auth.transport.requests import Request
_creds.refresh(Request())
_persist_creds()
log("Access token refreshed mid-session after auth error; retrying the call.")
except Exception as refresh_err:
log(f"Mid-session token refresh failed: {refresh_err}")
raise
return fn()
def _http_error_reason(e: Exception) -> str: