launchctl
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
не было явного «блокировщика» вообще — было только «загружен/не загружен»,
и это как раз одна из причин, почему путаница исторически возникла.
Как это работает
-
Plist-файл. Служба описывается XML property list — файлом
.plistс полямиLabel(уникальный идентификатор, обычно в
обратном DNS-формате, напримерcom.pavel.dailydigest),
ProgramArguments(команда и аргументы для запуска), условиями
запуска (RunAtLoad,StartInterval,StartCalendarInterval,
KeepAlive,WatchPaths) и параметрами окружения (EnvironmentVariables,
рабочая директория, куда писать stdout/stderr). -
Домены. В современном
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). -
Две линии команд, которые легко перепутать:
-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 намеренно из-за капчи на Циане. При попытке «поднять всё,
что должно работать» оба джоба включили обратно — и только по журналу
агента нашли, что один из них выключили специально, и откатили.
-
Устаревшие, но ещё живые
load/unloadделают примерно то же самое,
чтоbootstrap/bootout, но исторически неявно путают понятия
«загружен сейчас» и «должен грузиться дальше» — из-за чего Apple и ввела
отдельныеenable/disable.man launchctlпрямо помечаетload/
unloadкак legacy. -
Диагностика.
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.
Где встречается в обычной жизни
- Каждый раз, когда на Mac открывается Spotlight, работает автосохранение
в Pages, синхронизируется iCloud, обновляется Handoff между устройствами
— это всё фоновые агенты и демоны под управлениемlaunchd. - Когда после установки, например, Dropbox или Docker Desktop, приложение
«само» запускается при каждом входе в систему — это установщик молча
положил plist вLaunchAgentsи сделалbootstrap. - Мигающая иконка обновления в строке меню, которая появляется по расписанию
раз в день — почти всегда отдельный launchd-агент с
StartCalendarInterval, а не часть основного приложения. - Диагностика зависшего Mac через Activity Monitor: часть процессов с
«системными» именами вродеcom.apple.something— это как раз демоны
и агентыlaunchd, у каждого есть свой plist. - Автоматическое резервное копирование Time Machine по расписанию — тоже
реализовано как launchd-демон, а не как отдельный вечно работающий
процесс.
Где встречается в IT и бизнесе
- Личные и рабочие автоматизации на Mac: как в daily-digest — генерация
контента по расписанию, отправка отчётов, синхронизация файлов. Нужен,
когда задачу нельзя доверить облачному cron (например, она должна
выполняться именно на конкретной локальной машине — из-за доступа к
файлам, локальным моделям или специфичному железу). - CI/CD и build-агенты на macOS-раннерах: когда нужно, чтобы
сборочный агент (например, самостоятельно хостед GitHub Actions runner
или Jenkins agent) переживал перезагрузки и падения — его регистрируют
как launchd-демон, а не запускают вручную в терминале. - Локальные API/боты для внутренних инструментов: как у Паши —
yandexcpa.worker,smartis-tunnel,trener.bot_listener— постоянно
тикающие процессы, которым нужен супервизор с автоперезапуском. - Диагностика инцидентов: «почему процесс, который мы вчера выключили,
снова работает» — классический вопрос девопсу на macOS-инфраструктуре,
и ответ почти всегда в разницеunloadvsdisable. - MDM и корпоративное управление парком Mac: системные администраторы
разворачивают демоны черезlaunchdдля мониторинга, инвентаризации и
принудительных политик безопасности на сотнях корпоративных Mac.
Кто пользуется
Технически — все пользователи macOS, даже не подозревая об этом: сама
Apple запускает через launchd сотни системных демонов на любом Mac от
загрузки до входа в систему. Среди известных продуктов, которые ставят
собственные launchd-агенты при установке: Dropbox, Docker Desktop, Adobe
Creative Cloud, Homebrew (через brew services, который сам является
обёрткой над launchctl), 1Password (фоновая синхронизация). Разработчики
инструментов вроде pm2 или forever для Node.js на macOS в проде часто в
итоге всё равно упаковывают процесс в launchd-агента для настоящей
устойчивости к перезагрузкам, а не полагаются только на них.
Альтернативы и конкуренты
- cron — плюс: простой синтаксис расписания, есть почти везде в Unix.
Минус: не следит за живостью процесса, не умеет KeepAlive/перезапуск
при падении, нет GUI-доменов. - systemd (Linux) — плюс: богатая модель зависимостей между службами,
cgroups из коробки, единый стандарт почти для всех дистрибутивов Linux.
Минус: недоступен на macOS в принципе — это разные ОС и разные
архитектуры ядра. - supervisord / pm2 — плюс: кроссплатформенность, удобнее для
разработчиков веб-приложений, не требует знания plist/XML. Минус: это
ещё один процесс поверх системного, у которого свои точки отказа — если
сам supervisord не переживёт перезагрузку, ничего не поднимется. - screen/tmux с ручным запуском — плюс: нулевой порог входа, ничего
настраивать не нужно. Минус: не переживает перезагрузку и выход из
системы вообще, нет автоперезапуска при падении — то самое, что
подозревал Паша про@планировщики@футбол, у которых plist не
нашлось.
Когда НЕ стоит использовать
- Для задач, которые должны выполняться в облаке независимо от того,
включён ли конкретный Mac — потому чтоlaunchd-агент работает только
пока запущена и разблокирована именно эта машина; для таких задач нужен
облачный планировщик (cron-задача на VPS, Cloud Scheduler и т. п.). - Для очень коротких one-off скриптов, которые нужно запустить один раз
прямо сейчас — потому что накладные расходы на написание, размещение и
регистрацию plist не окупаются для разовой задачи; проще просто
выполнить команду в терминале. - Для процессов с сложными зависимостями запуска друг от друга (сначала
база данных, потом кэш, потом веб-сервер, с ожиданием готовности
каждого) — потому что уlaunchdнет встроенной модели графа
зависимостей, как уsystemd; такие сценарии обычно решают
оркестратором уровня Docker Compose или отдельным скриптом-обвязкой.
Связанные понятия
- launchd — сам менеджер служб, супервизор процессов в macOS, для
которогоlaunchctlслужит интерфейсом командной строки. - plist (property list) — формат XML-конфигурации macOS, которым
описывается служба дляlaunchd(и не только — вообще стандартный
формат настроек в системе). - systemd — аналог
launchdдля большинства дистрибутивов Linux,
с более сложной моделью зависимостей. - daemon — фоновый процесс без пользовательского интерфейса,
работающий независимо от того, вошёл ли кто-то в систему. - cron — более старый и простой планировщик задач по расписанию в
Unix-подобных системах, не следит за живостью процесса. - PID (process identifier) — числовой идентификатор запущенного
процесса в операционной системе; именно PID показываетlaunchctl list, чтобы понять, жива ли служба прямо сейчас.
Литература и источники
- Официальная документация:
man launchd.plistиman launchctl—
исчерпывающий и точный источник по всем ключам plist и подкомандам,
доступен прямо в терминале любого Mac. - Apple Developer Documentation, раздел «Creating Launch Daemons and
Agents» — официальное руководство Apple по написанию и регистрации
launchd-служб: developer.apple.com. - Wikipedia (en): статья «launchd» — история создания, сравнение с
init/systemd/upstart: en.wikipedia.org/wiki/Launchd. - Классическая статья Apple от команды CoreOS (2005) «Adopting launchd»
— искать в Google по названию, оригинал сохранён в архивах developer
docs Apple. - Книга Amit Singh, «Mac OS X Internals: A Systems Approach» (2006, en)
— глава про запуск системы и историю до-launchd механизмов, полезна
для контекста, почему launchd вообще понадобился. - launchd.info (неофициальный, но подробный community-справочник по
примерам plist и частым ошибкам) — искать в Google по «launchd.info
plist examples».
Где встретилось у меня
Вчера при проверке, какие фоновые сервисы должны работать на рабочем Mac,
Паша через Claude Code поднял два незагруженных launchd-джоба —
и один из них (cian-autodobor) оказался выключен намеренно из-за блокировки
API капчей, а не случайно. Разбор причины показал, что unload/bootout без
disable не переживает перезагрузку Mac, из-за чего процесс мог однажды
снова ожить сам. Заодно был выключен насовсем комплекс агента
«Яндекснедвиж_СРА» (worker, bot, autoplan) в связи с закрытием проекта
— с явным disable, чтобы он точно не поднялся сам.
Краткое резюме
launchctl— консольный интерфейс кlaunchd, системному супервизору
процессов в macOS; самlaunchd— это process 1, аналогinit/systemd.- Службы делятся на демоны (
/Library/LaunchDaemons, от root, без GUI) и
агенты (~/Library/LaunchAgents, от пользователя, с доступом к GUI). - Главная ловушка:
unload/bootoutостанавливает службу только сейчас,
disable— навсегда, вплоть до следующей перезагрузки; путать их —
типичная причина «процесс сам ожил после ребута». - Современный набор команд (
bootstrap/bootout/enable/disable,
с Yosemite 2014 года) точнее старого (load/unload), которое всё ещё
работает, но считается legacy. - Для диагностики:
launchctl list— что сейчас загружено и с каким PID,
launchctl print <domain>/<label>— полное состояние конкретной службы.