Files
obsidian-vault/personal/tech/eagle-dashboard-launchd.md

7.4 KiB
Raw Permalink Blame History

created, tags, updated
created tags updated
2026-06-25
eagle-dashboard
launchd
cron
macos
devops
2026-06-25

Eagle Dashboard — Launchd Integration

Обзор

Eagle Dashboard (eagle-dash) имеет полноценную поддержку launchd-джобов наравне с Hermes Eagle/Whale кронами. Пользователь может создавать, просматривать, запускать/останавливать и удалять launchd-агенты прямо из интерфейса дашборда.

Архитектура

Backend (main.py)

Три ключевые функции:

  • _load_launchd_plist(plist) — читает один .plist файл, парсит его, вызывает launchctl list для статуса (PID, LastExitStatus, enabled), возвращает унифицированный словарь.
  • _list_all_launchd() — читает все *.plist из ~/Library/LaunchAgents/, вызывает _load_launchd_plist() для каждого.
  • _list_launchd_crons() — фильтрует только те, у которых есть StartInterval или StartCalendarInterval.
  • _list_launchd_services() — фильтрует те, у которых НЕТ расписания (always-on демоны).

API endpoints

Endpoint Метод Описание
/api/crons?source=launchd GET Список launchd-кронов (с расписанием)
/api/services/launchd GET Список launchd-сервисов (без расписания, always-on)
/api/crons POST Создание нового launchd-агента (source=launchd)
/api/crons/launchd/{label} PATCH Обновление launchd-агента (plist)
/api/crons/launchd/{label} DELETE Удаление launchd-агента
/api/crons/launchd/{label}/toggle POST Вкл/выкл launchd-агента (launchctl load/unload)

Создание launchd-крона (POST /api/crons)

  • Поле source: "launchd" в body
  • name — Label для plist (если не начинается с com., добавляется префикс com.)
  • schedule — парсится как every Ns/m/h/dStartInterval в секундах
  • scriptProgramArguments (разбивается по пробелам)
  • workdirWorkingDirectory
  • run_at_load (boolean) — RunAtLoad в plist
  • Поля prompt, deliver, skills, toolsets, model, providerскрыты на фронте, не отправляются для launchd
  • Plist пишется атомарно через .plist.tmp.replace()
  • После записи — launchctl load -w

Чтение статуса launchd

launchctl list {label} на macOS (Sequoia 15.x, next-формат) возвращает openstep-style plist (не JSON, не tab-separated):

{
    "PID" = 93796;
    "LastExitStatus" = 0;
    "Label" = "com.eagle.dashboard";
    ...
}

Парсинг через regex: r'"PID"\s*=\s*(\d+)' и r'"LastExitStatus"\s*=\s*(-?\d+)'.

Логика статуса:

  • state = "running" — если PID > 0
  • state = "scheduled" — если есть расписание, процесс не запущен
  • state = "stopped" — если disabled (launchctl вернул non-zero)
  • state = "idle" — enabled, без расписания, без PID
  • last_status = None если процесс запущен (PID есть), иначе числовой exit code
  • last_status = null — ни разу не запускался

Фронтенд (templates/index.html)

  • Services вкладка — показывает Docker контейнеры, сервисы из registry, и секцию "Launchd (Always-On)" с always-on демонами. Карточки launchd-сервисов имеют кнопки Pause/Resume и Delete.
  • Crons вкладка — показывает Hermes Eagle/Whale кроны + launchd-кроны (только с расписанием). Для launchd-кронов показывается schedule, exit code, runs count (runs не реализован из-за отсутствия данных в launchd).
  • Модалка создания — при выборе source = launchd скрываются поля: Deliver, Prompt, Model+Provider, Skills+Toolsets; показывается чекбокс "Run on load" (runAtLoad, отправляется как run_at_load: boolean)
  • Schedule hint — для launchd показывается упрощённый хинт: только every Ns/m/h/d
  • Tab URL persistence — через history.replaceState + URLSearchParams (?tab=crons)
  • Контролы на launchd-сервисах (Services вкладка): кнопка ⏸ pause / ▶ resume (вызывает /api/crons/launchd/{label}/toggle), кнопка 🗑 delete (вызывает DELETE /api/crons/launchd/{label})

Ключевые решения

  1. Зачем отдельный launchd CRUD — чтобы пользователь мог управлять своими кронами из единого интерфейса, не открывая терминал.
  2. Разделение crons/services — launchd-агенты без StartInterval/StartCalendarInterval не показываются в Crons. Вместо этого они отображаются на Services вкладке как "Launchd (Always-On)".
  3. PID vs LastExitStatus — для запущенного процесса exit code неактуален, поэтому last_status = None при PID > 0.
  4. Next-формат — macOS Sequoia возвращает openstep plist (не tab-separated, не JSON): "Key" = value;. Парсинг через regex.
  5. Атомарная запись plist — через .tmp + .replace() чтобы не повредить plist при ошибке записи.
  6. _list_launchd_crons() vs _list_launchd_services() — обе читают все plist из _list_all_launchd(), но фильтруют по has_schedule. Используют общий _load_launchd_plist().

Файлы

  • /Users/admin/Developer/eagle-dash/main.py — backend
  • /Users/admin/Developer/eagle-dash/templates/index.html — frontend
  • /Users/admin/Developer/eagle-dash/.venv/ — virtualenv (FastAPI + uvicorn)
  • Запуск через launchd: ~/Library/LaunchAgents/com.eagle.dashboard.plist

Статус (2026-06-25)

  • Backend CRUD для launchd (create, read, update, delete, toggle)
  • Список launchd-кронов на Crons вкладке
  • Список always-on launchd-демонов на Services вкладке
  • Чекбокс RunAtLoad при создании
  • Правильные статусы: running / scheduled / idle / stopped + exit code
  • PID и LastExitStatus читаются из launchctl list (next-format)
  • Спрятаны поля для launchd (prompt, deliver, skills и т.д.)
  • Tab URL persistence
  • Кнопки Pause/Resume/Delete на карточках launchd (и в Crons, и в Services)
  • Разделение crons/services: с расписанием → Crons, без расписания → Services
  • _list_launchd_services() — новый endpoint /api/services/launchd