Schwer fassbare Liste der installierten Windows-Updates

Haben Sie sich jemals gefragt, warum eine Liste der installierten Windows-Updates generiert wird? Und über welche API soll es kommen? Ich werde versuchen, in dieser kleinen Studie Antworten auf diese und andere aufkommende Fragen zu geben.



Hintergrund oder wie alles begann.


Jedes Jahr findet in unserem Unternehmen eine Konferenz junger Fachkräfte statt, auf der jeder Teilnehmer das Problem einer Abteilung lösen kann (eine Themenliste wird im Voraus vorgeschlagen). Und die Abteilung für SPAS (Software- und Hardware-Support) hatte die folgende Aufgabe, die mich interessierte, und es ermöglichte mir, wieder zur Programmierung zurückzukehren (leider arbeite ich derzeit in diesem Unternehmen als einfacher Betreiber von NPPS).

Bisher wurden für jedes "TO" mit Hilfe von WSUS alle veröffentlichten Updates abgerufen und an alle Computer verteilt. In regelmäßigen Abständen erschienen auch TSB (Technical Service Bulletins), die darauf hinwiesen, dass die erforderlichen Updates in Form von isolierten Paketen installiert werden mussten. Infolgedessen sammeln wir Updates, die in WSUS nicht nachverfolgt werden können, aber nur über das Control Panel im Abschnitt "Installierte Updates" angezeigt werden können.



Visuelles Aktualisierungsschema

Es gibt Situationen, in denen die Workstation oder der Server "abstürzt" und Sie sie von einem vor einiger Zeit erstellten Image wiederherstellen müssen. Bei der Wiederherstellung von einem Image besteht die Möglichkeit, dass wir die benötigten Updates (die in Form von isolierten Paketen geliefert wurden) verlieren, die vor dem Absturz des Computers installiert wurden. So detailliert wie möglich erklärt, da die Klarstellungen bereits ein Geschäftsgeheimnis sein werden.

Aus diesem Grund entstand die Idee, ein Programm zu erstellen, mit dem diese Liste von Aktualisierungen (vorzugsweise remote über das lokale Netzwerk) extrahiert, in eine Datei / Datenbank geschrieben, die aktuelle Liste mit einer bestimmten Vorlage verglichen und über eines der Protokolle - SNMP, OPC - eine Nachricht an das SCADA-System gesendet werden kann.

Wie Sie vielleicht aus dem Titel des Artikels erraten haben, hatte ich bereits eine schwierige Aufgabe, die Methode zum Abrufen von Listen auszuwählen. Wie üblich habe ich mich entschlossen, in der Suchmaschine nach dem richtigen zu suchen, Fragen zu speziellen Ressourcen gestellt ( eins , zwei , aus irgendeinem Grund mochte der Stapelüberlauf auf Englisch meine Frage nicht und musste gelöscht werden), aber alle Antworten ergaben nicht das gewünschte Ergebnis. Deshalb musste ich es selbst herausfinden, worauf später noch eingegangen wird.

Konsolenbefehle


Beginnen wir mit einem einfachen und nutzen Sie das, was Windows uns bietet, ohne Tools von Drittanbietern zu verwenden. Dies kann mit den folgenden Befehlen erfolgen:

  • wmic qfe liste
  • systeminfo
  • dism / online / get-packages
  • über PowerShell:

    • Holen Sie sich Hotfix
    • Get-SilWindowsUpdate (nur in Server-Editionen verfügbar)
    • Get-WmiObject -Class win32_quickfixengineering - durch Zugriff auf die WMI-Klasse win32_quickfixengineering (über WMI etwas später)



Sie können die Liste über die grafische Oberfläche über das Standardelement der Systemsteuerung "Software" abrufen, aber wir können von dort nichts kopieren. Jedes Control Panel-Tool wird durch eine .cpl-Datei im Ordner Windows \ System dargestellt. CPL-Dateien im Windows-Systemordner werden beim Starten der Systemsteuerung automatisch heruntergeladen. Die Datei Appwiz.cpl ist für das Programmelement verantwortlich. Seine Analyse führte zu nichts.

