A package and nothing else, the tool the desktop's scripts depend on. Four Go tools: copy, paste (text, base64 for other types, empty when nothing), targets and session. The runtime is given no session words, but runs as the account in the machine's own namespace, so the session is found rather than configured: the process's DISPLAY, else the account's processes' DISPLAY and XAUTHORITY from /proc (the window manager's first), else the only X socket with ~/.Xauthority. Measured with both variables unset: :1 found through i3, the server answered. With no session every tool says so and runs nothing.
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)
|
|
}
|