Nicht nur hibbelige Millennials, auch altgediente Internetnutzer verlieren heute schnell die Geduld, wenn's mal nicht so flutscht. Dauert es länger als ein paar Sekunden, bis eine angeforderte Webseite im Browser erscheint, beginnt der User bereits, am Reload-Button herumzufummeln. Es ist aber auch wirklich nervig, falls nicht klar ist, was los ist. Deswegen erfand ein kluger Kopf schon vor fast 40 Jahren den Progress-Bar ([1]), der beruhigend auf den User einredet: "10% sind schon durch, und die restlichen 90% werden wir auch noch schaffen, und zwar in folgendem Tempo".
Auch kein Agententhriller aus Hollywood kommt ohne Progress-Bar aus. Wenn der Spion die Mainframe-Daten auf den USB-Stick abzieht, scheint der Fortschrittsbalken nur endlos langsam voranzuschreiten, während doch die Bösewichte nahen und alles aufzufliegen droht!
Abbildung 1: So mancher Hollywoodfilm wäre ohne Progress-Bar nur halb so spannend. |
Einige Unix-Tools bringen bereits Fortschrittsbalken mit. So lernt curl
normalerweise beim Einholen einer Webseite ganz zu Anfang, wieviele Bytes diese enthält und zeigt mit der Option -#
(oder --progress-bar
) das anschließende Eintrudeln der Daten an:
$ curl -# -o data http://...
########### 50.6%
Auch das oft zum massenweisen Kopieren von Diskdaten genutzte Tool dd
zeigt seit neuestem (ab GNU coreutils 8.24) den Fortschritt an, wenn der User wie in Abbildung 2 die Option status=progress
setzt.
Abbildung 2: Seit GNU coreutils 8.24 zeigt das Tool "dd" mit der Option status=progress den Fortschritt an. |
Freunde der Shell-Programmierung greifen zum Linux-Werkzeug pv
, das auch Utilities ohne eingebauten Fortschrittsbalken auf die Sprünge hilft. Als Zwischenstück zwischen zwei Abschnitte einer Pipe geklemmt, zeigt es mittels eines ASCII-Balkens den Fortschritt der Daten durch die Pipe an, in dem es die durchfließenden Bytes mitzählt. Damit es weiß, welcher Bruchteil der Daten schon geflossen ist und was noch ansteht, um in regelmässigen Abständen den Balken aufzufrischen, muss es vorab die Gesamtdatenmenge wissen, dann ergibt sich der angezeigte Prozentwert aus der Division von mitgezählten Bytes zur Gesamtmenge.
Abbildung 3: Das Tool "pv" zeigt den Fortschritt beim Einlesen einer Datei als Balken an. |
Einfach zwischen zwei Pipe-Abschnitte eingebaut, weiß pv
allerdings nichts über die zu erwartende Datenmenge und kann demnach nur bereits geflossene Bytes zählen (Abbildung 3 oben). Wer pv
in einem Pipe-Zwischenstück unter die Arme greifen möchte, kann die (eventuell vorab bekannte) Gesamtdatenmenge mit der Option -s bytes
angeben, dann malt pv
ebenfalls einen Fortschrittsbalken. Übergibt der User pv
allerdings den Namen einer Datei, kann es feststellen, wie groß diese ist, bevor es die Daten stückweise weiterreicht und ohne Hilfestellung den Fortschritt anzeigt, wie beim letzten Backup-Kommando in Abbildung 2.
Abbildung 4: Einer der futuristischten Fortschrittsbalken in der Softwareindustrie: npm install |
Wer gerne selbst Tools zusammenklopft, findet Progress-Bars zur verwendeten Programmiersprache oft auf Github. Für Go bietet sich der simple ASCII-Balken progressbar
an, den
$ go get github.com/schollz/progressbar
direkt von Github auf den Rechner holt. Listing 1 zeigt einen einfachen Web-Client webpgb
, der einen ihm auf der Kommandozeile übergebenen URL vom Netz holt und nebenher in einem Progress-Bar anzeigt, wie weit der Download schon fortgeschritten ist:
$ ./webpgb http://...
13%[##----------][16s:1m49s]
Dazu importiert Zeile 7 die Progressbar-Library als pb
ins Hauptprogramm, welches mit os.Args[1]
den ersten ihm überreichten Kommandozeilenparameter als URL interpretiert und mittels der Get-Funkion aus dem Standard-Paket net/http
vom Netz holt. Zum stückweisen Einsammeln der Daten definiert Zeile 12 einen Puffer als Go-Slice von 4096 Bytes Länge, den die Endlosschleife ab Zeile 36 solange mittels Read()
aus der HTTP-Antwort mit bis zu 4096 Zeichen befüllt, bis der Webserver den Rolladen schließt und EOF schickt, was Zeile 40 abfängt und main
beendet. Zwischenzeitlich frischt Zeile 39 mittels Add()
die Anzeige des Progressbars auf, in dem sie ihm die jeweils im Puffer liegende Anzahl von erhaltenen Bytes schickt.
01 package main 02 03 import ( 04 "os" 05 "net/http" 06 "io" 07 pb "github.com/schollz/progressbar" 08 ) 09 10 func main() { 11 resp, err := http.Get(os.Args[1]) 12 buffer := make([]byte, 4096) 13 14 if err != nil { 15 panic(err) 16 } 17 18 if resp.StatusCode != 200 { 19 panic(resp.StatusCode) 20 return 21 } 22 23 bar := pb.NewOptions( 24 int(resp.ContentLength), 25 pb.OptionSetTheme( 26 pb.Theme{Saucer: "#", 27 SaucerPadding: "-", 28 BarStart: "[", 29 BarEnd: "]"}), 30 pb.OptionSetWidth(30)) 31 32 bar.RenderBlank() 33 34 defer resp.Body.Close() 35 36 for { 37 n, err := resp.Body.Read(buffer) 38 if err == nil { 39 bar.Add(n) 40 } else if err == io.EOF { 41 return 42 } else { 43 panic(err) 44 } 45 } 46 }
Anfangs hat Zeile 23 eine neue Balkenstruktur in der Variablen bar
definiert und ihre Maximallänge auf die Gesamtzahl der aus dem Web-Request erwarteten Bytes initialisiert. Dabei definieren die Zeilen 24-30 auch noch kosmetische Einstellungen wie das ASCII-Zeichen für die sogenannte "Saucer" (Untertasse), das fliegende Objekt zur Illustration den Fortschritts, als "#", sowie den Balkenrahmen als "[]" und das Füllzeichen des leeren Balkens als "-".
Da die Daten aus der Web-Anfrage sowieso stückweise aus dem Netz eintreffen, fügt sich der Balken und die Logik, um ihn aufzufrischen, organisch in den Code ein. Falls langlaufende System-Calls die Laufzeit des Programms bestimmen, müssen diese umgeschrieben werden, damit ein Balken schrittweise voranschreiten kann und nicht etwa bis kurz vor Schluss still steht und dann ruckartig bis ans Ende schnalzt.
Wem nach etwas mehr Eye-Candy als nur Terminal-Buchstaben gelüstet, der kann als nächste Stufe mit einer Terminal-UI wie dem letztens vorgestellten Termui-Projekt ([2]) wie in Abbildung 5 illustriert punkten. Am Prinzip zeigt Listing 2 auch wie Systemfunktionen beim Einholen von URLs vom Web und grafische Elemente einer GUI sich generell verzahnen lassen.
Abbildung 5: Fortschrittsbalken beim Kopieren einer großen Datei in Go mit der Termui-Library. |
Da GUIs und damit der Fortschrittsbalken in einer Event-Schleife laufen, bietet sich asynchrones Einholen der Daten an, wie zum Beispiel in NodeJS. Dort springt der Code beim Eintreffen von Daten regelmässig Callbacks an, aus denen der findige Programmierer dann den Balken mit der eingetroffenen Datenmenge auffrischt.
In Go stehen hingegen zum Abfeuern nebenläufiger Programmteile und deren Synchronisation die letztens erörterten Goroutines und sogenannte Channels zur Verfügung [4]. Listing 2 zeigt, wie sich der lang dauernde Kopiervorgang einer großen Datei in Go mit einem Progressbar aus dem Termui-Paket illustrieren lässt.
Nachdem das Paket mit
$ go get github.com/gizak/termui
installiert und das Programm mit
$ go build cpgui.go
übersetzt wurde, kopiert der Aufruf cpgui foo
eine Datei foo
nach foo.bak
. Ist sie relativ groß (z.B. 100MB), zeichnet die aufgespannte UI einen Progress-Bar ins Terminal, dessen Balken sich schrittweise nach rechts vergrößert, bis er am rechten Ende des Diagramms angelangt ist, die Datei fertig kopiert ist und das Programm sich beendet (Abbildung 5).
Listing 2 definiert hierzu in Zeile 19 mit NewGauge()
aus dem termui
-Paket eine neue Struktur, die das UI-Element eines Fortschrittsbalkens repräsentiert. Der Anfangswert des von links nach rechts wachsenden Balkens ist mit dem Attribut Percent
auf 0 gesetzt, der Balken steht also bei 0% und damit ganz links und ist unsichtbar. Die Zeilen 21 bis 26 definieren noch weitere Attribute wie Farben der einzelnen Elemente, ihre Größe oder die Beschriftung des Grafikelements.
Zur Kommunikation zwischen den verschiedenen Programmteilen, die die Daten schreiben beziehungsweise den Balken auffrischen, nutzt Listing 2 die in den Zeilen 29 und 30 definierten Channels, update
und done
. Über update
teilt die Funktion backup()
(die den Channel ab Zeile 56 als Parameter mitbekommt) dem Hauptprogramm mit, wieviele Bytes sie gerade in die neue Datei geschrieben hat. main()
wartet dazu in einer parallel laufenden Goroutine in einer Endlosschleife ab Zeile 40 auf neue Daten im Channel und blockiert, falls noch keine anliegen. Kommt ein neuer Integerwert aus dem Channel hervor, setzt Zeile 41 das Percent-Attribut des Progress-Bars in g
auf den neuen von backup()
nach oben geschickten Prozentwert und zeichnet dann mit ui.Render(g)
das Grafikelement neu.
01 package main 02 03 import ( 04 ui "github.com/gizak/termui" 05 "io/ioutil" 06 "os" 07 "fmt" 08 "log" 09 ) 10 11 func main() { 12 file := os.Args[1]; 13 err := ui.Init() 14 if err != nil { 15 panic(err) 16 } 17 defer ui.Close() 18 19 g := ui.NewGauge() 20 g.Percent = 0 21 g.Width = 50 22 g.Height = 7 23 g.BorderLabel = "Copying" 24 g.BarColor = ui.ColorRed 25 g.BorderFg = ui.ColorWhite 26 g.BorderLabelFg = ui.ColorCyan 27 ui.Render(g) 28 29 update := make(chan int) 30 done := make(chan bool) 31 32 // wait for completion 33 go func() { 34 <-done 35 ui.StopLoop() 36 }() 37 38 // process updates 39 go func() { 40 for { 41 g.Percent = <-update 42 ui.Render(g) 43 } 44 }() 45 46 go backup(file, fmt.Sprintf("%s.bak", file), 47 update, done) 48 49 ui.Handle("/sys/kbd/q", func(ui.Event) { 50 ui.StopLoop() 51 }) 52 53 ui.Loop() 54 } 55 56 func backup(src string, dst string, 57 update chan int, done chan bool) error { 58 59 input, err := ioutil.ReadFile(src) 60 if err != nil { 61 log.Println(err) 62 done <- true 63 } 64 total := len(input) 65 total_written := 0 66 67 out, err := os.Create(dst) 68 if err != nil { 69 log.Println(err) 70 done <- true 71 } 72 73 lim := 4096 74 var chunk []byte 75 76 for len(input) >= lim { 77 chunk, input = input[:lim], input[lim:] 78 out.Write(chunk) 79 total_written += len(chunk) 80 update<- total_written*100/total 81 } 82 out.Write(input) 83 84 done <- true 85 return nil 86 }
Der zweite Channel, done
, erlaubt es der der Kopierfunktion backup()
, die auch diesen Channel als Parameter vom Hauptprogramm mitbekommt, das Programmende einzuleiten. Hierzu wartet das Hauptprogramm in einer ab Zeile 33 gestarteten Goroutine auf Daten im done
-Channel, und sobald welche vorliegen (weil Zeile 70 "true" hineingeschickt hat), löst Zeile 35 mit ui.StopLoop()
den Abschluss der UI-Eventschleife ein, was in Zeile 53 die GUI einreißt und die main()
-Funktion beendet.
Die Daten der zu kopierenden Datei liest die Funktion ReadFile()
aus dem mit go get io/ioutil
zu installierenden Paket ioutil
in einem Rutsch in ein Array Slice ein, das Daten vom Type Byte
beherbergt. Die Gesamtlänge der Datei in Bytes ermittelt die Funktion len()
in Zeile 64 und speichert sie in der Variablen total
. Zeile 67 legt dann die neue Datei mit der Endung .bak
auf der Festplatte an, und gibt in out
ein Writer-Interface zurück. Dieses bietet die Funktion Write()
an, die in Zeile 78 dann solange Brocken mit 4096 Bytes Länge hineinschreibt, bis alle Bytes der Originaldatei erfolgreich kopiert wurden. Den nächsten 4096 Bytes langen Brocken aus dem Puffer input
holt die Anweisung
chunk, input = input[:lim], input[lim:]
in Zeile 77 in den Array Slice chunk
und streicht die Daten gleichzeitig aus dem Original-Puffer input
. Die Schleife ab Zeile 76 wiederholt den Reigen, bis nur noch ein Rest mit weniger als 4096 Bytes im Array Slice input
verbleibt, den Zeile 82 außerhalb der For-Schleife dann auch noch in die neue Datei schreibt und damit den Kopiervorgang abschließt.
Damit der User den Kopiervorgang manuell unterbrechen kann, falls er es sich anders überlegt, definiert Zeile 49 einen Tastatur-Handler für die Taste "q", die mit StopLoop()
die GUI zusammenfaltet und das Programm ordnungsgemäß beendet.
Am Ende des Kopiervorgangs schickt Zeile 84 einen Wert in den done
-Channel, was das Hauptprogramm in Zeile 34 mitbekommt, den Block der Goroutine in Zeile 34 beendet, und mit ebenfalls ui.StopLoop()
zum geordneten Rückzug veranlasst.
Die Unterhaltung des Users während des Kopiervorgangs ist allerdings teuer erkauft: Das Schreiben der Daten in 4096-Byte-Brocken bremst bei größeren Dateien erheblich. Außerdem liest Listing 2 den Gesamtinhalt der zu kopierenden Datei erstmal in einem Rutsch in den Speicher, was bei Gigabyte-großen Trümmern vielleicht keine gute Idee ist. Für Hollywoods Filmindustrie reicht's aber allemal.
Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2018/12/snapshot/
Michael Schilli, "Klassiker neu verpackt": Linux-Magazin 10/18, S.XXX, <U>http://www.linux-magazin.de/ausgaben/2018/10/snapshot-7/<U>
"Fortschrittsbalken", https://de.wikipedia.org/wiki/Fortschrittsbalken
Michael Schilli, "Gleichzeitiges Arbeiten": Linux-Magazin 12/18, S.XXX, <U>http://www.linux-magazin.de/ausgaben/2018/11/snapshot-8/<U>
Hey! The above document had some coding errors, which are explained below:
Unknown directive: =desc