Klassik neu verpackt (Linux-Magazin, Oktober 2018)

Jedes Mal, wenn ich meinen Laptop zu Diagnosezwecken an einen Router anschließe, stellt sich die Frage, unter welcher dynamische zugewiesenen IP-Adresse der Router den Laptop sieht. Schließlich gilt es, die Routeradresse auf demselben Subnet in ein Browserfenster einzutippen, damit die Admin-Seite des Routers erscheint.

Hierzu tippe ich einige Male hektisch "ifconfig" in ein Terminalfenster und fiesle aus dem gedruckten Datenverhau unter "inet 192.168.1.1 netmask 0xfffffff0" die gewünschte Adresse heraus. Das müsste doch einfacher gehen! Wie wäre es mit einer sortierten Liste aller Netzwerkadapter der Geräts und deren zugewiesenen IP-Adressen? Auf dem hochpoppenden grafischen User Interface könnte der erfreute User dann dynamisch verfolgen, wie in einer bis dato statischen Liste ein soeben eingestöpselter USB-Adapter als neues Netzwerk-Interface samt per DHCP zugewiesener IP erscheint.

Aber halt, muss es dafür gleich eine richtige grafische Applikation sein, wie kürzlich im Programmier-Snapshot vorgestellt, und elegant mit Githubs Elektron-Framework programmiert ([2])? Freunde der Kommandozeile bevorzugen Terminal-UIs a la "top", denn diese lassen sich schnell starten, auslesen, und wieder schließen, ohne dass schnelle Tipper überhaupt das Terminalfenster verlassen oder zur ungeliebten Maus greifen müssen.

Abbildung 1: Dieser Rechner nutzt eine IP-Adresse im Subnet 192.168.147.1/24, um sich mit dem Router zu verbinden.

Abbildung 2: Das Go-Programm zeigt eine dynamische Liste mit Netzwerkanschlüssen an.

Bereits in petto

Was das Kommandozeilen-Tool ifconfig ausspuckt, hat das "net"-Paket in Go bereits in petto, und stellt es in Form einer Datenstruktur zur Schau. Listing 1 zeigt die Implementierung eines Helfer-Pakets ifconfig, das eine Funktion AsStrings() exportiert, die eine formatierte Liste aller erkannten Netzwerk-Interfaces auf dem laufenden Rechner zurückgibt.

Die Methode Interfaces() des Go-Pakets net gibt hierzu in Zeile 13 von Listing 1 eine Reihe von Netzwerk-Interface-Structs zurück, welche die for-Schleife ab Zeile 14 mittels range abklappert. Die range()-Funktion auf das gelieferte Slice (eine Art dynamischer Liste in Go) liefert pro Schleifendurchgang nicht nur das aktuelle Element, sondern auch gleich noch dessen Index im Slice hinzu, der hier aber nicht benötigt und deshalb der Pseudo-Variablen _ (Unterstrich) zugewiesen und damit weggeworfen wird.

Der String-Formatierer in Zeile 15 setzt die Variable network auf den Namen des Interfaces (zum Beispiel eth0 für den ersten gefundenen Ethernet-Adapter), und zwar mit einer Maximallänge von 10 Zeichen rechtsbündig formatiert. Die IP-Adressen, auf die das Interface hört, holt die Funktion Addrs() zutage. Wie in Go üblich, gibt sie zwei Parameter zurück, erst einen Slice mit gefundenen IPs und als dann eine Error-Variable, die hoffentlich auf nil gesetzt ist und damit anzeigt, dass alles gutgegangen ist. In Listing 1 in Zeile 17 ist die zweite Variable allerdings aus Platzspargründen auf _ (Unterstricht) gesetzt, und wirft damit etwaige Fehler weg, was in einem Produktionssystem freilich unterbleiben sollte.

Falls keine IP zugewiesen wurde, ist das gefundene Netzwerk-Interface offensichtlich nicht von Belang, und Zeile 19 springt mit continue zum nächsten. Von potentiell mehreren IPs pro Interface interessiert auf meinem einfach strukturierten Laptop nur die erste. Da dort unter Umständen statt der IP das Netzwerk im CIDR-Format steht (z.B. "192.168.1.1/24"), spaltet die Funktion Split() aus dem strings-Paket in Zeile 21 die Netzmaske ab, sodass in der Variablen split anschließend nur die reine IP in Stringform steht.

