Files
mesh-catalog/modules/dmenu/cmd/dmenu-tools/session.go
T
jochen 838a6e510b dmenu: the package, which makes the notifier's menu work, and a menu tool (hq to-be 42 phase 2.7)
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.
2026-10-04 13:02:23 +02:00

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)
}