the Worthless Writeup Library @ szy.lol

batmon and hddmon

Two Go practice apps, for monitoring the battery and my external hdds.


The simpler one of the two, batmon, reads /sys/class/power_supply/BAT0/charge_now in a loop, and monitors the rate it’s decreasing. Every 5%, it pops up a notification with notify-send(1) (not even attaching to the dbus directly, smh!) so I know that I should start looking for power sockets.

The other one, hddmon, is a little bit more complex. I use external USB3 hard drives to store data. USB3 hates me and I reciprocate, and sometimes the drives will just disconnect. This causes a bit of a mess, since because Deluge is usually running in the background, the mountpoint is busy and they cannot unmount automatically. This means I must manually stop Deluge (preferably quite quickly too, because once it notices the files are gone it might do something unpleasant), and then replug the drive and remount it. Since the issues happen silently, with the only notification being constant complaints from the kernel, I wrote hddmon to monitor dmesg output.

After starting, the program starts dmesg -w to get a feed of kernel messages, and starts grepping for I/O errors. Once an error is detected, it is sent to a batcher goroutine, which batches the errors into notifications. All that, so sometimes when I return from AFK, I can unlock my screen to this.

My desktop, covered in error messages from hddmon.

I need to get around to building that NAS.

Code listings
// batmon
package main

import (
	"fmt"
	"os/exec"
	"log"
	"strconv"
	"time"
	"io/ioutil"
	"strings"
)

func sendnotify(text string) {
	err := exec.Command("notify-send", "-a", "batmon", "-t", "10000", "-i", "battery-low-charging-symbolic", "Battery usage notification", text).Run()
	if err != nil {
		log.Panic(err)
	}
}

func readfile(fn string) int {
	raw, err := ioutil.ReadFile(fn)
	if err != nil {
		log.Print(err)
		return -1
	}
	text := strings.TrimRight(string(raw), "\n ")
	log.Printf("scanned %#v", text)
	kb, err := strconv.Atoi(text)
	if err != nil {
		log.Print(err)
		return -1
	}
	return kb
}
func pctg() int {
	now := readfile("/sys/class/power_supply/BAT0/charge_now")
	full := readfile("/sys/class/power_supply/BAT0/charge_full")
	if now == -1 || now == -1 {
		return -1
	}
	return now * 100 / full;
}

func main() {
	var last int
	for {
		pc := pctg()
		if pc == -1 {
			log.Printf("read %d%%!", pc)
			sendnotify("Failed to get batt %!")
		} else {
			diff := last - pc
			log.Printf("%d%% -> %d%%, d%dpp", last, pc, diff)
			if diff > 5 || (pc < 40 && diff > 2) {
				last = pc
				sendnotify(fmt.Sprintf("Battery state: %d%%", pc))
			} else if diff < 0 {
				last = pc // ensure increase always written
			}
		}
		time.Sleep(5*time.Second)
	}
}
// hddmon
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os/exec"
	"regexp"
	"strings"
	"time"
)

func sendnotify(text string) {
	log.Printf("sending notify: %#v", text)
	err := exec.Command("notify-send", "-u", "critical", "-i", "harddrive", "HDD ERROR", text).Run()
	if err != nil {
		log.Panic(err)
	}
}

var tosend = make(chan string, 100)

func batcher() {
	sb := strings.Builder{}
	for {
		skipped := false
	read:
		for {
			select {
			case msg := <-tosend:
				if sb.Len() < 200 {
					if sb.Len() != 0 {
						sb.WriteRune('\n')
					}
					sb.WriteString(msg)
				} else {
					skipped = true
				}
			default:
				break read
			}
		}
		if skipped {
			sb.WriteString("\n[...]")
		}
		if sb.Len() != 0 {
			sendnotify(sb.String())
			sb.Reset()
		}
		time.Sleep(10 * time.Second)
	}
}

var blkReg = regexp.MustCompile(`blk_update_request: I/O error, dev (sd.),`)
var ioReg = regexp.MustCompile(`I/O error on device (sd..), logical block`)
var fsReg = regexp.MustCompile(`EXT4-fs error \(device (sd..)\): .* comm ([a-zA-Z0-9_-]+): (.*)`)
var uaReg = regexp.MustCompile(`\[(sd.)\] tag.* (uas_[a-z_-]*)`)
var faReg = regexp.MustCompile(`\[(sd.)\] tag.* FAILED Result: (.*)`)

func check(_s []byte) {
	s := strings.ToValidUTF8(string(_s), "\ufffd")
	log.Println("msg:", s)
	m := blkReg.FindStringSubmatch(s)
	if m != nil {
		tosend <- fmt.Sprintf("Block I/O err on device %s", m[1])
	}
	m = ioReg.FindStringSubmatch(s)
	if m != nil {
		tosend <- fmt.Sprintf("I/O error on device %s", m[1])
	}
	m = fsReg.FindStringSubmatch(s)
	if m != nil {
		tosend <- fmt.Sprintf("EXT4 error on device %s, process %s: %s", m[1], m[2], m[3])
	}
	m = uaReg.FindStringSubmatch(s)
	if m != nil {
		tosend <- fmt.Sprintf("%s on device %s", m[2], m[1])
	}
	m = faReg.FindStringSubmatch(s)
	if m != nil {
		tosend <- fmt.Sprintf("Read fail on device %s; %s", m[1], m[2])
	}
}

type DmesgParser struct {
	io.Writer
	linebuf []byte
}

func (d DmesgParser) Write(p []byte) (n int, err error) {
	if d.linebuf == nil {
		d.linebuf = make([]byte, 0)
	}
	lines := bytes.Split(p, ([]byte)("\n"))
	if len(lines) == 1 {
		copy(d.linebuf, lines[0])
	} else {
		for i, v := range lines {
			if i == 0 {
				d.linebuf = append(d.linebuf, v...)
				check(d.linebuf)
			} else if i == len(lines)-1 {
				d.linebuf = d.linebuf[:0]
				d.linebuf = append(d.linebuf, v...)
			} else {
				check(v)
			}
		}
	}
	return len(p), nil
}

func dmesg() {
	cmd := exec.Command("dmesg", "-k", "-l", "err,crit,info", "-w")
	cmd.Stdout = DmesgParser{}
	cmd.Run()
}

func main() {
	go batcher()
	dmesg()
}