Event Driven Awesomewm Statusbar


Because creating thousands of processes a day to repeatedly learn that my brightness hadn’t changed was bothering me more than it probably should have.

TL;DR

Single long running process monitors metrics like battery, volume, brightness, wifi and emits events to stdout. awesomewm parses events and emits signals, based on which widgets can modify state.

On git: https://codeberg.org/ishmael/statusmon

Root cause

Almost all widget library options for awesomewm involve long-polling. While it’s acceptable for some metrics like battery where there is no async option available, it’s utterly inefficient for things like volume and brightness that rarely change.

Moreover, it results in creating a new process every two seconds, and end up not doing anything at all.

Solution

So instead of polling on every metric like volume, brightness, etc. I start a single process statusmon that writes events to stdout. Then, I’ll have awesomewm read from it and emit lua signals.

The Monitor

I chose golang for writing the statusmon monitor. This application is a perfect usecase for go’s goroutines and channels concurrency model.

Each metric component (volume, wifi, brightness, battery) will recieve a channel, to which events will be emitted. Since lua lacks a encoding / decoding library, emitted events will be serialized into an constant expression that reutrns lua tables, which will then be evaluated in a sandbox inside awesomewm.

Example:

TEXT
return {percent=45, provider="brightness"}
return {capacity=85, charging=true, batname="BAT0", provider="battery"}
return {connected=true, ssid="RAP", ip="10.147.11.89", provider="wifi"}
return {volume=0.289998, muted=false, provider="volume"}

Awesomewm integration

In awesomewm, I simply start the process with a line callback that safely evaluates lua tables and emits events based on the provider.

LUA
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
local statusd_pid = awful.spawn.with_line_callback("/usr/local/bin/statusmon", {
	stdout = function(line)
		local result = safeeval(line)
		local provider = result["provider"]

		if result.error then
			naughty.notify({ title = "statusd: " .. result.provider, text = result.error })
			return
		end
		awesome.emit_signal("statusd::" .. provider, result)
	end,

	exit = function(reason, code)
		naughty.notify({
			title = "statusd-cat exited",
			text = tostring(reason) .. " " .. tostring(code),
		})
	end,
})

Safe evaluation of lua is important, since we don’t want possible bugs in statusmon to result in arbitrary lua code execution inside awesomewm. So I created this safeeval function that sandboxes expressions to text-only lua code, with no access to any functions.

LUA
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
---Execute data-only lua code. No functions allowed.
---@param codeStr string
---@return table
local safeeval = function(codeStr)
	local code, err = load(codeStr, "", "t", {})
	if code == nil then
		naughty.notify({ title = codeStr, text = err, })
		return {}
	end

	local ok, result = pcall(code)
	if not ok or type(result) ~= "table" then
		naughty.notify({ title = "not table return for code ", text = codeStr, })
		return {}
	end

	if type(result.provider) ~= "string" or result.provider == "" then
		naughty.notify({ title = "safeeval: missing or invalid 'provider'", text = codeStr })
		return {}
	end

	return result
end

Integration with widgets is simple. Attach callbacks to appropriate statusd event, and modify state accordingly.

LUA
1
2
3
4
5
6
7
8
9
local wifi = wibox.widget.textbox("N/A")
awesome.connect_signal("statusd::wifi", function(data)
    if not data.connected then
		naughty.notify({title="No WIFI!", text="Hope you got ethernet :)))"})
        wifi:set_markup("NOWIFI")
    else
        wifi:set_markup("<b>" .. data.ssid .. "</b>: " .. data.ip)
    end
end)

End result

This ended up being one of the more successful side quests to come out of my binary exploitation rabbit holes.

Most importantly, I no longer have to think about all those unnecessary CPU cycles and battery power being spent polling brightness and volume every couple of seconds. That’s one less thing for my inner performance gremlin to obsess over.