launchctl

15 сентября 2026 · ~12 мин чтения

инструмент macos devops командная-строка

launchctl

launchctl — утилита командной строки в macOS для управления launchd,
системным менеджером служб: она загружает, выгружает, включает, выключает
и опрашивает состояние фоновых процессов (демонов и агентов), описанных
в plist-файлах.

История

launchctl появился вместе с launchd в Mac OS X 10.4 Tiger, в апреле 2005
года. До этого в macOS (тогда ещё Mac OS X) фоновые процессы запускались
вперемешку через унаследованные от BSD/Unix механизмы: init, cron,
inetd, SystemStarter и xinetd — четыре разных системы с разными
конфигами и разной логикой перезапуска. Apple свела всё это в один менеджер
процессов, и launchctl стал единственным входом в него из терминала.

Главная веха в истории launchctl — переход с OS X 10.10 Yosemite (2014) на
новый набор подкоманд. До Yosemite основными командами были load и unload.
Начиная с Yosemite Apple ввела bootstrap, bootout, enable, disable,
kickstart — более явное разделение между «состоянием в реестре» и
«состоянием в текущей рабочей сессии». Старые load/unload формально не
удалили (они работают до сих пор через слой совместимости), но именно
путаница между старым и новым набором команд регулярно приводит к багам вроде
того, что произошёл у Паши вчера: служба, которую выключили не той командой,
сама ожила после перезагрузки Mac.

Сейчас, в 2026 году, launchctl — часть Darwin, ядра macOS, и относительно
стабилен: с 2014 года синтаксис принципиально не менялся, хотя Apple
продолжает добавлять мелкие подкоманды (blame, plist, procinfo и т. п.)
для диагностики.

Что это такое

launchd — это process 1 в macOS, то есть первый процесс, который стартует
при загрузке системы (аналог init/systemd в Linux). Он запускает,
следит и при необходимости перезапускает все остальные процессы — от
системных демонов до пользовательских фоновых скриптов. launchctl
это просто клиент, консольная программа, которая разговаривает с launchd
через его сокет и отдаёт ему команды: «запусти вот это», «остановись»,
«покажи список», «забудь про эту службу насовсем».

Ключевое отличие launchd/launchctl от cron: cron умеет только
запускать задачи по расписанию и не следит, жив ли процесс после запуска.
launchd — полноценный супервизор: он может держать процесс постоянно
живым (KeepAlive), перезапускать его при падении, запускать по расписанию
(StartCalendarInterval), по таймеру (StartInterval), при появлении файла,
при подключении к сети (Sockets) или просто при загрузке системы
(RunAtLoad). Именно поэтому в daily-digest, о котором вы сейчас читаете,
запуск в 9:00 МСК сделан через launchd-агента, а не через cron.

Второе важное отличие — деление на демоны (daemons) и агенты
(agents)
. Демоны живут в /Library/LaunchDaemons (или /System/Library/ LaunchDaemons для системных), стартуют при загрузке системы ещё до входа
пользователя и работают от имени root, без доступа к GUI. Агенты живут в
~/Library/LaunchAgents (пользовательские) или /Library/LaunchAgents
(общие для всех пользователей), стартуют при входе конкретного пользователя
в сессию и имеют доступ к GUI-окружению — окнам, буферу обмена, уведомлениям.
Именно агент запускает daily-digest: com.pavel.dailydigest.plist лежит
в ~/Library/LaunchAgents.

launchctl vs systemctl (Linux): концептуально они делают одно и то же —
управляют системным менеджером служб (launchd и systemd соответственно).
Разница в философии конфигурации: launchd описывается декларативным XML
(plist), systemd — INI-подобными unit-файлами; у systemd заметно богаче
модель зависимостей между службами (After=, Requires=, targets), у
launchd эта модель проще и завязана в основном на триггеры запуска.

Аналогии из жизни

Ресепшен отеля с картотекой. launchd — это ресепшен, который знает про
каждый номер (службу) в отеле: должен ли он быть заселён прямо сейчас,
нужно ли впустить гостя по расписанию, нужно ли снова заселить номер, если
гость внезапно съехал раньше времени. launchctl — это то, чем вы
разговариваете с ресепшеном: «заселите номер 12», «выселите номер 12 до
следующего распоряжения». Где ломается: если вы просто попросили гостя
уйти на минутку («unload»), а не сказали ресепшену вычеркнуть номер из
плана заселений («disable»), то при следующей смене администратора
(перезагрузке) новый администратор по своей картотеке снова заселит номер —
он не знает про вашу устную договорённость, он знает только то, что
записано в картотеке.

Дирижёр и партитура. launchd — дирижёр оркестра, launchctl — то,
чем вы даёте дирижёру указания, а plist-файл — партитура для конкретного
музыканта: когда ему вступать, как часто играть, что делать, если он
сфальшивил (перезапустить). Дирижёр не импровизирует — он делает ровно то,
что написано в партитуре. Где ломается: партитуру дирижёр читает не
постоянно, а только в момент, когда музыканта «выводят на сцену»
(bootstrap/load). Если вы поправили ноты в партитуре, пока музыкант уже
играет, дирижёр не заметит правку, пока музыканта не выведут заново —
поэтому после правки plist почти всегда нужен unload + load
(или bootout + bootstrap), а не просто ожидание.

