openrazer (the official driver, daemon and library), polychromatic (the AUR tray, kept as found; its i3 line moves out of the i3 module into its own node-display-session contribution), forticlient (the AUR VPN client: its service declared, its configuration never read) and nm-applet (the desktop half of NetworkManager, apart from the server-side module). The openrazer daemon fails on both workstations because the account is not in the openrazer group; openrazer_check names it and the README carries the one-off step, since the account's user resource is zsh's. desktop.go learns to tell a program from another sharing its 15-character command name, so polychromatic's tools never count or end themselves, and a copies test holds the six carriers to one text.
170 lines
6.1 KiB
Go
170 lines
6.1 KiB
Go
package main
|
|
|
|
// NetworkManager's applet as the tools see it: its process, its XDG autostart entry (the package's),
|
|
// its settings in gsettings, and NetworkManager's overall state as nmcli gives it. The applet is also
|
|
// NetworkManager's secret agent, the program that asks for a network's password; the tools never ask
|
|
// NetworkManager for a connection, a secret or an address. Those are joined at the machine (the
|
|
// networkmanager module's README).
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
appletComm = "nm-applet"
|
|
appletBin = "/usr/bin/nm-applet"
|
|
entryName = "nm-applet.desktop"
|
|
restartAs = "mesh-nm-applet"
|
|
packageFor = "network-manager-applet"
|
|
stackUnit = "NetworkManager.service"
|
|
settingsSet = "org.gnome.nm-applet"
|
|
)
|
|
|
|
// Manager is NetworkManager's overall state: no connection, network or address.
|
|
type Manager struct {
|
|
State string `json:"state"`
|
|
Connectivity string `json:"connectivity"`
|
|
Wifi string `json:"wifi"`
|
|
Networking string `json:"networking"`
|
|
}
|
|
|
|
func (m *Machine) manager() (*Manager, error) {
|
|
args := []string{"-t", "-f", "STATE,CONNECTIVITY,WIFI,NETWORKING", "general"}
|
|
o := m.cmd(5*time.Second, nil, "nmcli", args...)
|
|
if err := failed(o, "nmcli", args...); err != nil {
|
|
return nil, err
|
|
}
|
|
f := strings.Split(strings.TrimSpace(o.Stdout), ":")
|
|
if len(f) != 4 {
|
|
return nil, fmt.Errorf("nmcli's general state is not four fields: %q", tail(o.Stdout, 200))
|
|
}
|
|
return &Manager{State: f[0], Connectivity: f[1], Wifi: f[2], Networking: f[3]}, nil
|
|
}
|
|
|
|
// settings are the applet's switches in gsettings: the notifications it shows and whether it shows
|
|
// itself.
|
|
func (m *Machine) settings() map[string]string {
|
|
out := map[string]string{}
|
|
o := m.cmd(5*time.Second, m.bus(), "gsettings", "list-recursively", settingsSet)
|
|
if o.Err != nil || o.Code != 0 {
|
|
return out
|
|
}
|
|
for _, l := range strings.Split(o.Stdout, "\n") {
|
|
f := strings.Fields(l)
|
|
if len(f) == 3 && f[0] == settingsSet && f[1] != "stamp" {
|
|
out[f[1]] = f[2]
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// StatusAnswer is what nm_applet_status answers.
|
|
type StatusAnswer struct {
|
|
Installed string `json:"installed,omitempty"`
|
|
Applet []Proc `json:"applet"`
|
|
StartedBy Autostart `json:"started_by"`
|
|
Settings map[string]string `json:"settings"`
|
|
Manager *Manager `json:"network_manager,omitempty"`
|
|
Unanswered string `json:"unanswered,omitempty"`
|
|
}
|
|
|
|
// Status reads the applet and NetworkManager's overall state.
|
|
func (m *Machine) Status() (StatusAnswer, error) {
|
|
s := StatusAnswer{Applet: m.procs(appletComm), StartedBy: m.autostart(entryName), Settings: m.settings()}
|
|
if s.Applet == nil {
|
|
s.Applet = []Proc{}
|
|
}
|
|
v, err := m.installed(packageFor)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
s.Installed = v
|
|
if s.Manager, err = m.manager(); err != nil {
|
|
s.Unanswered = err.Error()
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// RestartAnswer is what nm_applet_restart answers.
|
|
type RestartAnswer struct {
|
|
Ended []int `json:"ended"`
|
|
Killed []int `json:"killed,omitempty"`
|
|
Running []Proc `json:"running"`
|
|
Session Session `json:"session"`
|
|
Unit string `json:"unit"`
|
|
}
|
|
|
|
// Restart ends the applet and starts it again in the operator's session, under the account's service
|
|
// manager. NetworkManager and its connections are not touched: the applet is only their face.
|
|
func (m *Machine) Restart() (RestartAnswer, error) {
|
|
s, err := m.session()
|
|
if err != nil {
|
|
return RestartAnswer{}, err
|
|
}
|
|
a := RestartAnswer{Session: s, Unit: restartAs + ".service"}
|
|
a.Ended, a.Killed = m.stop(5*time.Second, appletComm)
|
|
if err := m.detach(s, restartAs, appletBin); err != nil {
|
|
return a, err
|
|
}
|
|
a.Running = m.waitFor(appletComm, 4*time.Second)
|
|
if len(a.Running) == 0 {
|
|
return a, fmt.Errorf("the applet was started as %s but no %s process appeared within 4 s: "+
|
|
"see `journalctl --user -u %s`", a.Unit, appletComm, a.Unit)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// CheckAnswer is what nm_applet_check answers.
|
|
type CheckAnswer struct {
|
|
OK bool `json:"ok"`
|
|
Findings []Finding `json:"findings"`
|
|
Starts []string `json:"starts"`
|
|
}
|
|
|
|
// Check verifies what the module promises and relies on: the package; one start (the package's
|
|
// autostart entry, which the session's dex runs); the applet running once in a session; and
|
|
// NetworkManager, which is the networkmanager module's, running.
|
|
func (m *Machine) Check() (CheckAnswer, error) {
|
|
a := CheckAnswer{Findings: []Finding{}, Starts: []string{}}
|
|
add := func(what, do string) { a.Findings = append(a.Findings, Finding{what, do}) }
|
|
v, err := m.installed(packageFor)
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
if v == "" {
|
|
add("the package "+packageFor+" is not installed", "push the module to the node")
|
|
}
|
|
entry := m.autostart(entryName)
|
|
if entry.Starts {
|
|
a.Starts = append(a.Starts, "XDG autostart: "+entry.From)
|
|
if entry.From != "/etc/xdg/autostart/"+entryName {
|
|
add("the account's own "+entry.From+" replaces the package's entry", "remove it, so the package's entry is the one start")
|
|
}
|
|
} else {
|
|
add("the applet does not start with the session ("+entry.Because+")", "remove ~/.config/autostart/"+entryName+" if it hides the package's entry")
|
|
}
|
|
if o := m.cmd(0, nil, "dex", "--version"); o.Err != nil {
|
|
add("dex, which runs the XDG autostart entries at login, is not installed", "assign the i3 module, which installs it and runs it")
|
|
}
|
|
for _, l := range m.i3Starts(appletComm) {
|
|
a.Starts = append(a.Starts, "window manager: "+l)
|
|
add("a second start: "+l, "remove the line; the package's autostart entry is the applet's one start")
|
|
}
|
|
if o := m.cmd(0, nil, "systemctl", "is-active", stackUnit); o.Err != nil || strings.TrimSpace(o.Stdout) != "active" {
|
|
add("NetworkManager ("+stackUnit+") is not running: the applet has nothing to show",
|
|
"assign the networkmanager module, which holds the node's uplink with it")
|
|
}
|
|
if _, err := m.session(); err == nil {
|
|
switch running := m.procs(appletComm); {
|
|
case len(running) == 0:
|
|
add("no applet runs in the desktop session", "nm_applet_restart")
|
|
case len(running) > 1:
|
|
add(fmt.Sprintf("%d applets run", len(running)), "nm_applet_restart ends them all and starts one")
|
|
}
|
|
}
|
|
a.OK = len(a.Findings) == 0
|
|
return a, nil
|
|
}
|