muxplex-deck 0.4.0__tar.gz → 0.4.1__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.4.0 → muxplex_deck-0.4.1}/PKG-INFO +1 -1
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/pyproject.toml +1 -1
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/cli.py +73 -12
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/main.py +2 -1
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/service.py +172 -12
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/README.md +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/deck_probe/__init__.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/deck_probe/__main__.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/deck_probe/capabilities.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/deck_probe/events.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/deck_probe/main.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/deck_probe/rendering.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/__init__.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/__main__.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/attention.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/config.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/device.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/device_real.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/emulator.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/focus.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/init_wizard.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/interaction.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/layout.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/rendering.py +1 -1
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/statusfile.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.4.1}/src/muxplex_deck/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: muxplex-deck
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
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>
|
|
@@ -251,11 +251,14 @@ def _get_install_info() -> dict:
|
|
|
251
251
|
return info
|
|
252
252
|
|
|
253
253
|
|
|
254
|
+
_PYPI_PROJECT_URL = "https://pypi.org/pypi/muxplex-deck/json"
|
|
255
|
+
|
|
256
|
+
|
|
254
257
|
def _check_for_update(info: dict) -> tuple[bool, str]:
|
|
255
258
|
"""Check if an update is available.
|
|
256
259
|
|
|
257
|
-
muxplex-deck is
|
|
258
|
-
|
|
260
|
+
muxplex-deck 0.4.0+ is published to PyPI, so both the git and pypi
|
|
261
|
+
comparison paths apply now. Editable installs are never flagged.
|
|
259
262
|
"""
|
|
260
263
|
if info["source"] == "editable":
|
|
261
264
|
return False, "editable install -- manage updates manually"
|
|
@@ -283,6 +286,24 @@ def _check_for_update(info: dict) -> tuple[bool, str]:
|
|
|
283
286
|
except Exception: # noqa: BLE001 -- any git/network failure means "upgrade to be safe"
|
|
284
287
|
return True, "check failed -- upgrading to be safe"
|
|
285
288
|
|
|
289
|
+
if info["source"] == "pypi":
|
|
290
|
+
try:
|
|
291
|
+
import httpx
|
|
292
|
+
|
|
293
|
+
response = httpx.get(
|
|
294
|
+
_PYPI_PROJECT_URL,
|
|
295
|
+
headers={"Accept": "application/json"},
|
|
296
|
+
timeout=10,
|
|
297
|
+
)
|
|
298
|
+
response.raise_for_status()
|
|
299
|
+
latest = response.json()["info"]["version"]
|
|
300
|
+
current = info["version"]
|
|
301
|
+
if latest == current:
|
|
302
|
+
return False, f"up to date (v{current})"
|
|
303
|
+
return True, f"update available (v{current} -> v{latest})"
|
|
304
|
+
except Exception: # noqa: BLE001 -- any network/parse failure means "upgrade to be safe"
|
|
305
|
+
return True, "could not check PyPI -- upgrading to be safe"
|
|
306
|
+
|
|
286
307
|
return True, "unknown install source -- could not check"
|
|
287
308
|
|
|
288
309
|
|
|
@@ -855,19 +876,54 @@ def _service_is_active() -> bool:
|
|
|
855
876
|
return service_is_active()
|
|
856
877
|
|
|
857
878
|
|
|
858
|
-
def update() -> None:
|
|
879
|
+
def update(*, force: bool = False) -> None:
|
|
859
880
|
"""Update muxplex-deck to the latest version and restart the service (if installed).
|
|
860
881
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
882
|
+
Respects how the tool was installed (see `_get_install_info`), mirroring
|
|
883
|
+
muxplex's own `upgrade()`: a `pypi` install upgrades from PyPI (package
|
|
884
|
+
name, so `uv tool install --force muxplex-deck` / `pip install --upgrade
|
|
885
|
+
muxplex-deck`); a `git` (or `unknown`) install keeps reinstalling from
|
|
886
|
+
git HEAD via `_REPO_URL`, exactly as before -- this honors an explicit
|
|
887
|
+
git install rather than migrating it. An `editable` install is left
|
|
888
|
+
alone entirely (dev checkout -- manage it via git yourself).
|
|
889
|
+
|
|
890
|
+
Now that a real PyPI release exists, the "already up to date" version
|
|
891
|
+
gate this module previously lacked (see AGENTS.md history) is real:
|
|
892
|
+
unless `force=True`, an install already at the latest version/commit
|
|
893
|
+
is reported and left untouched rather than reinstalled and restarted
|
|
894
|
+
for no code change. Reporting style (plain ` ERROR: ...` on stderr
|
|
895
|
+
equivalent) matches muxplex's.
|
|
866
896
|
"""
|
|
867
897
|
from .service import service_install
|
|
868
898
|
|
|
869
899
|
print("\nmuxplex-deck update\n")
|
|
870
900
|
|
|
901
|
+
info = _get_install_info()
|
|
902
|
+
commit_suffix = f" @ {info['commit'][:8]}" if info["commit"] else ""
|
|
903
|
+
print(
|
|
904
|
+
f" Installed: muxplex-deck {info['version']} (via {info['source']}{commit_suffix})"
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
if info["source"] == "editable":
|
|
908
|
+
print(
|
|
909
|
+
"\n Editable install detected -- manage updates via git yourself (no action taken).\n"
|
|
910
|
+
)
|
|
911
|
+
return
|
|
912
|
+
|
|
913
|
+
if force:
|
|
914
|
+
print(" Status: --force specified -- skipping version check")
|
|
915
|
+
else:
|
|
916
|
+
update_available, message = _check_for_update(info)
|
|
917
|
+
print(f" Status: {message}")
|
|
918
|
+
if not update_available:
|
|
919
|
+
print(
|
|
920
|
+
"\n Already up to date."
|
|
921
|
+
" Use 'muxplex-deck update --force' to reinstall anyway.\n"
|
|
922
|
+
)
|
|
923
|
+
return
|
|
924
|
+
|
|
925
|
+
install_target = "muxplex-deck" if info["source"] == "pypi" else f"git+{_REPO_URL}"
|
|
926
|
+
|
|
871
927
|
was_active = _service_is_active()
|
|
872
928
|
if was_active:
|
|
873
929
|
print(" Stopping service...")
|
|
@@ -896,7 +952,7 @@ def update() -> None:
|
|
|
896
952
|
uv_path = _find_uv()
|
|
897
953
|
if uv_path:
|
|
898
954
|
result = subprocess.run(
|
|
899
|
-
[uv_path, "tool", "install", "--force",
|
|
955
|
+
[uv_path, "tool", "install", "--force", install_target],
|
|
900
956
|
capture_output=True,
|
|
901
957
|
text=True,
|
|
902
958
|
check=False,
|
|
@@ -910,7 +966,7 @@ def update() -> None:
|
|
|
910
966
|
pip_path = _find_pip()
|
|
911
967
|
if pip_path:
|
|
912
968
|
result = subprocess.run(
|
|
913
|
-
[pip_path, "install", "--upgrade",
|
|
969
|
+
[pip_path, "install", "--upgrade", install_target],
|
|
914
970
|
capture_output=True,
|
|
915
971
|
text=True,
|
|
916
972
|
check=False,
|
|
@@ -989,11 +1045,16 @@ def main() -> None:
|
|
|
989
1045
|
"--json", action="store_true", help="Emit raw status as JSON"
|
|
990
1046
|
)
|
|
991
1047
|
|
|
992
|
-
sub.add_parser(
|
|
1048
|
+
update_parser = sub.add_parser(
|
|
993
1049
|
"update",
|
|
994
1050
|
aliases=["upgrade"],
|
|
995
1051
|
help="Update muxplex-deck to the latest version and restart the service",
|
|
996
1052
|
)
|
|
1053
|
+
update_parser.add_argument(
|
|
1054
|
+
"--force",
|
|
1055
|
+
action="store_true",
|
|
1056
|
+
help="Reinstall even if already at the latest version/commit",
|
|
1057
|
+
)
|
|
997
1058
|
|
|
998
1059
|
init_parser = sub.add_parser(
|
|
999
1060
|
"init",
|
|
@@ -1057,7 +1118,7 @@ def main() -> None:
|
|
|
1057
1118
|
status(getattr(args, "config", None), as_json=getattr(args, "json", False))
|
|
1058
1119
|
)
|
|
1059
1120
|
elif args.command in ("update", "upgrade"):
|
|
1060
|
-
update()
|
|
1121
|
+
update(force=getattr(args, "force", False))
|
|
1061
1122
|
elif args.command == "init":
|
|
1062
1123
|
from .init_wizard import run_init
|
|
1063
1124
|
|
|
@@ -54,7 +54,6 @@ import threading
|
|
|
54
54
|
import time
|
|
55
55
|
from urllib.parse import urlparse
|
|
56
56
|
|
|
57
|
-
from . import attention, focus, interaction, layout, rendering, views
|
|
58
57
|
from muxplex_client import (
|
|
59
58
|
AuthError,
|
|
60
59
|
MuxplexClient,
|
|
@@ -64,6 +63,8 @@ from muxplex_client import (
|
|
|
64
63
|
Settings,
|
|
65
64
|
UnreachableError,
|
|
66
65
|
)
|
|
66
|
+
|
|
67
|
+
from . import attention, focus, interaction, layout, rendering, views
|
|
67
68
|
from .config import Config
|
|
68
69
|
from .device import (
|
|
69
70
|
DeckDevice,
|
|
@@ -17,6 +17,21 @@ Ported near 1:1 from muxplex's own `service.py` (see that repo's
|
|
|
17
17
|
(unit/plist path, enable/start, resulting status) using the same ✓/!
|
|
18
18
|
2-space-indent style as `cli.doctor()` -- a silent success path left a real
|
|
19
19
|
user unsure whether `service install` had done anything at all.
|
|
20
|
+
|
|
21
|
+
`launchctl bootstrap` idempotency: unlike `systemctl start` (idempotent --
|
|
22
|
+
starting an already-active unit just succeeds), `launchctl bootstrap` exits
|
|
23
|
+
5 ("Input/output error") when the job label is ALREADY loaded. A real user
|
|
24
|
+
hit this running `service start` against a service that was already
|
|
25
|
+
healthy and running: the code used ``check=True``, so launchd's expected
|
|
26
|
+
"already loaded" refusal surfaced as an unhandled `CalledProcessError`
|
|
27
|
+
traceback instead of the benign no-op it actually is. `_launchd_bootstrap()`
|
|
28
|
+
is the shared, non-raising (`check=False`) helper both `_launchd_install()`
|
|
29
|
+
and `_launchd_start()` use; both treat exit 5 as success, and any other
|
|
30
|
+
nonzero exit as a genuine failure reported via launchctl's own stderr
|
|
31
|
+
(never swallowed) rather than a raw traceback. `_launchd_restart()` also
|
|
32
|
+
waits for `bootout` to actually finish (it returns before the job is fully
|
|
33
|
+
torn down) so the follow-up `bootstrap` doesn't race it into the same
|
|
34
|
+
exit-5 rejection for the wrong reason.
|
|
20
35
|
"""
|
|
21
36
|
|
|
22
37
|
from __future__ import annotations
|
|
@@ -26,6 +41,7 @@ import os
|
|
|
26
41
|
import shutil
|
|
27
42
|
import subprocess
|
|
28
43
|
import sys
|
|
44
|
+
import time
|
|
29
45
|
from pathlib import Path
|
|
30
46
|
|
|
31
47
|
# ---------------------------------------------------------------------------
|
|
@@ -39,6 +55,21 @@ _LAUNCHD_PLIST_DIR: Path = Path.home() / "Library" / "LaunchAgents"
|
|
|
39
55
|
_LAUNCHD_PLIST_PATH: Path = _LAUNCHD_PLIST_DIR / "com.muxplex-deck.plist"
|
|
40
56
|
_LAUNCHD_LABEL: str = "com.muxplex-deck"
|
|
41
57
|
|
|
58
|
+
# `launchctl bootstrap` exit code when the job label is already loaded. This
|
|
59
|
+
# is launchd's way of saying "already running" -- not a failure -- so it is
|
|
60
|
+
# handled as a benign no-op everywhere `_launchd_bootstrap()` is used, never
|
|
61
|
+
# swallowed silently for any OTHER exit code.
|
|
62
|
+
_LAUNCHD_ALREADY_LOADED_EXIT = 5
|
|
63
|
+
|
|
64
|
+
# `launchctl bootout` returns before the job has necessarily finished
|
|
65
|
+
# tearing down, so a `restart` (stop then start) can race it: bootstrap
|
|
66
|
+
# fires while the old job is still unloading and gets rejected with the
|
|
67
|
+
# same exit-5 "already loaded" the still-loading old job produces. Poll
|
|
68
|
+
# `service_is_active()` until the job is actually gone (bounded so a stuck
|
|
69
|
+
# teardown can't hang `restart` forever).
|
|
70
|
+
_LAUNCHD_BOOTOUT_POLL_INTERVAL_SECONDS = 0.2
|
|
71
|
+
_LAUNCHD_BOOTOUT_TIMEOUT_SECONDS = 5.0
|
|
72
|
+
|
|
42
73
|
# Elgato Stream Deck USB vendor id -- used to detect an existing udev rule.
|
|
43
74
|
_ELGATO_VENDOR_ID = "0fd9"
|
|
44
75
|
_UDEV_RULE_DIRS: tuple[Path, ...] = (
|
|
@@ -342,8 +373,36 @@ def _systemd_uninstall() -> None:
|
|
|
342
373
|
print()
|
|
343
374
|
|
|
344
375
|
|
|
376
|
+
def _report_systemctl_failure(
|
|
377
|
+
action: str, result: subprocess.CompletedProcess[str]
|
|
378
|
+
) -> None:
|
|
379
|
+
"""Print systemctl's own diagnostics for a genuine (non-idempotent) failure.
|
|
380
|
+
|
|
381
|
+
The most common ordinary-user trigger here is running `start`/`restart`
|
|
382
|
+
before `install` (unit not found) -- a real failure, but one that should
|
|
383
|
+
surface as a clear message, not a raw `CalledProcessError` traceback.
|
|
384
|
+
"""
|
|
385
|
+
stderr = (result.stderr or "").strip()
|
|
386
|
+
print(
|
|
387
|
+
f" ERROR: systemctl {action} failed (exit {result.returncode})",
|
|
388
|
+
file=sys.stderr,
|
|
389
|
+
)
|
|
390
|
+
if stderr:
|
|
391
|
+
print(f" {stderr}", file=sys.stderr)
|
|
392
|
+
|
|
393
|
+
|
|
345
394
|
def _systemd_start() -> None:
|
|
346
|
-
subprocess.run(
|
|
395
|
+
result = subprocess.run(
|
|
396
|
+
["systemctl", "--user", "start", "muxplex-deck"],
|
|
397
|
+
capture_output=True,
|
|
398
|
+
text=True,
|
|
399
|
+
check=False,
|
|
400
|
+
)
|
|
401
|
+
if result.returncode == 0:
|
|
402
|
+
_step_ok("Started the service")
|
|
403
|
+
else:
|
|
404
|
+
_report_systemctl_failure("start", result)
|
|
405
|
+
sys.exit(1)
|
|
347
406
|
|
|
348
407
|
|
|
349
408
|
def _systemd_stop() -> None:
|
|
@@ -351,7 +410,21 @@ def _systemd_stop() -> None:
|
|
|
351
410
|
|
|
352
411
|
|
|
353
412
|
def _systemd_restart() -> None:
|
|
354
|
-
|
|
413
|
+
# `systemctl restart` is a single atomic transaction (unlike launchd's
|
|
414
|
+
# separate bootout + bootstrap), so it needs no unload-race handling --
|
|
415
|
+
# and it is idempotent whether or not the unit was already running. The
|
|
416
|
+
# ordinary failure mode is the unit not being installed yet.
|
|
417
|
+
result = subprocess.run(
|
|
418
|
+
["systemctl", "--user", "restart", "muxplex-deck"],
|
|
419
|
+
capture_output=True,
|
|
420
|
+
text=True,
|
|
421
|
+
check=False,
|
|
422
|
+
)
|
|
423
|
+
if result.returncode == 0:
|
|
424
|
+
_step_ok("Restarted the service")
|
|
425
|
+
else:
|
|
426
|
+
_report_systemctl_failure("restart", result)
|
|
427
|
+
sys.exit(1)
|
|
355
428
|
|
|
356
429
|
|
|
357
430
|
def _systemd_status() -> None:
|
|
@@ -374,6 +447,71 @@ def _systemd_logs() -> None:
|
|
|
374
447
|
# ---------------------------------------------------------------------------
|
|
375
448
|
|
|
376
449
|
|
|
450
|
+
def _launchd_bootstrap() -> subprocess.CompletedProcess[str]:
|
|
451
|
+
"""Run `launchctl bootstrap` for the muxplex-deck plist, never raising.
|
|
452
|
+
|
|
453
|
+
`check=False` deliberately: `bootstrap` exits
|
|
454
|
+
`_LAUNCHD_ALREADY_LOADED_EXIT` (5, "Input/output error") when the job
|
|
455
|
+
label is already loaded -- launchd's way of saying "already running",
|
|
456
|
+
not a failure. Callers must inspect `returncode` themselves and decide
|
|
457
|
+
what that means for them; this helper only runs the command and hands
|
|
458
|
+
back the raw result.
|
|
459
|
+
"""
|
|
460
|
+
uid = os.getuid()
|
|
461
|
+
return subprocess.run(
|
|
462
|
+
["launchctl", "bootstrap", f"gui/{uid}", str(_LAUNCHD_PLIST_PATH)],
|
|
463
|
+
capture_output=True,
|
|
464
|
+
text=True,
|
|
465
|
+
check=False,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _report_launchctl_failure(
|
|
470
|
+
action: str, result: subprocess.CompletedProcess[str]
|
|
471
|
+
) -> None:
|
|
472
|
+
"""Print launchctl's own diagnostics for a genuine (non-idempotent) failure.
|
|
473
|
+
|
|
474
|
+
Only the well-known "already loaded" exit code is treated as benign by
|
|
475
|
+
callers; every other nonzero exit prints launchctl's own stderr (which
|
|
476
|
+
already includes its "re-run as root for richer errors" hint) so the
|
|
477
|
+
real failure is still visible -- just as a clear message instead of an
|
|
478
|
+
unhandled `CalledProcessError` traceback.
|
|
479
|
+
"""
|
|
480
|
+
stderr = (result.stderr or "").strip()
|
|
481
|
+
print(
|
|
482
|
+
f" ERROR: launchctl {action} failed (exit {result.returncode})",
|
|
483
|
+
file=sys.stderr,
|
|
484
|
+
)
|
|
485
|
+
if stderr:
|
|
486
|
+
print(f" {stderr}", file=sys.stderr)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _wait_for_launchd_unload(timeout: float | None = None) -> bool:
|
|
490
|
+
"""Poll until the launchd job is no longer loaded, or `timeout` elapses.
|
|
491
|
+
|
|
492
|
+
`launchctl bootout` returns before the job has necessarily finished
|
|
493
|
+
tearing down -- immediately re-bootstrapping afterward can race it and
|
|
494
|
+
get rejected with the same "already loaded" exit code a genuinely
|
|
495
|
+
still-running job would produce. Returns True once `service_is_active()`
|
|
496
|
+
reports the job gone; returns False if it was still present when the
|
|
497
|
+
timeout elapsed (the caller decides how to proceed -- see
|
|
498
|
+
`_launchd_restart`).
|
|
499
|
+
|
|
500
|
+
`timeout` defaults to `_LAUNCHD_BOOTOUT_TIMEOUT_SECONDS` read at CALL time
|
|
501
|
+
(not as a bound default argument) so tests can monkeypatch the module
|
|
502
|
+
constant and have it take effect.
|
|
503
|
+
"""
|
|
504
|
+
if timeout is None:
|
|
505
|
+
timeout = _LAUNCHD_BOOTOUT_TIMEOUT_SECONDS
|
|
506
|
+
deadline = time.monotonic() + timeout
|
|
507
|
+
while True:
|
|
508
|
+
if not service_is_active():
|
|
509
|
+
return True
|
|
510
|
+
if time.monotonic() >= deadline:
|
|
511
|
+
return False
|
|
512
|
+
time.sleep(_LAUNCHD_BOOTOUT_POLL_INTERVAL_SECONDS)
|
|
513
|
+
|
|
514
|
+
|
|
377
515
|
def _launchd_install() -> None:
|
|
378
516
|
print("\nmuxplex-deck service install (launchd)\n")
|
|
379
517
|
|
|
@@ -394,11 +532,16 @@ def _launchd_install() -> None:
|
|
|
394
532
|
_LAUNCHD_PLIST_PATH.write_text(plist_content)
|
|
395
533
|
_step_ok(f"Wrote plist: {_LAUNCHD_PLIST_PATH}")
|
|
396
534
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
535
|
+
result = _launchd_bootstrap()
|
|
536
|
+
if result.returncode == 0:
|
|
537
|
+
_step_ok("Loaded + started the service (launchctl bootstrap)")
|
|
538
|
+
elif result.returncode == _LAUNCHD_ALREADY_LOADED_EXIT:
|
|
539
|
+
_step_ok(
|
|
540
|
+
"Service was already loaded -- plist rewritten; run "
|
|
541
|
+
"`muxplex-deck service restart` to apply changes"
|
|
542
|
+
)
|
|
543
|
+
else:
|
|
544
|
+
_report_launchctl_failure("bootstrap", result)
|
|
402
545
|
|
|
403
546
|
if service_is_active():
|
|
404
547
|
_step_ok("Service is running")
|
|
@@ -434,10 +577,14 @@ def _launchd_uninstall() -> None:
|
|
|
434
577
|
|
|
435
578
|
|
|
436
579
|
def _launchd_start() -> None:
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
580
|
+
result = _launchd_bootstrap()
|
|
581
|
+
if result.returncode == 0:
|
|
582
|
+
_step_ok("Started the service (launchctl bootstrap)")
|
|
583
|
+
elif result.returncode == _LAUNCHD_ALREADY_LOADED_EXIT:
|
|
584
|
+
_step_ok("Service was already running (nothing to start)")
|
|
585
|
+
else:
|
|
586
|
+
_report_launchctl_failure("bootstrap", result)
|
|
587
|
+
sys.exit(1)
|
|
441
588
|
|
|
442
589
|
|
|
443
590
|
def _launchd_stop() -> None:
|
|
@@ -447,7 +594,20 @@ def _launchd_stop() -> None:
|
|
|
447
594
|
|
|
448
595
|
def _launchd_restart() -> None:
|
|
449
596
|
_launchd_stop()
|
|
450
|
-
|
|
597
|
+
if not _wait_for_launchd_unload():
|
|
598
|
+
_step_warn(
|
|
599
|
+
f"Service did not fully unload within "
|
|
600
|
+
f"{_LAUNCHD_BOOTOUT_TIMEOUT_SECONDS:.0f}s -- attempting restart anyway"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
result = _launchd_bootstrap()
|
|
604
|
+
if result.returncode == 0:
|
|
605
|
+
_step_ok("Restarted the service (launchctl bootstrap)")
|
|
606
|
+
elif result.returncode == _LAUNCHD_ALREADY_LOADED_EXIT:
|
|
607
|
+
_step_ok("Service is running")
|
|
608
|
+
else:
|
|
609
|
+
_report_launchctl_failure("bootstrap", result)
|
|
610
|
+
sys.exit(1)
|
|
451
611
|
|
|
452
612
|
|
|
453
613
|
def _launchd_status() -> None:
|
|
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
|
|
@@ -37,11 +37,11 @@ from __future__ import annotations
|
|
|
37
37
|
import re
|
|
38
38
|
from typing import cast
|
|
39
39
|
|
|
40
|
+
from muxplex_client import Session
|
|
40
41
|
from PIL import Image, ImageDraw, ImageFont
|
|
41
42
|
from StreamDeck.Devices.StreamDeck import StreamDeck
|
|
42
43
|
from StreamDeck.ImageHelpers import PILHelper
|
|
43
44
|
|
|
44
|
-
from muxplex_client import Session
|
|
45
45
|
from .device import DeckDevice
|
|
46
46
|
|
|
47
47
|
_KEY_LABEL_FONT_SIZE = 16
|
|
File without changes
|
|
File without changes
|