Cron unter Linux: Verlauf, Verwendung und Gerät


Der Klassiker schrieb, dass Happy Hours nicht eingehalten werden. In diesen wilden Zeiten gab es weder Programmierer noch Unix, aber heutzutage wissen Programmierer sehr gut: Anstelle von ihnen wird cron der Zeit folgen.


Befehlszeilenprogramme sind für mich sowohl Schwäche als auch Routine. sed, awk, wc, cut und andere alte Programme werden täglich von Skripten auf unseren Servern ausgeführt. Viele von ihnen sind als Aufgaben für cron konzipiert, einen Planer aus den 70er Jahren.


Lange Zeit habe ich cron oberflächlich verwendet, ohne auf Details einzugehen, aber nachdem ich einmal einen Fehler beim Ausführen des Skripts festgestellt hatte, beschloss ich, es gründlich herauszufinden. So erschien dieser Artikel beim Schreiben, in dem ich POSIX crontab kennenlernte, die wichtigsten Cron-Varianten in populären Linux-Distributionen und das Gerät einiger von ihnen.


Verwenden Sie Linux und führen Sie Aufgaben in cron aus? Interessiert an der Unix-Systemanwendungsarchitektur? Dann sind wir unterwegs!


Inhalt



Herkunft der Arten


Die regelmäßige Ausführung von Benutzer- oder Systemprogrammen ist ein offensichtlicher Bedarf für alle Betriebssysteme. Der Bedarf an Diensten, die eine zentralisierte Planung und Ausführung von Aufgaben ermöglichen, haben Programmierer daher schon sehr lange erkannt.


Unix-ähnliche Betriebssysteme haben ihren Stammbaum von Version 7 Unix, das in den 1970er Jahren von Bell Labs entwickelt wurde, einschließlich des berühmten Ken Thompson. Zusammen mit Version 7 Unix wurde auch cron bereitgestellt, ein Dienst zur regelmäßigen Ausführung von Superuser-Aufgaben.


Ein typisches modernes Cron ist ein einfaches Programm, aber der Algorithmus der Originalversion war noch einfacher: Der Dienst wurde einmal pro Minute aktiviert, las das Task-Plate aus einer einzelnen Datei (/ etc / lib / crontab) und führte für den Superuser die Aufgaben aus, die in der aktuellen Minute ausgeführt werden sollten .


Anschließend wurden mit allen Unix-ähnlichen Betriebssystemen erweiterte Optionen für einen einfachen und nützlichen Dienst bereitgestellt.


Verallgemeinerte Beschreibungen des Crontab-Formats und der Grundprinzipien des Dienstprogramms im Jahr 1992 wurden in den Hauptstandard für Unix-ähnliche Betriebssysteme - POSIX - aufgenommen, und somit wurde Cron aus dem De-facto-Standard zum De-jure-Standard.


Im Jahr 1987 veröffentlichte Paul Vixie nach Befragung von Unix-Benutzern nach Vorschlägen für Cron eine weitere Version des Daemons, die einige der Probleme des herkömmlichen Cron behebt und die Syntax von Tabellendateien erweitert.


Mit der dritten Version begann Vixie cron, die Anforderungen von POSIX zu erfüllen. Außerdem verfügte das Programm über eine liberale Lizenz, oder es gab überhaupt keine Lizenz, außer für die Wünsche in README: Der Autor gibt keine Garantien, Sie können den Namen des Autors nicht löschen und Sie können das Programm nur mit verkaufen Quellcode. Diese Anforderungen erwiesen sich als kompatibel mit den Prinzipien der freien Software, die in jenen Jahren immer beliebter wurden. Einige der wichtigsten Linux-Distributionen, die Anfang der 90er Jahre erschienen, nahmen Vixie cron als System und entwickeln es noch weiter.


Insbesondere entwickeln Red Hat und SUSE die Vixie-Cron-Crony-Gabel, während Debian und Ubuntu das ursprüngliche Vixie-Cron mit vielen Patches verwenden.


