muxplex-deck 0.5.1__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.
Files changed (29) hide show
  1. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/PKG-INFO +1 -1
  2. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/pyproject.toml +1 -1
  3. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/cli.py +26 -28
  4. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/config.py +3 -3
  5. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/service.py +67 -0
  6. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/README.md +0 -0
  7. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/deck_probe/__init__.py +0 -0
  8. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/deck_probe/__main__.py +0 -0
  9. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/deck_probe/capabilities.py +0 -0
  10. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/deck_probe/events.py +0 -0
  11. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/deck_probe/main.py +0 -0
  12. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/deck_probe/rendering.py +0 -0
  13. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/__init__.py +0 -0
  14. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/__main__.py +0 -0
  15. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/attention.py +0 -0
  16. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/device.py +0 -0
  17. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/device_real.py +0 -0
  18. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/emulator.py +0 -0
  19. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/focus.py +0 -0
  20. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/hidhelp.py +0 -0
  21. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/init_wizard.py +0 -0
  22. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/interaction.py +0 -0
  23. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/layout.py +0 -0
  24. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/main.py +0 -0
  25. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/rendering.py +0 -0
  26. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/statusfile.py +0 -0
  27. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/usbnode.py +0 -0
  28. {muxplex_deck-0.5.1 → muxplex_deck-0.5.2}/src/muxplex_deck/views.py +0 -0
  29. {muxplex_deck-0.5.1 → 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.1
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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "muxplex-deck"
3
- version = "0.5.1"
3
+ version = "0.5.2"
4
4
  description = "Hardware probe/diagnostic app for the Elgato Stream Deck+ (seed of the muxplex sidecar)"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
@@ -593,38 +594,35 @@ def check_server_reachable(server_url: str, ca_file: Path | None) -> tuple[str,
593
594
 
594
595
 
595
596
  def check_service_status() -> tuple[str, str]:
596
- """Best-effort service-installed/running check (systemd or launchd)."""
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
+
597
608
  if sys.platform == "darwin":
598
- import os as _os
609
+ manager, tool = "launchd", "launchctl"
610
+ else:
611
+ manager, tool = "systemd", "systemctl"
599
612
 
600
- uid = _os.getuid()
601
- try:
602
- result = subprocess.run(
603
- ["launchctl", "print", f"gui/{uid}/com.muxplex-deck"],
604
- capture_output=True,
605
- text=True,
606
- timeout=5,
607
- check=False,
608
- )
609
- except FileNotFoundError:
610
- return "warn", "Service: launchctl not found"
611
- if result.returncode == 0:
612
- 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():
613
617
  return "warn", "Service: not installed -- run: muxplex-deck service install"
614
618
 
615
- try:
616
- result = subprocess.run(
617
- ["systemctl", "--user", "is-active", "muxplex-deck"],
618
- capture_output=True,
619
- text=True,
620
- timeout=5,
621
- check=False,
622
- )
623
- except FileNotFoundError:
624
- return "warn", "Service: systemctl not found -- run muxplex-deck directly"
625
- if result.returncode == 0:
626
- return "ok", f"Service: {result.stdout.strip()} (systemd)"
627
- 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
+ )
628
626
 
629
627
 
630
628
  _CHECK_MARKS: dict[str, str] = {
@@ -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
- "Create it with at least a 'server_url' field, e.g.:\n"
149
- ' {"server_url": "https://<your-server>:8088"}\n'
150
- "See README.md for the full example."
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:
@@ -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
  # ---------------------------------------------------------------------------
@@ -307,6 +309,22 @@ def service_is_active() -> bool:
307
309
  return result.returncode == 0
308
310
 
309
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
+
310
328
  def _enable_linger() -> None:
311
329
  """Best-effort `loginctl enable-linger` so the service survives logout.
312
330
 
@@ -346,6 +364,37 @@ def _print_next_steps() -> None:
346
364
  print()
347
365
 
348
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
+
349
398
  def _systemd_install() -> None:
350
399
  print("\nmuxplex-deck service install (systemd --user)\n")
351
400
 
@@ -362,6 +411,15 @@ def _systemd_install() -> None:
362
411
  subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
363
412
  _step_ok("Reloaded the systemd user daemon")
364
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
+
365
423
  subprocess.run(
366
424
  ["systemctl", "--user", "enable", "--now", "muxplex-deck"], check=True
367
425
  )
@@ -567,6 +625,15 @@ def _launchd_install() -> None:
567
625
  _LAUNCHD_PLIST_PATH.write_text(plist_content)
568
626
  _step_ok(f"Wrote plist: {_LAUNCHD_PLIST_PATH}")
569
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
+
570
637
  result = _launchd_bootstrap()
571
638
  if result.returncode == 0:
572
639
  _step_ok("Loaded + started the service (launchctl bootstrap)")
File without changes