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.
97 lines
4.2 KiB
Python
97 lines
4.2 KiB
Python
"""Check for an App Installer update for the running MSIX package.
|
|
|
|
The out-of-store desktop installs are App Installer owned: the OS registered
|
|
the .appinstaller URI as the package's update source at install, so the
|
|
OS can tell us whether a newer package version is available. The desktop
|
|
shows its own prompt + tears down before the OS applies the swap.
|
|
|
|
Run with the BUNDLED payload python (the winrt package ships there):
|
|
|
|
<payload>/tools/<python-entry>/python.exe -P -m hermes_cli.windows_appinstaller_update
|
|
|
|
Exit codes:
|
|
0 no update available, or this process has no package identity
|
|
2 update available (the caller decides whether to prompt/teardown)
|
|
1 error, including a missing registered App Installer update source
|
|
|
|
API shape (pywinrt projection, verified against the installed winrt 3.2.1):
|
|
``Package.current`` is a static PROPERTY; the update check is the instance
|
|
method ``Package.check_update_availability_async`` — ``PackageManager`` has
|
|
no such method. ``PackageUpdateAvailability`` members (verified live in the
|
|
installed projection): UNKNOWN=0, NO_UPDATES=1, AVAILABLE=2, REQUIRED=3,
|
|
ERROR=4. Only the specific "no package identity" HRESULT (0x80073D54) means
|
|
not-packaged; every other failure is an unknown, never a "no update".
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
|
|
# HRESULT 0x80073D54 — "The process has no package identity." The one
|
|
# failure that positively identifies a non-packaged (dev) run.
|
|
_NO_PACKAGE_IDENTITY_HRESULT = -2147009196
|
|
|
|
|
|
def _load_projection():
|
|
from winrt.windows.applicationmodel import Package, PackageUpdateAvailability
|
|
|
|
return Package, PackageUpdateAvailability
|
|
|
|
|
|
def main() -> int:
|
|
try:
|
|
Package, PackageUpdateAvailability = _load_projection()
|
|
except ImportError as exc:
|
|
# The winrt module is absent (older payload without the dep): the
|
|
# update check cannot run. Report unknown, not "no update".
|
|
print(json.dumps({"available": None, "error": f"winrt import failed: {exc}"}))
|
|
return 1
|
|
|
|
# Property access, not a call: a packaged process gets the Package
|
|
# instance; a dev run raises the package-identity HRESULT.
|
|
try:
|
|
package = Package.current
|
|
except OSError as exc:
|
|
if getattr(exc, "winerror", None) == _NO_PACKAGE_IDENTITY_HRESULT:
|
|
print(json.dumps({"available": False, "reason": "not-packaged"}))
|
|
return 0
|
|
print(json.dumps({"available": None, "error": f"package identity failed: {exc}"}))
|
|
return 1
|
|
except Exception as exc: # noqa: BLE001 — projection shape surprises are unknowns
|
|
print(json.dumps({"available": None, "error": f"package identity failed: {exc}"}))
|
|
return 1
|
|
|
|
try:
|
|
source = package.get_app_installer_info()
|
|
if source is None:
|
|
print(json.dumps({
|
|
"available": None,
|
|
"reason": "no-app-installer-source",
|
|
"error": "No App Installer update source is registered. Install Hermes through its .appinstaller file to enable updates.",
|
|
}))
|
|
return 1
|
|
source_uri = source.uri.absolute_uri
|
|
result = package.check_update_availability_async().get()
|
|
availability = PackageUpdateAvailability(result.availability)
|
|
except Exception as exc: # noqa: BLE001
|
|
print(json.dumps({"available": None, "error": f"check failed: {exc}"}))
|
|
return 1
|
|
|
|
# Only AVAILABLE / REQUIRED mean "a newer package exists". NO_UPDATES is
|
|
# the one honest "no". UNKNOWN and ERROR are NOT "no update" — reporting
|
|
# them as such would suppress a real update prompt — so they surface as
|
|
# an unknown with the availability name as the extended error.
|
|
if availability == PackageUpdateAvailability.NO_UPDATES:
|
|
print(json.dumps({"available": False, "availability": availability.name, "source_uri": source_uri}))
|
|
return 0
|
|
|
|
if availability in (PackageUpdateAvailability.AVAILABLE, PackageUpdateAvailability.REQUIRED):
|
|
print(json.dumps({"available": True, "availability": availability.name, "source_uri": source_uri}))
|
|
return 2
|
|
|
|
print(json.dumps({"available": None, "error": f"availability unknown: {availability.name}"}))
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|