Aus Linux-Magazin 10/2018

Mit Go programmierte Terminaloberfläche zeigt Netzwerkadapter

© Ekaterina Kondratova, 123RF

Auch Liebhaber der Kommandozeile wissen ein klassisches Terminal-UI zu schätzen. Mike Schilli zeigt die Entwicklung am Beispiel eines Go-Programms, das dynamisch Netzwerkinterfaces und IPs anzeigt.

Jedes Mal, wenn ich meinen Laptop zu Diagnosezwecken an einen Router anschließe, stellt sich die Frage, unter welcher dynamisch 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 (Abbildung 1).

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

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

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. Ich meine: Das muss einfacher gehen! Wie wäre es mit einer sortierten Liste aller Netzwerkadapter der Geräts und deren zugewiesenen IP-Adressen? Auf dem hochpoppenden grafischen Userinterface könnte der erfreute User dann dynamisch verfolgen, wie in einer bis dato statischen Liste ein soeben eingestöpselter USB-Adapter als neues Netzwerkinterface samt per DHCP zugewiesener IP erscheint.

Online PLUS

Im Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/videos/

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 à la »top« – die lassen sich schnell starten, auslesen und wieder schließen, ohne dass schnelle Tipper überhaupt das Terminalfenster verlassen oder zur ungeliebten Maus greifen müssen.

Bereits in petto

Was das Kommandozeilentool »ifconfig« ausspuckt, hat das »net«-Paket in Go bereits in petto und stellt es in Form einer Datenstruktur zur Schau. Listing1 zeigt die Implementierung eines Helfer-Pakets »ifconfig«, das eine Funktion »AsStrings()« exportiert, die wiederum eine formatierte Liste aller erkannten Netzwerkinterfaces auf dem laufenden Rechner zurückgibt.

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

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 }

Der String-Formatierer in Zeile 15 setzt die Variable »network« auf den Namen des Interface (etwa »eth0« für den ersten gefundenen Ethernet-Adapter), und zwar mit einer Maximallänge von zehn 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 dann eine Error-Variable, die hoffentlich auf »nil« gesetzt ist und damit anzeigt, dass alles gut gegangen ist. In Listing 1 in Zeile 17 ist die zweite Variable aus Platzspargründen auf »_« gesetzt und wirft damit etwaige Fehler weg, was in einem Produktionssystem unterbleiben sollte.

Falls das Gerät keine IP zugewiesen bekommen hat, ist das gefundene Netzwerkinterface nicht von Belang und Zeile 19 springt mit »continue« zum nächsten. Von potenziell 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 (etwa »192.168.1.1/24«), spaltet die Funktion »Split()« aus dem »strings«-Paket in Zeile 21 die Netzmaske ab, sodass in der Variablen »addr« anschließend nur die reine IP in String-Form steht.

Weil ich zu Hause noch mit altem IPv4 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.

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 kompiliert 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 erzeugte statische »*.a«-Datei im Go-Pfad »$GOPATH« finden, normalerweise unter »~/go« im Homeverzeichnis. Firmiert die Library unter dem Namen »ifconfig«, muss ihr Sourcecode 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 »ifconfig.a« an, die der Go-Compiler später dort mit dem Hauptprogramm statisch verlinkt.

Als Terminal-GUI kommt das Projekt »termui« zum Einsatz [3]. Das Schöne an Go: Der 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, kompiliert 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 2-D-Raum und fängt dann Events ab wie “Terminalfenster wurde verkleinert” oder “Tastenkombination [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 Netzwerkinterfaces mit ihren IPs auflistet, sowie eine am unteren Rand klebende Textbox, die den User nur daran erinnert, dass zum Beenden des Programms die Taste [q] zu drücken ist.

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 }

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« das GUI, indem es das Textfenster sauber putzt und das Terminal in den Zustand für Grafikausgaben versetzt. Beim Programmschluss macht die Funktion »Close()« diese rückgängig – heraus kommt ein normales Textterminal. Dank des standardmäßig in Go angebotenen Schlüsselworts »defer« erfolgt die Planung des Reinemachens in Zeile 18, doch die Putzkolonne schreitet erst nach Verlassen der Hauptfunktion »main« zur Tat.

