103 lines
4.0 KiB
Python
103 lines
4.0 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
|
|
import winrt.windows.foundation.collections
|
|
|
|
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())
|