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

108 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
created: 2026-06-25
tags: [eagle-dashboard, launchd, cron, macos, devops]
updated: 2026-06-25
---
# Eagle Dashboard — Launchd Integration
## Обзор
Eagle Dashboard ([eagle-dash](https://github.com/mallexxx/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/d``StartInterval` в секундах
- `script``ProgramArguments` (разбивается по пробелам)
- `workdir``WorkingDirectory`
- `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)
- [x] Backend CRUD для launchd (create, read, update, delete, toggle)
- [x] Список launchd-кронов на Crons вкладке
- [x] Список always-on launchd-демонов на Services вкладке
- [x] Чекбокс RunAtLoad при создании
- [x] Правильные статусы: running / scheduled / idle / stopped + exit code
- [x] PID и LastExitStatus читаются из launchctl list (next-format)
- [x] Спрятаны поля для launchd (prompt, deliver, skills и т.д.)
- [x] Tab URL persistence
- [x] Кнопки Pause/Resume/Delete на карточках launchd (и в Crons, и в Services)
- [x] Разделение crons/services: с расписанием → Crons, без расписания → Services
- [x] `_list_launchd_services()` — новый endpoint `/api/services/launchd`