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//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 ()" 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) }