Автоматизація в macOS з допомогою Python і launchctl

avtomatizaciya v macos s pomoshhyu python i launchctl53 Автоматизація в 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 (обидві стоять по десять доларів). Вони злегка полегшують справу, але можна обійтися і без них.

    Простий конфіг: запуск по часу

    Почнемо з самого легкого — запуску чого-небудь в заданий час. Ось як виглядає найпростіший варіант конфига.

    123456789101112131415161718 <?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>назва</string> <key>ProgramArguments</key> <array> <string>шлях до файлу</string> </array> <key>StartCalendarInterval</key> <dict> <key>Minute</key><integer>30</integer> <key>Hour</key><integer>1</integer> <key>Day</key><integer>6</integer> </dict></dict></plist>

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

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

    В моєму прикладі виставлено час 1:30 кожну суботу. Якщо ви знесе ключ Day, скрипт почне запускатися в половині другого кожну ніч, а якщо приберете і Hour, то кожні півгодини. Думаю, ви зрозуміли як це працює. Аналогічна запис у crontab виглядала б як

    1 0 30 1 * 6 <шлях до файлу>

    Якщо команда, яку ви запускаєте, приймає аргументи, то їх потрібно перелічити після шляхи, додавши додаткові поля <string>. Наприклад:

    12345678910 <key>ProgramArguments</key><array><string>say</string><string>В Петропавловську-Камчатському полночь</string></array><key>StartCalendarInterval</key><dict><key>Minute</key><integer>0</integer><key>Hour</key><integer>15</integer></dict>

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

    Тонкощі активації

    На жаль, зворотна сторона гнучкості — це развесистость налаштувань. Навіть включати і вимикати конфіги launchd можна кількома способами. Ось старий і найбільш простий. Для завантаження пиши:

    1 $ launchctl load -w ~/Library/LaunchAgents/<конфіг.plist>

    І для вивантаження:

    1 $ launchctl unload -w ~/Library/LaunchAgents/<конфіг.plist>

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

    Після того як щось міняєте в конфіги, його треба вивантажувати і завантажувати заново.

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

    1 $ launchctl bootstrap gui/<твій UID> <шлях до файлу>

    І для вивантаження:

    1 $ launchctl bootout gui/<твій UID> <шлях до файлу>

    Дізнатися свій UID можеш командою id -u. Перший користувач комп’ютера зазвичай записаний під номером 502.

    Інша команда, яку добре пам’ятати, — це list. Щоб перевірити, які з твоїх конфіги завантажені, ви можете написати:

    1 $ launchctl list | grep <назва>

    avtomatizaciya v macos s pomoshhyu python i launchctl54 Автоматизація в macOS з допомогою Python і launchctl

    Знову ж таки — існує більш сучасний, більш просунутий і, звичайно, більш замороченный метод:

    1 $ launchctl print <домен>/<UID>

    На виході буде куди більше інформації, ніж при запиті списку. Але знову ж таки, використовувати print зовсім не обов’язково. У відповідь на питання про те, коли застарілі команди перестануть працювати, хтось з розробників відповів на форумі, що на старий синтаксис занадто багато зав’язано, щоб прибирати його.

    Параметри завантажування серіалів

    Як бачите, поки що все досить просто, особливо якщо ігнорувати нововведення. Щоб порадувати вас більш корисним прикладом, розповім, як я за допомогою launchd налаштував собі скачування нових серій серіалів.

    За основу я взяв фід з showRSS — цей сервіс дозволяє зареєструватися і зробити персоналізований фід з магнітними посиланнями на нові серії серіалів, які ви дивіться.

    avtomatizaciya v macos s pomoshhyu python i launchctl55 Автоматизація в macOS з допомогою Python і launchctlНа showRSS є канали для більшості актуальних серіалів

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

    avtomatizaciya v macos s pomoshhyu python i launchctl56 Автоматизація в macOS з допомогою Python і launchctl

    Щоб нові серії автоматично додавалися в Transmission, я написав невеликий скрипт і налаштував йому автозапуск раз на годину за допомогою launchctl. Зверніть увагу, що у налаштуваннях Transmission потрібно буде включити RPC (Enable remote access).

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

    showrss-launchd.py

    123456789101112131415161718192021222324252627282930313233343536373839404142434445 import urllib2import sysfrom time import sleepimport transmissionrpcimport subprocessimport bs4import logging user = ‘<твій номер>’logfile = ‘/Users/<ім’я користувача>/Library/Logs/showrss.log’ search = ‘http://showrss.info/user/’ + user + ‘.rss?magnets=true&namespaces=true&name=clean&quality=null&re=null’lastid = subprocess.check_output(‘defaults read my lastshow’, shell=True)[:-1] logging.basicConfig(filename=logfile,level=logging.DEBUG,format=’%(asctime)s %(message)s’)logging.info(‘Starting’) try: page = urllib2.urlopen(search).read()except: logging.exception(“can’t get page”) sys.exit() soup = bs4.BeautifulSoup(page, “html.parser”)items = soup.findAll(‘item’) number = 0for item in items: if item.guid.string == lastid: break number += 1 logging.info(‘Found new episodes:’ + str(number)) if number > 0: subprocess.call([‘open’, ‘-jg’, ‘/Applications/Transmission.app’]) sleep(5) tc = transmissionrpc.Client(‘localhost’, port=9091) for x in range(number): applescript = ‘display notification “{}” with title “Downloading”‘.format(items[x].title.contents[0]) subprocess.call([‘osascript’, ‘e’, applescript]) tc.add_torrent(items[x].enclosure[‘url’]) subprocess.call([‘defaults’, ‘write’, ‘my’, ‘lastshow’, items[0].guid.string])

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

    Чисто маковських особливостей тут дві. Перша — збереження і завантаження GUID останньої серії за допомогою команди defaults. У цьому немає необхідності: цілком можна зберігати цю змінну в звичайному текстовому файлі і вказати в конфіги launchctl каталог, в якому він лежить ключ WorkingDirectory). Але мені хотілося продемонструвати defaults як одну з цікавих можливостей.

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

    1 $ defaults write my lastshow <guid>

    Друга використана мною маковська фішка — це оповіщення. Щоб показати повідомлення при початку завантаження, я виконую однострочник на AppleScript (display notification … with title …). Якщо будете переносити скрипт в іншу ОС, просто видаліть рядок 41.

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

    com.and.showrss.StartInterval.plist

    1234567891011121314 <?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.and.showrss</string> <key>ProgramArguments</key> <array> <string>/Users/and/Develop/ShowRSS/showrss-launchd.py</string> </array> <key>StartInterval</key> <integer>3600</integer></dict></plist>

    Тут вам знадобиться замінити шлях до скрипта своїм і за бажанням поміняти назву файлу, мітку та інтервал запуску.

    На жаль, поєднати StartInterval і StartCalendarInterval, щоб скрипт працював через рівні проміжки часу з якогось за якусь годину або день, неможливо. У таких випадках треба або писати обгортку, яка буде активувати і деактивувати запуск по StartInterval, і саму її запускати по StartCalendarInterval, або генерувати величезний конфіг із зазначенням кожного запуску.

    Ще, як бачите, я заморочился з правильним логированием за допомогою модуля logging — щоб потім було зручно дивитися, що сталося, через утиліту Console. Але якщо ви ставите на автозапуск програму, яка виводить дані в stdout, то launchd може для вас їх відловлювати і складати в текстовий файл. Для цього додайте в конфіг рядки:

    12345 <key>StandardOutPath</key><string>шлях до файлу</string> <key>StandardErrorPath</key><string>шлях до файлу</string>

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

    Створюємо свій «дропбокс»

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

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

    Якщо вас таке рішення не влаштовує, можете, наприклад, заливати файли на власний FTP. Це не тільки буде надійніше, але і дозволить позбутися сторінки з попереднім переглядом картинки, яку в обов’язковому порядку показує Transfer.sh. Я, до речі, в підсумку зробив собі дві папки: одна — для свого хостингу, одна — для Transfer.sh.

    uploader-transfersh.py

    12345678910111213141516171819202122232425262728293031323334353637383940414243 import osimport loggingimport subprocessimport jsonfrom urllib import quote path = ‘<тека>’logfile = ‘/Users/<ім’я>/Library/Logs/uploader-transfersh.log’ def notify(title, message): applescript = ‘display notification “{}” with title “{}”‘.format(message, title) subprocess.call([‘osascript’, ‘e’, applescript]) logging.basicConfig(filename=logfile,level=logging.DEBUG,format=’%(asctime)s %(message)s’)logging.info(‘Starting at’ + os.getcwd()) if not os.path.exists(‘list.txt’): open(‘list.txt’, ‘w+’) oldlist = [‘.DS_Store’]else: oldlist = json.loads(open(‘list.txt’, ‘r’).read()) newlist = os.listdir(path)files = list(set(newlist) – set(oldlist))logging.info(‘Found new files:’ + ‘ ‘.join(files)) urls = []filename for in files: try: url = subprocess.check_output([‘curl’, ‘–upload-file’, path + filename, ‘https://transfer.sh/’ + quote(filename)]) urls.append(url) logging.info(‘Завантаження’ + filename) except: notify(‘Error uploading’ + filename, ‘See’ + logfile) logging.exception(“can’t upload” + filename) if len(files) > 0: applescript = ‘set to the clipboard “{}”‘.format(‘n’.join(urls)) subprocess.call(‘osascript’, ‘e’, applescript) logging.info(‘ ‘.join(urls)) open(‘list.txt’, ‘w+’).write(json.dumps(newlist)) notify(‘Upload complete’, ‘ ‘.join(files))

    Жодних нестандартних модулів на цей раз не знадобиться, пропишіть тільки правильні шляхи в самому початку. Скрипт читає файл з попереднім станом вказаної папки (або створює його, якщо запускається вперше), звіряє з поточними і, якщо знаходить нові файли, почергово заливає їх на Transfer.sh, зберігаючи посилання, які він видає у відповідь.

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

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

    com.and.transfersh.WatchPaths.plist

    123456789101112131415161718 <?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.and.transfersh</string> <key>ProgramArguments</key> <array> <string>/Users/and/Develop/Uploader/uploader-transfersh.py</string> </array> <key>WatchPaths</key> <array> <string>/Users/and/Desktop/Transfer.sh/</string> </array> <key>WorkingDirectory</key> <string>/Users/and/Develop/Uploader/</string></dict></plist>

    Зверніть увагу, що крім шляхи для запуску самого скрипта його шлях ще раз вказаний як WorkingDirectory. На цей раз ми зберігаємо дані в текстовий файл, тому заодно демонструю, як задати папку, яку скрипт буде вважати поточної, — в ній лежатиме «відвантаження».

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

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

    Також відсутня підтримка папок, але її при бажанні нескладно реалізувати.

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

    1234567891011121314 for i, filename in enumerate(files): if filename.find(‘Screen Shot’) > -1 and os.stat(path + filename).st_size > 500000: width = int(subprocess.check_output([‘mdls’, ‘-name’, ‘kMDItemPixelWidth’, path + filename]).split(‘= ‘)[1][:-1]) if width > 2000: subprocess.call([‘sips’, ‘Z’, str(width/2), path + filename]) if subprocess.check_output([‘mdls’, ‘-name’, ‘kMDItemKind’, path + filename]).find(‘JPEG’) is -1: fileout = filename.split(‘.’)[0] + ‘.jpg’ subprocess.call([‘sips’, ‘-s’, ‘format’, ‘jpeg’, path + filename, ‘–out’, path + fileout]) os.remove(path + filename) filename = fileout files[i] = filename

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

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

    Бекап і шифрування даних при підключенні флешки

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

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

    avtomatizaciya v macos s pomoshhyu python i launchctl57 Автоматизація в macOS з допомогою Python і launchctl

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

    backup.sh

    123456 if [ -d /Volumes/BACKUP/ ]then hdiutil attach /Volumes/BACKUP/Stuff.dmg cp -fr /Users/and/Desktop/TopSecret /Volumes/Stuff/ osascript -e ‘Display notification with title “Backup complete”‘fi

    Шляхи, звичайно, треба буде поправити на свої. Зверніть увагу на умову в самому початку: воно потрібно, тому що launchd сам не буде перевіряти, який саме носій підключений, — він запускає скрипт при підключенні будь-якого тома. Відповідно, перевіряти, чи є потрібний шлях, доводиться вже в скрипті.

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

    com.and.backup.StartOnMount.plist

    1234567891011121314 <?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.and.backup</string> <key>ProgramArguments</key> <array> <string>/Users/and/Develop/backup.sh</string> </array> <key>StartOnMount</key> <true ” /></dict></plist>

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

    Інші можливості

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

    EnvironmentVariables дозволяє задати змінні оточення спеціально для вашого скрипта. Виглядає це так:

    12345 <key>EnvironmentVariables</key><dict> <key>PATH</key> <string>/bin:/usr/bin:/usr/local/bin</string></dict>

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

    Ключ KeepAlive вкаже launchd на те, що скрипт повинен працювати весь час. Якщо він, наприклад, впаде, система буде намагатися підняти його знову.

    12 <key>KeepAlive</key><true/>

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

    12345 <key>KeepAlive</key><dict> <key>Crashed</key> <true ” /></dict>

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

    Ну і щоб бідовий скрипт не зжер всі ресурси машини в самий невідповідний момент, можна задати різні обмеження:

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

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

    Висновок

    Як ви могли помітити, у launchd є і недоліки. Той же crontab — це проста таблиця, в якій наочно видно, що і коли запускається. З launchctl в цьому плані все непросто: конфіги розкидані по різних папках, можуть бути включені або виключені, так і всередині них все зрозуміло далеко не з першого погляду.

    Якщо нічого, крім запуску часу, вам не потрібно, то launchd не варто мороки — ви можете спокійно використовувати cron (цікаво, що навіть він у macOS працює поверх launchd). Що до можливості прив’язувати скрипти до папок, то вона доступна в Automator — знову ж відповідні конфіги просто створяться автоматично.

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

    Посилання

    • Офіційна документація
    • Сайт розробників LaunchControl з докладною документацією та прикладами
    • Пост, в якому детально розбирається новий синтаксис launchctl
    Сподобалася стаття? Поділитися з друзями:
    Всезнайко - Корисні поради