Quadratisch, praktisch

Der Layout-Algorithmus in »termui« ordnet die ihm übergebenen Widgets in einem 12-mal-12-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 zwölf von insgesamt zwölf Rasterquadraten. Der zweite Parameter ist ein Offset als Abstandshalter, der hier mit »0« ungenutzt bleibt.

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

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

Die in Zeile 40 aufgerufene Funktion »Align()« baut das Widget-Raster intern auf, das darauf folgende »Render()« bringt die ganze Chose auf den Schirm. Nun gilt es nur noch, etwaige auftretende Events abzufangen, was beispielsweise passieren soll, falls der User das Terminalfenster vergrößert oder verkleinert. In diesem Fall springt wegen des Handlers »/sys/wnd/resize« in Zeile 45 die 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 es nicht ruckelt.

Nun ist ja die Listbox mit den angezeigten Netzwerkinterfaces anfangs noch leer, denn »Items«, vom Typ her ein in Go so genanntes “Field” (Attribut) einer »struct« (gemischten Datenstruktur), wurde anfangs noch nicht initialisiert. Das holt nun Zeile 54 nach, mit dem einmal pro Sekunde aufgerufenen Handler für das Event »/timer/1s«, das 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 ordnungsgemäß beenden kann, ohne dass das Terminal im Grafikmodus steckenbleibt und 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 mit »StopLoop()« die Event-Schleife.

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

Das Programm kompiliert mit

$ go build iftop.go

und falls das Paket »ifconfig« in Listing 1 richtig installiert ist (siehe oben), findet es der Compiler auch, linkt alles zusammen und erzeugt ein Binary, das mit 3 MByte zwar nicht schlank ist, aber alles enthält, um es auf eine andere Maschine mit ähnlichem Betriebssystem zu kopieren und dort zu starten.

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 Netzwerkinterfaces mit ihren IP-Adressen.

Abbildung 3: Das Termui-Demo-Dashboard liefert weitere Informationen.

Abbildung 3: Das Termui-Demo-Dashboard liefert weitere Informationen.

Wer das dynamische Verhalten testen will, kann nun ein Netzwerkkabel in den Laptop (gerne auch über USB) und in einen Router einstö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.

Das Termui-Projekt bietet noch allerhand Augenschmaus im Terminal, von Progress-Bar bis zur Balkengrafik (Abbildung 3). Und Termui ist nicht das einzige Terminal-UI-Framework mit Go: Auch »gocui«, »clui«, »wm«, »tui-go« und weitere buhlen um die Gunst der Entwickler. Der Blogeintrag auf [4] bietet eine Übersicht über Vor- und Nachteile der einzelnen Spielarten. Es wäre durchaus möglich, mit einem dieser Frameworks eine ausgefuchste Terminal-Applikation wie Lazygit [5] zu bauen oder eine Art Norton Commander (auf Linux als Midnight Commander bekannt) zur Dateimanipulation im Retro-Look – 35 Jahre nach MS-DOS wieder modern!

Infos

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

  2. Michael Schilli, “Schlaues Ausmustern”: Linux-Magazin 08/2018, S. 88, https://www.linux-magazin.de/ausgaben/2018/08/snapshot-5/

  3. Terminal-UI-Projekt auf Github: https://github.com/gizak/termui

  4. Einführung in “Text-Based User Interfaces” bei Applied Go:https://appliedgo.net/tui/

  5. Lazygit, Terminal User Interface für Git-Kommandos: https://github.com/jesseduffield/lazygit

Der Autor

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 mailto:mschilli@perlmeister.com beantwortet er gerne Fragen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben