Перейти к основному содержимому

Как делать сервисы на macOS через launchd

Оглавление
logo

На macOS часто хочется сделать то же, что на Linux делается через systemd: запустить скрипт как сервис, автоматически стартовать его при входе в систему или выполнять по расписанию. В macOS для этого есть launchd — встроенный менеджер процессов и задач.

Если коротко: для пользовательских задач обычно нужен LaunchAgent, а для системных — LaunchDaemon. В этой статье разберём, как выбрать правильный вариант, как написать .plist и как убедиться, что всё работает.

Что такое launchd
#

launchd — это базовый механизм запуска сервисов в macOS. Он умеет:

  • запускать задачи при входе пользователя;
  • держать процесс в фоне и перезапускать его при падении;
  • запускать команды по расписанию;
  • писать stdout/stderr в файлы;
  • автоматически подхватывать конфигурацию из .plist.

По сути, это не «отдельная утилита», а часть самой системы.

LaunchAgent или LaunchDaemon
#

Самый важный выбор — где будет жить задача.

LaunchAgent
#

Подходит, если скрипт:

  • работает от имени текущего пользователя;
  • должен иметь доступ к пользовательскому окружению;
  • может открывать GUI-приложения;
  • нужен только после логина.

Типичный путь:

~/Library/LaunchAgents/

LaunchDaemon
#

Подходит, если задача:

  • должна работать без логина пользователя;
  • запускается от root или системной учётки;
  • обслуживает системный сервис;
  • не должна зависеть от GUI.

Типичный путь:

/Library/LaunchDaemons/

Для большинства «домашних» автоматизаций на Mac — бэкапы, синхронизации, периодические скрипты — почти всегда нужен LaunchAgent.

Минимальный пример
#

Допустим, у вас есть скрипт:

/Users/beaverblogger/Documents/scripts/youtube-cookies/sync_yt_cookies.sh

И вы хотите запускать его раз в сутки. Для этого создаём файл:

~/Library/LaunchAgents/com.tatarinovms.youtube-cookies-sync.plist

Пример содержимого:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>com.tatarinovms.youtube-cookies-sync</string>

    <key>ProgramArguments</key>
    <array>
      <string>/Users/beaverblogger/Documents/scripts/youtube-cookies/sync_yt_cookies.sh</string>
    </array>

    <key>WorkingDirectory</key>
    <string>/Users/beaverblogger/Documents/scripts/youtube-cookies</string>

    <key>StartCalendarInterval</key>
    <dict>
      <key>Hour</key>
      <integer>9</integer>
      <key>Minute</key>
      <integer>0</integer>
    </dict>

    <key>StandardOutPath</key>
    <string>/Users/tatarinovms/Library/Logs/youtube-cookies-sync.out.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/tatarinovms/Library/Logs/youtube-cookies-sync.err.log</string>
  </dict>
</plist>

Это уже полноценный сервис по расписанию.

Какие ключи чаще всего нужны
#

Label
#

Уникальное имя сервиса. Обычно используют обратный DNS-стиль:

com.company.app-name

ProgramArguments
#

Команда, которую нужно запустить. Лучше указывать полный путь к скрипту или бинарнику.

WorkingDirectory
#

Полезно, если скрипт использует относительные пути.

StartCalendarInterval
#

Запуск по времени. Можно указать:

  • только Hour и Minute — каждый день в это время;
  • Weekday — по дням недели;
  • Day — по числам месяца;
  • комбинации.

RunAtLoad
#

Если поставить true, сервис запустится сразу после загрузки агента.

KeepAlive
#

Если true, launchd будет пытаться держать процесс живым и перезапускать его. Для бесконечных демонов это удобно, но для одноразовых скриптов по расписанию обычно не нужно.

StandardOutPath и StandardErrorPath
#

Очень полезно для отладки. Без логов легко потерять ошибку.

Как установить сервис
#

После создания .plist нужно загрузить его в launchd:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.tatarinovms.youtube-cookies-sync.plist
launchctl enable gui/$(id -u)/com.tatarinovms.youtube-cookies-sync

Если конфиг уже был загружен и вы его меняете, обычно удобно сначала выгрузить старую версию:

launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.tatarinovms.youtube-cookies-sync.plist

Потом снова выполнить bootstrap.

Как проверить, что всё работает
#

Проверка plist
#

plutil -lint ~/Library/LaunchAgents/com.tatarinovms.youtube-cookies-sync.plist

Если файл валидный, увидите OK.

Проверка статуса
#

launchctl print gui/$(id -u)/com.tatarinovms.youtube-cookies-sync

Там можно увидеть:

  • путь к plist;
  • состояние агента;
  • путь к stdout/stderr;
  • последний код выхода.

Ручной запуск
#

Для теста удобно временно выполнить сам скрипт руками:

~/Documents/scripts/youtube-cookies/sync_yt_cookies.sh

Это особенно полезно перед тем, как отдавать задачу launchd.

Практические советы
#

1. Делайте скрипт идемпотентным
#

Сервис может запускаться повторно. Лучше, чтобы повторный запуск не ломал состояние.

2. Не полагайтесь на интерактивную среду
#

В launchd обычно нет привычного shell-окружения из терминала. Поэтому:

  • используйте абсолютные пути;
  • явно задавайте PATH, если он нужен;
  • не рассчитывайте на alias и функции shell.

3. Логируйте в файл
#

Если задача не стартует, stdout/stderr в лог-файлы экономят много времени.

4. Не quitiть чужой браузер или приложение без проверки
#

Если ваш скрипт запускает Firefox, Safari или другое GUI-приложение, сначала определите, запускали ли вы его сами. Иначе можно случайно закрыть то, что пользователь уже открыл вручную.

5. Для фона и GUI — LaunchAgent
#

Если сервис должен открыть браузер, нажать что-то в UI или читать пользовательские cookies, LaunchAgent почти всегда правильнее LaunchDaemon.

Когда лучше не использовать launchd
#

Иногда проще и честнее использовать:

  • обычный cron, если задача очень простая и не зависит от GUI;
  • at или одноразовый запуск, если нужен только разовый таймер;
  • shortcuts или Automator, если задача чисто пользовательская и без сложной логики.

Но если нужен нормальный сервис на macOS, launchd — стандартный путь.

Мой рабочий шаблон
#

Я обычно придерживаюсь такой схемы:

  1. Скрипт делает одну вещь.
  2. У него есть полный путь к интерпретатору или бинарнику.
  3. Есть отдельный лог-файл.
  4. Сервис оформлен через LaunchAgent.
  5. Конфиг проверяется через plutil -lint.
  6. После установки я смотрю launchctl print и тестирую вручную.

Так проще отлаживать и проще поддерживать.

Вывод и финал
#

Если на macOS хочется «сделать сервис», почти всегда нужно идти через launchd.

  • LaunchAgent — для пользовательских задач и GUI.
  • LaunchDaemon — для системных фоновых сервисов.
  • .plist — это конфигурация запуска.
  • launchctl bootstrap и launchctl print — базовые команды для установки и проверки.

Related