muxplex-deck 0.5.2__tar.gz → 0.5.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/PKG-INFO +1 -1
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/pyproject.toml +1 -1
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/cli.py +94 -2
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/init_wizard.py +46 -9
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/service.py +135 -5
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/README.md +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/deck_probe/__init__.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/deck_probe/__main__.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/deck_probe/capabilities.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/deck_probe/events.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/deck_probe/main.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/deck_probe/rendering.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/__init__.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/__main__.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/attention.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/config.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/device.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/device_real.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/emulator.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/focus.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/hidhelp.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/interaction.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/layout.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/main.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/rendering.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/statusfile.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/usbnode.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/views.py +0 -0
- {muxplex_deck-0.5.2 → muxplex_deck-0.5.3}/src/muxplex_deck/wsl.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: muxplex-deck
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Hardware probe/diagnostic app for the Elgato Stream Deck+ (seed of the muxplex sidecar)
|
|
5
5
|
Author: Amplifier
|
|
6
6
|
Author-email: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
|
|
@@ -593,6 +593,60 @@ def check_server_reachable(server_url: str, ca_file: Path | None) -> tuple[str,
|
|
|
593
593
|
return "warn", f"Server check failed: {exc}"
|
|
594
594
|
|
|
595
595
|
|
|
596
|
+
def check_federation_key_auth(
|
|
597
|
+
server_url: str, federation_key: str, *, verify: bool | str = True
|
|
598
|
+
) -> tuple[str, str]:
|
|
599
|
+
"""Issue ONE authenticated request to confirm `federation_key` is actually accepted.
|
|
600
|
+
|
|
601
|
+
`/api/instance-info` (what `check_server_reachable`/`fetch_instance_info`
|
|
602
|
+
call) is unauthenticated by design on the server -- it proves TLS and
|
|
603
|
+
reachability, but nothing about a credential. That gap is exactly what
|
|
604
|
+
let `init` once print a green "Server reachable" check, write config,
|
|
605
|
+
and offer to install the service for a federation key that was simply
|
|
606
|
+
wrong -- the user only found out once the running service couldn't
|
|
607
|
+
authenticate (see AGENTS.md).
|
|
608
|
+
|
|
609
|
+
This hits `GET /api/sessions` instead: authenticated, read-only, the
|
|
610
|
+
same lightweight endpoint the sidecar's own active loop already polls
|
|
611
|
+
(see `main.py`), with the key sent as `Authorization: Bearer` -- the
|
|
612
|
+
exact scheme the muxplex server's `auth.py` checks. Returns one of:
|
|
613
|
+
|
|
614
|
+
("ok", ...) -- HTTP 200: the key is accepted.
|
|
615
|
+
("fail", ...) -- HTTP 401: the key is definitively rejected.
|
|
616
|
+
Callers must not save a key that gets this back.
|
|
617
|
+
("warn", ...) -- anything else (timeout, connection error,
|
|
618
|
+
unexpected status): could not verify one way or
|
|
619
|
+
the other. This is NOT evidence the key is bad --
|
|
620
|
+
callers must not print a false success, but also
|
|
621
|
+
must not treat an unrelated network hiccup as a
|
|
622
|
+
rejected key.
|
|
623
|
+
"""
|
|
624
|
+
import httpx
|
|
625
|
+
|
|
626
|
+
url = f"{server_url.rstrip('/')}/api/sessions"
|
|
627
|
+
headers = {"Authorization": f"Bearer {federation_key}"}
|
|
628
|
+
try:
|
|
629
|
+
with httpx.Client(verify=verify, timeout=5.0) as client:
|
|
630
|
+
resp = client.get(url, headers=headers)
|
|
631
|
+
except httpx.TimeoutException as exc:
|
|
632
|
+
return (
|
|
633
|
+
"warn",
|
|
634
|
+
f"Could not verify federation key -- server check timed out: {exc}",
|
|
635
|
+
)
|
|
636
|
+
except httpx.ConnectError as exc:
|
|
637
|
+
return "warn", f"Could not verify federation key -- server unreachable: {exc}"
|
|
638
|
+
except Exception as exc: # noqa: BLE001 -- verification must degrade to warn, never raise
|
|
639
|
+
return "warn", f"Could not verify federation key: {exc}"
|
|
640
|
+
|
|
641
|
+
if resp.status_code == 200:
|
|
642
|
+
return "ok", "Federation key accepted by server"
|
|
643
|
+
if resp.status_code == 401:
|
|
644
|
+
return "fail", "Federation key rejected by server (401 Unauthorized)"
|
|
645
|
+
return "warn", (
|
|
646
|
+
f"Could not verify federation key -- unexpected response HTTP {resp.status_code}"
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
|
|
596
650
|
def check_service_status() -> tuple[str, str]:
|
|
597
651
|
"""Service-state check: not installed / installed but not running / running.
|
|
598
652
|
|
|
@@ -629,6 +683,13 @@ _CHECK_MARKS: dict[str, str] = {
|
|
|
629
683
|
"ok": "\033[32m\u2713\033[0m",
|
|
630
684
|
"fail": "\033[31m\u2717\033[0m",
|
|
631
685
|
"warn": "\033[33m!\033[0m",
|
|
686
|
+
# Distinct from "warn": "warn" means a check RAN and found something to
|
|
687
|
+
# flag; "unknown" means the check could not be run at all yet (e.g. the
|
|
688
|
+
# currently-running process hasn't published a status write of its own
|
|
689
|
+
# yet). Conflating the two is what let a dying process's stale-but-
|
|
690
|
+
# recent status snapshot get reported as definite failures right after
|
|
691
|
+
# `service restart` -- see AGENTS.md's restart-race incident.
|
|
692
|
+
"unknown": "\033[36m?\033[0m",
|
|
632
693
|
}
|
|
633
694
|
|
|
634
695
|
|
|
@@ -789,7 +850,7 @@ def status(config_path: str | None = None, *, as_json: bool = False) -> int:
|
|
|
789
850
|
fall back to a direct probe -- this keeps `status` useful even before
|
|
790
851
|
the service has ever been installed.
|
|
791
852
|
"""
|
|
792
|
-
from .service import service_is_active
|
|
853
|
+
from .service import service_is_active, service_main_pid
|
|
793
854
|
from .statusfile import read_status
|
|
794
855
|
|
|
795
856
|
running = service_is_active()
|
|
@@ -825,13 +886,44 @@ def status(config_path: str | None = None, *, as_json: bool = False) -> int:
|
|
|
825
886
|
|
|
826
887
|
if data is None:
|
|
827
888
|
print_check(
|
|
828
|
-
"
|
|
889
|
+
"unknown",
|
|
829
890
|
"No status file found even though the service is running -- it "
|
|
830
891
|
"may have just started. Try: muxplex-deck service logs",
|
|
831
892
|
)
|
|
832
893
|
print()
|
|
833
894
|
return 0
|
|
834
895
|
|
|
896
|
+
# Is this snapshot from the process running RIGHT NOW, or from a
|
|
897
|
+
# PREVIOUS incarnation? Age alone can't tell: a process that's about to
|
|
898
|
+
# be replaced (e.g. by `service restart`) can write its LAST status
|
|
899
|
+
# moments before exiting, so it still looks "fresh" by age even though
|
|
900
|
+
# it no longer reflects reality -- exactly the restart-race incident in
|
|
901
|
+
# AGENTS.md (2 false failures reported from a dying process's stale-but-
|
|
902
|
+
# recent write). Comparing the recorded pid to the service manager's own
|
|
903
|
+
# live MainPID is the reliable signal; fall back to the age check only
|
|
904
|
+
# when the live pid can't be determined at all (unsupported platform,
|
|
905
|
+
# command failure).
|
|
906
|
+
current_pid = service_main_pid()
|
|
907
|
+
recorded_pid = data.get("pid")
|
|
908
|
+
if current_pid is not None:
|
|
909
|
+
is_current = recorded_pid == current_pid
|
|
910
|
+
else:
|
|
911
|
+
is_current = (
|
|
912
|
+
time.time() - data.get("updated_at", 0)
|
|
913
|
+
) <= _STATUS_STALE_THRESHOLD_SECONDS
|
|
914
|
+
|
|
915
|
+
if not is_current:
|
|
916
|
+
print_check(
|
|
917
|
+
"unknown",
|
|
918
|
+
f"Status not yet available for the running process (pid "
|
|
919
|
+
f"{current_pid if current_pid is not None else '?'}) -- the "
|
|
920
|
+
f"published status is from a previous run (pid {recorded_pid}). "
|
|
921
|
+
"This is expected right after (re)starting; try again in a "
|
|
922
|
+
"moment, or: muxplex-deck service logs",
|
|
923
|
+
)
|
|
924
|
+
print()
|
|
925
|
+
return 0
|
|
926
|
+
|
|
835
927
|
age = time.time() - data.get("updated_at", 0)
|
|
836
928
|
if age > _STATUS_STALE_THRESHOLD_SECONDS:
|
|
837
929
|
print_check(
|
|
@@ -207,10 +207,24 @@ def _resolve_ca(
|
|
|
207
207
|
def _resolve_federation_key(
|
|
208
208
|
key_file_path: Path,
|
|
209
209
|
*,
|
|
210
|
+
server_url: str,
|
|
211
|
+
ca_file: str | None,
|
|
210
212
|
non_interactive: bool,
|
|
211
213
|
input_func: Callable[[str], str],
|
|
212
214
|
getpass_func: Callable[[str], str],
|
|
213
215
|
) -> None:
|
|
216
|
+
"""Resolve, VALIDATE, and write the federation key.
|
|
217
|
+
|
|
218
|
+
A pasted/loaded key is never written on faith. `/api/instance-info`
|
|
219
|
+
(already used above to verify the server) is unauthenticated and proves
|
|
220
|
+
nothing about a credential -- this is exactly the gap that let `init`
|
|
221
|
+
once print a green "Server reachable" check, write config, and offer to
|
|
222
|
+
install the service for a federation key that was simply wrong; the
|
|
223
|
+
user only found out once the running service couldn't authenticate
|
|
224
|
+
(see AGENTS.md). `cli.check_federation_key_auth` issues one real
|
|
225
|
+
authenticated request and this loops until it's accepted, or the user
|
|
226
|
+
gives up (Ctrl-C/EOF, handled by `run_init`'s caller).
|
|
227
|
+
"""
|
|
214
228
|
if key_file_path.exists() and key_file_path.stat().st_size > 0:
|
|
215
229
|
if non_interactive:
|
|
216
230
|
print(f" Federation key: keeping existing {key_file_path}")
|
|
@@ -237,17 +251,38 @@ def _resolve_federation_key(
|
|
|
237
251
|
"read ~/.config/muxplex/federation_key there. SSH fallback:"
|
|
238
252
|
)
|
|
239
253
|
print(f" scp <your-server>:.config/muxplex/federation_key {key_file_path}")
|
|
240
|
-
pasted = getpass_func("Federation key (or path): ")
|
|
241
|
-
value = pasted.strip()
|
|
242
|
-
candidate_path = Path(value).expanduser() if value else None
|
|
243
254
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
255
|
+
verify: bool | str = ca_file if ca_file else True
|
|
256
|
+
key_value = ""
|
|
257
|
+
while True:
|
|
258
|
+
pasted = getpass_func("Federation key (or path): ")
|
|
259
|
+
value = pasted.strip()
|
|
260
|
+
candidate_path = Path(value).expanduser() if value else None
|
|
261
|
+
|
|
262
|
+
if candidate_path is not None and candidate_path.is_file():
|
|
263
|
+
key_value = candidate_path.read_text(encoding="utf-8").strip()
|
|
264
|
+
else:
|
|
265
|
+
key_value = value
|
|
248
266
|
|
|
249
|
-
|
|
250
|
-
|
|
267
|
+
if not key_value:
|
|
268
|
+
raise _InitError("no federation key provided")
|
|
269
|
+
|
|
270
|
+
status, message = cli_mod.check_federation_key_auth(
|
|
271
|
+
server_url, key_value, verify=verify
|
|
272
|
+
)
|
|
273
|
+
if status == "ok":
|
|
274
|
+
cli_mod.print_check("ok", "Federation key: verified against server")
|
|
275
|
+
break
|
|
276
|
+
if status == "fail":
|
|
277
|
+
cli_mod.print_check("fail", message)
|
|
278
|
+
print(" Try again -- paste the correct key, or a path to it.")
|
|
279
|
+
continue
|
|
280
|
+
# status == "warn": could not verify one way or the other (network
|
|
281
|
+
# hiccup, timeout) -- not evidence the key itself is wrong, so
|
|
282
|
+
# don't loop forever on something re-pasting can't fix. Still,
|
|
283
|
+
# never claim a check that didn't actually run succeeded.
|
|
284
|
+
cli_mod.print_check("warn", message)
|
|
285
|
+
break
|
|
251
286
|
|
|
252
287
|
key_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
253
288
|
key_file_path.parent.chmod(0o700)
|
|
@@ -311,6 +346,8 @@ def _run_init_impl(
|
|
|
311
346
|
key_file_raw = raw.get("key_file") or config_mod.DEFAULT_KEY_FILE
|
|
312
347
|
_resolve_federation_key(
|
|
313
348
|
config_mod._expand(key_file_raw),
|
|
349
|
+
server_url=server_url,
|
|
350
|
+
ca_file=ca_file_value,
|
|
314
351
|
non_interactive=non_interactive,
|
|
315
352
|
input_func=input_func,
|
|
316
353
|
getpass_func=getpass_func,
|
|
@@ -72,6 +72,20 @@ _LAUNCHD_ALREADY_LOADED_EXIT = 5
|
|
|
72
72
|
_LAUNCHD_BOOTOUT_POLL_INTERVAL_SECONDS = 0.2
|
|
73
73
|
_LAUNCHD_BOOTOUT_TIMEOUT_SECONDS = 5.0
|
|
74
74
|
|
|
75
|
+
# `systemctl restart` / `launchctl bootstrap` both return once the new
|
|
76
|
+
# process has been LAUNCHED, not once it has done anything -- the new
|
|
77
|
+
# sidecar needs at least one poll cycle to open the device (or notice it
|
|
78
|
+
# can't) and publish its first status write via `statusfile.StatusReporter`.
|
|
79
|
+
# Until then, `status.json` still holds the PREVIOUS process's last
|
|
80
|
+
# snapshot, which `muxplex-deck status` would otherwise present as current,
|
|
81
|
+
# healthy truth (the restart-race incident in AGENTS.md: 2 false failures
|
|
82
|
+
# read from a dying process's stale-but-recent write, moments before it
|
|
83
|
+
# exited). `restart` polls for a status write from the NEW process (by pid,
|
|
84
|
+
# via `service_main_pid()`) before reporting success, bounded so a genuinely
|
|
85
|
+
# slow-starting sidecar can't hang the command forever.
|
|
86
|
+
_RESTART_STATUS_POLL_INTERVAL_SECONDS = 0.2
|
|
87
|
+
_RESTART_STATUS_TIMEOUT_SECONDS = 5.0
|
|
88
|
+
|
|
75
89
|
# Elgato Stream Deck USB vendor id -- used to detect an existing udev rule.
|
|
76
90
|
_ELGATO_VENDOR_ID = "0fd9"
|
|
77
91
|
_UDEV_RULE_DIRS: tuple[Path, ...] = (
|
|
@@ -309,6 +323,70 @@ def service_is_active() -> bool:
|
|
|
309
323
|
return result.returncode == 0
|
|
310
324
|
|
|
311
325
|
|
|
326
|
+
def service_main_pid() -> int | None:
|
|
327
|
+
"""Best-effort PID of the currently-running muxplex-deck service process.
|
|
328
|
+
|
|
329
|
+
Never raises -- matches `service_is_active()`'s contract: any failure
|
|
330
|
+
(service manager missing, service not running, unexpected/unparseable
|
|
331
|
+
output) reads as "cannot determine", which callers must treat as "don't
|
|
332
|
+
know", never as a false positive or negative on its own.
|
|
333
|
+
|
|
334
|
+
This is what lets `status()` (and `_wait_for_fresh_status()` below) tell
|
|
335
|
+
whether a published `status.json` (`statusfile.build_status()`'s own
|
|
336
|
+
`pid` field) was written by the process running RIGHT NOW under the
|
|
337
|
+
service, or by a PREVIOUS incarnation whose last write can look
|
|
338
|
+
deceptively fresh by age alone -- see the restart-race incident in
|
|
339
|
+
AGENTS.md: the old process's last write happened moments before it
|
|
340
|
+
exited, so an age-only staleness check saw it as "recent" and reported
|
|
341
|
+
it as current truth. Comparing pids is the only way to tell those apart.
|
|
342
|
+
"""
|
|
343
|
+
if _is_darwin():
|
|
344
|
+
uid = os.getuid()
|
|
345
|
+
try:
|
|
346
|
+
result = subprocess.run(
|
|
347
|
+
["launchctl", "print", f"gui/{uid}/{_LAUNCHD_LABEL}"],
|
|
348
|
+
capture_output=True,
|
|
349
|
+
text=True,
|
|
350
|
+
check=False,
|
|
351
|
+
)
|
|
352
|
+
except FileNotFoundError:
|
|
353
|
+
return None
|
|
354
|
+
if result.returncode != 0:
|
|
355
|
+
return None
|
|
356
|
+
for line in result.stdout.splitlines():
|
|
357
|
+
stripped = line.strip()
|
|
358
|
+
if stripped.startswith("pid = "):
|
|
359
|
+
try:
|
|
360
|
+
return int(stripped[len("pid = ") :].strip())
|
|
361
|
+
except ValueError:
|
|
362
|
+
return None
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
result = subprocess.run(
|
|
367
|
+
[
|
|
368
|
+
"systemctl",
|
|
369
|
+
"--user",
|
|
370
|
+
"show",
|
|
371
|
+
"muxplex-deck",
|
|
372
|
+
"--property=MainPID",
|
|
373
|
+
"--value",
|
|
374
|
+
],
|
|
375
|
+
capture_output=True,
|
|
376
|
+
text=True,
|
|
377
|
+
check=False,
|
|
378
|
+
)
|
|
379
|
+
except FileNotFoundError:
|
|
380
|
+
return None
|
|
381
|
+
if result.returncode != 0:
|
|
382
|
+
return None
|
|
383
|
+
try:
|
|
384
|
+
pid = int(result.stdout.strip())
|
|
385
|
+
except ValueError:
|
|
386
|
+
return None
|
|
387
|
+
return pid if pid > 0 else None
|
|
388
|
+
|
|
389
|
+
|
|
312
390
|
def service_is_installed() -> bool:
|
|
313
391
|
"""Is the muxplex-deck unit/plist file present on disk?
|
|
314
392
|
|
|
@@ -502,6 +580,56 @@ def _systemd_stop() -> None:
|
|
|
502
580
|
subprocess.run(["systemctl", "--user", "stop", "muxplex-deck"], check=False)
|
|
503
581
|
|
|
504
582
|
|
|
583
|
+
def _wait_for_fresh_status(timeout: float | None = None) -> bool:
|
|
584
|
+
"""Poll until `status.json`'s recorded pid matches the service's live
|
|
585
|
+
MainPID, or `timeout` elapses.
|
|
586
|
+
|
|
587
|
+
See `_RESTART_STATUS_TIMEOUT_SECONDS`'s docstring for why this exists:
|
|
588
|
+
the process a restart just launched needs at least one poll cycle
|
|
589
|
+
before it publishes its own status, and comparing pids (not wall-clock
|
|
590
|
+
age) is the only reliable way to tell "this snapshot belongs to the
|
|
591
|
+
process running right now" from "this is the previous process's last
|
|
592
|
+
write" -- the exact restart-race incident in AGENTS.md.
|
|
593
|
+
|
|
594
|
+
Returns True once a matching status is observed; False if `timeout`
|
|
595
|
+
elapses first. Never raises -- `service_main_pid()` and
|
|
596
|
+
`statusfile.read_status()` are both best-effort/never-raise themselves.
|
|
597
|
+
"""
|
|
598
|
+
from . import statusfile
|
|
599
|
+
|
|
600
|
+
if timeout is None:
|
|
601
|
+
timeout = _RESTART_STATUS_TIMEOUT_SECONDS
|
|
602
|
+
deadline = time.monotonic() + timeout
|
|
603
|
+
while True:
|
|
604
|
+
current_pid = service_main_pid()
|
|
605
|
+
if current_pid is not None:
|
|
606
|
+
data = statusfile.read_status()
|
|
607
|
+
if data is not None and data.get("pid") == current_pid:
|
|
608
|
+
return True
|
|
609
|
+
if time.monotonic() >= deadline:
|
|
610
|
+
return False
|
|
611
|
+
time.sleep(_RESTART_STATUS_POLL_INTERVAL_SECONDS)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _report_restart_result(success_message: str) -> None:
|
|
615
|
+
"""Print the final restart line: success only once fresh status is
|
|
616
|
+
actually observed, otherwise an honest "still waiting" warning.
|
|
617
|
+
|
|
618
|
+
Shared by systemd and launchd so both platforms make the same promise:
|
|
619
|
+
never print a success line for a step (the new process being up and
|
|
620
|
+
reporting) that was not actually verified -- see AGENTS.md's restart-
|
|
621
|
+
race incident, which this directly closes.
|
|
622
|
+
"""
|
|
623
|
+
if _wait_for_fresh_status():
|
|
624
|
+
_step_ok(success_message)
|
|
625
|
+
else:
|
|
626
|
+
_step_warn(
|
|
627
|
+
"Restart command sent, but the service has not published fresh "
|
|
628
|
+
f"status within {_RESTART_STATUS_TIMEOUT_SECONDS:.0f}s -- it may "
|
|
629
|
+
"still be starting. Check: muxplex-deck status"
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
|
|
505
633
|
def _systemd_restart() -> None:
|
|
506
634
|
# `systemctl restart` is a single atomic transaction (unlike launchd's
|
|
507
635
|
# separate bootout + bootstrap), so it needs no unload-race handling --
|
|
@@ -513,12 +641,12 @@ def _systemd_restart() -> None:
|
|
|
513
641
|
text=True,
|
|
514
642
|
check=False,
|
|
515
643
|
)
|
|
516
|
-
if result.returncode
|
|
517
|
-
_step_ok("Restarted the service")
|
|
518
|
-
else:
|
|
644
|
+
if result.returncode != 0:
|
|
519
645
|
_report_systemctl_failure("restart", result)
|
|
520
646
|
sys.exit(1)
|
|
521
647
|
|
|
648
|
+
_report_restart_result("Restarted the service")
|
|
649
|
+
|
|
522
650
|
|
|
523
651
|
def _systemd_status() -> None:
|
|
524
652
|
subprocess.run(
|
|
@@ -704,13 +832,15 @@ def _launchd_restart() -> None:
|
|
|
704
832
|
|
|
705
833
|
result = _launchd_bootstrap()
|
|
706
834
|
if result.returncode == 0:
|
|
707
|
-
|
|
835
|
+
success_message = "Restarted the service (launchctl bootstrap)"
|
|
708
836
|
elif result.returncode == _LAUNCHD_ALREADY_LOADED_EXIT:
|
|
709
|
-
|
|
837
|
+
success_message = "Service is running"
|
|
710
838
|
else:
|
|
711
839
|
_report_launchctl_failure("bootstrap", result)
|
|
712
840
|
sys.exit(1)
|
|
713
841
|
|
|
842
|
+
_report_restart_result(success_message)
|
|
843
|
+
|
|
714
844
|
|
|
715
845
|
def _launchd_status() -> None:
|
|
716
846
|
uid = os.getuid()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|