A packaged Windows build reported "Microsoft Store returned invalid
JSON" on every update check. The Store checker, the App Installer
checker and the relaunch waiter were resolved under the payload's repo
snapshot (<payload>/hermes-agent/apps/desktop/scripts/), but
INERT_SNAPSHOT_DIRS has excluded apps/ and scripts/ from that snapshot
since f6713f2762. The installed 2026.6523.3313.0 package has no apps/
directory at all. The payload python then failed to open the script,
exited 2 with empty stdout, and parseResult reported invalid JSON.
The two checkers are python that runs under the payload interpreter, so
they belong with the core code, like the other hermes_cli helpers the
desktop runs with -m:
- apps/desktop/scripts/check-store-update.py becomes
hermes_cli/windows_store_update.py and check-appinstaller-update.py
becomes hermes_cli/windows_appinstaller_update.py (git mv).
- The runner spawns `python -P -m <module>` with PYTHONPATH set to the
bundled core code, then the venv dependencies: the order the bin
launchers give sys.path. -P keeps the working directory off sys.path.
- The relaunch waiter is PowerShell run outside the package, not core
code. It ships as an app resource (win.extraResources) and resolves
from process.resourcesPath.
The Python tests now import the modules, and a real `python -P -m`
subprocess test proves a checker failure still reaches the desktop as
JSON. A vitest checks the packaging config ships the waiter.
103 lines
4.1 KiB
Python
103 lines
4.1 KiB
Python
"""Run a Store check, download, or install with the packaged interpreter.
|
|
|
|
The desktop keeps the backend alive during download, then stops it before
|
|
requesting installation. Each invocation reports one final JSON result.
|
|
Exit 0 means success, 2 means a check found updates, and 1 means failure.
|
|
|
|
Run with the bundled payload python: ``python -P -m hermes_cli.windows_store_update``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import site
|
|
import sys
|
|
import time
|
|
|
|
|
|
def perform(context, mode, wait, completed_state) -> dict:
|
|
"""Use the same update objects for the query and the requested operation."""
|
|
updates = wait(context.get_app_and_optional_store_package_updates_async())
|
|
packages = [update.package.id.full_name for update in updates]
|
|
result = {"available": bool(updates), "packages": packages, "ok": True}
|
|
if mode == "check" or not updates:
|
|
return result
|
|
request = {
|
|
"download": context.request_download_store_package_updates_async,
|
|
"install": context.request_download_and_install_store_package_updates_async,
|
|
}[mode]
|
|
outcome = wait(request(updates))
|
|
if outcome.overall_state != completed_state:
|
|
result.update(ok=False, error=f"Microsoft Store {mode}: {outcome.overall_state.name}")
|
|
return result
|
|
|
|
|
|
def _wait(operation):
|
|
"""Keep the STA responsive while Store displays consent and progress UI."""
|
|
import win32con
|
|
import win32event
|
|
import win32gui
|
|
from winrt.windows.foundation import AsyncStatus
|
|
|
|
deadline = time.monotonic() + 1800
|
|
while operation.status == AsyncStatus.STARTED:
|
|
if time.monotonic() >= deadline:
|
|
operation.cancel()
|
|
raise TimeoutError("Microsoft Store request timed out")
|
|
win32event.MsgWaitForMultipleObjects((), False, 50, win32con.QS_ALLINPUT)
|
|
if win32gui.PumpWaitingMessages():
|
|
operation.cancel()
|
|
raise RuntimeError("Microsoft Store request was interrupted")
|
|
return operation.get_results()
|
|
|
|
|
|
def _execute(mode: str, window_handle: int | None) -> dict:
|
|
from winrt.runtime.interop import initialize_with_window
|
|
from winrt.windows.services.store import StoreContext, StorePackageUpdateState
|
|
import winrt.windows.applicationmodel # noqa: F401 — projected Package values
|
|
import winrt.windows.foundation.collections # noqa: F401 — projected sequence
|
|
|
|
if mode != "check" and not window_handle:
|
|
raise ValueError("A desktop window is required for Microsoft Store consent")
|
|
context = StoreContext.get_default()
|
|
if window_handle:
|
|
initialize_with_window(context, window_handle)
|
|
return perform(context, mode, _wait, StorePackageUpdateState.COMPLETED)
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--mode", choices=("check", "download", "install"), default="check")
|
|
parser.add_argument("--hwnd", type=int)
|
|
args = parser.parse_args(argv)
|
|
# The packaged interpreter receives its dependencies through PYTHONPATH.
|
|
# Process their .pth files so pywin32 can find its extensions and DLLs.
|
|
for directory in os.environ.get("PYTHONPATH", "").split(os.pathsep):
|
|
if directory:
|
|
site.addsitedir(directory)
|
|
try:
|
|
from winrt.runtime import ApartmentType, init_apartment, uninit_apartment
|
|
|
|
init_apartment(ApartmentType.SINGLE_THREADED)
|
|
except Exception as exc:
|
|
print(json.dumps({"available": None, "ok": False, "error": str(exc)}), flush=True)
|
|
return 1
|
|
try:
|
|
try:
|
|
result = _execute(args.mode, args.hwnd)
|
|
except Exception as exc:
|
|
# Missing Store acquisition is not evidence that the app is current.
|
|
result = {"available": None, "ok": False, "error": f"Microsoft Store request failed: {exc}"}
|
|
finally:
|
|
# _execute's COM objects must be released before apartment shutdown.
|
|
uninit_apartment()
|
|
print(json.dumps(result), flush=True)
|
|
if not result["ok"]:
|
|
return 1
|
|
return 2 if args.mode == "check" and result["available"] else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|