muxplex-deck 0.5.0__tar.gz → 0.5.2__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.0 → muxplex_deck-0.5.2}/PKG-INFO +1 -1
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/pyproject.toml +1 -1
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/cli.py +97 -35
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/config.py +3 -3
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/hidhelp.py +23 -4
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/service.py +82 -7
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/README.md +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/deck_probe/__init__.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/deck_probe/__main__.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/deck_probe/capabilities.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/deck_probe/events.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/deck_probe/main.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/deck_probe/rendering.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/__init__.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/__main__.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/attention.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/device.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/device_real.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/emulator.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/focus.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/init_wizard.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/interaction.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/layout.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/main.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/rendering.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/statusfile.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/usbnode.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/src/muxplex_deck/views.py +0 -0
- {muxplex_deck-0.5.0 → muxplex_deck-0.5.2}/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.2
|
|
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>
|
|
@@ -16,6 +16,7 @@ import argparse
|
|
|
16
16
|
import contextlib
|
|
17
17
|
import json
|
|
18
18
|
import platform
|
|
19
|
+
import shutil
|
|
19
20
|
import subprocess
|
|
20
21
|
import sys
|
|
21
22
|
import time
|
|
@@ -323,7 +324,7 @@ def check_config_file(config_path: str | None = None) -> tuple[str, str]:
|
|
|
323
324
|
resolved = config_mod._resolve_config_path(config_path)
|
|
324
325
|
if resolved.exists():
|
|
325
326
|
return "ok", f"Config: {resolved}"
|
|
326
|
-
return "warn", f"Config: {resolved} (not yet created --
|
|
327
|
+
return "warn", f"Config: {resolved} (not yet created -- run: muxplex-deck init)"
|
|
327
328
|
|
|
328
329
|
|
|
329
330
|
def check_federation_key(key_file: Path) -> tuple[str, str]:
|
|
@@ -496,9 +497,16 @@ def check_hid_openable() -> tuple[str, str]:
|
|
|
496
497
|
return "ok", "HID: device in use by the muxplex-deck service (expected)"
|
|
497
498
|
|
|
498
499
|
from . import hidhelp
|
|
500
|
+
from . import wsl as wsl_mod
|
|
499
501
|
|
|
500
502
|
hint = ""
|
|
501
|
-
if
|
|
503
|
+
if wsl_mod.detect().is_wsl:
|
|
504
|
+
# The udev-rule hints below don't apply on WSL -- see
|
|
505
|
+
# hidhelp.udev_guidance()'s WSL gate. explain_environment()'s W7
|
|
506
|
+
# guidance (the proven per-attach chown), already surfaced above
|
|
507
|
+
# this line in `doctor()`, covers this instead.
|
|
508
|
+
pass
|
|
509
|
+
elif sys.platform not in ("darwin", "win32") and not udev_rule_exists():
|
|
502
510
|
hint = hidhelp.HID_HINT_RUN_SERVICE_INSTALL
|
|
503
511
|
elif sys.platform not in ("darwin", "win32"):
|
|
504
512
|
hint = hidhelp.HID_HINT_RULE_EXISTS_BUT_STILL_FAILED
|
|
@@ -586,38 +594,35 @@ def check_server_reachable(server_url: str, ca_file: Path | None) -> tuple[str,
|
|
|
586
594
|
|
|
587
595
|
|
|
588
596
|
def check_service_status() -> tuple[str, str]:
|
|
589
|
-
"""
|
|
597
|
+
"""Service-state check: not installed / installed but not running / running.
|
|
598
|
+
|
|
599
|
+
Three distinct states, not two -- `service_is_installed()` (unit/plist
|
|
600
|
+
file exists) and `service_is_active()` (currently running) are checked
|
|
601
|
+
independently, so an installed-but-crash-looping service is reported
|
|
602
|
+
honestly instead of as "not installed" (which used to tell the user to
|
|
603
|
+
re-run `service install` for a service that was already installed and
|
|
604
|
+
failing -- see AGENTS.md for the incident this fixes).
|
|
605
|
+
"""
|
|
606
|
+
from .service import service_is_active, service_is_installed
|
|
607
|
+
|
|
590
608
|
if sys.platform == "darwin":
|
|
591
|
-
|
|
609
|
+
manager, tool = "launchd", "launchctl"
|
|
610
|
+
else:
|
|
611
|
+
manager, tool = "systemd", "systemctl"
|
|
592
612
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
capture_output=True,
|
|
598
|
-
text=True,
|
|
599
|
-
timeout=5,
|
|
600
|
-
check=False,
|
|
601
|
-
)
|
|
602
|
-
except FileNotFoundError:
|
|
603
|
-
return "warn", "Service: launchctl not found"
|
|
604
|
-
if result.returncode == 0:
|
|
605
|
-
return "ok", "Service: installed (launchd)"
|
|
613
|
+
if shutil.which(tool) is None:
|
|
614
|
+
return "warn", f"Service: {tool} not found -- run muxplex-deck directly"
|
|
615
|
+
|
|
616
|
+
if not service_is_installed():
|
|
606
617
|
return "warn", "Service: not installed -- run: muxplex-deck service install"
|
|
607
618
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
)
|
|
616
|
-
except FileNotFoundError:
|
|
617
|
-
return "warn", "Service: systemctl not found -- run muxplex-deck directly"
|
|
618
|
-
if result.returncode == 0:
|
|
619
|
-
return "ok", f"Service: {result.stdout.strip()} (systemd)"
|
|
620
|
-
return "warn", "Service: not installed -- run: muxplex-deck service install"
|
|
619
|
+
if service_is_active():
|
|
620
|
+
return "ok", f"Service: installed and running ({manager})"
|
|
621
|
+
|
|
622
|
+
return "warn", (
|
|
623
|
+
f"Service: installed ({manager}) but not running -- check: "
|
|
624
|
+
"muxplex-deck service logs"
|
|
625
|
+
)
|
|
621
626
|
|
|
622
627
|
|
|
623
628
|
_CHECK_MARKS: dict[str, str] = {
|
|
@@ -669,11 +674,40 @@ def doctor(config_path: str | None = None) -> int:
|
|
|
669
674
|
# this adds no output there -- see hidhelp.explain_environment().
|
|
670
675
|
from . import hidhelp
|
|
671
676
|
|
|
672
|
-
|
|
677
|
+
env_guidances = hidhelp.explain_environment()
|
|
678
|
+
for guidance in env_guidances:
|
|
673
679
|
checks.append((guidance.status, guidance.message))
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
680
|
+
env_states = {g.state for g in env_guidances}
|
|
681
|
+
|
|
682
|
+
if "W7" in env_states:
|
|
683
|
+
# The W7 guidance just appended above already says the device is
|
|
684
|
+
# attached but can't be opened, plus the proven per-attach `chown`
|
|
685
|
+
# fix -- check_deck_detected()/check_hid_openable() would only
|
|
686
|
+
# restate the same fact across two more (partly contradictory)
|
|
687
|
+
# lines. See the WSL cold-start bug report: "3 lines describe one
|
|
688
|
+
# problem". Skip both; nothing more to add.
|
|
689
|
+
pass
|
|
690
|
+
else:
|
|
691
|
+
deck_status, deck_message = check_deck_detected(config_path)
|
|
692
|
+
if deck_message == _NO_DEVICE_GUIDANCE and env_states & {
|
|
693
|
+
"W1",
|
|
694
|
+
"W2",
|
|
695
|
+
"W3",
|
|
696
|
+
"W4",
|
|
697
|
+
"W5",
|
|
698
|
+
"W6",
|
|
699
|
+
}:
|
|
700
|
+
# A WSL-specific state above already explains exactly where
|
|
701
|
+
# the device is (or precisely why it isn't visible to this OS
|
|
702
|
+
# yet) -- the generic "check the cable" guidance would
|
|
703
|
+
# flatly contradict it (see bug report: located @ BUSID 1-4,
|
|
704
|
+
# immediately followed by "check your cable").
|
|
705
|
+
deck_message = (
|
|
706
|
+
"Stream Deck: not detected on this OS yet -- see the WSL "
|
|
707
|
+
"guidance above."
|
|
708
|
+
)
|
|
709
|
+
checks.append((deck_status, deck_message))
|
|
710
|
+
checks.append(check_hid_openable())
|
|
677
711
|
|
|
678
712
|
server_url = cfg.server_url if cfg is not None else raw.get("server_url", "")
|
|
679
713
|
checks.append(check_server_reachable(server_url, ca_file))
|
|
@@ -1044,6 +1078,34 @@ def update(*, force: bool = False) -> None:
|
|
|
1044
1078
|
# ---------------------------------------------------------------------------
|
|
1045
1079
|
|
|
1046
1080
|
|
|
1081
|
+
# A settled-enough bound for sysfs to enumerate a just-attached device --
|
|
1082
|
+
# generous enough to absorb the usbip attach -> udev/sysfs lag that
|
|
1083
|
+
# produced a false W6 ("not visible yet") immediately followed by a
|
|
1084
|
+
# `doctor` run that found the device fine, but tight enough that a
|
|
1085
|
+
# genuinely-stuck attach still fails promptly.
|
|
1086
|
+
_ATTACH_SETTLE_ATTEMPTS = 5
|
|
1087
|
+
_ATTACH_SETTLE_DELAY_SECONDS = 0.2
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def _find_usb_node_with_settle(usbnode_mod: Any, vendor_id: str) -> Any:
|
|
1091
|
+
"""Poll for the USB node with a bounded settle window after an attach.
|
|
1092
|
+
|
|
1093
|
+
A successful `wsl.attach()` can return before sysfs has finished
|
|
1094
|
+
enumerating the device on this side -- checking `find_usb_node` only
|
|
1095
|
+
once produced a false negative (W6: "attached but Linux doesn't see it
|
|
1096
|
+
yet") moments before the very next `doctor` run found the device
|
|
1097
|
+
working. Retries a few times with a short delay instead of declaring
|
|
1098
|
+
failure on the very first miss.
|
|
1099
|
+
"""
|
|
1100
|
+
node = usbnode_mod.find_usb_node(vendor_id)
|
|
1101
|
+
for _ in range(_ATTACH_SETTLE_ATTEMPTS - 1):
|
|
1102
|
+
if node is not None:
|
|
1103
|
+
return node
|
|
1104
|
+
time.sleep(_ATTACH_SETTLE_DELAY_SECONDS)
|
|
1105
|
+
node = usbnode_mod.find_usb_node(vendor_id)
|
|
1106
|
+
return node
|
|
1107
|
+
|
|
1108
|
+
|
|
1047
1109
|
def wsl_attach(*, vendor_id: str = "0fd9") -> int:
|
|
1048
1110
|
"""`muxplex-deck wsl attach` -- attach the Stream Deck to this WSL2 distro.
|
|
1049
1111
|
|
|
@@ -1119,7 +1181,7 @@ def wsl_attach(*, vendor_id: str = "0fd9") -> int:
|
|
|
1119
1181
|
else:
|
|
1120
1182
|
print_check("ok", f"Already attached (BUSID {device.busid})")
|
|
1121
1183
|
|
|
1122
|
-
node = usbnode_mod
|
|
1184
|
+
node = _find_usb_node_with_settle(usbnode_mod, vendor_id)
|
|
1123
1185
|
if node is None:
|
|
1124
1186
|
print_check("warn", hidhelp.w6_message(device))
|
|
1125
1187
|
print()
|
|
@@ -145,9 +145,9 @@ def load_config(config_path: str | None = None) -> Config:
|
|
|
145
145
|
if not path.exists():
|
|
146
146
|
raise ConfigError(
|
|
147
147
|
f"Config file not found: {path}\n"
|
|
148
|
-
"
|
|
149
|
-
'
|
|
150
|
-
"
|
|
148
|
+
"Run: muxplex-deck init\n"
|
|
149
|
+
"(or create it by hand with at least a 'server_url' field, e.g.:\n"
|
|
150
|
+
' {"server_url": "https://<your-server>:8088"})'
|
|
151
151
|
)
|
|
152
152
|
|
|
153
153
|
try:
|
|
@@ -52,7 +52,10 @@ HID_HINT_RULE_EXISTS_BUT_STILL_FAILED = (
|
|
|
52
52
|
" A udev rule exists but the device still could not be opened."
|
|
53
53
|
)
|
|
54
54
|
|
|
55
|
-
_UDEV_RULE_CONTENT =
|
|
55
|
+
_UDEV_RULE_CONTENT = (
|
|
56
|
+
'SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", MODE="0660", '
|
|
57
|
+
'GROUP="plugdev", TAG+="uaccess"'
|
|
58
|
+
)
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
@dataclass(frozen=True)
|
|
@@ -369,8 +372,24 @@ def udev_guidance() -> Guidance | None:
|
|
|
369
372
|
|
|
370
373
|
Returns `None` when udev is not live (P4: never print a command we
|
|
371
374
|
know will fail here) -- `explain_environment()`'s U-DEAD guidance
|
|
372
|
-
takes its place in that case instead.
|
|
375
|
+
takes its place in that case instead. Also returns `None` on WSL:
|
|
376
|
+
`udevadm control --reload-rules` can report success there while the
|
|
377
|
+
rule still never fires for a usbip-attached device (see AGENTS.md
|
|
378
|
+
"U7" -- a real WSL user followed this exact block and lost ~40
|
|
379
|
+
minutes to a rule that never took effect). On WSL the only proven
|
|
380
|
+
remediation is the per-attach `sudo chown` in `w7_message()`, already
|
|
381
|
+
surfaced by `explain_environment()` -- callers show that instead (see
|
|
382
|
+
`service._warn_if_no_udev_rule`).
|
|
383
|
+
|
|
384
|
+
The install command is a single `echo | sudo tee` line rather than a
|
|
385
|
+
`<<'EOF' ... EOF` heredoc: this text gets copy-pasted into a terminal,
|
|
386
|
+
and a heredoc terminator that isn't at column 0 (as it wasn't here,
|
|
387
|
+
once the surrounding block got indented for display) is silently
|
|
388
|
+
swallowed by the shell, leaving the user stuck at a `>` continuation
|
|
389
|
+
prompt. A single line has no terminator to misplace.
|
|
373
390
|
"""
|
|
391
|
+
if wsl.detect().is_wsl:
|
|
392
|
+
return None
|
|
374
393
|
if not usbnode.udev_is_live():
|
|
375
394
|
return None
|
|
376
395
|
return Guidance(
|
|
@@ -379,8 +398,8 @@ def udev_guidance() -> Guidance | None:
|
|
|
379
398
|
f"No udev rule found for the Stream Deck (vendor id {_ELGATO_VENDOR_ID}).\n"
|
|
380
399
|
"Without it, the service (running as your user, not root) will fail to\n"
|
|
381
400
|
"open the device. Install a rule once:\n\n"
|
|
382
|
-
" sudo tee
|
|
383
|
-
|
|
401
|
+
f" echo '{_UDEV_RULE_CONTENT}' | sudo tee "
|
|
402
|
+
"/etc/udev/rules.d/70-streamdeck.rules >/dev/null\n"
|
|
384
403
|
" sudo udevadm control --reload-rules && sudo udevadm trigger\n\n"
|
|
385
404
|
"Then unplug and replug the Stream Deck (or re-run `muxplex-deck wsl attach`\n"
|
|
386
405
|
"under WSL), and make sure you're in the `plugdev` group:\n"
|
|
@@ -44,6 +44,8 @@ import sys
|
|
|
44
44
|
import time
|
|
45
45
|
from pathlib import Path
|
|
46
46
|
|
|
47
|
+
from . import config as config_mod
|
|
48
|
+
|
|
47
49
|
# ---------------------------------------------------------------------------
|
|
48
50
|
# Constants
|
|
49
51
|
# ---------------------------------------------------------------------------
|
|
@@ -232,20 +234,28 @@ def _warn_if_no_udev_rule() -> None:
|
|
|
232
234
|
|
|
233
235
|
Delegates the actual guidance text to `hidhelp.udev_guidance()` -- the
|
|
234
236
|
single home for this string (see WSL_COLD_START_SPEC.md P6). That
|
|
235
|
-
function returns `None` when udev isn't actually running
|
|
236
|
-
print a command known to fail here)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
function now returns `None` both when udev isn't actually running
|
|
238
|
+
(P4: never print a command known to fail here) and when this is WSL
|
|
239
|
+
(the reload can report success there while the rule still never
|
|
240
|
+
fires for a usbip-attached device -- see AGENTS.md "U7"; a real WSL
|
|
241
|
+
user followed this exact block and lost ~40 minutes). On a healthy
|
|
242
|
+
non-WSL platform this prints nothing, exactly as before. On WSL,
|
|
243
|
+
show the environment-appropriate guidance instead (the proven
|
|
244
|
+
per-attach `chown`) rather than leaving the user with nothing.
|
|
240
245
|
"""
|
|
241
246
|
if udev_rule_exists():
|
|
242
247
|
return
|
|
243
248
|
from . import hidhelp
|
|
244
249
|
|
|
245
250
|
guidance = hidhelp.udev_guidance()
|
|
246
|
-
if guidance is None:
|
|
251
|
+
if guidance is not None:
|
|
252
|
+
_print_guidance_block(guidance.message)
|
|
247
253
|
return
|
|
248
|
-
|
|
254
|
+
|
|
255
|
+
from . import wsl as wsl_mod
|
|
256
|
+
|
|
257
|
+
if wsl_mod.detect().is_wsl:
|
|
258
|
+
_print_environment_guidance()
|
|
249
259
|
|
|
250
260
|
|
|
251
261
|
def _print_environment_guidance() -> None:
|
|
@@ -299,6 +309,22 @@ def service_is_active() -> bool:
|
|
|
299
309
|
return result.returncode == 0
|
|
300
310
|
|
|
301
311
|
|
|
312
|
+
def service_is_installed() -> bool:
|
|
313
|
+
"""Is the muxplex-deck unit/plist file present on disk?
|
|
314
|
+
|
|
315
|
+
Independent of `service_is_active()` -- a service that is installed but
|
|
316
|
+
crash-looping (or simply stopped) is still installed. `doctor`'s
|
|
317
|
+
`check_service_status` used to conflate "not active" with "not
|
|
318
|
+
installed" and recommend `service install` for a service that was
|
|
319
|
+
already installed and failing; this is the file-existence half of the
|
|
320
|
+
fix (see AGENTS.md for the incident: 1113 restarts, then told to
|
|
321
|
+
"install" a service that was already there).
|
|
322
|
+
"""
|
|
323
|
+
if _is_darwin():
|
|
324
|
+
return _LAUNCHD_PLIST_PATH.exists()
|
|
325
|
+
return _SYSTEMD_UNIT_PATH.exists()
|
|
326
|
+
|
|
327
|
+
|
|
302
328
|
def _enable_linger() -> None:
|
|
303
329
|
"""Best-effort `loginctl enable-linger` so the service survives logout.
|
|
304
330
|
|
|
@@ -338,6 +364,37 @@ def _print_next_steps() -> None:
|
|
|
338
364
|
print()
|
|
339
365
|
|
|
340
366
|
|
|
367
|
+
def _config_ready() -> tuple[bool, str | None]:
|
|
368
|
+
"""Return (True, None) if config is loadable, else (False, ConfigError message).
|
|
369
|
+
|
|
370
|
+
Calls the exact same `config.load_config()` the installed unit's own
|
|
371
|
+
`ExecStart` (`... run`, no `--config` flag) will call at startup, so
|
|
372
|
+
"ready to install" and "ready to run" can never diverge. A config that
|
|
373
|
+
fails this check would make the service crash immediately once
|
|
374
|
+
started, and `Restart=always` (`KeepAlive` on launchd) would then
|
|
375
|
+
restart it every few seconds forever -- see the `service install`
|
|
376
|
+
crash-loop incident in AGENTS.md (1113 restarts on a fresh machine
|
|
377
|
+
with no config yet).
|
|
378
|
+
"""
|
|
379
|
+
try:
|
|
380
|
+
config_mod.load_config(None)
|
|
381
|
+
except config_mod.ConfigError as exc:
|
|
382
|
+
return False, str(exc)
|
|
383
|
+
return True, None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _print_config_not_ready(config_error: str | None) -> None:
|
|
387
|
+
"""Explain why install is stopping short of enabling/starting the service."""
|
|
388
|
+
_print_guidance_block(
|
|
389
|
+
"Not enabling or starting yet -- configuration is incomplete:\n"
|
|
390
|
+
+ (config_error or "unknown configuration error")
|
|
391
|
+
)
|
|
392
|
+
print()
|
|
393
|
+
print(" Run muxplex-deck init to create it, then run this command again:")
|
|
394
|
+
print(" muxplex-deck service install")
|
|
395
|
+
print()
|
|
396
|
+
|
|
397
|
+
|
|
341
398
|
def _systemd_install() -> None:
|
|
342
399
|
print("\nmuxplex-deck service install (systemd --user)\n")
|
|
343
400
|
|
|
@@ -354,6 +411,15 @@ def _systemd_install() -> None:
|
|
|
354
411
|
subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
|
|
355
412
|
_step_ok("Reloaded the systemd user daemon")
|
|
356
413
|
|
|
414
|
+
# Never enable/start a unit whose config we already know will make it
|
|
415
|
+
# crash on launch -- see `_config_ready()`. The unit file above is
|
|
416
|
+
# harmless to have on disk either way; only the "make it run" steps are
|
|
417
|
+
# gated.
|
|
418
|
+
config_ready, config_error = _config_ready()
|
|
419
|
+
if not config_ready:
|
|
420
|
+
_print_config_not_ready(config_error)
|
|
421
|
+
return
|
|
422
|
+
|
|
357
423
|
subprocess.run(
|
|
358
424
|
["systemctl", "--user", "enable", "--now", "muxplex-deck"], check=True
|
|
359
425
|
)
|
|
@@ -559,6 +625,15 @@ def _launchd_install() -> None:
|
|
|
559
625
|
_LAUNCHD_PLIST_PATH.write_text(plist_content)
|
|
560
626
|
_step_ok(f"Wrote plist: {_LAUNCHD_PLIST_PATH}")
|
|
561
627
|
|
|
628
|
+
# Never bootstrap (load + start) a job whose config we already know will
|
|
629
|
+
# make it crash on launch -- see `_config_ready()`. `KeepAlive` would
|
|
630
|
+
# then have launchd relaunch it forever. The plist above is harmless to
|
|
631
|
+
# have on disk either way; only the "make it run" step is gated.
|
|
632
|
+
config_ready, config_error = _config_ready()
|
|
633
|
+
if not config_ready:
|
|
634
|
+
_print_config_not_ready(config_error)
|
|
635
|
+
return
|
|
636
|
+
|
|
562
637
|
result = _launchd_bootstrap()
|
|
563
638
|
if result.returncode == 0:
|
|
564
639
|
_step_ok("Loaded + started the service (launchctl bootstrap)")
|
|
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
|