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 }