Выключатель света с памятью и без. Представьте два вида выключателей:
обычный — если вы вручную вырубите автомат в щитке, при следующем включении
электричества свет снова загорится сам, если выключатель был в положении
«вкл». А есть выключатель с блокировкой: вы можете физически заблокировать
его в положении «выкл», и тогда даже после восстановления электричества
свет не загорится, пока вы сами не снимете блокировку. unload/stop
— это первый выключатель: гасит свет сейчас, но не трогает «настройку по
умолчанию». disable — это блокировка: меняет саму настройку по умолчанию.
Где ломается: аналогия скрывает, что в реальном launchctl до Yosemite
не было явного «блокировщика» вообще — было только «загружен/не загружен»,
и это как раз одна из причин, почему путаница исторически возникла.

Как это работает

  1. Plist-файл. Служба описывается XML property list — файлом
    .plist с полями Label (уникальный идентификатор, обычно в
    обратном DNS-формате, например com.pavel.dailydigest),
    ProgramArguments (команда и аргументы для запуска), условиями
    запуска (RunAtLoad, StartInterval, StartCalendarInterval,
    KeepAlive, WatchPaths) и параметрами окружения (EnvironmentVariables,
    рабочая директория, куда писать stdout/stderr).

  2. Домены. В современном launchctl служба живёт не «вообще», а в
    конкретном домене: system (весь компьютер), user/<uid> (все сессии
    пользователя с этим UID) или gui/<uid> (текущая GUI-сессия
    пользователя). Это домен и определяет команду: sudo launchctl bootstrap system /Library/LaunchDaemons/x.plist для системного демона,
    launchctl bootstrap gui/501 ~/Library/LaunchAgents/x.plist для
    пользовательского агента (501 — обычно UID первого созданного
    пользователя на Mac).

  3. Две линии команд, которые легко перепутать:
    - bootstrap <domain> <plist> / bootout <domain> <plist> — «явить»
    службу в конкретном домене прямо сейчас / убрать её оттуда прямо
    сейчас. Работают на уровне текущей runtime-сессии.
    - enable <domain>/<label> / disable <domain>/<label> — переключают
    персистентный флаг «должна ли служба вообще запускаться» — этот
    флаг переживает перезагрузку и хранится в системном реестре
    overrides (/var/db/com.apple.xpc.launchd/disabled.plist и его
    аналоги по доменам), а не в самом plist-файле.

Именно на этой развилке вчера ошиблись: yandexcpa.worker был просто
disabled в реестре без явной причины в документации, а cian-autodobor
был disabled намеренно из-за капчи на Циане. При попытке «поднять всё,
что должно работать» оба джоба включили обратно — и только по журналу
агента нашли, что один из них выключили специально, и откатили.

  1. Устаревшие, но ещё живые load/unload делают примерно то же самое,
    что bootstrap/bootout, но исторически неявно путают понятия
    «загружен сейчас» и «должен грузиться дальше» — из-за чего Apple и ввела
    отдельные enable/disable. man launchctl прямо помечает load/
    unload как legacy.

  2. Диагностика. launchctl list показывает все загруженные в домене
    службы: PID (или -, если не запущена), последний код выхода и label.
    launchctl print gui/501/com.pavel.dailydigest выводит полное состояние
    конкретной службы: триггеры запуска, переменные окружения, путь к
    бинарнику, последнюю ошибку.

# типичная последовательность при правке своего launchd-агента
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.pavel.dailydigest.plist
# ...правим plist...
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.pavel.dailydigest.plist
launchctl enable gui/$(id -u)/com.pavel.dailydigest
launchctl list | grep dailydigest

disable ≠ unload

unload/bootout останавливает службу только на эту сессию — после перезагрузки Mac она поднимется снова, если раньше не была выключена явно через disable. Это ровно тот баг, что случился 25 августа с cian-autodobor: его выгрузили, но не задизейблили, и после ребута он ожил сам. Если процесс нужно остановить навсегда — всегда disable, а не только unload/bootout.

Где встречается в обычной жизни

Где встречается в IT и бизнесе

Кто пользуется

Технически — все пользователи macOS, даже не подозревая об этом: сама
Apple запускает через launchd сотни системных демонов на любом Mac от
загрузки до входа в систему. Среди известных продуктов, которые ставят
собственные launchd-агенты при установке: Dropbox, Docker Desktop, Adobe
Creative Cloud, Homebrew (через brew services, который сам является
обёрткой над launchctl), 1Password (фоновая синхронизация). Разработчики
инструментов вроде pm2 или forever для Node.js на macOS в проде часто в
итоге всё равно упаковывают процесс в launchd-агента для настоящей
устойчивости к перезагрузкам, а не полагаются только на них.

Альтернативы и конкуренты

Когда НЕ стоит использовать

Связанные понятия

Литература и источники

Где встретилось у меня

Вчера при проверке, какие фоновые сервисы должны работать на рабочем Mac,
Паша через Claude Code поднял два незагруженных launchd-джоба —
и один из них (cian-autodobor) оказался выключен намеренно из-за блокировки
API капчей, а не случайно. Разбор причины показал, что unload/bootout без
disable не переживает перезагрузку Mac, из-за чего процесс мог однажды
снова ожить сам. Заодно был выключен насовсем комплекс агента
«Яндекснедвиж_СРА» (worker, bot, autoplan) в связи с закрытием проекта
— с явным disable, чтобы он точно не поднялся сам.

Краткое резюме