Die Ausgabe des Konsolenbefehls kann in eine Datei umgeleitet und dann analysiert werden. Dies ist jedoch falsch, plus ein Programmaufruf (nach den Regeln des Sicherheitsrates funktioniert dies nicht) und es besteht keine Frage, ob die Liste remote empfangen werden soll. Daher schlage ich vor, dass Sie einfach die Befehle aufrufen, die Anzahl der Aktualisierungen in jeder Liste mit der Liste über die Systemsteuerung vergleichen und unsere Untersuchung fortsetzen.

Formal können alle Methoden zum Abrufen der Liste der Aktualisierungen in zwei Gruppen unterteilt werden: lokal und Netzwerk.

Lokale und Netzwerkmethoden zum Abrufen von Informationen

Alle Methoden wurden an sauberen Systemabbildern (Windows 7, 8, Server 2012 R2) mit integrierten Updates getestet. Nach jedem Update über das Update Center von offiziellen Microsoft-Servern wurde eine zusätzliche Überprüfung durchgeführt. Lassen Sie uns näher auf jeden einzelnen eingehen.

WUA


WUApi (Windows Update Agent-API) - Verwenden der Windows Update Agent-API. Die naheliegendste Option, deren Name für sich selbst spricht. Wir werden dafür die Wuapi.dll-Bibliothek verwenden.
Hinweis: Im Folgenden werde ich zur Vereinfachung alle Ergebnisse in die Liste einbetten. Das mag nicht rational sein, aber dann schien es mir eine gute Idee zu sein.
Implementierungsbeispiel
using WUApiLib; public static List<string> listUpdateHistory() { //WUApi List<string> result = new List<string>(200); try { UpdateSession uSession = new UpdateSession(); IUpdateSearcher uSearcher = uSession.CreateUpdateSearcher(); uSearcher.Online = false; ISearchResult sResult = uSearcher.Search("IsInstalled=1 And IsHidden=0"); string sw = "   WUApi: " + sResult.Updates.Count; result.Add(sw); foreach (WUApiLib.IUpdate update in sResult.Updates) { result.Add(update.Title); } } catch (Exception ex) { result.Add("-   : " + ex.Message); } return result; } 


Es gibt eine zweite Variante dieser Methode: Update-Sitzung - Empfangen von Informationen durch Herstellen einer Verbindung zur Windows Update Agent-Update-Sitzung (in diesem Fall arbeiten wir nicht direkt mit der Bibliothek).

Implementierungsbeispiel
 public static List<string> Sessionlist(string pc) { List<string> result = new List<string>(50); //    object sess = null; object search = null; object coll = null; try { sess = Activator.CreateInstance(Type.GetTypeFromProgID("Microsoft.Update.Session", pc)); search = (sess as dynamic).CreateUpdateSearcher(); int n = (search as dynamic).GetTotalHistoryCount(); int kol = 0; //coll = (search as dynamic).QueryHistory(1, n); coll = (search as dynamic).QueryHistory(0, n); result.Add("  Update.Session: " + n); foreach (dynamic item in coll as dynamic) { if (item.Operation == 1) result.Add(item.Title); kol++; //Console.WriteLine(": " + kol); } result.Add("  : " + kol); } catch (Exception ex) { result.Add("-   : " + ex.Message); } finally { if (sess != null) Marshal.ReleaseComObject(sess); if (search != null) Marshal.ReleaseComObject(search); if (coll != null) Marshal.ReleaseComObject(coll); } return result; } 


Microsoft schlägt die Remote- Verwendung der API vor .

Die Hauptnachteile dieser beiden Methoden bestehen darin, dass Sie keine KB-Fixes finden können, die nicht über Windows Update verteilt werden. Sie können nur sehen, was durch den Update-Agenten selbst gegangen ist, dh diese Option passt nicht zu uns.

DISM


Wartung und Verwaltung von Bereitstellungsimages ist ein Befehlszeilentool, mit dem ein Windows-Image gewartet oder ein Image einer Windows-Vorinstallationsumgebung (Windows PE) vorbereitet werden kann. Es ist ein Ersatz für Package Manager (Pkgmgr.exe), PEimg und Intlcfg.

Dieses Dienstprogramm wird verwendet, um Updates und Service Packs in das System-Image zu integrieren. Windows-Updates sind separate Module, die auf verschiedene Arten dargestellt werden können:

  • CAB-Dateien (Kabinett) - Archive. Entwickelt für die Verteilung und Installation mit Windows Update-Modulen in einem automatisierten Modus.
  • MSU-Dateien (Microsoft Update Standalone Package) - ausführbare Dateien. Entwickelt für die Verteilung und Installation durch Benutzer selbst im manuellen Modus über den Microsoft Update-Katalog. Tatsächlich handelt es sich um eine Paketmenge, die aus CAB-, XML- und TXT-Dateien besteht.

Der zuvor erwähnte Befehl dism / online / get-packages zeigt grundlegende Informationen zu allen Paketen im wim-Image / aktuellen System an. Microsoft hat sich um uns gekümmert und bietet NuGet-Pakete für die bequeme Verwendung der API an.

Implementierungsbeispiel
 using Microsoft.Dism; public static List<string> DISMlist() { List<string> result = new List<string>(220); try { DismApi.Initialize(DismLogLevel.LogErrors); var dismsession = DismApi.OpenOnlineSession(); var listupdate = DismApi.GetPackages(dismsession); int ab = listupdate.Count; //Console.WriteLine("   DISM: " + ab); string sw = "   DISM: " + ab; result.Add(sw); foreach (DismPackage feature in listupdate) { result.Add(feature.PackageName); //result.Add($"[ ] {feature.PackageName}"); //result.Add($"[ ] {feature.InstallTime}"); //result.Add($"[ ] {feature.ReleaseType}"); } } catch (Exception ex) { result.Add("-   : " + ex.Message); } return result; } 


Die Anzahl der Aktualisierungen stimmte mit der Anzahl aus der Liste der Systemsteuerung bis zur ersten Aktualisierung über das Kontrollzentrum überein - danach wurde die Anzahl der Aktualisierungen geringer (214, 209), obwohl sie logischerweise zunehmen sollten. Ausgabebeispiele Vor dem Aktualisieren , Nach dem Aktualisieren .

Was der Grund dafür ist, kann ich nur spekulieren - vielleicht haben einige Updates die vorherigen ersetzt, daher wurde die Anzahl geringer.

Wenig später stieß ich auf ein Dienstprogramm aus dem chinesischen DISM ++ , das nicht auf der DISM-API oder der DISM Core-API basiert, aber in den Bibliotheken nicht die Methoden haben, die ich brauche, also habe ich diese Idee aufgegeben und weiter gesucht.

WSUS


Windows Server Update Services ( WSUS ) ist ein Server zum Aktualisieren von Betriebssystemen und Microsoft-Produkten. Der Update-Server wird mit der Microsoft-Website synchronisiert und lädt Updates herunter, die im Unternehmens-LAN verteilt werden können. Wieder ein spezielles Tool, das für die Arbeit mit Updates entwickelt wurde.

Wird nur auf Server-Editionen von Windows verteilt, daher wurde der folgende Stand bereitgestellt:

  • Das Hauptsystem ist Windows Server 2016.
  • Über das Hyper-V-Virtualisierungssystem wurden zwei Client-Betriebssysteme bereitgestellt:
    • Windows 8.1
    • Windows 7


Alle Systeme sind mit einem einzigen virtuellen lokalen Netzwerk verbunden, jedoch ohne Zugang zum Internet .

Einige Tipps
Um dem neuen System keine Festplattenpartition zuzuweisen, verwende ich WinNTSetup und installiere das System auf VHD-Festplatten. Der Bootloader, der mit Windows 7 (Professional / Ultimate-Editionen) beginnt, kann hervorragend von einem Festplatten-Image booten. Die so erhaltenen Scheiben können sicher in Hyper-V verwendet werden - Sie töten zwei Fliegen gleichzeitig mit einer Klappe. Denken Sie daran, vorab eine Kopie des BCD-Repositorys mit dem Befehl bcdedit / export e: \ bcd_backup.bcd zu erstellen .

Ich wollte AD nicht für die Verteilung von Updates konfigurieren, daher habe ich den Pfad zum WSUS-Server einfach in Gruppenrichtlinien registriert:

Einstellungen

Achten Sie unbedingt auf den Port, da ich aufgrund eines Tippfehlers (8350 statt 8530) keine Updates auf Client-Computern erhalten konnte, obwohl alles korrekt ausgeführt wurde. Außerdem unterscheiden sich die Namen der Elemente in Gruppenrichtlinien unter Windows 7 und Windows 8.

Um den Bericht über WSUS zu erhalten, müssen Sie das Paket zusätzlich installieren - das System benachrichtigt Sie darüber.

Und jetzt ein kleiner Code
 //      using Microsoft.UpdateServices.Administration; public static List<string> GetWSUSlist(params string[] list) { List<string> result = new List<string>(200); //    string namehost = list[0]; // ,     string = "example1"; string servername = list[1]; //  string = "WIN-E1U41FA6E55"; string Username = list[2]; string Password = list[3]; try { ComputerTargetScope scope = new ComputerTargetScope(); IUpdateServer server = AdminProxy.GetUpdateServer(servername, false, 8530); ComputerTargetCollection targets = server.GetComputerTargets(scope); // Search targets = server.SearchComputerTargets(namehost); // To get only on server FindTarget method IComputerTarget target = FindTarget(targets, namehost); result.Add(" : " + target.FullDomainName); IUpdateSummary summary = target.GetUpdateInstallationSummary(); UpdateScope _updateScope = new UpdateScope(); // See in UpdateInstallationStates all other properties criteria //_updateScope.IncludedInstallationStates = UpdateInstallationStates.Downloaded; UpdateInstallationInfoCollection updatesInfo = target.GetUpdateInstallationInfoPerUpdate(_updateScope); int updateCount = updatesInfo.Count; result.Add(" -   - " + updateCount); foreach (IUpdateInstallationInfo updateInfo in updatesInfo) { result.Add(updateInfo.GetUpdate().Title); } } catch (Exception ex) { result.Add("-   : " + ex.Message); } return result; } public static IComputerTarget FindTarget(ComputerTargetCollection coll, string computername) { foreach (IComputerTarget target in coll) { if (target.FullDomainName.Contains(computername.ToLower())) return target; } return null; } 


Da es kein Internet gibt, sieht die Situation mit Updates wie im folgenden Screenshot aus:



Das Verhalten ist ähnlich wie bei WUApi - wenn Updates nicht durchlaufen wurden, wissen sie nichts darüber. Daher funktioniert diese Methode nicht mehr.

Wmi


Windows Management Instrumentation ( WMI ) in der wörtlichen Übersetzung ist ein Windows-Verwaltungs-Toolkit.

WMI ist ein von Microsoft implementierter Standard für die Verwaltung eines Unternehmens über das Internet zur zentralen Verwaltung und Überwachung verschiedener Teile einer Computerinfrastruktur, auf der eine Windows-Plattform ausgeführt wird. WMI ist ein offenes einheitliches System von Zugriffsschnittstellen zu allen Parametern des Betriebssystems, der Geräte und Anwendungen, die darin ausgeführt werden.

Mit dieser Methode können Sie Daten sowohl vom lokalen Computer als auch remote im lokalen Netzwerk empfangen. Für den Zugriff auf WMI-Objekte wird eine bestimmte WMI-Abfragesprache (WQL) verwendet, eine der Varianten von SQL. Wir erhalten die Liste über die WMI-Klasse win32_quickfixengineering .

Implementierungsbeispiel
 using System.Management; public static List<string> GetWMIlist(params string[] list) { List<string> result = new List<string>(200); //    ManagementScope Scope; string ComputerName = list[0]; string Username = list[1]; string Password = list[2]; int kol = 0; if (!ComputerName.Equals("localhost", StringComparison.OrdinalIgnoreCase)) { //    ,      //  . ConnectionOptions Conn = new ConnectionOptions(); Conn.Username = Username; Conn.Password = Password; //      «NTLMDOMAIN:»  NTLM  ,       NTLM. Conn.Authority = "ntlmdomain:DOMAIN"; Scope = new ManagementScope(String.Format("\\\\{0}\\root\\CIMV2", ComputerName), Conn); } else Scope = new ManagementScope(String.Format("\\\\{0}\\root\\CIMV2", ComputerName), null); try { Scope.Connect(); ObjectQuery Query = new ObjectQuery("SELECT * FROM Win32_QuickFixEngineering"); ManagementObjectSearcher Searcher = new ManagementObjectSearcher(Scope, Query); foreach (ManagementObject WmiObject in Searcher.Get()) { result.Add(WmiObject["HotFixID"].ToString()); //Console.WriteLine("{0,-35} {1,-40}", "HotFixID", WmiObject["HotFixID"]);// String //result.Add(); /*result.Add("{0,-17} {1}", " : ", WmiObject["Description"]); result.Add("{0,-17} {1}", ": ", WmiObject["Caption"]); result.Add("{0,-17} {1}", " : ", WmiObject["InstalledOn"]);*/ kol++; } result.Add("  " + kol); } catch (Exception ex) { result.Add("-   : " + ex.Message); } return result; } 


Quantitativ stimmt alles überein (auch nach Aktualisierungen), daher wurde beschlossen, diese Methode zu verwenden. Für die programmgesteuerte Erstellung von WMI-Anforderungen empfehle ich Ihnen, das folgende Dienstprogramm zu verwenden - WMI Delphi Code Creator . Dank ihr habe ich meinen Code etwas anders betrachtet und beschlossen, ein Leerzeichen aus diesem Programm zu verwenden.

XML


Die mit der WMI-Methode erhaltenen Daten haben mich nicht aufgehalten, und ich habe mich für das „Surface Reverse Engineering“ entschieden. Wir werden das Process Monitor- Dienstprogramm aus der Sysinternals Suite- Softwaresammlung verwenden, um Dateien und Registrierungszweige zu identifizieren, die beim Aufrufen der oben aufgeführten Konsolenbefehle und beim Zugriff auf das Element "Installierte Updates" über die Systemsteuerung verwendet werden.

Meine Aufmerksamkeit wurde auf die Datei wuindex.xml im Ordner C: \ Windows \ servicing \ Packages \ gelenkt. Um es zu analysieren, wurde das folgende Programm geschrieben:

Anwendungsbeispiel für die Konsole
 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml; using System.Text.RegularExpressions; using System.IO; namespace XMLviewer { class Program { static void Main(string[] args) { string writePath = AppDomain.CurrentDomain.BaseDirectory + "XML  " + Environment.MachineName + ".txt"; if (!File.Exists(writePath)) { Console.WriteLine("  txt "); } else { Console.WriteLine(" XML .txt ,   "); File.Delete(writePath); } //      KB Regex regex = new Regex(@"KB[0-9]{6,7}"); //Regex(@"(\w{2}\d{6,7}) ?"); //SortedSet    ,     ""     SortedSet<string> spisok = new SortedSet<string>(); XmlDocument xDoc = new XmlDocument(); string path = "C:\\Windows\\servicing\\Packages\\wuindex.xml"; //   xml xDoc.Load(path); int kol = 0; //-  int total = 0; //-    xml int total2 = 0; //-   XmlNodeList name = xDoc.GetElementsByTagName("Mappings"); foreach (XmlNode xnode in name) { //Console.WriteLine(xnode.Name); kol++; XmlNode attr = xnode.Attributes.GetNamedItem("UpdateId"); //Console.WriteLine(attr.Value); foreach (XmlNode childnode in xnode.ChildNodes) { XmlNode childattr = childnode.Attributes.GetNamedItem("Package"); total++; //Console.WriteLine(childattr.Value); MatchCollection matches = regex.Matches(childattr.Value); if (matches.Count > 0) { foreach (Match match in matches) //Console.WriteLine(match.Value); spisok.Add(match.Value); } else { //Console.WriteLine("  "); } } } try { StreamWriter sw = new StreamWriter(writePath); foreach (string element in spisok) { //Console.WriteLine(element); sw.WriteLine(element); total2++; } sw.Close(); } catch (Exception ex) { Console.WriteLine(": " + ex.Message); } //Console.WriteLine("\n"); Console.WriteLine(" : " +kol); Console.WriteLine("    xml: " + total); Console.WriteLine(" KB : " + total2); Console.WriteLine("    ."); Console.Read(); } } } 


Leider ist diese Datei nicht auf allen Systemen zu finden, und das Prinzip ihrer Erzeugung und Aktualisierung ist mir ein Rätsel geblieben. Daher passt diese Methode auch hier nicht zu uns.

Cbs


Hier kommen wir zu dem, womit all diese Methoden verbunden sind. Als ich die Analyse der Process Monitor-Protokolle fortsetzte, identifizierte ich die folgenden Ordner und Dateien.

Die Datei DataStore.edb im Ordner C: \ Windows \ SoftwareDistribution \ DataStore . Dies ist eine Datenbank, die den Verlauf aller Aktualisierungen der installierten Windows-Version enthält, einschließlich derjenigen, die nur in der Warteschlange stehen.

Das Programm ESEDatabaseView wurde zum Analysieren der Datei DataStore.edb verwendet. In der Datenbank befindet sich eine tbUpdates-Tabelle, deren Inhalt schwer zu interpretieren ist.

TbUpdates-Tabelle in ESEDatabaseView

Nachdem ich auf den TiWorker.exe- Prozess aufmerksam gemacht wurde, der jedes Mal aufgerufen wurde, wenn ich ein Element in der Systemsteuerung öffnete. Er "ging" durch viele Ordner, von denen einer mich auf den richtigen Weg führte.

C: \ Windows \ SoftwareDistribution ist ein Ordner, in dem Windows Update Updates auf einen Computer herunterlädt und installiert. Außerdem werden Informationen zu allen zuvor installierten Updates gespeichert.

WinSxS-Ordner unter C: \ Windows \ winsxs . Dies ist der Dienstordner des Windows-Betriebssystems, in dem zuvor installierte Versionen von Systemkomponenten gespeichert werden. Aufgrund des Vorhandenseins ist es bei Bedarf möglich, ein Rollback auf eine ältere Version des Updates durchzuführen.

C: \ Windows \ servicing - die Hauptkomponente des gesamten Systems, deren Name Component-Based Servicing (CBS) lautet.

CBS ist ein komponentenbasierter Dienst, der Teil von Windows ist und in den Windows Update-Dienst integriert ist. Im Gegensatz zum FBS-Dienst ( File-Based Servicing) (für Betriebssysteme vor Windows Vista), bei dem Dateien direkt in den Systemverzeichnissen aktualisiert wurden, führte CBS eine ganze Hierarchie von Verzeichnissen und eine ganze Familie (Stapel) von Modulen / Dienstbibliotheken ein.

CbsApi.dll ist die Hauptbibliothek für die CBS-Technologieunterstützung. Es gibt keine offenen Methoden, daher konnte ich es nicht direkt verwenden. Microsoft verwendet TrustedInstaller.exe und TiWorker.exe, um auf die Methoden dieser Bibliothek zuzugreifen, und zeigt bereits über diese Prozesse die benötigten Daten an. Datensätze werden in C: \ Windows \ Logs \ CBS \ CBS.log verwaltet .

Zum Zeitpunkt der Erstellung des Prototyps des Programms (Sie können den Mai 2019 in den Screenshots sehen) gab es keine russischsprachigen Informationen über CBS, aber Ende August gab es einen sehr guten Blog-Artikel - http://datadump.ru/component-based-servicing . Ein sehr interessanter Artikel, der meine Erfahrung bestätigte und die notwendigen Informationen sammelte. Und mehr zum Thema: http://www.outsidethebox.ms/17988/

Fazit


Microsoft hat die triviale Aufgabe, eine Liste mit Updates zu erhalten, zu kompliziert gemacht und diesen Prozess nicht ganz offensichtlich gemacht. All dies geschieht aus Sicherheitsgründen, jedoch nicht aus Gründen der Benutzerfreundlichkeit. Ich stimme dem Autor des Artikels zu - Vorhersehbarkeit und Transparenz fehlten beim Erhalt von Updates.

Als Ergebnis der Studie wurde das folgende Programm geschrieben, dessen Demonstration in diesem Video zu sehen ist:


Die Pläne hinzuzufügen:

  1. Vergleichen der Liste der erforderlichen Aktualisierungen mit der empfangenen;
  2. Senden Sie das Ergebnis über SNMP / OPC (wenn jemand Erfahrung hat, teilen Sie es in den Kommentaren mit);
  3. Organisieren Sie die Installation der fehlenden "Offline" -Updates aus dem angegebenen Ordner.

Wenn Sie mehr Methoden kennen, um nicht nur eine Liste mit Updates, sondern auch zusätzlichen Komponenten (Adobe Flash, Acrobat Reader usw.) zu erhalten, oder andere interessante Vorschläge haben, schreiben Sie darüber in den Kommentaren oder in privaten Nachrichten - ich freue mich über Feedback . Und nehmen Sie an der Umfrage zu diesem Artikel teil - damit ich weiß, ob meine Erfahrungen mit dem Habrahabr-Publikum interessant sein werden.

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


All Articles