
Klassik ütles, et õnnelikud tunnid ei vaata kella. Neil metsalistel aegadel ei olnud veel ei programmeerijaid ega Unixit, kuid tänapäeval on programmeerijad kindlad: nende nimel jälgib aega cron.
Käsurea utiliidid on mulle samal ajal nii nõrkus kui rutiin. sed, awk, wc, cut ja teised vanad programmid käivitavad skriptid meie serverites igapäevaselt. Paljusid neist on vormistatud cron'i ülesannetena, planeerija, mis pärineb 70-ndatest.
Olen pikka aega kasutanud cron'i pinnapealselt, süvenemata detailidesse, kuid ühel päeval, silmitsi seistes skripti käivitamise veaga, otsustasin süvitsi uurida. Nii sai kirjutatud see artikkel, mille koostamisel tutvusin POSIX crontab'i, levinumate cron'i variantidega populaarses Linuxi jaotustes ning nende struktuuriga.
Kasutate Linuxit ja käitate ülesandeid cron'is? Kas teid huvitab Unix'i süsteemirakenduste arhitektuur? Siis oleme õigel teel!
Sisu
Liikide päritolu
Kasutajate või süsteemiprogrammide perioodiline täitmine on igas operatsioonisüsteemis ilmne vajadus. Seetõttu on programmeerijad juba ammu aru saanud vajadusest teenuste järele, mis võimaldavad ülesandeid tsentraliseeritult planeerida ja täita.
Unix-tüüpi operatsioonisüsteemid pärinevad Version 7 Unix'ist, mis töötati välja 1970. aastatel Bell Labs'is, sealhulgas tuntud Ken Thompson'i (Ken Thompson) poolt. Koos Version 7 Unix'iga tuli ka cron, teenus regulaarselt ülesannete täitmiseks superkasutaja jaoks.
Tüüpiline kaasaegne cron on lihtne programm, kuid originaalversiooni töö algoritm oli veelgi lihtsam: teenus ärkas iga minuti järel, luges ülesandeid ühes failis (/etc/lib/crontab) ja täitis superkasutaja jaoks need ülesanded, mis pidid olema täidetud jooksva minuti jooksul.
Hiljem täiustatud variandid sellest lihtsast ja kasulikust teenusest on saadaval kõikides Unix-tüüpi operatsioonisüsteemides.
Crontab forma ja utiliidi põhialuste üldkirjeldused, mis olid 1992. aastal lisatud Unix-sarnaste operatsioonisüsteemide peamisse standardisse — POSIX — muutsid cron'i de-fakto standardist de-jure standardiks.
1987. aastal välja andnud Paul Vixie, küsides Unix'i kasutajatelt soovitusi croni osas, teiste versiooni demon, mis lahendab traditsiooniliste croni teatud probleeme ja laiendab tabelifailide süntaksit.
Kolmandas versioonis vastas Vixie cron POSIX'i nõuetele ja programmi vaba litsents, tõsi küll, ei olnud seal üldse mingit litsentsi, välja arvatud soovid README-s: autor ei anna mingeid garantiisid, autori nime ei tohi eemaldada ja programmi võib müüa ainult koos lähtekoodiga. Need nõuded osutusid ühilduvaks toona populaarsust kogunud vabavara põhimõtetega, seega mõned võtmetähtsusega Linuxi distributsioonidest, mis ilmusid 90ndate alguses, võtsid Vixie cron'i süsteemiks ja arendavad seda siiani.
Eelkõige Red Hat ja SUSE arendavad Vixie cron'i forki — cronie, samas kui Debian ja Ubuntu kasutavad originaali Vixie cron'i paljude patch'idega.
Alustuseks tutvume POSIX-is kirjeldatud kronditööriistaga crontab, seejärel uurime Vixie croni süntaksi laiendusi ja Vixie croni variatsioonide kasutust populaarsetes Linuxi distributsioonides. Ja lõpuks, kirsiks tordil — cron-demonite tööpõhimõttest rääkimine.
POSIX crontab
Kuigi originaalne cron töötas alati superkasutaja jaoks, on tänapäevased ajastajad enamasti suunatud tavaliste kasutajate ülesannetele, mis on turvalisem ja mugavam.
Cronid koosnevad kahest programmist: pidevalt töötav demon cron ja kasutajatele kergesti kättesaadav utiliit crontab. Viimane võimaldab redigeerida iga süsteemi kasutaja spetsiifilisi ülesandeid, demon aga käivitab ülesandeid nii kasutaja kui süsteemi tabelitest.
V ei ole demonite käitumist kirjeldatud ning ainult kasutajakäepide on formaliseeritud. . Kasutajate ülesannete käivitamise mehhanismide olemasolu oletatakse muidugi, kuid detailselt ei ole seda kirjeldatud.
crontab utiliidi kaudu saab teha neli asja: redigeerida kasutaja ülesannete tabelit redigeerijas, laadida tabel faili kaudu, kuvada praegust ülesannete tabelit ja kustutada ülesannete tabel. Näidised crontab utiliidi töö kohta:
crontab -e # redigeerida ülesannete tabelit
crontab -l # kuvada ülesannete tabelit
crontab -r # kustutada ülesannete tabel
crontab path/to/file.crontab # laadida ülesannete tabel failistKutsudes crontab -e kasutatakse redigeerijat, mis on määratud standardse keskkonnamuutuja EDITOR.
Ülesanded on kirjeldatud järgmises formaadis:
# строки-комментарии игнорируются
#
# задача, выполняемая ежеминутно
* * * * * /path/to/exec -a -b -c
# задача, выполняемая на 10-й минуте каждого часа
10 * * * * /path/to/exec -a -b -c
# задача, выполняемая на 10-й минуте второго часа каждого дня и использующая перенаправление стандартного потока вывода
10 2 * * * /path/to/exec -a -b -c > /tmp/cron-job-output.logEsimesed viis väljade kirjet: minutid [1..60], tunnid [0..23], kuu päevad [1..31], kuud [1..12], nädalapäevad [0..6], kus 0 on pühapäev. Viimane, kuues väli — string, mis täidetakse standardse käsurea tõlgiga.
Esimeses viies väljas võib väärtusi eraldada komadega:
# задача, выполняемая в первую и десятую минуты каждого часа
1,10 * * * * /path/to/exec -a -b -cVõi hüüdmisega:
# задача, выполняемая в каждую из первых десяти минут каждого часа
0-9 * * * * /path/to/exec -a -b -cKasutajate juurdepääs ülesannete planeerimisele on reguleeritud POSIX failides cron.allow ja cron.deny, millel on vastavalt loetletud kasutajad, kellel on juurdepääs crontab'ile, ja kasutajad, kellel ei ole programmis juurdepääsu. Nende failide asukohta standard ei reguleeri.
Käivitavatele programmidele peab standardi kohaselt edastama vähemalt neli keskkonnaparameetrit:
- HOME — kasutaja kodu kataloog.
- LOGNAME — kasutaja sisselogimisnimi.
- PATH — tee, kus saab leida süsteemi üldkasutatavad utiliidid.
- SHELL — tee kasutatava käskude tõlgendi juurde.
Tähtis on märkida, et POSIX ei ütle midagi selle kohta, kust need parameetrite väärtused tulevad.
Müügi hitt — Vixie cron 3.0pl1
Populaarsete cron variantide ühine eelkäija on Vixie cron 3.0pl1, mille esitles comp.sources.unix’i postitus 1992. aastal. Selle versiooni peamisi omadusi vaatame lähemalt.
Vixie cron sisaldab kahte programmi (cron ja crontab). Nagu tavaliselt, vastutab deemon süsteemide tööde tabeli ja üksikute kasutajate tööde tabelite lugemise ning käivitamise eest, samas kui utiliit crontab vastutab kasutajate tabelite redigeerimise eest.
Töötabel ja konfiguratsioonifailid
Superkasutaja tööde tabel asub /etc/crontab. Süsteemi tabeli süntaks vastab Vixie cron süntaksile, sealjuures, et kuuendas veerus on märgitud kasutaja nimi, kelle nimel ülesanne käivitub:
# Запускается ежеминутно от пользователя vlad
* * * * * vlad /path/to/execTavade tavade kasutajate ülesanded asuvad /var/cron/tabs/username ja kasutavad ühist süntaksit. Crontab utiliidi käivitamisel kasutaja nimel redigeeritakse just neid faile.
Kasutaja loendite haldamine, kellel on juurdepääs crontab’ile, toimub failides /var/cron/allow ja /var/cron/deny, kuhu piisab, kui lisada kasutajanimi eraldi reale.
Täpsem süntaks
Võrreldes POSIX crontabiga sisaldab Paul Vixie lahendus mitmeid väga kasulikke muudatusi utiliidi ülesannete tabelite süntaksis.
Saadaval on uus tabeli süntaks: näiteks võib nädalapäevi või kuukäike nimetada (Mon, Tue jne):
# Запускается ежеминутно по понедельникам и вторникам в январе
* * * Jan Mon,Tue /path/to/execVõib määrata sammu, mille kaudu ülesanded käivitatakse:
# Запускается с шагом в две минуты
*/2 * * * Mon,Tue /path/to/execSammud ja intervallid võivad olla segatud:
# Запускается с шагом в две минуты в первых десять минут каждого часа
0-10/2 * * * * /path/to/execToetatakse arusaadavaid alternatiive tavaliseks süntaksiks (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly):
# Запускается после перезагрузки системы
@reboot /exec/on/reboot
# Запускается раз в день
@daily /exec/daily
# Запускается раз в час
@hourly /exec/dailyÜlesannete täitmise keskkond
Vixie cron võimaldab muuta käivitatavate rakenduste keskkonda.
Keskkonnamuutujad USER, LOGNAME ja HOME ei ole lihtsalt deemonis saadavad, vaid need võetakse faili kaudu. . Muutuja PATH saab väärtuse «/usr/bin:/bin», samas kui SHELL — «/bin/sh». Kõikide muutujate, välja arvatud LOGNAME, väärtusi saab muuta kasutajate tabelites.
Mõningaid keskkonnamuutujad (eelkõige SHELL ja HOME) kasutab cron ise ülesande käivitamiseks. Nii võib välja näha bash'i kasutamine standardse sh asemel kasutajaülesannete käivitamiseks:
SHELL=/bin/bash
HOME=/tmp/
# exec käivitatakse bash'is /tmp/-s
* * * * * /path/to/execLõpuks edastatakse kõik keskkonnamuutujad, mis on tabelis määratletud (kui cron neid kasutab või protsess nõuab), käivitatavale ülesandele.
Crontab'i failide redigeerimiseks kasutatakse toimetajat, mis on määratud keskkonnamuutujas VISUAL või EDITOR. Kui need muutujad ei ole crontab'i käivitanud keskkonnas määratud, kasutatakse «/usr/ucb/vi» (ucb on tõenäoliselt California Ülikooli, Berkeley nimi).
cron Debianis ja Ubuntu’s
Debiani ja selle derivatiivsete jaotuste arendajad on välja andnud Vixie cron 3.0pl1 versioonist. Tabelifailide süntaksis ei ole erinevusi, see on kasutajate jaoks sama Vixie cron. Suurimad uued funktsioonid: toetus , ja .
Mitte vähemtuntud, kuid käegakatsutavaid muudatusi on konfiguratsioonifailide ja ülesannete tabelite asukoht.
Kohandatud tabelid Debianis asuvad kaustas /var/spool/cron/crontabs, süsteemitabel samuti seal — /etc/crontab. Pakettide spetsiifilised ülesannete tabelid asetatakse kausta /etc/cron.d, kust cron daemon need automaatselt loeb. Kasutajate juurdepääsu haldab failid /etc/cron.allow ja /etc/cron.deny.
Vaikimisi käsurea tõlgina kasutatakse endiselt /bin/sh, mille rolli Debianis täidab väike POSIX-ühilduv shell , mis käivitub ilma mingit konfiguratsiooni lugemata (mitteinteraktiivses režiimis).
Ise cron käivitatakse viimastes Debian'i versioonides läbi systemd, ning käivitamise konfiguratsiooni saab vaadata failist /lib/systemd/system/cron.service. Teenuse konfiguratsioonis pole midagi erilist, iga keerukam ülesannete haldamine on võimalik läbi keskkonnamuutujate, mis on otse määratud iga kasutaja crontabis.
cronie RedHatis, Fedoras ja CentOSis
— Vixie croni 4.1 fork. Nagu Debiani puhul, süntaks ei ole muutunud, kuid on lisatud PAM ja SELinuxi tugi, klastri töö, failide jälgimine inotify abil ja muid võimalusi.
Vaikimisi konfiguratsioon asub tavapärastel kohtadel: süsteemitabel — /etc/crontab, paketid asetavad oma tabelid /etc/cron.d, kasutajatabelid jõuavad /var/spool/cron/crontabs.
Demon käivitatakse systemd halduses, teenuse konfiguratsioon — /lib/systemd/system/crond.service.
Red Hat-põhistes distributsioonides kasutatakse vaikimisi /bin/sh, milleks on standardne bash. Tuleb märkida, et cron ülesannete käivitamisel läbi /bin/sh käivitub bash POSIX-ühilduvas režiimis ja ei loe lisakonfiguratsiooni, töötades mitteinteraktiivses režiimis.
cronie SLES'is ja openSUSE's
Saksa distributsioon SLES ja selle derivaat openSUSE kasutavad endiselt cronie. Demon käivitub samuti systemd all, teenuse konfiguratsioon asub /usr/lib/systemd/system/cron.service. Konfiguratsioon: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. /bin/sh režiimiks on sama bash, mis on käivitatud POSIX-ühilduvas mitteinteraktiivses režiimis.
Vixie cron'i struktuur
Kaasaegsed cron'i järeltulijad ei ole Vixie cron'iga võrreldes radikaalselt muutunud, kuid on siiski omandanud uusi võimalusi, mis ei ole vajalikud programmi tööpõhimõtete mõistmiseks. Paljud neist laiendustest on halvasti vormistatud ja segavad koodi. Originaalkood cron'ist Paul Vixie'i täitmisel on lugemine puhas nauding.
Seetõttu otsustasin cron'i seadmete analüüsi teha Vixie cron 3.0pl1 näitel, mis on ühine mõlemale cron'i arengu harule. Näiteid lihtsustan, eemaldades loetavust keerukaks tegevad ifdef'id ja kõrvaldades teisejärgulised üksikasjad.
Demonite tööd saab jagada mitmeks etapiks:
- Programmi initsialiseerimine.
- Ülesannete loendi kogumine ja uuendamine.
- Cron'i peatsükli toimimine.
- Ülesande käivitamine.
Käsitleme neid järjest.
Initsialiseerimine
Käivitusprotsessi argumentide kontrollimise järel seadistab cron signaalide SIGCHLD ja SIGHUP töötlemise. Esimene logib teate, kui tütartöö lõpetab, teine sulgeb logifaili failikirjeldaja:
signal(SIGCHLD, sigchld_handler);
signal(SIGHUP, sighup_handler);Croni demon töötleb alati ühte, ainult superkasutaja rollis ja cron peadirigeerimisest. Järgmised käsud loovad lukufaili protsessi PID-iga, veenduvad, et kasutaja on õige, ja muudavad töökausta peadirigeerimiseks:
acquire_daemonlock(0);
set_cron_uid();
set_cron_cwd();Seati vaikimisi tee, mida kasutatakse protsesside käivitamisel:
setenv("PATH", _PATH_DEFPATH, 1);Siis demon „demoniseeritakse”: luuakse alamkoopia protsessist fork-käskluse abil ja uus sessioon alamprotsessis (setsid-käsklus). Vanema protsessi jaoks pole enam vajadust — ja see lõpetab oma töö:
switch (fork()) {
case -1:
/* kriitiline viga ja töö lõpetamine */
exit(0);
break;
case 0:
/* alamprotsess */
(void) setsid();
break;
default:
/* vanema protsessi lõpetamine */
_exit(0);
}
Vanema protsessi lõpetamine vabastab lukufaili lukku. Lisaks tuleb PID uuendada failis alamprotsessiks. Pärast seda täidetakse ülesannete baas:
/* повторный захват лока */
acquire_daemonlock(0);
/* Заполнение БД */
database.head = NULL;
database.tail = NULL;
database.mtime = (time_t) 0;
load_database(&database);Seejärel liigub cron peatsesse töötsüklisse. Kuid enne seda tasub vaatata ülesannete nimekirja koormust.
Ülesannete nimekirja kogumine ja uuendamine
Ülesannete loetelu laadimise eest vastutab funktsioon load_database. See kontrollib peamist süsteemi crontab'i ja kasutajafailide kaustat. Kui failid ja kaust ei ole muutunud, siis ülesannete loetelu ei loeta uuesti. Vastupidisel juhul hakkab moodustuma uus ülesannete loetelu.
Süsteemifaili laadimine, mis sisaldab spetsiaalsete failinimede ja tabelite jaoks:
/* если файл системной таблицы изменился, перечитываем */
if (syscron_stat.st_mtime) {
process_crontab("root", "*system*",
SYSCRONTAB, &syscron_stat,
&new_db, old_db);
}Kasutajate tabelite laadimine tsüklis:
while (NULL != (dp = readdir(dir))) {
char fname[MAXNAMLEN+1],
tabname[MAXNAMLEN+1];
/* Punktiga failide lugemine ei ole vajalik */
if (dp->d_name[0] == '.')
continue;
(void) strcpy(fname, dp->d_name);
sprintf(tabname, CRON_TAB(fname));
process_crontab(fname, fname, tabname,
&statbuf, &new_db, old_db);
}
Pärast seda asendatakse vana andmebaas uuega.
Ülaltoodud näidetes veendub funktsiooni process_crontab kutsumine, et vastav kasutaja ja tabeli failinimi eksisteerivad (kui see ei ole superkasutaja), ja seejärel kutsub üles load_user. Viimane loeb faili rida-realt:
while ((status = load_env(envstr, file)) >= OK) {
switch (status) {
case ERR:
free_user(u);
u = NULL;
goto done;
case FALSE:
e = load_entry(file, NULL, pw, envp);
if (e) {
e->next = u->crontab;
u->crontab = e;
}
break;
case TRUE:
envp = env_set(envp, envstr);
break;
}
}Siin määratakse kas keskkonnamuutuja (näiteks VAR=väärtus) funktsioonide load_env / env_set abil või loetakse ülesande kirjeldust (***** /path/to/exec) funktsiooni load_entry kaudu.
Load_entry funktsiooni tagastatud entiteet entry on meie ülesanne, mis pannakse üldisesse ülesannete nimekirja. Funktsiooni sees toimub aeganõudev ajaformaadi parsimine, meid huvitab rohkem keskkonnamuutujate ja ülesande käivitamise parameetrite loomine:
/* пользователь и группа для запуска задачи берутся из passwd*/
e->uid = pw->pw_uid;
e->gid = pw->pw_gid;
/* шелл по умолчанию (/bin/sh), если пользователь не указал другое */
e->envp = env_copy(envp);
if (!env_get("SHELL", e->envp)) {
sprintf(envstr, "SHELL=%s", _PATH_BSHELL);
e->envp = env_set(e->envp, envstr);
}
/* домашняя директория */
if (!env_get("HOME", e->envp)) {
sprintf(envstr, "HOME=%s", pw->pw_dir);
e->envp = env_set(e->envp, envstr);
}
/* путь для поиска программ */
if (!env_get("PATH", e->envp)) {
sprintf(envstr, "PATH=%s", _PATH_DEFPATH);
e->envp = env_set(e->envp, envstr);
}
/* имя пользовтеля всегда из passwd */
sprintf(envstr, "%s=%s", "LOGNAME", pw->pw_name);
e->envp = env_set(e->envp, envstr);Praeguse ülesannete nimekirjaga töötab peamine tsükkel.
Peamine tsükkel
Original Unix Version 7 cron töötas üsna lihtsalt: see luges konfiguratsiooni silmusena, käivitas superkasutajaga käesoleva minuti ülesandeid ja magas kuni järgmise minuti alguseni. See lihtne lähenemine nõudis vanadel masinatel liiga palju ressursse.
SysV-s pakuti alternatiivset versiooni, kus deemon magas kas kõige lähemal minutil, mil ülesanne on määratletud, või 30 minutit. Sellises režiimis kulus vähem ressursse konfiguratsiooni lugemiseks ja ülesannete kontrollimiseks, kuid ülesannete nimekirja kiire värskendamine muutus ebamugavaks.
Vixie cron tagas ülesannete nimekirjade kontrollimise juurde minutis, kuna 80ndate lõpuks oli standardsetel Unix masinatel ressursse oluliselt rohkem:
/* первичная загрузка задач */
load_database(&database);
/* запустить задачи, поставленные к выполнению после перезагрузки системы */
run_reboot_jobs(&database);
/* сделать TargetTime началом ближайшей минуты */
cron_sync();
while (TRUE) {
/* выполнить задачи, после чего спать до TargetTime с поправкой на время, потраченное на задачи */
cron_sleep();
/* перечитать конфигурацию */
load_database(&database);
/* собрать задачи для данной минуты */
cron_tick(&database);
/* перевести TargetTime на начало следующей минуты */
TargetTime += 60;
}
Ülesannete täitmisega tegeleb funktsioon cron_sleep, mis kutsub esile funktsioonid job_runqueue (ülesannete läbimine ja käivitamine) ja do_command (iga eraldi ülesande käivitamine). Viimast funktsiooni on mõistlik põhjalikumalt vaadata.
Ülesande käivitamine
Funktsioon do_command on kirjutatud heas Unix-stiilis, mis tähendab, et asünkroonsete ülesannete täitmiseks teeb see fork'i. Vanemprotsess jätkab ülesannete käivitamist, lastes protsess tegeleb selle ülesande ettevalmistamisega:
switch (fork()) {
case -1:
/* ei suutnud fork'i teostada */
break;
case 0:
/* lapsprotsess: igaks juhuks proovime taas maha võtta peamise luku */
acquire_daemonlock(1);
/* liigume ülesande protsessi loomise juurde */
child_process(e, u);
/* pärast lõpetamist lapsprotsess lõpetab töö */
_exit(OK_EXIT);
break;
default:
/* vanemprotsess jätkab tööd */
break;
}child_processis on palju loogikat: see võtab enda peale standardseid väljundeid ja vigu ning edastab need e-postile (kui ülesannete tabelis on määratud keskkonnamuutuja MAILTO) ning lõpuks ootab põhiprotsessi lõppemist.
Ülesande protsess luuakse veel ühe forkiga:
switch (vfork()) {
case -1:
/* kui viga, siis töö lõpetatakse kohe */
exit(ERROR_EXIT);
case 0:
/* lapsprotsess loob uue sessiooni, terminali jne.
*/
(void) setsid();
/*
* edasi on sõnumite seadistamine, jätame selle lühiduse huvides vahele
*/
/* kausta, kasutaja ja grupi muutmine,
* st protsess ei ole enam superkasutaja
*/
setgid(e->gid);
setuid(e->uid);
chdir(env_get("HOME", e->envp));
/* käivitame käsu
*/
{
/* keskkonnamuutuja SHELL näitab tõlgendajat käivitamiseks */
char *shell = env_get("SHELL", e->envp);
/* protsess käivitub ilma vanemprotsessi keskkonna edastamiseta,
* st just nii, nagu on kirjeldatud kasutaja tööde tabelis */
execle(shell, shell, "-c", e->cmd, (char *)0, e->envp);
/* viga — ja protsess ei käivitunud? lõpetame töö */
perror("execl");
_exit(ERROR_EXIT);
}
break;
default:
/* protsess jätkab tööd: ootab lõpetamise ja väljundi */
break;
}Nii, see ongi kogu cron. Mõned huvitavad üksikasjad, nagu kaugkasutajate arvestamine, jätsin vahele, aga põhitähtsus on välja toodud.
Järelsõna
Cron — üllatavalt lihtne ja kasulik programm, mis on loodud parimates Unix-maailma traditsioonides. See ei tee midagi ülearust, kuid täidab oma ülesandeid suurepäraselt juba mitu aastakümmet. Ubuntu versiooni koodi tutvumine ei võtnud aega rohkem kui tund ja mulle tõi see suure rõõmu! Loodan, et sain seda ka teiega jagada.
Ma ei tea, kuidas teie jaoks, aga mul on natuke kurb mõista, et tänapäeva programmeerimine oma kalduvusega liialdamisele ja ülemõistetele ei paku ammu juba sellist lihtsust.
On palju tänapäevaseid alternatiive cronile: systemd-timers võimaldavad korraldada keerulisi süsteeme koos sõltuvustega, fcron'is saab paindlikumalt reguleerida ülesannete ressursside tarbimist. Aga isiklikult on mulle alati piisatud kõige lihtsamatest crontab'idest.
Kokkuvõttes, armastage Unix'i, kasutage lihtsaid programme ja ärge unustage lugeda man'e oma platvormi jaoks!
Allikas: habr.com
