Create a bootable USB stick with terminal UI display
Drive Discovery
Meanwhile, the Go routine, which remains active in the background, runs in an infinite loop. At the beginning the init
variable from line 17 has a value of true
. As soon as the function has checked out all the existing devices after the first pass of the for
loop, line 33 changes the init
variable to false
.
Now things start happening thick and fast. The for
loop repeatedly fires up after the Sleep
statement in line 34 and reads the current device entries again and again. If a new device is found that is not yet in the seen
map, line 29 copies the path for the entry to the drivech
Go channel. The main program snaps it up from there, after having eagerly waited in line 56 in a blocking state (but asynchronously in a Go routine) for the results in Listing 3.
Listing 3
isoflash.go
001 package main 002 003 import ( 004 "flag" 005 "fmt" 006 ui "github.com/gizak/termui/v3" 007 "github.com/gizak/termui/v3/widgets" 008 "os" 009 "path" 010 ) 011 012 func main() { 013 flag.Parse() 014 if flag.NArg() != 1 { 015 usage("Argument missing") 016 } 017 isofile := flag.Arg(0) 018 _, err := os.Stat(isofile) 019 if err != nil { 020 usage(fmt.Sprintf("%v\n", err)) 021 } 022 023 if err = ui.Init(); err != nil { 024 panic(err) 025 } 026 var globalError error 027 defer func() { 028 if globalError != nil { 029 fmt.Printf("Error: %v\n", globalError) 030 } 031 }() 032 defer ui.Close() 033 034 p := widgets.NewParagraph() 035 p.SetRect(0, 0, 55, 3) 036 p.Text = "Insert USB Stick" 037 p.TextStyle.Fg = ui.ColorBlack 038 ui.Render(p) 039 040 pb := widgets.NewGauge() 041 pb.Percent = 100 042 pb.SetRect(0, 2, 55, 5) 043 pb.Label = " " 044 pb.BarColor = ui.ColorBlack 045 046 done := make(chan error) 047 update := make(chan int) 048 confirm := make(chan bool) 049 050 uiEvents := ui.PollEvents() 051 drivech := driveWatch(done) 052 053 var usbPath string 054 055 go func() { 056 usbPath = <-drivech 057 058 size, err := driveSize(usbPath) 059 if err != nil { 060 done <- err 061 return 062 } 063 064 p.Text = fmt.Sprintf("Write to %s " + 065 "(%s)? Hit 'y' to continue.\n", 066 usbPath, size) 067 ui.Render(p) 068 }() 069 070 go func() { 071 for { 072 pb.Percent = <-update 073 ui.Render(pb) 074 } 075 }() 076 077 go func() { 078 <-confirm 079 p.Text = fmt.Sprintf("Copying to %s ...\n", usbPath) 080 ui.Render(p) 081 update <- 0 082 err := cpChunks(isofile, usbPath, update) 083 if err != nil { 084 done <- err 085 } 086 p.Text = fmt.Sprintf("Done.\n") 087 update <- 0 088 ui.Render(p, pb) 089 }() 090 091 for { 092 select { 093 case err := <-done: 094 if err != nil { 095 globalError = err 096 return 097 } 098 case e := <-uiEvents: 099 switch e.ID { 100 case "q", "<C-c>": 101 return 102 case "y": 103 confirm <- true 104 } 105 } 106 } 107 } 108 109 func usage(msg string) { 110 fmt.Printf("%s\n", msg) 111 fmt.Printf("usage: %s iso-file\n", 112 path.Base(os.Args[0])) 113 os.Exit(1) 114 }
To discover the USB stick's storage capacity, Listing 2 runs the sfdisk -s /dev/sdd
command in line 43. The standard output of the shell command, triggered in Go via the os.Exec
package, contains a single integer value that indicates the capacity of the stick in kilobytes. Line 52 truncates the line break from the resulting string. Line 53 uses Atoi()
from the strconv package to convert the string into an integer. Line 58 divides the result by 1MB, so that the capacity in gigabytes is finally output in floating-point format.
The function returns the value, nicely formatted as a string, so that the user can verify in the UI that it is really a USB stick and not a (far larger) hard disk.
Better with a UI
A tool with a user interface, even if it is only a terminal application, is far easier to use than one that only uses the standard output. This is especially true where the user is required to make selections or confirm entries.
The main program in Listing 3 uses the termui terminal UI, which we looked at in a previous issue [1]. The user interface shown in the illustrations at the end of this article consists of two widgets located one above the other in the main window of the terminal UI.
The upper widget is a text widget for the p
variable, which provides status messages to the user and displays new instructions. The lower widget, referenced by the variable pb
, is a progress bar of the Gauge
type. It receives updates via a Go channel and moves the bar from left to right to reflect the incoming percentage values.
But before this can happen, line 14 in Listing 3 first checks whether the main program was actually called as required with an ISO file as a parameter. If not, the code branches to the help page (usage()
) starting in line 109. For the internal communication between the different parts of the program, the code uses no less than five different channels, although Go programmers should only make sparing use of these according to the official guidelines.
The drivech
channel discussed earlier reports freshly plugged in USB sticks to the blocking Go routine in line 56. The update
channel supports communication between the data copier, cpChunks()
from Listing 1, and the main program. As soon as the copier reports a new percentage value, line 72 unblocks and stores the percentage value of the progress bar in the pb
variable. The following call to the function Render()
refreshes the UI and makes sure that the bar also visibly moves. When all the data have been received on the USB stick, line 87 resets the progress bar to zero percent.
Keyboard input such as Ctrl+C or Q is also intercepted by the event loop triggered in line 50 using PollEvents()
on the uiEvents
channel. Line 98 analyzes the pressed key and triggers the end of the program for the two abort sequences. If the stick has already been detected, the Go routine puts the brakes on in line 77 to wait for data from the confirm
channel in line 78. If the user presses Y, line 103 feeds the event into the confirm
channel. Line 78 picks it up and opens the flood gates for the copy action.
Deferred or Canceled?
The done
channel in turn is used by the main program to control when the UI should be packed away and the program terminated. The problem arises here that a terminal UI cannot simply write to Stderr
or abort the program with panic()
if a serious error occurs: Stderr
is blocked in graphics mode, and an abruptly aborted program would leave an unusable terminal that users could only fix by closing the terminal window and opening a new one.
The code from Listing 1 helps to feed potentially fatal errors into the done
channel, where line 93 from Listing 3 fields them and stores them in the globalError
variable declared in line 26. The clever sequence of defer
statements in lines 27 and 32 ensures that the UI is always closed first and that only then is the error leading to program termination in globalError
output to stdout
.
Successive defer
statements are executed in reverse order: Go builds a defer
stack by executing the first entries last. Since the defer
in line 27 outputs the global error and the defer
in line 32 breaks down the UI, the main program always breaks down the UI first and then outputs the error. Doing this the opposite way would mean losing the error.
« Previous 1 2 3 Next »
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
Canonical Bumps LTS Support to 12 years
If you're worried that your Ubuntu LTS release won't be supported long enough to last, Canonical has a surprise for you in the form of 12 years of security coverage.
-
Fedora 40 Beta Released Soon
With the official release of Fedora 40 coming in April, it's almost time to download the beta and see what's new.
-
New Pentesting Distribution to Compete with Kali Linux
SnoopGod is now available for your testing needs
-
Juno Computers Launches Another Linux Laptop
If you're looking for a powerhouse laptop that runs Ubuntu, the Juno Computers Neptune 17 v6 should be on your radar.
-
ZorinOS 17.1 Released, Includes Improved Windows App Support
If you need or desire to run Windows applications on Linux, there's one distribution intent on making that easier for you and its new release further improves that feature.
-
Linux Market Share Surpasses 4% for the First Time
Look out Windows and macOS, Linux is on the rise and has even topped ChromeOS to become the fourth most widely used OS around the globe.
-
KDE’s Plasma 6 Officially Available
KDE’s Plasma 6.0 "Megarelease" has happened, and it's brimming with new features, polish, and performance.
-
Latest Version of Tails Unleashed
Tails 6.0 is based on Debian 12 and includes GNOME 43.
-
KDE Announces New Slimbook V with Plenty of Power and KDE’s Plasma 6
If you're a fan of KDE Plasma, you'll be thrilled to hear they've announced a new Slimbook with an AMD CPU and the latest version of KDE Plasma desktop.
-
Monthly Sponsorship Includes Early Access to elementary OS 8
If you want to get a glimpse of what's in the pipeline for elementary OS 8, just set up a monthly sponsorship to help fund its continued existence.