Machen wir uns zunächst mit dem in POSIX beschriebenen benutzerdefinierten Dienstprogramm crontab vertraut. Anschließend analysieren wir die in Vixie cron vorgestellten Syntaxerweiterungen und die Verwendung von Vixie cron-Variationen in gängigen Linux-Distributionen. Und schließlich ist die Kirsche auf dem Kuchen eine Analyse des Cron Daemon-Geräts.


Posix crontab


Wenn der ursprüngliche Cron immer für den Superuser funktioniert hat, erledigen moderne Scheduler häufig die Aufgaben normaler Benutzer, was sicherer und bequemer ist.


Cron s werden mit zwei Programmen ausgeliefert: dem ständig laufenden Cron-Daemon und dem Crontab-Dienstprogramm, das den Benutzern zur Verfügung steht. Mit letzterem können Sie aufgabentabellen bearbeiten, die für jeden Benutzer im System spezifisch sind, während der Dämon Aufgaben aus Benutzer- und Systemtabellen startet.


Der POSIX-Standard beschreibt das Verhalten des Dämons nicht und nur das Crontab- Benutzerprogramm wird formalisiert. Das Vorhandensein von Mechanismen zum Starten von Benutzeraufgaben ist natürlich impliziert, wird jedoch nicht im Detail beschrieben.


Mit dem Dienstprogramm crontab können Sie vier Dinge tun: Bearbeiten Sie die Benutzeraufgabentabelle im Editor, laden Sie die Tabelle aus der Datei, zeigen Sie die aktuelle Aufgabentabelle an und löschen Sie die Aufgabentabelle. Beispiele für das Dienstprogramm crontab:


crontab -e #    crontab -l #    crontab -r #    crontab path/to/file.crontab #      

Beim Aufruf von crontab -e wird der in der Standard-Umgebungsvariablen EDITOR angegebene Editor verwendet.


Die Aufgaben selbst werden im folgenden Format beschrieben:


 # -  # # ,   * * * * * /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.log 

Die ersten fünf Datensatzfelder: Minuten [1..60], Stunden [0..23], Tage des Monats [1..31], Monate [1..12], Wochentage [0..6], wobei 0 - Sonntag. Das letzte, sechste Feld ist eine Zeichenfolge, die vom Standardbefehlsinterpreter ausgeführt wird.


In den ersten fünf Feldern können die Werte mit einem Komma aufgelistet werden:


 # ,         1,10 * * * * /path/to/exec -a -b -c 

Oder durch einen Bindestrich:


 # ,          0-9 * * * * /path/to/exec -a -b -c 

Der Benutzerzugriff auf die Aufgabenplanung wird in den POSIX-Dateien cron.allow und cron.deny geregelt, in denen jeweils Benutzer mit Zugriff auf crontab und Benutzer ohne Zugriff auf das Programm aufgelistet sind. Der Standard regelt nicht den Speicherort dieser Dateien.


Laufende Programme müssen gemäß dem Standard mindestens vier Umgebungsvariablen übergeben werden:


  1. HOME ist das Home-Verzeichnis des Benutzers.
  2. LOGNAME - Benutzeranmeldung.
  3. PATH ist der Pfad, über den die Standardsystemdienstprogramme gefunden werden.
  4. SHELL ist der Pfad zur verwendeten Shell.

Es ist bemerkenswert, dass POSIX nichts darüber aussagt, woher die Werte für diese Variablen stammen.


Bestseller - Vixie cron 3.0pl1


Der gemeinsame Vorfahr der beliebten Cron-Varianten ist Vixie Cron 3.0pl1, das auf der Mailingliste comp.sources.unix von 1992 aufgeführt ist. Die Hauptmerkmale dieser Version werden wir genauer betrachten.


Vixie Cron gibt es in zwei Programmen (Cron und Crontab). Wie üblich ist der Dämon für das Lesen und Starten von Aufgaben aus der Systemaufgabentabelle und den Aufgabentabellen einzelner Benutzer verantwortlich, und das Dienstprogramm crontab ist für das Bearbeiten von Benutzertabellen verantwortlich.


Aufgabentabelle und Konfigurationsdateien


