summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortranch <tranch.xiao@gmail.com>2026-05-07 23:54:28 +0800
committertranch <tranch.xiao@gmail.com>2026-05-08 00:13:26 +0800
commitd7810e84657fa8c3cf8ec94f047b72c9e109d178 (patch)
treee9b34ea0557da0a56ae4d3b7c1dfb64e3cf30938
parenta0376dbbe409a3d71405e9c8c60f31ee5d8cd341 (diff)
downloadaccelerator-installer-main.tar.gz
accelerator-installer-main.zip
fix: prevent install loop from hanging after success and add timeout handlingHEADv1.0.10main
Previously, on the plugin_download_finish event we only broke out of the receive loop when 'confirmed' was False. In the normal flow 'confirmed' is always True, so the loop kept waiting for a REPLY whose 'result' was non-None. When the server returns 'result: null' (a common success reply), no branch matched and the loop blocked forever on client.recv(), making the installer appear successful but never exit. The download_finish handler now breaks unconditionally, since the event itself already indicates a completed install. Also add timeout handling to the receive loop: - IDLE_TIMEOUT: max gap between any two server messages, guards against silent server stalls. - OVERALL_TIMEOUT: hard upper bound on the entire install as a fallback. Both are enforced by wrapping client.recv() with asyncio.wait_for; on timeout a TimeoutError is raised and propagates after the connection is cleanly closed in the finally block. Co-authored-by: llm-git <llm-git@ttll.de> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
-rw-r--r--decky_client.py58
1 files changed, 40 insertions, 18 deletions
diff --git a/decky_client.py b/decky_client.py
index 597a9b3..f5c7c2f 100644
--- a/decky_client.py
+++ b/decky_client.py
@@ -17,6 +17,10 @@ EVENT = 3
# Default store URL
DEFAULT_STORE_URL = "https://plugins.deckbrew.xyz/plugins"
+# Timeouts (seconds) for the install loop
+IDLE_TIMEOUT = 60 # max gap between any two messages from server
+OVERALL_TIMEOUT = 300 # hard upper bound for the whole install
+
# Store type mapping
STORE_TYPE_NAMES = {
0: "default",
@@ -49,7 +53,6 @@ class DeckyClient:
with urllib.request.urlopen(url, timeout=5) as response:
return response.read().decode().strip()
-
async def connect(self, token: str) -> None:
"""Connect and perform WebSocket handshake."""
self.reader, self.writer = await asyncio.open_connection(self.host, self.port)
@@ -229,8 +232,26 @@ async def run_installer(target_id: int, store_url: str) -> None:
await client.send(CALL, "utilities/install_plugin",
[artifact_url, plugin_name, version_name, hash_, 0])
+ loop = asyncio.get_event_loop()
+ deadline = loop.time() + OVERALL_TIMEOUT
+
while True:
- msg = await client.recv()
+ remaining = deadline - loop.time()
+ if remaining <= 0:
+ print("\r" + " " * 30 + "\r", end="", file=sys.stderr, flush=True)
+ raise TimeoutError(
+ f"Installation exceeded overall timeout of {OVERALL_TIMEOUT}s"
+ )
+
+ try:
+ msg = await asyncio.wait_for(client.recv(),
+ timeout=min(IDLE_TIMEOUT, remaining))
+ except asyncio.TimeoutError:
+ print("\r" + " " * 30 + "\r", end="", file=sys.stderr, flush=True)
+ raise TimeoutError(
+ f"No message from server for {IDLE_TIMEOUT}s; install appears stuck"
+ ) from None
+
if msg is None:
print("\r" + " " * 30 + "\r", end="", file=sys.stderr, flush=True)
log("Connection closed by server.")
@@ -267,12 +288,9 @@ async def run_installer(target_id: int, store_url: str) -> None:
log(f"Installation successful: {msg.get('args')}")
installation_finished = True
success = True
- # if already confirmed, we expect a REPLY after this event,
- # so we wait for it to confirm success
- if not confirmed:
- break
+ break
- elif m_type == REPLY and msg.get('result') is not None:
+ elif m_type == REPLY and msg.get("result") is not None:
print("\r" + " " * 30 + "\r", end="", file=sys.stderr, flush=True)
log(f"Server reply: {msg.get('result')}")
if installation_finished:
@@ -303,20 +321,21 @@ async def configure_store_url(store_url: str) -> None:
await client.connect(token)
# First, set the store type to 2 (custom)
- log(f"Setting store type to custom (2)...")
+ log("Setting store type to custom (2)...")
await client.send(CALL, "utilities/settings/set", ["store", 2])
msg = await client.recv()
if msg is None:
raise RuntimeError("Connection closed by server")
-
+
if msg.get("type") == ERROR:
log(f"Server error setting store type: {msg.get('error')}")
raise RuntimeError(f"Failed to set store type: {msg.get('error')}")
-
- log(f"Store type set to custom")
+
+ log("Store type set to custom")
log(f"Setting custom store URL: {store_url}")
- await client.send(CALL, "utilities/settings/set", ["store-url", store_url])
+ await client.send(CALL, "utilities/settings/set",
+ ["store-url", store_url])
msg = await client.recv()
if msg is None:
raise RuntimeError("Connection closed by server")
@@ -353,8 +372,9 @@ async def get_store_url() -> str:
raise RuntimeError("Connection closed by server")
if msg.get("type") == REPLY:
- store_type = msg.get('result')
- store_type_name = STORE_TYPE_NAMES.get(store_type, f"unknown ({store_type})")
+ store_type = msg.get("result")
+ store_type_name = STORE_TYPE_NAMES.get(store_type,
+ f"unknown ({store_type})")
log(f"Current store type: {store_type_name}")
elif msg.get("type") == ERROR:
log(f"Server error: {msg.get('error')}")
@@ -362,7 +382,8 @@ async def get_store_url() -> str:
# Get store URL
log("Getting configured store URL...")
- await client.send(CALL, "utilities/settings/get", ["store-url", DEFAULT_STORE_URL])
+ await client.send(CALL, "utilities/settings/get",
+ ["store-url", DEFAULT_STORE_URL])
msg = await client.recv()
if msg is None:
@@ -371,7 +392,7 @@ async def get_store_url() -> str:
m_type = msg.get("type")
if m_type == REPLY:
- store_url = msg.get('result')
+ store_url = msg.get("result")
log(f"Current store URL: {store_url}")
return store_url
elif m_type == ERROR:
@@ -392,7 +413,8 @@ if __name__ == "__main__":
description="Decky Loader Client - Manage plugins and settings",
formatter_class=argparse.RawDescriptionHelpFormatter
)
- subparsers = parser.add_subparsers(dest="command", help="Available commands")
+ subparsers = parser.add_subparsers(dest="command",
+ help="Available commands")
# Install subcommand
install_parser = subparsers.add_parser(
@@ -402,7 +424,7 @@ if __name__ == "__main__":
install_parser.add_argument(
"--store-url",
default="http://127.0.0.1:1337/plugins",
- help="Plugin store URL to fetch plugins from (default: http://127.0.0.1:1337/plugins)"
+ help="Plugin store URL to fetch plugins from"
)
install_parser.add_argument(
"--target-id",