Moment bun al zilei! Astăzi vom vorbi despre automatizarea în macOS, și anume utilitarul Launchctl, care este probabil familiar tuturor utilizatorilor experimentați de macOS, dar nu toată lumea își folosește funcționalitatea utilă. În acest articol, voi vorbi despre configurarea Launchctl și despre cum poate fi folosit în practică.
Datorită flexibilității setărilor lansate, acest serviciu a înlocuit o listă întreagă de sisteme vechi care proveneau de la Unix în macOS. Gestionează procesul de încărcare a sistemului de operare și a serviciilor (în loc de init), răspunde la conexiunile de rețea (în loc de inetd) și, de asemenea, rulează scripturi în timp (în loc de cron) și în diferite condiții. În acest articol, voi folosi aceste posibilități bogate pentru a configura orice automatizare: lansarea de scripturi la un anumit moment, lansarea când un fișier este plasat într-un folder, când un fișier este schimbat și când o unitate flash sau orice alt dispozitiv de stocare extern este conectat.
Voi scrie în special despre launchctl, deoarece lucrez la macOS, dar dacă preferați Linux, puteți împrumuta ideile și scripturile pe care le vom scrie și să faceți același lucru cu systemd. Acest sistem este similar cu launchd și este prezent în multe distribuții moderne. Cu toate acestea, setările sale sunt fundamental diferite și nu voi încerca să le analizez în paralel.
Conţinut
Demoni și agenți
Fișierele cu reguli sunt XML cu extensia .plist. În interior sunt instrucțiuni care îi spun lui launchd ce să lanseze și când. Aceste fișiere sunt localizate în sistem în 5 foldere:
- ~/Library/LaunchAgents — agenți ai utilizatorului curent; l-a redus și l-a convertit în JPEG. Codul rezultat este prezentat mai jos - îl puteți adăuga în funcție de nevoile dvs. și îl puteți introduce lângă linia 28.
1234567891011121314 | pentru i, nume de fișier în enumerate(fișiere): if filename.find(‘Captură de ecran’) > -1 și os.stat(cale + nume fișier).st_size > 500000: lățime = int(subprocess.check_output([‘mdls’, ‘-name’, ‘kMDItemPixelWidth’, cale + nume fișier]).split(‘= ‘)[1][:-1]) dacă lățime > 2000: subprocess.call(['sips', 'Z', str(width/2), cale + nume de fișier]) if subprocess.check_output(['mdls', '-name', 'kMDItemKind', cale + nume de fișier] ).find('JPEG') este -1: fileout = filename.split('.')[0] + '.jpg' subprocess.call(['sips', '-s', 'format', 'jpeg' ', cale + nume fișier, '–out', cale + ieșire fișier]) os.remove(cale + nume fișier) nume fișier = fișier fișier [i] = nume fișier |
Ecranele mai mari de 500 KB vor fi convertite în JPG, iar dacă sunt mai largi de 2000 de pixeli (adică ecran complet în cazul meu), scriptul le va reduce și la jumătate.
Ce altceva poate fi îmbunătățit? De exemplu, puteți face ca folderul să fie curățat pe măsură ce fișierele primite sunt descărcate. Există o directivă lansată specială pentru aceasta: dacă înlocuiți WatchPaths cu QueueDirectories în configurație, scriptul va fi apelat până când directorul (sau directoarele) specificat devin gol. În acest caz, va trebui să aveți grijă să ștergeți singur fișierele, altfel launchd va rula scriptul în cercuri.
Copiere de rezervă și criptare a datelor atunci când conectați o unitate flash
Ca o a treia sarcină, am plănuit să fac un script care să ruleze automat atunci când conectați o unitate flash sau un hard disk cu un anumit nume, apoi montați un sistem de fișiere criptat (după ce ați cerut o parolă) și aruncați anumite fișiere în el. Cu alte cuvinte, depozitare improvizată, dar de încrederepentru date importante.
Habar n-aveam că sarcina se va dovedi atât de banală încât nu va fi aproape nimic de înțeles. Containerele criptate în macOS sunt create prin mijloace standard. Rulați DiskUtility, apăsați pe File ? Imagine noua? Imagine goală, completați numele fișierului, numele volumului și alegeți tipul de criptare (AES 128 sau 256 biți). Recipientul este gata! La montare, macOS va cere o parolă.
Rămâne să scriem un script care să conecteze containerul simultan cu apariția unei unități flash sau hard disk și să copieze automat datele. Cu siguranță nu veți avea nevoie de Python de data aceasta - Bash va fi suficient. Așa a ieșit la mine.
backup.sh
123456 | dacă [ -d /Volumes/BACKUP/ ]atunci hdiutil atașează /Volumes/BACKUP/Stuff.dmg cp -fr /Users/and/Desktop/TopSecret /Volumes/Stuff/ osascript -e 'Afișează notificarea cu titlul „Backup” complet"'fi |
Desigur, va fi necesar să corectăm căile. Rețineți condiția de la început: este necesară deoarece launchd în sine nu va verifica ce suport este montat - va rula scriptul atunci când este montat orice volum. În consecință, este necesar să se verifice dacă calea necesară există deja în script.
Și așa va arăta configurația lansată.
com.and.backup.StartOnMount.plist
1234567891011121314 | Eticheta com.and.backup ProgramArguments /Users/and/Develop/backup.sh StartOnMount |
Puteți adăuga eliminarea datelor de pe disc sau, dimpotrivă, puteți utiliza rsync pentru a păstra același conținut al folderului și al containerului.
Alte posibilitati
Există și alte funcții și setări interesante în launchd. Sunt necesare mai ales pentru nevoile interne ale sistemului, dar cine știe la ce perversiuni tehnice ne va conduce mâine?
Variabile de mediuvă permite să setați variabile de mediu special pentru scriptul dvs. Arata cam asa:
12345 | EnvironmentVariables PATH /bin:/usr/bin:/usr/local/bin |
Dacă doriți, puteți face un chroot complet setând cheia RootDirectory sau forțați ca sarcina să fie efectuată în numele unui anumit utilizator sau grup (UserName și GroupName).
Tasta KeepAlive va spune launchd că scriptul ar trebui să ruleze tot timpul. Dacă cade, de exemplu, sistemul va încerca să o ridice din nou.
12 | Păstrează-te în viață |
Adăugând cheile SuccessfulExit și Crashed la KeepAlive, puteți specifica o repornire numai dacă sarcina a fost finalizată cu succes sau dacă s-a prăbușit cu o eroare (adică a returnat un cod diferit de zero).
12345 | KeepAlive s-a prăbușit |
În același mod, este posibil să se susțină operarea unei sarcini numai dacă cel puțin o rețea este disponibilă (sau nici una nu este disponibilă) - cheia NetworkState este responsabilă pentru aceasta - sau atunci când o anumită cale (PathState) există sau nu există exista. Puteți chiar alege starea unui alt job (OtherJobEnabled).
Ei bine, pentru ca scriptul prost să nu consume toate resursele mașinii în cel mai inoportun moment, puteți stabili diverse restricții:
- CPU — cantitatea de timp de procesor în secunde care poate fi cheltuită;
- FileSize — dimensiunea maximă a fișierului care poate fi creată;
- NumberOfFiles — numărul maxim de fișiere deschise simultan;
- Date — cantitatea maximă de date (în octeți) care poate fi procesată.
Și pentru cazuri foarte complexe, există un sistem în două etape de limitare soft (SoftResourceLimit) și limitare hard (HardResourceLimit). Dacă primul este depășit, sistemul va trimite procesului un semnal precum SIGXCPU sau SIGXFSZ și va termina forțat.el, numai dacă nu se liniștește și trece o limită grea.
Concluzie
După cum probabil ați observat, launchd are și dezavantajele sale. Același crontab este un tabel simplu care arată clar ce rulează și când. Cu launchctl în acest sens, totul nu este ușor: configurațiile sunt împrăștiate în diferite foldere, pot fi pornite sau oprite, iar totul în interiorul lor este departe de a fi clar la prima vedere.
Dacă nu aveți nevoie de altceva decât de timpul de lansare, atunci launchd nu merită probleme - puteți utiliza cron în siguranță (este interesant că chiar și acesta funcționează pe deasupra launchd în macOS). În ceea ce privește capacitatea de a lega scripturi la foldere, aceasta este disponibilă în Automator - din nou, configurațiile corespunzătoare vor fi pur și simplu create automat.
Cu toate acestea, dacă doriți să stăpâniți un instrument puternic și versatil, launchd este cu siguranță alegerea. Chiar dacă nu trebuie să scrieți totul și totul, va fi util să învățați principiile sistemului.
Legătură
- Documentație oficială
- Site-ul pentru dezvoltatori LaunchControl cu documentație detaliată și exemple
- O postare care detaliază noua sintaxă launchctl
Diferența față de demonii agent este destul de subtilă: demonii sunt procese care pornesc imediat după pornirea computerului, iar agenții pot funcționa numai după conectarea în sistem (prin urmare, nu există demoni pentru un anumit utilizator). În plus, demonii după activare lucrează continuu, iar agenții lucrează de obicei în anumite condiții.
Ne vom ocupa în mod specific de agenți și pentru uz personal, așa că primul director din listă va fi cel mai bun.
Există două shell-uri grafice pentru crearea fișierelor de configurare lansate - LaunchControl și Lingon (ambele costă zece dolari fiecare). Ele ușurează puțin lucrurile, dar te poți descurca fără ele.
Configurare simplă: începe după timp
Să începem cu cel mai ușor lucru - să începem ceva la un moment dat. Așa arată cea mai simplă versiune a configurației.
123456789101112131415161718 | Nume etichetă Calea fișierului ProgramArguments StartCalendarInterval Minut30 Hour1 Day6 |
În ciuda aspectului întins, structura aici este destul de simplă. În dicționarul principal () există chei, urmate de parametrii acestora. Uneori acestea sunt șiruri, alteori matrice, alteori dicționare imbricate.
Înlocuiți cuvântul „nume” cu un nume (de obicei „com.domain.name” - de exemplu, am numit agentul de testare com.and.launchtest), specificați calea către executabil ca prim parametru al ProgramArguments și apoi specificați în câte și în ce zile ale săptămânii să alerge.
În exemplul meu, ora este setată la 1:30 în fiecare sâmbătă. Dacă scoți cheiaZiua, scriptul va începe să ruleze la două și jumătate în fiecare noapte, iar dacă eliminați și Hour, atunci la fiecare jumătate de oră. Cred că înțelegi cum funcționează. O intrare similară în crontab ar arăta ca
1 | 0 30 1 * 6 |
Dacă comanda pe care o executați primește argumente, acestea trebuie listate după cale prin adăugarea de câmpuri suplimentare. Exemplu:
1 2 3 4 5 6 7 8 9 10 | ProgramArgumentedepozitareÎn Petropavlovsk-Kamchatsky miezul nopții ÎnceputCalendarIntervalMinut0Oră15 |
Când totul este gata, salvați fișierul în ~/Library/LaunchAgents/. Mai corect ar fi să scrieți imediat condițiile de lansare în nume, astfel încât să fie mai ușor de navigat ulterior. În configurația mea de testare am salvat ca com.and.launchtest.StartInterval.plist.
Subtilități ale activării
Din păcate, dezavantajul flexibilității este gama largă de setări. Chiar și configurațiile lansate pot fi activate și dezactivate în mai multe moduri. Iată cea mai veche și cea mai simplă. Pentru a descărca, scrie:
1 | $ launchctl load -w ~/Library/LaunchAgents/ |
Și pentru descărcare:
1 | $ launchctl unload -w ~/Library/LaunchAgents/ |
Tasta -w include și indicatorul activat, care ne salvează un pas (launchctl enable) și activează imediat configurația. Rețineți că după încărcarea computerului și conectarea la sistem, toți agenții aflați în folderele corespunzătoare vor fi încărcați automat. De aceea, este convenabil să adăugați și -w la descărcare - atunci launchctl își va aminti că configurația este inactivă.
După ce ați schimbat ceva în configurații, acesta trebuie descărcat și descărcat din nou.
Puteți folosi în siguranță aceste comenzi, dar dacă deschideți man, veți afla că sunt considerate depreciate și sunt acceptate doar pentru compatibilitate.O modalitate mai corectă este să utilizați comenzile bootstrap și bootout. Acestea necesită specificarea, pe lângă calea către fișierul de configurare, a domeniului-țintă, care constă din domeniul și UID-ul utilizatorului. Echipele întregi vor arăta astfel:
1 | $ launchctl bootstrap gui/ |
Și pentru descărcare:
1 | $ launchctl bootout gui/ |
Puteți afla UID-ul dvs. cu comanda id -u. Primul utilizator al computerului este de obicei înregistrat sub numărul 502.
O altă comandă bună de reținut este lista. Pentru a verifica care dintre configurațiile dvs. sunt încărcate, puteți scrie:
1 | $ launchctl list grep |
Din nou, există o metodă mai modernă, mai avansată și, desigur, mai înghețată:
1 | $ launchctl print / |
Ieșirea va avea mult mai multe informații decât atunci când solicitați o listă. Dar din nou, nu trebuie să utilizați deloc tipărirea. Ca răspuns la o întrebare despre când comenzile învechite vor înceta să funcționeze, un dezvoltator a comentat pe forum că există prea multă utilizare a vechei sintaxe pentru a o elimina.
Opțiuni pentru descărcarea seriei
După cum puteți vedea, totul este destul de simplu până acum, mai ales dacă ignorați inovațiile. Pentru a vă mulțumi cu un exemplu mai util, vă voi spune cum m-am configurat să descarc episoade noi de seriale TV folosind launchd.
Ca bază, am luat un feed de la showRSS - acest serviciu vă permite să vă înregistrați și să faceți un feed personalizat cu link-uri magnetice către noi serii de seriale pe care le vizionați.
showRSS are canale pentru majoritatea serialelor actuale
Toate numai în engleză, dar exact de asta am nevoie. Dacă îți place și showRSS, nuuitați să ajustați calitatea după alcătuirea listei de serii, altfel veți primi totul de mai multe ori în diferite rezoluții.
Pentru a adăuga episoade noi automat la Transmission, am scris un mic script și l-am setat să ruleze automat o dată pe oră folosind launchctl. Vă rugăm să rețineți că RPC (Activare acces la distanță) va trebui să fie activat în setările de transmisie.
Pentru a nu expune interfața la exterior, puteți, de asemenea, să limitați accesul acolo și să permiteți conectarea numai de la mașina locală (vezi captura de ecran). Următorul este fișierul sursă de script complet. Tot ce trebuie să faceți este să instalați dependențele (pip install bs4 transmissionrpc), să adăugați numărul de utilizator din showRSS și să specificați folderul pentru a salva încărcarea.
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()cu excepția: logare. exception(„nu pot obține pagina”) sys.exit() supa = bs4.BeautifulSoup(pagina, „html.parser”) items = soup.findAll('item') număr = 0pentru elementul din articole: if item.guid .string == last: break number += 1 logging.info('S-au găsit episoade noi:' + str(number)) dacă numărul > 0: subprocess.call([‘open’, ‘-jg’, ‘/Applications/Transmission.app’]) sleep(5) tc = transmissionrpc.Client(‘localhost’, port=9091) for x ininterval (număr): applescript = 'afișează notificarea „{}” cu titlul „Descărcare”'.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]) |
Codul se explică în mare parte: scriptul descarcă un fișier RSS, îl analizează cu Beautiful Soup, caută în matricea de link-uri rezultată cea mai recentă serie descărcată cu un GUID și apoi introduce transmisii mai noi una câte una (după ce o porniți mai întâi și oferindu-i cinci secunde să se încarce).
Există două caracteristici pur mac aici. Primul este să salvați și să încărcați GUID-ul celei mai recente serii folosind comanda implicită. Acest lucru nu este necesar: este foarte posibil să salvați această variabilă într-un fișier text obișnuit și să specificați directorul în care se află (cheia WorkingDirectory) în configurația launchctl. Dar am vrut să demonstrez valorile implicite ca una dintre posibilitățile interesante.
Pentru ca scriptul să funcționeze, înainte de prima rulare, trebuie să notați GUID-ul seriei de la care va începe descărcarea (nu va fi descărcat singur):
1 | Valorii implicite $ scriu ultimul meu spectacol |
Al doilea chip de mac pe care l-am folosit este notificarea. Pentru a afișa o notificare când începe descărcarea, execut o singură linie în AppleScript (afișează notificarea... cu titlu...). Dacă veți transfera scriptul pe alt sistem de operare, ștergeți linia 41.
Tot ce rămâne este să adăugați configurația launchd. De data aceasta nu o vom lansa conform intervalului calendaristic, ci pur și simplu în fiecare oră. Pentru aceasta avem nevoie de cheia StartInterval.
com.and.showrss.StartInterval.plist
1234567891011121314 | Etichetați com.and.showrss ProgramArguments/Users/and/Develop/ShowRSS/showrss-launchd.py StartInterval 3600 |
Aici va trebui să înlocuiți calea către script cu propria dvs. și, opțional, să schimbați numele fișierului, eticheta și intervalul de lansare.
Din păcate, nu este posibil să combinați StartInterval și StartCalendarInterval, astfel încât scriptul să ruleze la intervale regulate de la o oră sau o zi. În astfel de cazuri, este necesar fie să scrieți un wrapper care să activeze și să dezactiveze startul pe StartInterval și să îl porniți pe StartCalendarInterval, fie să generați o configurație uriașă care specifică fiecare început.
De asemenea, după cum puteți vedea, m-am deranjat cu înregistrarea corectă folosind modulul de înregistrare - astfel încât mai târziu să fie convenabil să văd ce s-a întâmplat prin utilitarul Console. Dar dacă rulați automat un program care scoate date în stdout, atunci launchd îl poate captura pentru dvs. și îl poate compila într-un fișier text. Pentru a face acest lucru, adăugați următoarele linii la config:
12345 | Cale fișier StandardOutPath Cale fișier StandardErrorPath |
În același mod, puteți scrie StandardInPath cu un fișier care va fi alimentat la intrarea scriptului.
Ne creăm „dropbox”
Să trecem la sarcina numărul doi: aflați cum să rulați scripturi când apar fișiere noi într-un anumit folder. În același timp, vom face un simplu analog Dropbox. Mai precis, este un analog al versiunilor timpurii ale Dropbox, care a copiat imediat linkul pentru a descărca un fișier în clipboard atunci când îl aruncați într-un folder dat.
Am ales Transfer.sh ca găzduire. Este gratuit, fără reclame și vă permite să descărcați un fișier cu o singură comandă de pe terminal. Dacă doriți, puteți chiar să îl puneți pe server și să conectați o găleată S3.
Dacă nu sunteți mulțumit de o astfel de soluție, puteți, de exemplu, să încărcați fișiere pe propriul dvs. FTP. Nu numai că va fi mai de încredere,dar vă va permite și să scăpați de pagina de previzualizare a imaginii pe care Transfer.sh o arată fără greșeală. Apropo, am ajuns să-mi fac două foldere: unul pentru găzduirea mea, unul pentru Transfer.sh.
uploader-transfersh.py
12345678910111213141516171819202122232425262728293031323334353637383940414243 | import osimport loggingimport subprocessimport jsonfrom urllib import citat cale = ''logfile = '/Users//Library/Logs/uploader-transfersh.log' def notify(titlu, mesaj): applescript = 'afișează notificarea „{}” cu titlu „{}”'.format(mesaj, titlu) subprocess.call(['osascript', 'e', applescript]) logging.basicConfig(filename=logfile,level=logging.DEBUG,format='%( asctime) s %(message)s')logging.info('Începând de la' + os.getcwd()) dacă nu 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(listă veche))logging.info('Fișiere noi găsite:' + ' '.join(fișiere)) urls = []nume fișier pentru fișiere: încercați: url = subprocess.check_output(['curl', '– upload-file', cale + nume fișier, 'https://transfer.sh/' + quote(nume fișier)]) urls.append(url) logging.info('Se încarcă' + nume fișier) cu excepția: notify('Eroare la încărcare' + nume fișier , „Vezi” + fișier jurnal) logging.exception(„nu se poate încărca” + nume fișier) dacă len(fișiere) > 0: applescript = 'setat în clipboard „{}”'.format('n'.join(urls)) subprocess.call('osascript', 'e', applescript) logging.info(' '.join (urls )) open('list.txt', 'w+').write(json.dumps(newlist)) notify('Încărcare finalizată', ''.join(fişiere)) |
De data aceasta nu veți avea nevoie de module non-standard, scrieți doar căile corecte de la început. Scriptul citește un fișier cu starea anterioară a folderului specificat(sau creează unul dacă este rulat pentru prima dată), compară cu cele actuale și, dacă găsește fișiere noi, le încarcă pe Transfer.sh unul câte unul, salvând linkurile pe care le oferă ca răspuns.
Lista de link-uri este apoi copiată în clipboard folosind o singură linie în AppleScript (am vrut să folosesc comanda pbcopy în acest scop, dar ceva nu s-a adunat: atunci când a fost sunat prin launchd, clipboard-ul a fost șters din motive necunoscute) . La sfârșit, noua listare a catalogului este serializată și salvată într-un fișier text, apoi este afișată o notificare - ca în exemplul anterior.
Acum, cel mai important lucru este configurația autostart.
com.and.transfersh.WatchPaths.plist
123456789101112131415161718 | Etichetați com.and.transfersh ProgramArguments /Users/and/Develop/Uploader/uploader-transfersh.py WatchPaths /Users/and/Desktop/Transfer.sh/ WorkingDirectory /Users/and/Develop/Uploader/ |
Vă rugăm să rețineți că, pe lângă calea pentru rularea scriptului în sine, calea acestuia este din nou specificată ca WorkingDirectory. De data aceasta salvăm datele într-un fișier text, așa că, în același timp, vă demonstrez cum să setați folderul pe care scriptul îl va considera a fi folderul curent - va conține „livrare”.
Tasta WatchPaths poate fi folosită și pentru a monitoriza modificările fișierelor. Sintaxa în acest caz este aceeași.
Desigur, nu am avut un analog complet Dropbox. În special, fișierele nu sunt șterse de pe server atunci când le ștergeți local (cu toate acestea, Transfer.sh le va șterge în continuare după o lună).
De asemenea, nu există suport pentru foldere, dar este ușor de implementat dacă se dorește.
În general, un mare avantaj al acestei metode este că, dacă se dorește, poate fi rafinată la discreția ta. De exemplu, am vrut să fac astfel încât atunci când puneți o imagine cu cuvintele Captură de ecran în numele fișierului, scenariul să fie