Die Superuser-Aufgabentabelle befindet sich in / etc / crontab. Die Syntax der Systemtabelle entspricht der Syntax von Vixie cron, angepasst an die Tatsache, dass in der sechsten Spalte der Name des Benutzers angegeben ist, in dessen Auftrag die Aufgabe gestartet wird:


 #     vlad * * * * * vlad /path/to/exec 

Allgemeine Benutzeraufgabentabellen befinden sich in / var / cron / tabs / username und verwenden die allgemeine Syntax. Wenn das Dienstprogramm crontab gestartet wird, werden diese Dateien im Auftrag des Benutzers bearbeitet.


Die Benutzerlisten mit Zugriff auf crontab werden in den Dateien / var / cron / allow und / var / cron / verweigern verwaltet, wobei es ausreicht, den Benutzernamen als separate Zeile hinzuzufügen.


Erweiterte Syntax


Im Vergleich zu POSIX crontab enthält die Lösung von Paul Vixie einige sehr nützliche Änderungen an der Syntax der Utility-Task-Tabelle.


Eine neue Tabellensyntax ist verfügbar geworden: Sie können beispielsweise Wochentage oder Monate nach Namen angeben (Mo, Di usw.):


 #         * * * Jan Mon,Tue /path/to/exec 

Sie können den Schritt angeben, durch den Aufgaben gestartet werden:


 #       */2 * * * Mon,Tue /path/to/exec 

Schritte und Intervalle können gemischt werden:


 #             0-10/2 * * * * /path/to/exec 

Es werden intuitive Alternativen zur regulären Syntax unterstützt (Neustart, jährlich, jährlich, monatlich, wöchentlich, täglich, Mitternacht, stündlich):


 #     @reboot /exec/on/reboot #     @daily /exec/daily #     @hourly /exec/daily 

Task-Ausführungsumgebung


Mit Vixie cron können Sie die Umgebung laufender Anwendungen ändern.


Die Umgebungsvariablen USER, LOGNAME und HOME werden nicht nur vom Daemon bereitgestellt, sondern aus der passwd-Datei übernommen . Die PATH-Variable erhält den Wert "/ usr / bin: / bin" und SHELL den Wert "/ bin / sh". Die Werte aller Variablen außer LOGNAME können in Benutzertabellen geändert werden.


Einige Umgebungsvariablen (hauptsächlich SHELL und HOME) werden von cron selbst verwendet, um die Aufgabe auszuführen. So könnte es aussehen, wenn Sie bash anstelle des Standard-sh verwenden, um benutzerdefinierte Aufgaben auszuführen:


 SHELL=/bin/bash HOME=/tmp/ # exec   bash-  /tmp/ * * * * * /path/to/exec 

Letztendlich werden alle in der Tabelle definierten Umgebungsvariablen (von cron verwendet oder für den Prozess erforderlich) an die laufende Task übertragen.


Das Dienstprogramm crontab verwendet den in der Umgebungsvariablen VISUAL oder EDITOR angegebenen Editor zum Bearbeiten von Dateien. Wenn diese Variablen in der Umgebung, in der crontab gestartet wurde, nicht definiert sind, wird "/ usr / ucb / vi" verwendet (ucb ist wahrscheinlich die University of California, Berkeley).


Cron auf Debian und Ubuntu


Debian- und Derivate-Entwickler haben eine stark modifizierte Version von Vixie Cron Version 3.0pl1 veröffentlicht. Es gibt keine Unterschiede in der Syntax von Tabellendateien. Für Benutzer ist dies das gleiche Vixie-Cron. Größte neue Funktionen: Syslog- , SELinux- und PAM- Unterstützung.


Von den weniger auffälligen, aber greifbaren Änderungen - dem Speicherort der Konfigurationsdateien und Aufgabentabellen.


Benutzertabellen in Debian befinden sich im Verzeichnis / var / spool / cron / crontabs, die Systemtabelle befindet sich noch in / etc / crontab. Debian-spezifische Aufgabentabellen werden in /etc/cron.d abgelegt, von wo aus der Cron-Daemon sie automatisch liest. Die Benutzerzugriffskontrolle wird durch die Dateien /etc/cron.allow und /etc/cron.deny geregelt.


