Network diagnostics with Go

No Longer Toy-Sized

Moving on from the stopwatch example, the actual application checks the network in the background and periodically refreshes the results of all tests in the graphical interface.

The program compiled from Listing 4 goes by the name of wifi, but it can be applied to wired networks in exactly the same way. To display the test results, it uses the tview project's table widget in line 10. A total of five rows, each with two columns, contain a description of the test on the left and the dynamically refreshed result on the right (see Figures 1 and 2).

Listing 4

wifi.go

¤¤nonumber
01 package main
02
03 import (
04  "strings"
05  "github.com/rivo/tview"
06 )
07
08 func main() {
09  app := tview.NewApplication()
10  table := tview.NewTable().SetBorders(true)
11  table.SetBorder(true).SetTitle("Wifi Monitor v1.0")
12
13  newPlugin(app, table, "Time", clock)
14  newPlugin(app, table, "Ping", ping, "www.google.com")
15  newPlugin(app, table, "Ping", ping, "8.8.8.8")
16  newPlugin(app, table, "Ifconfig", nifs)
17  newPlugin(app, table, "HTTP", httpGet, "https://youtu.be")
18
19  err := app.SetRoot(table, true).SetFocus(table).Run()
20  if err != nil {
21   panic(err)
22  }
23 }
24
25 func newPlugin(app *tview.Application, table *tview.Table,
26   field string, fu func(...string) chan string, arg ...string) {
27  if len(arg) > 0 {
28   field += " " + strings.Join(arg, " ")
29  }
30
31  row := table.GetRowCount()
32  table.SetCell(row, 0, tview.NewTableCell(field))
33
34  ch := fu(arg...)
35
36  go func() {
37   for {
38    select {
39    case val := <-ch:
40     app.QueueUpdateDraw(func() {
41      table.SetCell(row, 1, tview.NewTableCell(val))
42     })
43    }
44   }
45  }()
46 }

When defining the window and table decorations, you need to look carefully. The table widget has a SetBorders() function that determines whether or not the table draws row and column lines. On the other hand, line 11 calls SetBorder() (singular). SetBorder() does not refer to the table, but instead to the box (a container) in which the table is located. The call draws a border around the application, along with a headline at the top.

Lumped Together

Each table row is now assigned a test program. The ticking clock ends up in the first row, the two network pings in rows 2 and 3, the display of the local IPs in row 4, and the HTTP request to the YouTube server in row 5. The newPlugin() function integrates these plugins with the table rows. The calls are each given a pointer to the application and its table in lines 13 to 17. And there are two more parameters: a description of each test as a string and a function that executes the test.

As you can see from the signature of newPlugin() in line 25, the function expects the test function fu passed to it to be in an interesting format. To accommodate all applications, the test function accepts a variable number of string arguments (...string) and returns a channel where the caller can later fetch results of the string type. Listing 2 has already provided an example of this type of test function: clock() creates a stopwatch whose current timestamp the table now displays every second in its first row.

To associate the test function with the next available table row, line 32 appends a new row to the table for each call. Then line 34 calls the test function, which in turn returns a channel and keeps its network test running in the background for all eternity. To intercept the results for the individual test, line 36 starts a new concurrent goroutine with an infinite loop that uses a select statement to listen on the channel. When a string arrives, line 41 uses table.SetCell to refresh the contents of the assigned table field.

In order for the content of the updated graphical elements to actually appear on the screen, I need to forward the instruction to the GUI manager. This is done by the app.QueueUpdateDraw() function, which tells the GUI to redraw the table field when it gets around to it during the next refresh.

Ding-Dong

I now need to integrate the new network tests into the table. Each test consists of a function that accepts an optional string argument and returns a channel. It starts the test task assigned to it and keeps it running while returning the results to the caller via the channel.

Listing 5 uses the ping() function to ping servers or their IP addresses; it expects either a hostname or an IP address as an argument. It returns a channel to the caller, which it keeps populating with ping results.

Listing 5

ping.go

¤¤nonumber
01 package main
02
03 import (
04  "fmt"
05  "github.com/prometheus-community/pro-bing"
06  "time"
07 )
08
09 func ping(addr ...string) chan string {
10  ch := make(chan string)
11  firstTime := true
12
13  go func() {
14   for {
15    pinger, err := probing.NewPinger(addr[0])
16    pinger.Timeout, _ = time.ParseDuration("10s")
17
18    if err != nil {
19     ch <- err.Error()
20     time.Sleep(10 * time.Second)
21     continue
22    }
23
24    if firstTime {
25     ch <- "Pinging ..."
26     firstTime = false
27    }
28
29    pinger.Count = 3
30    err = pinger.Run()
31
32    if err != nil {
33     ch <- err.Error()
34     time.Sleep(10 * time.Second)
35     continue
36    }
37
38    stats := pinger.Statistics()
39    ch <- fmt.Sprintf("%v ", stats.Rtts)
40    time.Sleep(10 * time.Second)
41   }
42  }()
43  return ch
44 }

With a similar interface as the command-line ping utility, Listing 5 uses the pro-bing package from GitHub to send ICMP packets to the specified address. The package is fetched from GitHub in line 5. The new pinger instance created in line 15 sets a timeout of 10 seconds in line 16. When the timer for a request expires, the pinger assumes that something went wrong and the server cannot be reached. Another cause for a failure could be a problem with the name resolution for the server. Line 33 will inject an error message into the channel. Line 34 then waits for 10 seconds, and then continue in the following line jumps to the next iteration of the infinite for loop starting in line 14 and tries again.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Housecleaning

    This month, Mike Schilli writes a quick terminal UI in Go to help track down space hogs on his hard disk.

  • At a Glance

    Using extensions in Go and Ruby, Mike Schilli adapts the WTF terminal dashboard tool to meet his personal needs.

  • Let's Go!

    Released back in 2012, Go flew under the radar for a long time until showcase projects such as Docker pushed its popularity. Today, Go has become the language of choice of many system programmers.

  • DIY Scoreboard

    We look at a broadcast video system network that uses Python code to control a video router and check out another program that creates a scoreboard.

  • Finding Processes

    How do you find a process running on a Linux system by start time? The question sounds trivial, but the answer is trickier than it first appears.

comments powered by Disqus
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.

Learn More

News