Weil ich zuhause noch mit alten IPv4-Adressen arbeite, blockt Zeile 24 IPv6-Adressen ab. Der Aufruf net.ParseIP(addr).To4() versucht, gefundene Adressen ins IPv4-Format umzuwandeln, was nur für IPv4-Adtressen klappt, und bei IPv6-Adressen einen Fehlerwert ungleich nil zurückgibt. Wessen Home-Setup mit IPv6 auf der Höhe der Zeit ist, wirft die Filterbedingung natürlich raus und erhält so auch IPv6-Adressen im Setup.

Listing 1: ifconfig.go

    01 package ifconfig
    02 
    03 import (
    04   "fmt"
    05   "net"
    06   "sort"
    07   "strings"
    08 )
    09 
    10 func AsStrings() []string {
    11   var list []string
    12 
    13   ifaces, _ := net.Interfaces()
    14   for _, iface := range ifaces {
    15     network := fmt.Sprintf("%10s",
    16       iface.Name)
    17     addrs, _ := iface.Addrs()
    18     if len(addrs) == 0 {
    19       continue
    20     }
    21     split := strings.Split(
    22       addrs[0].String(), "/")
    23     addr := split[0]
    24     if net.ParseIP(addr).To4() != nil {
    25       network += " " + addr
    26       list = append(list, network)
    27     }
    28   }
    29   sort.Strings(list)
    30   return list
    31 }

Zeile 29 sortiert die formatierte Liste noch alphabetisch, bevor die Return-Anweisung in der folgenden Zeile sie an den Aufrufer zurückgibt.

Compiler stellt sich doof

Zu beachten ist bei der Namensgebung in Go, dass Funktionen in einem Paket wie ifconfig in Listing 1, die mit einem Kleinbuchstaben beginnen, nicht exportiert werden. Falls das importierende Hauptprogramm eine im Paket implementierte Funktion as_strings() aufriefe, gäbe sich der Go-Compiler verständnislos, und würde schlicht behaupten, dass es eine solche Funktion nicht gibt. Vielmehr muss die Funktion in ifconfig mit einem Großbuchstaben beginnen: Das großgeschriebene AsStrings() findet dann später auch das Hauptprogramm.

Go compiliert ja bekanntlich alles, was zu einem Programm gehört, in ein statisches Binary. Damit der Compiler beim Zusammenstecken des Hauptprogramms später den Code in Listing 1 findet, muss er die daraus erstellte statische *.a-Datei im Go-Pfad $GOPATH finden, normalerweise unter ~/go im Home-Verzeichnis. Firmiert die Library unter dem Namen ifconfig, muss ihr Source-Code dort in einem neu angelegten Verzeichnis unter src landen und von dort mit go install installiert werden:

    $ dir=~/go/src/ifconfig
    $ mkdir $dir
    $ cp ifconfig.go $dir
    $ cd $dir
    $ go install

Diese Befehlsfolge legt unter pkg/linux_amd64 im Go-Pfad die statische Library iftop.a an, die der Go-Compiler später dort mit dem Hauptprogramm statisch verlinkt.

Als Terminal-GUI kommt das Github-Projekt termui zum Einsatz ([3]). Das Schöne an Go: Ihr Code wird auch direkt vom Web auf dem heimischen Rechner installiert:

    go get -u github.com/gizak/termui

Das get-Kommando holt ihn von Github ab, compiliert ihn und installiert die erzeugten Libraries im Go-Pfad, wo der Compiler sie später findet, falls ein Go-Programm sie wünscht. Das Flag <-u> instruiert go get, nicht nur das verlangte Paket zu installieren, sondern auch eventuell davon abhängige Pakete auf den neuesten Stand zu bringen.

Aufregende Ereignisse

Wie andere GUIs kommt auch termui Event-basiert daher. Der User definiert anfangs einige Widgets wie List- oder Textboxen, arrangiert diese mit einem Layouter im 2D-Raum und fängt dann Events ab wie "Terminalfenster wurde verkleinert" oder "Tastaturkombination Ctrl-C wurde gedrückt" oder "Der Timer, der jede Sekunde anspringt, ist gerade abgelaufen". Im vorliegenden Fall definiert Listing 2, wie im Screenshot in Abbildung 2 ersichtlich, zwei verschiedene Widgets: Eine Listbox am oberen Rand, die als Einträge die verfügbaren Netzwerk-Interfaces mit ihren IPs auflistet, sowie eine am unteren Rand klebende Textbox, die den User lediglich mit einem Textabsatz daran erinnert, dass zum Beenden des Programms die Taste "q" zu drücken ist.

