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
-
AlmaLinux OS Kitten 10 Gives Power Users a Sneak Preview
If you're looking to kick the tires of AlmaLinux's upstream version, the developers have a purrfect solution.
-
Gnome 47.1 Released with a Few Fixes
The latest release of the Gnome desktop is all about fixing a few nagging issues and not about bringing new features into the mix.
-
System76 Unveils an Ampere-Powered Thelio Desktop
If you're looking for a new desktop system for developing autonomous driving and software-defined vehicle solutions. System76 has you covered.
-
VirtualBox 7.1.4 Includes Initial Support for Linux kernel 6.12
The latest version of VirtualBox has arrived and it not only adds initial support for kernel 6.12 but another feature that will make using the virtual machine tool much easier.
-
New Slimbook EVO with Raw AMD Ryzen Power
If you're looking for serious power in a 14" ultrabook that is powered by Linux, Slimbook has just the thing for you.
-
The Gnome Foundation Struggling to Stay Afloat
The foundation behind the Gnome desktop environment is having to go through some serious belt-tightening due to continued financial problems.
-
Thousands of Linux Servers Infected with Stealth Malware Since 2021
Perfctl is capable of remaining undetected, which makes it dangerous and hard to mitigate.
-
Halcyon Creates Anti-Ransomware Protection for Linux
As more Linux systems are targeted by ransomware, Halcyon is stepping up its protection.
-
Valve and Arch Linux Announce Collaboration
Valve and Arch have come together for two projects that will have a serious impact on the Linux distribution.
-
Hacker Successfully Runs Linux on a CPU from the Early ‘70s
From the office of "Look what I can do," Dmitry Grinberg was able to get Linux running on a processor that was created in 1971.