It requires x11-display, names itself in TERMINAL, and contributes its X resources to the xresources slot normal: today's palette and clipboard keys, JetBrainsMono Nerd Font, 10000 lines of scrollback, all scoped to XTerm* instead of every Xt program. It owns no file. The tools are the seat's open, which starts a terminal through the account's service manager so it outlives the runtime's restarts, and font (in force, what fontconfig resolves it to, set for new terminals) and colours.
256 lines
9.3 KiB
Go
256 lines
9.3 KiB
Go
package desktop
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A machine in a directory: a process table, the X servers' socket directory and a runtime base.
|
|
type fakeMachine struct {
|
|
t *testing.T
|
|
proc, x11, runtime string
|
|
uid int
|
|
}
|
|
|
|
func newMachine(t *testing.T) *fakeMachine {
|
|
root, err := os.MkdirTemp("", "desk")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { os.RemoveAll(root) })
|
|
m := &fakeMachine{t: t, proc: filepath.Join(root, "p"), x11: filepath.Join(root, "x"), runtime: filepath.Join(root, "r"), uid: os.Getuid()}
|
|
for _, d := range []string{m.proc, m.x11, filepath.Join(m.runtime, strconv.Itoa(m.uid))} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (m *fakeMachine) socket(path string) {
|
|
l, err := net.Listen("unix", path)
|
|
if err != nil {
|
|
m.t.Fatal(err)
|
|
}
|
|
m.t.Cleanup(func() { l.Close() })
|
|
}
|
|
|
|
func (m *fakeMachine) process(pid int, comm string, start int, env ...string) {
|
|
dir := filepath.Join(m.proc, strconv.Itoa(pid))
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
m.t.Fatal(err)
|
|
}
|
|
os.WriteFile(filepath.Join(dir, "environ"), []byte(strings.Join(env, "\x00")+"\x00"), 0o600)
|
|
os.WriteFile(filepath.Join(dir, "comm"), []byte(comm+"\n"), 0o644)
|
|
// pid (comm) state ppid pgrp session tty tpgid flags minflt cminflt majflt cmajflt utime stime
|
|
// cutime cstime priority nice threads itrealvalue starttime ...
|
|
stat := strconv.Itoa(pid) + " (" + comm + ") S 1 1 1 0 -1 0 0 0 0 0 0 0 0 0 20 0 1 0 " + strconv.Itoa(start) + " 0 0"
|
|
os.WriteFile(filepath.Join(dir, "stat"), []byte(stat), 0o644)
|
|
}
|
|
|
|
func (m *fakeMachine) finder(logind func(string) (map[string]string, error), prefer ...string) Finder {
|
|
return Finder{Proc: m.proc, X11Sockets: m.x11, RuntimeBase: m.runtime, UID: m.uid, Prefer: prefer, Logind: logind}
|
|
}
|
|
|
|
func active(id string) (map[string]string, error) {
|
|
return map[string]string{"Active": "yes", "State": "active", "Remote": "no", "Type": "x11"}, nil
|
|
}
|
|
|
|
func TestTheSessionIsFoundInTheWindowManagersEnvironmentAndOnlyItsWordsAreRead(t *testing.T) {
|
|
m := newMachine(t)
|
|
m.socket(filepath.Join(m.x11, "X1"))
|
|
run := filepath.Join(m.runtime, strconv.Itoa(m.uid))
|
|
m.socket(filepath.Join(run, "bus"))
|
|
m.process(100, "lemurs-child", 5, "DISPLAY=:1", "XDG_SESSION_ID=1")
|
|
m.process(200, "i3", 10, "DISPLAY=:1", "XAUTHORITY=/home/op/.Xauthority", "XDG_SESSION_ID=1",
|
|
"XDG_SESSION_TYPE=x11", "XDG_CURRENT_DESKTOP=i3", "XDG_RUNTIME_DIR="+run,
|
|
"DBUS_SESSION_BUS_ADDRESS=unix:path=/tmp/dbus-private", "NPM_TOKEN=secret", "OPENAI_API_KEY=secret")
|
|
m.process(300, "zsh", 50, "TERM=xterm") // no display: not a candidate
|
|
|
|
s, err := m.finder(active, "i3").Find()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.FoundIn.PID != 200 || s.Display != ":1" || s.XAuthority != "/home/op/.Xauthority" || s.ID != "1" || s.Type != "x11" || s.Desktop != "i3" {
|
|
t.Fatalf("session: %+v", s)
|
|
}
|
|
if s.Bus != "unix:path="+filepath.Join(run, "bus") || s.SessionBus != "unix:path=/tmp/dbus-private" {
|
|
t.Fatalf("the user manager's bus first, the session's private one reported beside it: %q %q", s.Bus, s.SessionBus)
|
|
}
|
|
if s.Active == nil || !*s.Active {
|
|
t.Fatal("logind's word is carried")
|
|
}
|
|
env := strings.Join(s.Env([]string{"PATH=/usr/bin", "DISPLAY=:9", "HOME=/home/op"}), "\n")
|
|
for _, want := range []string{"PATH=/usr/bin", "HOME=/home/op", "DISPLAY=:1", "XAUTHORITY=/home/op/.Xauthority", "DBUS_SESSION_BUS_ADDRESS=unix:path=" + filepath.Join(run, "bus"), "XDG_RUNTIME_DIR=" + run} {
|
|
if !strings.Contains(env, want) {
|
|
t.Errorf("env lacks %s:\n%s", want, env)
|
|
}
|
|
}
|
|
if strings.Contains(env, ":9") || strings.Contains(env, "secret") || strings.Contains(env, "NPM_TOKEN") {
|
|
t.Fatalf("the base's display is replaced and no other word of the session's process passes:\n%s", env)
|
|
}
|
|
b, _ := json.Marshal(s)
|
|
if strings.Contains(string(b), "secret") {
|
|
t.Fatal("the answer carries a word outside the session's")
|
|
}
|
|
}
|
|
|
|
func TestWithoutAPreferenceTheOldestProcessOfTheLiveSessionWins(t *testing.T) {
|
|
m := newMachine(t)
|
|
m.socket(filepath.Join(m.x11, "X0"))
|
|
m.process(410, "xterm", 90, "DISPLAY=:0", "XDG_SESSION_ID=3")
|
|
m.process(400, "openbox", 20, "DISPLAY=:0", "XDG_SESSION_ID=3")
|
|
s, err := m.finder(nil).Find()
|
|
if err != nil || s.FoundIn.PID != 400 {
|
|
t.Fatalf("%+v %v", s, err)
|
|
}
|
|
if s.RuntimeDir != filepath.Join(m.runtime, strconv.Itoa(m.uid)) || s.Bus != "" {
|
|
t.Fatalf("an absent runtime directory word falls back to the account's, and no bus socket means no bus: %+v", s)
|
|
}
|
|
}
|
|
|
|
func TestALeftoverProcessOfAnEndedSessionIsNotTheSession(t *testing.T) {
|
|
m := newMachine(t)
|
|
m.process(500, "i3", 10, "DISPLAY=:2", "XDG_SESSION_ID=7") // no X2 socket
|
|
_, err := m.finder(active, "i3").Find()
|
|
if !IsNoSession(err) || !strings.Contains(err.Error(), "socket is gone") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
var answer map[string]any
|
|
if json.Unmarshal([]byte(err.Error()), &answer) != nil || answer["error"] != "no-graphical-session" {
|
|
t.Fatalf("the refusal is structured: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestNoProcessWithADisplayIsAClearNoSession(t *testing.T) {
|
|
m := newMachine(t)
|
|
m.process(600, "sshd", 1, "SSH_CONNECTION=x")
|
|
_, err := m.finder(active).Find()
|
|
if !IsNoSession(err) || !strings.Contains(err.Error(), "no process of uid") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|
|
|
|
func TestAnActiveLocalSessionBeatsAnInactiveOneAndARemoteOneIsRefused(t *testing.T) {
|
|
m := newMachine(t)
|
|
m.socket(filepath.Join(m.x11, "X0"))
|
|
m.socket(filepath.Join(m.x11, "X1"))
|
|
m.process(700, "i3", 5, "DISPLAY=:0", "XDG_SESSION_ID=a")
|
|
m.process(800, "i3", 9, "DISPLAY=:1", "XDG_SESSION_ID=b")
|
|
logind := func(id string) (map[string]string, error) {
|
|
if id == "a" {
|
|
return map[string]string{"Active": "no", "State": "online", "Remote": "no"}, nil
|
|
}
|
|
return map[string]string{"Active": "yes", "State": "active", "Remote": "no"}, nil
|
|
}
|
|
s, err := m.finder(logind, "i3").Find()
|
|
if err != nil || s.FoundIn.PID != 800 || s.Display != ":1" {
|
|
t.Fatalf("the active session: %+v %v", s, err)
|
|
}
|
|
remote := func(string) (map[string]string, error) {
|
|
return map[string]string{"Active": "yes", "Remote": "yes"}, nil
|
|
}
|
|
if _, err := m.finder(remote).Find(); !IsNoSession(err) {
|
|
t.Fatalf("a remote session is not the operator's desktop: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAWaylandSessionIsFoundByItsCompositorsSocket(t *testing.T) {
|
|
m := newMachine(t)
|
|
run := filepath.Join(m.runtime, strconv.Itoa(m.uid))
|
|
m.socket(filepath.Join(run, "wayland-1"))
|
|
m.process(900, "sway", 3, "WAYLAND_DISPLAY=wayland-1", "XDG_RUNTIME_DIR="+run, "SWAYSOCK=/run/x.sock")
|
|
s, err := m.finder(nil, "sway").Find()
|
|
if err != nil || s.Type != "wayland" || s.WaylandDisplay != "wayland-1" {
|
|
t.Fatalf("%+v %v", s, err)
|
|
}
|
|
if !strings.Contains(strings.Join(s.Env(nil), " "), "SWAYSOCK=/run/x.sock") {
|
|
t.Fatal("the compositor's socket word passes")
|
|
}
|
|
}
|
|
|
|
func TestADisplayOnAnotherHostIsNotTheLocalSession(t *testing.T) {
|
|
for d, want := range map[string]bool{":0": true, ":1.0": true, "unix:2": true, "localhost:10.0": false, "host:0": false, "": false, ":x": false} {
|
|
if _, ok := DisplayNumber(d); ok != want {
|
|
t.Errorf("%q: %v", d, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestACommandIsBoundedAndItsFailureNamed(t *testing.T) {
|
|
r := Exec(context.Background(), os.Environ(), []byte("hello"), "cat")
|
|
if !r.OK() || r.Stdout != "hello" {
|
|
t.Fatalf("%+v", r)
|
|
}
|
|
r = Exec(context.Background(), os.Environ(), nil, "sh", "-c", "echo no >&2; exit 3")
|
|
if r.OK() || r.Code != 3 || !strings.Contains(r.Err().Error(), "exited 3: no") {
|
|
t.Fatalf("%+v", r)
|
|
}
|
|
r = Exec(context.Background(), os.Environ(), nil, "no-such-program-here")
|
|
if r.OK() || r.Code != 127 {
|
|
t.Fatalf("%+v", r)
|
|
}
|
|
r = Exec(context.Background(), os.Environ(), nil, "sh", "-c", "head -c 400000 /dev/zero")
|
|
if !r.Truncated || len(r.Stdout) != MostOutput {
|
|
t.Fatalf("cut at %d: %d %v", MostOutput, len(r.Stdout), r.Truncated)
|
|
}
|
|
}
|
|
|
|
func TestArgumentsAreReadStrictly(t *testing.T) {
|
|
a := Args{"name": " x ", "n": float64(3), "f": 1.5, "b": true, "l": []any{"a", "b"}}
|
|
if v, err := a.Text("name"); err != nil || v != "x" {
|
|
t.Fatal(v, err)
|
|
}
|
|
if _, err := a.Text("missing"); err == nil {
|
|
t.Fatal("a missing required text")
|
|
}
|
|
if n, err := a.Whole("n", 0, 1, 5); err != nil || n != 3 {
|
|
t.Fatal(n, err)
|
|
}
|
|
if _, err := a.Whole("f", 0, 0, 5); err == nil {
|
|
t.Fatal("1.5 is not whole")
|
|
}
|
|
if _, err := a.Whole("n", 0, 4, 5); err == nil {
|
|
t.Fatal("out of range")
|
|
}
|
|
if b, given, err := a.Bool("b"); !b || !given || err != nil {
|
|
t.Fatal("bool")
|
|
}
|
|
if _, _, err := a.Bool("name"); err == nil {
|
|
t.Fatal("text is not a bool")
|
|
}
|
|
if l, err := a.Strings("l"); err != nil || len(l) != 2 {
|
|
t.Fatal(l, err)
|
|
}
|
|
if _, err := a.OneOf("name", "", "y", "z"); err == nil {
|
|
t.Fatal("not one of")
|
|
}
|
|
}
|
|
|
|
func TestAPathIsKeptInsideTheHome(t *testing.T) {
|
|
t.Setenv("MESH_OPERATOR_HOME", "/home/op")
|
|
for in, want := range map[string]string{"~/a.png": "/home/op/a.png", "b/c": "/home/op/b/c", "/home/op/d": "/home/op/d", "~": "/home/op"} {
|
|
if got, err := InHome(in); err != nil || got != want {
|
|
t.Errorf("%s: %s %v", in, got, err)
|
|
}
|
|
}
|
|
for _, out := range []string{"/etc/passwd", "~/../other", "../x"} {
|
|
if _, err := InHome(out); err == nil {
|
|
t.Errorf("%s was accepted", out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPropertiesAreParsed(t *testing.T) {
|
|
p := ParseProperties("Active=yes\nState=active\nDisplay=\n")
|
|
if p["Active"] != "yes" || p["State"] != "active" || p["Display"] != "" {
|
|
t.Fatal(p)
|
|
}
|
|
}
|