Автоматизация в macOS с помощью Python и launchctl

Автоматизация в macOS Python launchctl

Доброго времени суток! Сегодня мы поговорим об автоматизации в macOS, а именно про утилиту Launchctl, с которой наверняка знакомы все опытные пользователи macOS, но далеко не все пользуются ее полезным функционалом. В этой статье я расскажу о настройке Launchctl и о том, как ее можно использовать на практике.

Благодаря гибкости настроек launchd, этот сервис заменил в macOS целый список более старых систем, которые пришли из Unix. Он управляет процессом загрузки ОС и сервисов (вместо init), он реагирует на подключения по сети (вместо inetd), он же запускает скрипты по времени (вместо cron) и при разных условиях. В этой статье я воспользуюсь этими богатыми возможностями для настройки всякой автоматизации: запуска скриптов в определенное время, запуск при помещении файла в папку, при изменении файла и при подключении флешки или какого-нибудь другого внешнего накопителя.

Рекомендуем:

Я буду писать именно про launchctl, поскольку работаю в macOS, но если вы предпочитаете Linux, то можете позаимствовать идеи и скрипты, которые мы будем писать, и проделать все то же самое при помощи systemd. Эта система похожа на launchd и есть во многих современных дистрибутивах. Однако ее настройки в корне отличаются, и параллельно разбирать еще и их я не возьмусь.

Демоны и агенты

Файлы с правилами — это XML с расширением .plist. Внутри есть инструкции, которые указывают launchd, что и когда запускать. Эти файлы находятся в системе в 5 папках:

  • ~/Library/LaunchAgents — агенты текущего пользователя;
  • /Library/LaunchAgents — агенты для всех пользователей;
  • /Library/LaunchDaemons — демоны для всех пользователей;
  • /System/Library/LaunchAgents — системные агенты (входят в состав macOS);
  • /System/Library/LaunchDaemons— системные демоны.

Отличие демонов от агентов довольно тонкое: демоны — это процессы, которые запускаются сразу после загрузки компа, а агенты могут работать только после входа в систему (соответственно, демонов для конкретного пользователя не бывает). Кроме того демоны после активирования работают непрерывно, а агенты обычно срабатывают при определенных условиях.

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

Для создания конфигурационных файлов launchd существует две графические оболочки — LaunchControl и Lingon (обе стоят по десять долларов). Они слегка облегчают дело, но можно обойтись и без них.

Простой конфиг: запуск по времени

Начнем с самого легкого — запуска чего-нибудь в заданное время. Вот как выглядит самый простой вариант конфига.

Несмотря на развесистый вид, структура здесь довольно простая. Внутри основного словаря (<dict>) идут ключи и следом — параметры к ним. Иногда это строки, иногда массивы, иногда вложенные словари.

Заменяйте слово «название» на какое-нибудь название (обычно «com.домен.имя» — я, например, назвал тестовый агент com.and.launchtest), укажите путь к исполняемому файлу в качестве первого параметра ProgramArguments, а потом задайте, во сколько и по каким дням недели запускать.

В моем примере выставлено время 1:30 каждую субботу. Если вы снесете ключ Day, скрипт начнет запускаться в половине второго каждую ночь, а если уберете и Hour, то каждые полчаса. Думаю, вы поняли как это работает. Аналогичная запись в crontab выглядела бы как

Если команда, которую вы запускаете, принимает аргументы, то их нужно перечислить после пути, добавив дополнительные поля <string>. Например:

Когда все будет готово, сохраняем файл в ~/Library/LaunchAgents/. Было бы правильнее сразу прописать в названии условия запуска, чтобы потом было проще ориентироваться. В моем тестовом конфиге я сохранил как com.and.launchtest.StartInterval.plist.

Тонкости активации

К сожалению, обратная сторона гибкости — это развесистость настроек. Даже включать и выключать конфиги launchd можно несколькими способами. Вот старый и наиболее простой. Для загрузки пиши:

И для выгрузки:

Ключ -w заодно включает флаг enabled, что экономит нам один шаг (launchctl enable) и сразу активирует конфиг. Помни, что после загрузки компьютера и входа в систему все агенты, лежащие в соответствующих папках, будут загружены автоматически. Именно поэтому при выгрузке удобно тоже добавлять -w — тогда launchctl запомнит, что конфиг неактивен.