Die Standard-Shell / bin / sh wird weiterhin als Standard-Shell verwendet. Debian fungiert als kleine POSIX-kompatible Dash- Shell, die ohne Lesen einer Konfiguration gestartet wird (im nicht interaktiven Modus).


Cron selbst wird in den neuesten Versionen von Debian über systemd gestartet, und die Startkonfiguration kann unter /lib/systemd/system/cron.service eingesehen werden. Die Konfiguration des Dienstes enthält nichts Besonderes. Eine feinere Aufgabenverwaltung kann über Umgebungsvariablen erfolgen, die direkt in der Crontab jedes Benutzers deklariert sind.


cronie auf RedHat, Fedora und CentOS


cronie - Gabel von Vixie cron Version 4.1. Wie in Debian wurde die Syntax nicht geändert, aber die Unterstützung für PAM und SELinux, das Arbeiten in einem Cluster, das Verfolgen von Dateien mithilfe von inotify und andere Funktionen wurden hinzugefügt.


Die Standardkonfiguration befindet sich an den üblichen Stellen: Die Systemtabelle befindet sich in / etc / crontab, Pakete legen ihre Tabellen in /etc/cron.d ab, Benutzertabellen befinden sich in / var / spool / cron / crontabs.


Der Daemon wird unter systemd ausgeführt. Die Dienstkonfiguration lautet /lib/systemd/system/crond.service.


Beim Start verwenden Red Hat-ähnliche Distributionen standardmäßig / bin / sh, deren Rolle Standard-Bash ist. Es ist zu beachten, dass beim Ausführen von Cron-Tasks über / bin / sh die Bash-Shell im POSIX-kompatiblen Modus startet und im nicht interaktiven Modus keine zusätzliche Konfiguration liest.


cronie in SLES und openSUSE


Die deutsche SLES-Distribution und ihr openSUSE-Derivat verwenden dieselbe Cronie. Der Daemon wird hier auch unter systemd ausgeführt. Die Dienstkonfiguration befindet sich in /usr/lib/systemd/system/cron.service. Konfiguration: / etc / crontab, /etc/cron.d, / var / spool / cron / tabs. As / bin / sh verhält sich wie die gleiche Bash, die im POSIX-kompatiblen nicht interaktiven Modus gestartet wird.


Vixie Cron Gerät


Moderne Nachkommen von Cron haben sich im Vergleich zu Vixie Cron nicht radikal verändert, aber dennoch neue Fähigkeiten erworben, die nicht erforderlich sind, um die Prinzipien des Programms zu verstehen. Viele dieser Erweiterungen sind chaotisch und verwirren den Code. Der ursprüngliche Cron-Quellcode von Paul Vixie ist eine Freude zu lesen.


Aus diesem Grund habe ich mich entschlossen, das Cron-Gerät am Beispiel eines gemeinsamen Programms für beide Bereiche der Cron-Entwicklung zu analysieren - Vixie cron 3.0pl1. Ich werde die Beispiele vereinfachen, indem ich ifdefs entferne, die das Lesen erschweren, und die sekundären Details weglasse.


Die Arbeit des Dämons kann in mehrere Phasen unterteilt werden:


  1. Initialisierung des Programms.
  2. Sammeln und aktualisieren Sie die Liste der auszuführenden Aufgaben.
  3. Die Haupt-Cron-Loop-Operation.
  4. Aufgabenstart.

Sortieren wir sie der Reihe nach.


Initialisierung


Beim Start installiert cron nach Überprüfung der Prozessargumente die Signalhandler SIGCHLD und SIGHUP. Der erste protokolliert den Abschluss des untergeordneten Prozesses, der zweite schließt den Dateideskriptor der Protokolldatei:


 signal(SIGCHLD, sigchld_handler); signal(SIGHUP, sighup_handler); 

