[2026-06-25] eagle: personal/tech/eagle-dashboard-launchd.md
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
---
|
||||
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:
|
||||
|
||||
```
|
||||
{
|
||||
"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 демонами
|
||||
- **Crons вкладка** — показывает Hermes Eagle/Whale кроны + launchd-кроны (только с расписанием)
|
||||
- **Модалка создания** — при выборе `source = launchd` скрываются поля: Deliver, Prompt, Model+Provider, Skills+Toolsets; показывается чекбокс "Run on load" (`runAtLoad`)
|
||||
- **Schedule hint** — для launchd показывается упрощённый хинт: только `every Ns/m/h/d`
|
||||
- **Tab URL persistence** — через `history.replaceState` + URLSearchParams (`?tab=crons`)
|
||||
|
||||
## Ключевые решения
|
||||
|
||||
1. **Зачем отдельный launchd CRUD, а не plist вручную** — чтобы пользователь мог управлять своими кронами из единого интерфейса, не открывая терминал.
|
||||
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). Парсинг через regex.
|
||||
5. **Атомарная запись plist** — через `.tmp` + `.replace()` чтобы не повредить 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
|
||||
Reference in New Issue
Block a user