После того как что-то меняете в конфиге, его нужно выгружать и загружать заново.

Можешь спокойно пользоваться этими командами, однако если откроешь man, то узнаешь, что они считаются устаревшими и поддерживаются лишь для совместимости. Более правильный способ — использовать команды bootstrap и bootout. Они требуют указывать, помимо пути к файлу конфигурации, domain-target, который состоит из домена и UID пользователя. Целиком команды будут выглядеть вот так:

И для выгрузки:

Узнать свой UID можешь командой id -u. Первый пользователь компьютера обычно записан под номером 502.

Другая команда, которую хорошо помнить, — это list. Чтобы проверить, какие из твоих конфигов загружены, можете написать:

macos launchctl

Опять же — существует более современный, более продвинутый и, конечно, более замороченный метод:

На выходе будет куда больше информации, чем при запросе списка. Но опять же, использовать print совершенно не обязательно. В ответ на вопрос о том, когда устаревшие команды перестанут работать, кто-то из разработчиков ответил на форуме, что на старый синтаксис слишком много завязано, чтобы убирать его.

Настройка скачивания сериалов

Как видите, пока что все довольно просто, особенно если игнорировать нововведения. Чтобы порадовать вас более полезным примером, расскажу, как я при помощи launchd настроил себе скачивание новых серий сериалов.

За основу я взял фид с showRSS — этот сервис позволяет зарегистрироваться и сделать персонализированный фид с магнитными ссылками на новые серии сериалов, которые вы смотрите.

python launchctl
На showRSS есть фиды для большинства актуальных сериалов

Всё исключительно на английском, но это именно то, что мне нужно. Если вам showRSS тоже придется по нраву, не забудь после составления списка сериалов настроить качество, а то будете получать все несколько раз в разном разрешении.

Чтобы новые серии автоматически добавлялись в Transmission, я написал небольшой скрипт и настроил ему автозапуск раз в час при помощи launchctl. Обратите внимание, что в настройках Transmission нужно будет включить RPC (Enable remote access).

Чтобы не светить интерфейс наружу, там же можно ограничить доступ и разрешить подключаться только с локальной машины (см. скриншот). Далее — полный исходник скрипта. Вам остается только поставить зависимости (pip install bs4 transmissionrpc), добавить свой номер пользователя с showRSS и указать папку для сохранения лога.

showrss-launchd.py

Код по большей части не требует пояснений: скрипт скачивает файл RSS, парсит его при помощи Beautiful Soup, ищет в полученном массиве ссылок последнюю скачанную серию по GUID и затем по одной засовывает более новые в Transmission (предварительно запустив его и дав пять секунд на загрузку).

Чисто маковских особенностей здесь две. Первая — сохранение и загрузка GUID последней серии при помощи команды defaults. Необходимости в этом нет: вполне можно хранить эту переменную в обычном текстовом файле и указать в конфиге launchctl каталог, в котором он лежит (ключ WorkingDirectory). Но мне хотелось продемонстрировать defaults как одну из интересных возможностей.

Для работы скрипта перед первым запуском нужно записать GUID серии, с которой начнется скачивание (сама она скачана не будет):

Вторая использованная мной маковская фишка — это оповещения. Чтобы показать сообщение при начале загрузки, я выполняю однострочник на AppleScript (display notification … with title …). Если будете переносить скрипт в другую ОС, просто удалите строку 41.

Осталось только добавить конфиг launchd. На этот раз будем запускать не по календарному интервалу, а просто каждый час. Для этого нам понадобится ключ StartInterval.

com.and.showrss.StartInterval.plist

Здесь вам понадобится заменить путь к скрипту своим и по желанию поменять название файла, метку и интервал запуска.

К сожалению, совместить StartInterval и StartCalendarInterval, чтобы скрипт работал через равные промежутки времени с какого-то по какой-то час или день, невозможно. В таких случаях надо либо писать обертку, которая будет активировать и деактивировать запуск по StartInterval, и саму ее запускать по StartCalendarInterval, либо генерировать огромный конфиг с указанием каждого запуска.

