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

103 lines
6.3 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:
```
{
"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