Der Cron-Daemon im System arbeitet immer alleine, nur als Superuser und aus dem Cron-Hauptverzeichnis. Die folgenden Aufrufe erstellen eine Dateisperre mit der PID des Daemon-Prozesses, stellen sicher, dass der Benutzer korrekt ist, und ändern das aktuelle Verzeichnis in das Hauptverzeichnis:


 acquire_daemonlock(0); set_cron_uid(); set_cron_cwd(); 

Der Standardpfad wird festgelegt, der beim Starten der Prozesse verwendet wird:


 setenv("PATH", _PATH_DEFPATH, 1); 

Dann wird der Prozess „dämonisiert“: Er erstellt eine untergeordnete Kopie des Prozesses, indem er fork und eine neue Sitzung im untergeordneten Prozess aufruft (Aufruf von setsid). Der übergeordnete Prozess ist nicht mehr erforderlich - und der Auftrag wird abgeschlossen:


 switch (fork()) { case -1: /*      */ exit(0); break; case 0: /*   */ (void) setsid(); break; default: /*     */ _exit(0); } 

Durch das Beenden des übergeordneten Prozesses wird die Sperre für die Sperrdatei aufgehoben. Außerdem müssen Sie die PID in der Datei auf das untergeordnete Element aktualisieren. Danach wird die Aufgabendatenbank gefüllt:


 /*    */ acquire_daemonlock(0); /*   */ database.head = NULL; database.tail = NULL; database.mtime = (time_t) 0; load_database(&database); 

Weitere Cron fahren mit dem Hauptarbeitszyklus fort. Schauen Sie sich vorher das Laden der Aufgabenliste an.


Sammeln und Aktualisieren der Aufgabenliste


Die Funktion load_database ist für das Laden der Aufgabenliste verantwortlich. Es überprüft die Crontab des Hauptsystems und das Verzeichnis mit Benutzerdateien. Wenn sich die Dateien und das Verzeichnis nicht geändert haben, wird die Liste der Aufgaben nicht erneut gelesen. Andernfalls beginnt sich eine neue Aufgabenliste zu bilden.


Herunterladen einer Systemdatei mit speziellen Datei- und Tabellennamen:


 /*     ,  */ if (syscron_stat.st_mtime) { process_crontab("root", "*system*", SYSCRONTAB, &syscron_stat, &new_db, old_db); } 

Laden von Benutzertabellen in einer Schleife:


 while (NULL != (dp = readdir(dir))) { char fname[MAXNAMLEN+1], tabname[MAXNAMLEN+1]; /*      */ 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); } 

Dann wird die alte Datenbank durch eine neue ersetzt.


In den obigen Beispielen stellt das Aufrufen der Funktion process_crontab sicher, dass der Benutzer vorhanden ist, der dem Tabellendateinamen entspricht (es sei denn, es handelt sich um den Superuser), und ruft dann load_user auf. Letzterer liest die Datei selbst bereits Zeile für Zeile:


 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; } } 

Hier wird entweder die Umgebungsvariable (Zeilen der Form VAR = Wert) durch die Funktionen load_env / env_set festgelegt oder die Aufgabenbeschreibung (* * * * * / path / to / exec) wird von der Funktion load_entry gelesen.


Die von load_entry zurückgegebene Eintragsentität ist unsere Aufgabe, die in der allgemeinen Liste der Aufgaben aufgeführt ist. In der Funktion selbst wird eine lange Analyse des Zeitformats durchgeführt, aber wir sind mehr an der Bildung von Umgebungsvariablen und Startparametern für Aufgaben interessiert:


 /*         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); 

Der Hauptzyklus arbeitet auch mit der aktuellen Liste der Aufgaben.


Hauptzyklus


Das ursprüngliche Cron aus Version 7 Unix funktionierte ganz einfach: In einem Zyklus las ich die Konfiguration erneut, führte die Aufgaben der aktuellen Minute als Superuser aus und schlief bis zum Beginn der nächsten Minute. Dieser einfache Ansatz auf älteren Maschinen erforderte zu viele Ressourcen.


In SysV wurde eine alternative Version vorgeschlagen, bei der der Dämon entweder bis zur nächsten Minute, für die die Aufgabe definiert wurde, oder für 30 Minuten einschlief. Ressourcen zum erneuten Lesen der Konfiguration und zum Überprüfen von Aufgaben in diesem Modus wurden weniger verbraucht, es wurde jedoch unpraktisch, die Liste der Aufgaben schnell zu aktualisieren.


Vixie cron überprüfte einmal pro Minute wieder die Aufgabenlisten, da die Ressourcen auf Standard-Unix-Computern Ende der 80er Jahre viel größer geworden waren:


 /*    */ load_database(&database); /*  ,       */ run_reboot_jobs(&database); /*  TargetTime    */ cron_sync(); while (TRUE) { /*  ,     TargetTime    ,    */ cron_sleep(); /*   */ load_database(&database); /*      */ cron_tick(&database); /*  TargetTime     */ TargetTime += 60; } 