Еще, как видите, я заморочился с правильным логированием при помощи модуля logging — чтобы потом было удобно смотреть, что произошло, через утилиту Console. Но если вы ставите на автозапуск программу, которая выводит данные в stdout, то launchd может для вас их отлавливать и складывать в текстовый файл. Для этого добавьте в конфиг строчки:

Точно так же можно прописать StandardInPath с файлом, который будет скармливаться на вход скрипту.

Создаем свой «дропбокс»

Переходим к задаче номер два: научимся запускать скрипты при появлении новых файлов в определенной папке. Заодно смастерим себе простенький аналог Dropbox. Точнее — аналог ранних версий Dropbox, которые сразу же копировали в буфер обмена ссылку на скачивание файла, когда кидаешь его в заданную папку.

В качестве хостинга я выбрал Transfer.sh. Он бесплатен, не показывает рекламы и позволяет загрузить файл одной командой из терминала. При желании можно даже поставить его на свой сервер и подключить бакет S3.

Если вас такое решение не устраивает, можете, например, заливать файлы на собственный FTP. Это не только будет надежнее, но и позволит избавиться от страницы с предпросмотром картинки, которую в обязательном порядке показывает Transfer.sh. Я, кстати, в итоге сделал себе две папки: одна — для своего хостинга, одна — для Transfer.sh.

uploader-transfersh.py

Никаких нестандартных модулей на этот раз не понадобится, пропишите только правильные пути в самом начале. Скрипт читает файл с предыдущим состоянием указанной папки (или создает его, если запускается впервые), сверяет с текущим и, если находит новые файлы, поочередно заливает их на Transfer.sh, сохраняя ссылки, которые тот выдает в ответ.

Список ссылок затем копируется в буфер обмена при помощи однострочника на AppleScript (я хотел использовать для этих целей команду pbcopy, но что-то не срослось: при вызове через launchd буфер по невыясненным причинам очищался). Под конец новый листинг каталога сериализуется и сохраняется в текстовый файл, а затем отображается оповещение — как и в предыдущем примере.

Теперь самое главное — конфиг для автоматического запуска.

com.and.transfersh.WatchPaths.plist

Обратите внимание, что помимо пути для запуска самого скрипта его путь еще раз указан как WorkingDirectory. На этот раз мы сохраняем данные в текстовый файл, поэтому заодно демонстрирую, как задать папку, которую скрипт будет считать текущей, — в ней будет лежать «отгрузка».

Ключ WatchPaths можно использовать и для слежения за изменением файлов. Синтаксис в этом случае такой же.

Конечно, у нас вышел не полный аналог Dropbox. В частности, файлы не удаляются с сервера, когда стираешь их локально (впрочем, Transfer.sh все равно потрет их через месяц).

Также отсутствует поддержка папок, но ее при желании несложно реализовать.

Вообще, огромный плюс этого метода в том, что при желании его можно доработать по собственному усмотрению. Например, я захотел сделать так, чтобы, когда кладешь картинку со словами Screen Shot в названии файла, скрипт бы ее уменьшал и конвертировал в JPEG. Получившийся код привожу ниже — можете подпилить в соответствии со своими нуждами и вставить в районе строки 28.

Скрины размером более 500 Кбайт будут конвертированы в JPG, а если они шире 2000 пикселей (то есть полный экран в моем случае), то скрипт еще и уменьшит их вдвое.

Что еще можно улучшить? К примеру, можете сделать так, чтобы папка очищалась по мере загрузки поступающих файлов. Для этого существует специальная директива launchd: если заменить в конфиге WatchPaths на QueueDirectories, то скрипт будет вызываться до тех пор, пока указанный каталог (или каталоги) не станут пустыми. В этом случае нужно будет самостоятельно позаботиться об удалении файлов, иначе launchd будет гонять скрипт по кругу.

Бэкап и шифрование данных при подключении флешки

В качестве третьей задачи я планировал сделать скрипт, который будет автоматически запускаться, когда вы подключаете флешку или жесткий диск с определенным названием, затем монтировать зашифрованную файловую систему (предварительно спросив пароль) и скидывать в нее определенные файлы. Другими словами, импровизированное, но надежное хранилище для важных данных.

