Files
mesh-catalog/modules/xorg/internal/desktop/run.go
T
jochen b2c170acda xorg: the display server holds node-display-server and writes the session's start (hq ADR 0208)
The X server, its start and its tools as one module. It provides x11-display with
the machine's reach, gated by the host's seat capability. It writes a block at the
start of ~/.xinitrc: the account's environment, an explicit import into the user
manager, the mesh's X resources merged without cpp, autorandr, the xinitrc slots,
~/.xinitrc.local, and the session's exec from the last slot.

Its Go tools serve the seat's displays and layout (autorandr profiles keyed by
EDID), and set-mode, primary, dpi, input devices and settings, keyboard, a
screenshot (xwd decoded in Go) and the server's log. internal/desktop is how
every desktop tool finds the operator's session from the runtime, which has none:
from the session's own processes, reading only its words, confirmed with logind.
2026-10-04 13:21:47 +02:00

233 lines
7.1 KiB
Go

package desktop
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"time"
)
// Bounds on a command a tool runs: well below the runtime's 30 s call limit, and an answer that
// fits in a tool's reply.
const (
DefaultTimeout = 10 * time.Second
MostOutput = 256 << 10
)
// Result is what one command did.
type Result struct {
Command []string `json:"command"`
Code int `json:"exit_code"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
Truncated bool `json:"truncated,omitempty"`
TimedOut bool `json:"timed_out,omitempty"`
}
// OK is whether the command ran and exited 0.
func (r Result) OK() bool { return r.Code == 0 && !r.TimedOut }
// Err is the command's failure as an error naming it and what it said, or nil.
func (r Result) Err() error {
if r.OK() {
return nil
}
said := strings.TrimSpace(r.Stderr)
if said == "" {
said = strings.TrimSpace(r.Stdout)
}
if r.TimedOut {
return fmt.Errorf("%s did not finish in time", strings.Join(r.Command, " "))
}
return fmt.Errorf("%s exited %d: %s", strings.Join(r.Command, " "), r.Code, said)
}
// Runner runs a command with an environment and answers what it did. Tools take one, so their
// tests replace the machine with a table of answers.
type Runner func(ctx context.Context, env []string, stdin []byte, name string, args ...string) Result
// Exec is the machine's Runner: the command in its own process group, ended with everything it
// started at the deadline, each stream cut at MostOutput.
func Exec(ctx context.Context, env []string, stdin []byte, name string, args ...string) Result {
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, DefaultTimeout)
defer cancel()
}
res := Result{Command: append([]string{name}, args...)}
cmd := exec.Command(name, args...)
cmd.Env = env
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if stdin != nil {
cmd.Stdin = bytes.NewReader(stdin)
}
out, errb := &capped{}, &capped{}
cmd.Stdout, cmd.Stderr = out, errb
if err := cmd.Start(); err != nil {
res.Code = 127
res.Stderr = err.Error()
return res
}
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
var err error
select {
case err = <-done:
case <-ctx.Done():
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
err = <-done
res.TimedOut = true
}
res.Stdout, res.Stderr = out.String(), errb.String()
res.Truncated = out.cut || errb.cut
var exit *exec.ExitError
switch {
case err == nil:
case errors.As(err, &exit):
res.Code = exit.ExitCode()
if res.Code < 0 {
res.Code = 128
}
default:
res.Code = 1
if res.Stderr == "" {
res.Stderr = err.Error()
}
}
return res
}
// capped keeps the first MostOutput bytes written to it. Its buffer is a field, not embedded: an
// embedded bytes.Buffer brings ReadFrom along, and io.Copy would use it and never call Write.
type capped struct {
buf bytes.Buffer
cut bool
}
func (c *capped) Write(p []byte) (int, error) {
if room := MostOutput - c.buf.Len(); room < len(p) {
if room > 0 {
c.buf.Write(p[:room])
}
c.cut = true
return len(p), nil
}
return c.buf.Write(p)
}
func (c *capped) String() string { return c.buf.String() }
// Desk is what a desktop tool needs: how to find the session, and how to run a command.
type Desk struct {
Find func() (*Session, error)
Run Runner
// Base is the environment a command starts from, before the session's words.
Base []string
}
// Machine is the real Desk, preferring the named processes as the session's.
func Machine(prefer ...string) Desk {
return Desk{
Find: func() (*Session, error) { return Find(prefer...) },
Run: Exec,
Base: os.Environ(),
}
}
// InSession runs a command in the operator's session, or answers NoSession.
func (d Desk) InSession(ctx context.Context, name string, args ...string) (Result, *Session, error) {
s, err := d.Find()
if err != nil {
return Result{}, nil, err
}
return d.Run(ctx, s.Env(d.Base), nil, name, args...), s, nil
}
// InSessionWith is InSession with standard input.
func (d Desk) InSessionWith(ctx context.Context, stdin []byte, name string, args ...string) (Result, *Session, error) {
s, err := d.Find()
if err != nil {
return Result{}, nil, err
}
return d.Run(ctx, s.Env(d.Base), stdin, name, args...), s, nil
}
// Plain runs a command with the base environment: for what needs no session.
func (d Desk) Plain(ctx context.Context, name string, args ...string) Result {
return d.Run(ctx, d.Base, nil, name, args...)
}
// AsUser runs a command with the account's own runtime directory and bus, and no display.
func (d Desk) AsUser(ctx context.Context, name string, args ...string) Result {
return d.Run(ctx, UserEnv(d.Base, os.Getuid()), nil, name, args...)
}
// Launched is how a program was started in the session.
type Launched struct {
Unit string `json:"unit,omitempty"`
PID int `json:"pid,omitempty"`
How string `json:"how"`
}
// Launch starts a program in the operator's session that outlives the call and the runtime.
//
// **Not as a child of this process.** The runtime is a system service; everything it starts is in
// its control group, and the service manager ends that group whenever the runtime restarts — which
// is every push that changes it. So the program is handed to the account's own service manager as a
// transient unit (`systemd-run --user`), with the session's words set on it, and lives as long as the
// operator's user manager does. Without a user manager it is started detached as a last resort, and
// the answer says it will end with the runtime.
func (d Desk) Launch(ctx context.Context, s *Session, name string, argv ...string) (Launched, error) {
if len(argv) == 0 {
return Launched{}, errors.New("nothing to launch")
}
env := s.Env(d.Base)
unit := "mesh-" + name + "-" + token()
args := []string{"--user", "--collect", "--quiet", "--unit=" + unit}
for _, w := range []string{"DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_SESSION_TYPE", "XDG_CURRENT_DESKTOP", "XDG_SESSION_DESKTOP", "I3SOCK", "SWAYSOCK"} {
if v := lookup(env, w); v != "" {
args = append(args, "--setenv="+w+"="+v)
}
}
args = append(args, "--")
args = append(args, argv...)
res := d.Run(ctx, env, nil, "systemd-run", args...)
if res.OK() {
return Launched{Unit: unit, How: "a transient unit of the account's service manager; ends when it exits or when the operator logs out"}, nil
}
if s.Bus != "" {
return Launched{}, res.Err()
}
cmd := exec.Command(argv[0], argv[1:]...)
cmd.Env = env
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if err := cmd.Start(); err != nil {
return Launched{}, err
}
pid := cmd.Process.Pid
go func() { _ = cmd.Wait() }()
return Launched{PID: pid, How: "detached from the runtime with no user manager to hand it to; it ends when the runtime restarts"}, nil
}
func lookup(env []string, name string) string {
for i := len(env) - 1; i >= 0; i-- {
if k, v, ok := strings.Cut(env[i], "="); ok && k == name {
return v
}
}
return ""
}
func token() string {
b := make([]byte, 4)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}