Research 026/04 counted two plain dmenu calls failing with dmenu installed nowhere. Measured, they are one line on each workstation: dunst's `dmenu = /usr/bin/dmenu -p dunst:`. Installing the package fixes both by existing; the line stays the dunst module's. No claim: node-launcher is not in the controller's seat table yet, and the module says so. Two Go tools: menu, shaped like that seat's verb (chosen line, index, typed, cancelled, timed out within 25 s), and session.
178 lines
5.6 KiB
Go
178 lines
5.6 KiB
Go
package main
|
|
|
|
// session.go is the same file in the bundles whose tools act in the operator's graphical session
|
|
// (xclip, dmenu): how a process the node's tool runtime launched reaches that session.
|
|
//
|
|
// The runtime is a system service running as the operator account (novox/hq ADR 0175 §4), in the
|
|
// machine's own mount namespace, and is given no session words: no DISPLAY, no XAUTHORITY. An X
|
|
// server accepts a client that names its display and presents the cookie in the authority file, and
|
|
// both are the account's: the display's socket is in /tmp/.X11-unix, and the cookie file is
|
|
// readable by the account. So the session is found, not configured:
|
|
//
|
|
// 1. the process's own DISPLAY, when the runtime happens to have one;
|
|
// 2. else the DISPLAY and XAUTHORITY of the account's own running processes, read from
|
|
// /proc/<pid>/environ (the window manager's, by preference), whose socket exists;
|
|
// 3. else the only X socket there is, with the authority file in the account's home.
|
|
//
|
|
// When none is found the tool says that no graphical session of the account is running, and does
|
|
// nothing.
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// Session is the operator's X session as a tool reaches it.
|
|
type Session struct {
|
|
Display string `json:"display"`
|
|
XAuthority string `json:"xauthority,omitempty"`
|
|
// FoundBy says how: "environment", "process <pid> (<name>)" or "socket".
|
|
FoundBy string `json:"found_by"`
|
|
}
|
|
|
|
// Env is what a command needs to reach the session.
|
|
func (s Session) Env() []string {
|
|
env := []string{"DISPLAY=" + s.Display}
|
|
if s.XAuthority != "" {
|
|
env = append(env, "XAUTHORITY="+s.XAuthority)
|
|
}
|
|
return env
|
|
}
|
|
|
|
// Where the session is looked for. Tests point these at a tree of their own.
|
|
var (
|
|
procRoot = "/proc"
|
|
x11Sockets = "/tmp/.X11-unix"
|
|
getenv = os.Getenv
|
|
myUID = os.Getuid
|
|
)
|
|
|
|
// sessionWMs are the programs whose environment is the session's own, preferred over any other
|
|
// process's (a terminal's child may carry a stale or forwarded DISPLAY).
|
|
var sessionWMs = map[string]bool{"i3": true, "sway": true, "xinit": true, "i3bar": true, "picom": true, "dunst": true}
|
|
|
|
func accountHome() string {
|
|
if h := strings.TrimSpace(getenv("MESH_OPERATOR_HOME")); h != "" {
|
|
return h
|
|
}
|
|
if h := strings.TrimSpace(getenv("HOME")); h != "" {
|
|
return h
|
|
}
|
|
h, _ := os.UserHomeDir()
|
|
return h
|
|
}
|
|
|
|
// socketOf is the local socket of a display such as ":1" or ":1.0", or "" for a remote one.
|
|
func socketOf(display string) string {
|
|
if !strings.HasPrefix(display, ":") {
|
|
return ""
|
|
}
|
|
n := strings.TrimPrefix(display, ":")
|
|
if i := strings.IndexByte(n, '.'); i >= 0 {
|
|
n = n[:i]
|
|
}
|
|
if _, err := strconv.Atoi(n); err != nil {
|
|
return ""
|
|
}
|
|
return filepath.Join(x11Sockets, "X"+n)
|
|
}
|
|
|
|
func exists(p string) bool {
|
|
_, err := os.Stat(p)
|
|
return err == nil
|
|
}
|
|
|
|
// findSession answers the account's X session, or an error saying there is none.
|
|
func findSession() (Session, error) {
|
|
if d := strings.TrimSpace(getenv("DISPLAY")); d != "" {
|
|
if s := socketOf(d); s == "" || exists(s) {
|
|
return Session{Display: d, XAuthority: getenv("XAUTHORITY"), FoundBy: "environment"}, nil
|
|
}
|
|
}
|
|
type seen struct {
|
|
Session
|
|
wm bool
|
|
count int
|
|
}
|
|
found := map[string]*seen{}
|
|
entries, _ := os.ReadDir(procRoot)
|
|
for _, e := range entries {
|
|
pid, err := strconv.Atoi(e.Name())
|
|
if err != nil || !e.IsDir() {
|
|
continue
|
|
}
|
|
dir := filepath.Join(procRoot, e.Name())
|
|
info, err := os.Stat(dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if st, ok := info.Sys().(*syscall.Stat_t); !ok || int(st.Uid) != myUID() {
|
|
continue
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(dir, "environ"))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var display, auth string
|
|
for _, kv := range strings.Split(string(raw), "\x00") {
|
|
switch {
|
|
case strings.HasPrefix(kv, "DISPLAY="):
|
|
display = strings.TrimPrefix(kv, "DISPLAY=")
|
|
case strings.HasPrefix(kv, "XAUTHORITY="):
|
|
auth = strings.TrimPrefix(kv, "XAUTHORITY=")
|
|
}
|
|
}
|
|
if display == "" {
|
|
continue
|
|
}
|
|
if s := socketOf(display); s == "" || !exists(s) {
|
|
continue
|
|
}
|
|
comm, _ := os.ReadFile(filepath.Join(dir, "comm"))
|
|
name := strings.TrimSpace(string(comm))
|
|
key := display + "\x00" + auth
|
|
if found[key] == nil {
|
|
found[key] = &seen{Session: Session{Display: display, XAuthority: auth, FoundBy: fmt.Sprintf("process %d (%s)", pid, name)}}
|
|
}
|
|
f := found[key]
|
|
f.count++
|
|
if sessionWMs[name] && !f.wm {
|
|
f.wm = true
|
|
f.FoundBy = fmt.Sprintf("process %d (%s)", pid, name)
|
|
}
|
|
}
|
|
if len(found) > 0 {
|
|
all := make([]*seen, 0, len(found))
|
|
for _, f := range found {
|
|
all = append(all, f)
|
|
}
|
|
sort.Slice(all, func(i, k int) bool {
|
|
if all[i].wm != all[k].wm {
|
|
return all[i].wm
|
|
}
|
|
if all[i].count != all[k].count {
|
|
return all[i].count > all[k].count
|
|
}
|
|
return all[i].Display < all[k].Display
|
|
})
|
|
return all[0].Session, nil
|
|
}
|
|
sockets, _ := filepath.Glob(filepath.Join(x11Sockets, "X*"))
|
|
if len(sockets) == 1 {
|
|
s := Session{Display: ":" + strings.TrimPrefix(filepath.Base(sockets[0]), "X"), FoundBy: "socket"}
|
|
if a := filepath.Join(accountHome(), ".Xauthority"); exists(a) {
|
|
s.XAuthority = a
|
|
}
|
|
return s, nil
|
|
}
|
|
if len(sockets) > 1 {
|
|
return Session{}, fmt.Errorf("no process of this account names its X display, and there are %d X sockets in %s: which one is the operator's session cannot be told", len(sockets), x11Sockets)
|
|
}
|
|
return Session{}, fmt.Errorf("no graphical session of this account is running on this machine: no process of the account has DISPLAY set, and there is no X socket in %s. A desktop tool acts only while the operator is logged in to the graphical session", x11Sockets)
|
|
}
|