Я не подозревал, что задача окажется настолько тривиальной, что разбирать будет почти что нечего. Шифрованные контейнеры в macOS создаются штатными средствами. Запускайте DiskUtility, жмите File → New Image → Blank Image, заполняйте название файла, название тома и выбирайте тип шифрования (AES 128 или 256 бит). Контейнер готов! При его монтировании macOS будет спрашивать пароль.

автоматизация macos

Осталось написать скриптик, который будет подключать контейнер одновременно с появлением флешки или жесткого диска и автоматически копировать данные. Python на этот раз точно не понадобится — хватит Bash. У меня вышло вот так.

backup.sh

Пути, конечно, нужно будет поправить на свои. Обратите внимание на условие в самом начале: оно нужно, потому что launchd сам не будет проверять, какой именно носитель подключен, — он запускает скрипт при подключении любого тома. Соответственно, проверять, существует ли нужный путь, приходится уже в скрипте.

А вот как будет выглядеть конфиг launchd.

com.and.backup.StartOnMount.plist

Можете добавить удаление данных с диска или, наоборот, использовать rsync, чтобы поддерживать содержимое папки и контейнера одинаковым.

Рекомендуем:

Другие возможности

У launchd есть и другие интересные функции и настройки. По большей части они нужны для внутрисистемных нужд, но кто знает, на какие технические извраты нас потянет завтра?

EnvironmentVariables позволяет задать переменные окружения специально для вашего скрипта. Выглядит это так:

При желании можно сделать полноценный chroot, задав ключ RootDirectory, или заставить задачу выполняться от имени определенного пользователя или группы (UserName и GroupName).

Ключ KeepAlive укажет launchd на то, что скрипт должен работать все время. Если он, например, упадет, система будет пытаться поднять его снова.

Добавив к KeepAlive ключи SuccessfulExit и Crashed, можно задать перезапуск только в том случае, если задача завершилась успешно или только если упала с ошибкой (то есть вернула ненулевой код).

Точно так же можно поддерживать работу задачи, только если доступна хотя бы одна сеть (или не доступна ни одна) — за это отвечает ключ NetworkState — или когда существует или не существует определенный путь (PathState). Можно даже прицепиться к состоянию другой задачи (OtherJobEnabled).

Ну и чтобы бедовый скрипт не сожрал все ресурсы машины в самый неподходящий момент, можно задать разные ограничения:

  • CPU — количество процессорного времени в секундах, которые разрешено потратить;
  • FileSize — максимальный размер файла, который разрешено создавать;
  • NumberOfFiles — максимальное число одновременно открытых файлов;
  • Data — максимальное количество данных (в байтах), которые разрешено обрабатывать.

Ну и для совсем сложных случаев есть двухступенчатая система из мягкого ограничения (SoftResourceLimit) и жесткого ограничения (HardResourceLimit). При превышении первого система вышлет процессу сигнал вроде SIGXCPU или SIGXFSZ, а принудительно завершит его, только если тот не успокоится и перейдет жесткий лимит.

Заключение

Как вы могли заметить, у launchd есть и недостатки. Тот же crontab — это простая таблица, в которой наглядно видно, что и когда запускается. С launchctl в этом плане все непросто: конфиги разбросаны по разным папкам, могут быть включены или выключены, да и внутри них все понятно далеко не с первого взгляда.

Если ничего, кроме запуска по времени, вам в итоге не нужно, то launchd не стоит мороки — вы можешь спокойно использовать cron (занятно, что даже он в macOS работает поверх launchd). Что до возможности привязывать скрипты к папкам, то она доступна в Automator — опять же соответствующие конфиги просто создадутся автоматически.

Однако если вы предпочитаете освоить один мощный и универсальный инструмент, то выбор определенно за launchd. Даже если скриптование всего и вся вам в итоге не пригодится, изучить принципы работы системы будет небесполезно.

Ссылки
  • Официальная документация
  • Сайт разработчиков LaunchControl с подробной документацией и примерами
  • Пост, в котором подробно разбирается новый синтаксис launchctl

Понравилась статья? Поделиться с друзьями:
Добавить комментарий