Die Funktion cron_sleep, die die Funktionen job_runqueue (Aufzählung und Start von Aufgaben) und do_command (Beginn jeder einzelnen Aufgabe) aufruft, ist direkt an der Ausführung von Aufgaben beteiligt. Die letzte Funktion sollte genauer betrachtet werden.


Aufgabenstart


Die Funktion do_command wird in einem guten Unix-Stil ausgeführt, dh sie verzweigt sich für die asynchrone Ausführung von Aufgaben. Der übergeordnete Prozess startet weiterhin Aufgaben, der untergeordnete Prozess bereitet den Aufgabenprozess vor:


 switch (fork()) { case -1: /*   fork */ break; case 0: /*  :          */ acquire_daemonlock(1); /*      */ child_process(e, u); /*       */ _exit(OK_EXIT); break; default: /*     */ break; } 

In child_process steckt viel Logik: Es nimmt Standardausgaben und Fehlerflüsse auf sich selbst auf, sodass es dann an Mail gesendet werden kann (wenn die Umgebungsvariable MAILTO in der Aufgabentabelle angegeben ist), und wartet schließlich auf den Abschluss des Hauptaufgabenprozesses.


Der Aufgabenprozess wird von einer anderen Gabelung gebildet:


 switch (vfork()) { case -1: /*      */ exit(ERROR_EXIT); case 0: /* -   ,   .. */ (void) setsid(); /* *     ,    */ /*  ,    , *       */ setgid(e->gid); setuid(e->uid); chdir(env_get("HOME", e->envp)); /*    */ { /*   SHELL      */ char *shell = env_get("SHELL", e->envp); /*       , *    ,       */ execle(shell, shell, "-c", e->cmd, (char *)0, e->envp); /*  —    ?   */ perror("execl"); _exit(ERROR_EXIT); } break; default: /*    :      */ break; } 

Hier im Allgemeinen und die ganze Cron. Ich habe einige interessante Details ausgelassen, zum Beispiel die Berücksichtigung von Remotebenutzern, aber die Hauptsache skizziert.


Nachwort


Cron ist ein überraschend einfaches und nützliches Programm, das in den besten Traditionen der Unix-Welt erstellt wurde. Sie macht nichts Überflüssiges, aber sie macht ihren Job in den letzten Jahrzehnten bemerkenswert. Das Kennenlernen des Codes der mit Ubuntu gelieferten Version dauerte nicht länger als eine Stunde und ich hatte viel Spaß! Hoffe ich konnte es mit dir teilen.


Ich weiß nichts über Sie, aber es ist ein wenig traurig für mich zu erkennen, dass die moderne Programmierung mit ihrer Tendenz, sich zu komplizieren und zu abstrahieren, längst nicht mehr so ​​einfach ist.


Es gibt viele moderne Alternativen zu cron: Mit systemd-timern können Sie komplexe Systeme mit Abhängigkeiten organisieren. In fcron können Sie den Ressourcenverbrauch nach Aufgaben flexibler steuern. Aber ich persönlich hatte immer die einfachste Crontab.


Mit einem Wort, liebe Unix, benutze einfache Programme und vergiss nicht, Mana für deine Plattform zu lesen!

Source: https://habr.com/ru/post/de468061/


All Articles