fix: catch helper timeouts in run(); report probed state when a command fails

run() now turns subprocess.TimeoutExpired into rc=124, so the poll loop and
on_connect can no longer die on a stalled lgtv.sh (34adf28 only guarded the
command path). A failed ON/OFF publishes a fresh status probe instead of the
requested state. Payload decode uses errors="replace"; safe_handle (34adf28)
already kept bad bytes from killing paho's thread.

tests/test_bridge.py replays the reviewer's three repros.

Fixes-finding: uncaught-helper-timeout-stops-bridge
Fixes-finding: failed-power-commands-publish-success-state
Fixes-finding: malformed-mqtt-payload-kills-network-loop

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mortdecai
2026-10-24 11:06:03 -04:00
parent 2f96013794
commit cc0e023941
2 changed files with 78 additions and 10 deletions
+24 -10
View File
@@ -44,9 +44,15 @@ log = logging.getLogger(__name__)
def run(cmd, timeout=60):
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True, timeout=timeout
)
# Every caller (poll loop, on_connect, commands) routes through here, so a
# stalled helper becomes a plain failure instead of an exception.
try:
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True, timeout=timeout
)
except subprocess.TimeoutExpired:
log.warning(f"timed out after {timeout}s: {cmd}")
return "timeout", 124
return result.stdout.strip(), result.returncode
@@ -129,23 +135,31 @@ def safe_handle(client, msg):
def handle_command(client, msg):
command = msg.payload.decode().strip().upper()
command = msg.payload.decode(errors="replace").strip().upper()
log.info(f"Received command: {command}")
if command == "ON":
tv_on()
ok = tv_on()
elif command == "OFF":
ok = tv_off()
else:
log.warning(f"Unknown command: {command}")
return
if not ok:
# Failed command: report what the TV actually is, not what was asked.
state = get_tv_state()
client.publish(STATE_TOPIC, state, retain=True)
log.warning(f"{command} failed; published probed state: {state}")
elif command == "ON":
# Publish ON optimistically immediately — WoL takes ~20s to appear on network.
# The 30s poll loop will correct it if it failed.
client.publish(STATE_TOPIC, "ON", retain=True)
log.info("State published: ON (optimistic)")
elif command == "OFF":
tv_off()
else:
time.sleep(3)
client.publish(STATE_TOPIC, "OFF", retain=True)
log.info("State published: OFF")
else:
log.warning(f"Unknown command: {command}")
return
def main():