Nachdem Zeile 4 die Termui-Features unter dem Kürzel "t" importiert hat, initialisiert das Hauptprogramm mit der Funktion Init() auf das Paketkürzel t die GUI, indem es das Textfenster sauber putzt und das Terminal in den Zustand für Grafikausgaben versetzt. Beim Programmschluss macht die Funktion Close() diese wieder rückgängig, und heraus kommt wieder ein normales Text-Terminal. Dank des standardmäßig in Go angebotenen Schlüsselworts "defer" erfolgt die Planung des Großreinemachens in Zeile 18, doch die Putzkolonne schreitet erst nach dem Verlassen der Hauptfunktion main zur Tat.

Quadratisch praktisch

Der Layout-Algorithmus in termui ordnet die ihm übergebenen Widgets in einem 12x12-Raster an. Die Funktion AddRows() in Zeile 33 nimmt als Argumente mittels NewRow() erzeugte Layout-Zeilen an, deren Argumente wiederum mittels NewCol() erzeugte Spalten sind. Letztere Funktion nimmt als erstes Argument die Breite der Spalte entgegen, in Listing 1 sind das alle 12 von insgesamt 12 Rasterquadraten. Der zweite Parameter ist ein Offset als Abstandshalter, der hier mit 0 ungenutzt bleibt.

Die in Zeile 40 aufgerufene Funktion Align() baut das Widget-Raster intern auf und das darauffolgende Render() bringt die ganze Chose auf den Schirm. Nun gilt es nur noch, etwaige auftretende Events abzufangen, wie etwa was passieren soll, falls der User das Terminalfenster vergrößert oder verkleinert. In diesem Fall springt wegen des Handlers /sys/wnd/resize in Zeile 45 der Raster-Engine an, denn der Code holt erst mit TermWidth() die neue Terminalbreite ein und ruft dann die Align()-Methode zum erneuten Platzieren der Widgets im Raum auf. Der folgende Aufruf von Render() frischt die Anzeige in einem Rutsch auf, damit's nicht ruckelt.

Listing 2: iftop.go

    01 package main
    02 
    03 import (
    04   t "github.com/gizak/termui"
    05   "ifconfig"
    06   "log"
    07 )
    08 
    09 var listItems = []string{}
    10 
    11 func main() {
    12   err := t.Init()
    13   if err != nil {
    14     log.Fatalln("Termui init failed")
    15   }
    16 
    17   // Cleanup UI on exit
    18   defer t.Close()
    19 
    20   // Listbox displaying interfaces
    21   lb := t.NewList()
    22   lb.Height = 10
    23   lb.BorderLabel = "Networks"
    24   lb.BorderFg = t.ColorGreen
    25   lb.ItemFgColor = t.ColorBlack
    26 
    27   // Textbox
    28   txt := t.NewPar("Type 'q' to quit.")
    29   txt.Height = 3
    30   txt.BorderFg = t.ColorGreen
    31   txt.TextFgColor = t.ColorBlack
    32 
    33   t.Body.AddRows(
    34     t.NewRow(
    35       t.NewCol(12, 0, lb)),
    36     t.NewRow(
    37       t.NewCol(12, 0, txt)))
    38 
    39   // Initial rendering
    40   t.Body.Align()
    41   t.Render(t.Body)
    42 
    43   // Resize widgets when term window
    44   // gets resized
    45   t.Handle("/sys/wnd/resize",
    46     func(t.Event) {
    47       t.Body.Width = t.TermWidth()
    48       t.Body.Align()
    49       t.Render(t.Body)
    50     })
    51 
    52   // Refresh every second
    53   t.Handle("/timer/1s", func(t.Event) {
    54     lb.Items = ifconfig.AsStrings()
    55     t.Render(t.Body)
    56   })
    57 
    58   // Keyboard input
    59   t.Handle("/sys/kbd/C-c", func(t.Event) {
    60     t.StopLoop()
    61   })
    62   t.Handle("/sys/kbd/q", func(t.Event) {
    63     t.StopLoop()
    64   })
    65 
    66   t.Loop()
    67 }

Nun ist ja die Listbox mit den angezeigten Netzwerk-Interfaces anfangs noch leer, denn Items, vom Typ her ein in Go sogenanntes "field" (Attribut) einer "struct" (gemischten Datenstruktur), wurde anfangs noch nicht initialisiert. Dies holt nun Zeile 54 nach, mit dem einmal pro Sekunde aufgerufenen Handler für den Event /timer/1s, der die Funktion AsStrings() aus dem Paket ifconfig in Listing 1 aufruft und der Listbox einen bereits vorformatierten Slice von Strings mit den Netzwerk-Adaptern und ihren IPs zuweist. Damit die aufgefrischte Liste auch auf dem Schirm erscheint, ist ein erneuter Aufruf der Funktion Render() des Grafikmanagers erforderlich.

Ende gut, alles gut

Damit der User das Programm auch ordnungsgemäß beenden kann, ohne dass das Terminal im Grafikmodus steckenbleibt und damit unbenutzbar wird, fangen die Handler /sys/kbd/C-c und /sys/kbd/q in den Zeilen 59 und 62 die Tastatureingaben Ctrl-C und "q" ab und stoppen in diesen Fällen mit StopLoop() die EventSchleife.

Dies wiederum lässt die in Zeile 66 während des gesamten Programmlaufs blockierende Funktion Loop() zurückkehren, und main() neigt sich dem Ende zu. Doch halt, der vorher in Zeile abgesetzte Befehl defer t.Close() ruft noch schnell den Müllmann des Grafikmanagers zuhilfe, der alles aufräumt, um das Terminal so zurückzulassen, wie er es beim Programmstart vorgefunden hat.

Das Programm compiliert sich mit

    $ go build iftop.go

und falls das Paket ifconfig in Listing 1 vorher richtig installiert wurde (siehe oben), findet es der Compiler auch, linkt alles zusammen und erzeugt ein Binary, das mit knapp 3MB zwar nicht gerade schlank ist, aber alles notwendige enthält, um es einfach auf eine andere Maschine mit ähnlichem Betriebssystem zu kopieren und dort zu starten. Keine Zauberei!

Wer das Programm mit ./iftop schließlich von der Kommandozeile startet, sieht einen aufgeräumten Bildschirm mit den in Abbildung 2 gezeigten Elementen: der zunächst leeren Listbox oben und dem Textfeld unten. Nach einer Sekunde, wenn der Timer zum ersten Mal abgelaufen ist, erscheint dann in der Listbox eine Liste der Netzwerk-Interfaces mit ihren IP-Adressen. Wer das dynamische Verhalten testen will, kann nun ein Netzwerkkabel in den Laptop (gerne auch über USB) und in einen Router eingestöpseln und sehen, wie praktisch sofort ein neuer Eintrag in der Listbox erscheint. Umgekehrt sollte ein bereits bestehender Eintrag verschwinden, sobald zum Beispiel das Wireless-Signal des Laptops ausgestellt wird.

Abbildung 3: Termui Demo-Dashboard (Quelle: Github)

Das Termui-Projekt bietet noch allerhand Augenschmaus im Terminal, vom Progress-Bar bis zur Balkengrafik ist alles dabei (Abbildung 3). Und Termui ist beileibe nicht das einzige Terminal-UI-Framework mit Go: Auch gocui, clui, wm, tui-go und einige mehr buhlen um die Gunst der Entwickler, alle auf Github. Der Blogeintrag auf [4] bietet eine gute Übersicht über die Vor- und Nachteile der einzelnen Spielarten. Es wäre durchaus im Rahmen des Möglichen, mit einem dieser Frameworks eine ausgefuchste Terminal-Applikation wie "Lazygit" ([5]) zu bauen, oder zum Beispiel eine Art Norton-Commander (auf Linux unter "Midnight-Commander" bekannt) zur Dateimanipulation im Retro-Look, 35 Jahre nach der Sturm- und Drangzeit von MS-DOS wieder top-modisch!

Infos

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2018/10/snapshot/

[2]

Michael Schilli, "Schlaues Ausmustern": Linux-Magazin 08/2018, S.88, <U>http://www.linux-magazin.de/ausgaben/2018/08/snapshot-5/<U>

[3]

Das Terminal-UI-Projekt "TermUI" auf Github: https://github.com/gizak/termui

[4]

"Text-Based User Interfaces", Applied Go, https://appliedgo.net/tui/

[5]

"Lazygit" https://github.com/jesseduffield/lazygit

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 5:

Unknown directive: =desc