A seat is hardware; a graphical session is state
Two questions that had been answered by one capability. graphical-session asks whether a session is running now; seat asks whether one could ever run here. Assignment needs the second -- a headless server can never have a display server, a workstation with nothing installed yet can, and until now those looked identical. The mesh would have assigned xorg to the server and found out at apply time. "seat" rather than "display", and the distinction matters most on a phone. An Android device plainly has a screen and has no seat: nothing there is going to take a DRM device and present an X or Wayland session on it. A capability called display would answer yes and be useless. This one answers no, which is true and useful. Read from what the kernel reports about its connectors, which distinguishes the two ways of not having one: a machine with a graphics card and nothing plugged in is a different thing from a machine with no graphics at all, and somebody deciding where a desktop goes wants to know which they are looking at. Verified against this workstation: it reports card1-DP-1 and card1-DP-2, which are the two monitors actually connected, and ignores the DisplayPort and HDMI that are not -- and the writeback connector reporting "unknown", which counting would have handed a seat to machines that have none.
This commit is contained in:
@@ -16,6 +16,9 @@ const (
|
|||||||
CapFirewall = "firewall"
|
CapFirewall = "firewall"
|
||||||
CapOverlay = "overlay"
|
CapOverlay = "overlay"
|
||||||
CapGraphicalSession = "graphical-session"
|
CapGraphicalSession = "graphical-session"
|
||||||
|
// CapSeat is hardware: somewhere a display server COULD run. CapGraphicalSession above is
|
||||||
|
// state: whether one IS running. Assignment needs the first.
|
||||||
|
CapSeat = "seat"
|
||||||
CapPrivileged = "privileged"
|
CapPrivileged = "privileged"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -172,6 +175,7 @@ func Default(runner Runner) []Detector {
|
|||||||
return []Detector{
|
return []Detector{
|
||||||
privileged{},
|
privileged{},
|
||||||
graphicalSession{},
|
graphicalSession{},
|
||||||
|
seat{},
|
||||||
commandCapability{
|
commandCapability{
|
||||||
name: CapContainerRuntime, command: "docker", args: []string{"info", "--format", "{{.ServerVersion}}"},
|
name: CapContainerRuntime, command: "docker", args: []string{"info", "--format", "{{.ServerVersion}}"},
|
||||||
why: "asks the daemon for its version — a running daemon, not an installed client",
|
why: "asks the daemon for its version — a running daemon, not an installed client",
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package profile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A seat is somewhere a display server could run: a graphics device with a display attached.
|
||||||
|
//
|
||||||
|
// **Not the same question as `graphical-session`**, and the difference is what makes it worth
|
||||||
|
// having. That one asks whether a session is running *now*, which is state; this asks whether one
|
||||||
|
// could ever run here, which is hardware. Deciding whether to assign a display server needs the
|
||||||
|
// second — a headless server can never have one, a workstation with nothing installed yet can,
|
||||||
|
// and until this existed those two looked identical. The mesh would have assigned xorg to the
|
||||||
|
// server and found out at apply time.
|
||||||
|
//
|
||||||
|
// **`seat` rather than `display`, and the distinction matters most on a phone.** An Android
|
||||||
|
// device plainly has a screen and has no seat: nothing there is going to take a DRM device and
|
||||||
|
// present an X or Wayland session on it. A capability called `display` would answer *yes* and be
|
||||||
|
// useless; `seat` answers *no*, which is the true and useful answer.
|
||||||
|
//
|
||||||
|
// The term is logind's, and it means what is wanted here: a set of hardware one person sits at.
|
||||||
|
|
||||||
|
// SeatSource is where the kernel reports what is attached. A variable so a test can point it at a
|
||||||
|
// directory it made, rather than at whatever this machine happens to have.
|
||||||
|
var SeatSource = "/sys/class/drm"
|
||||||
|
|
||||||
|
type seat struct{}
|
||||||
|
|
||||||
|
func (seat) Name() string { return CapSeat }
|
||||||
|
|
||||||
|
func (seat) Detect(context.Context) Verdict {
|
||||||
|
const how = "/sys/class/drm/*/status — a connector the kernel reports as connected"
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(SeatSource)
|
||||||
|
if err != nil {
|
||||||
|
// No DRM subsystem at all: a container, a phone, a machine with no graphics stack. Said
|
||||||
|
// as what was looked for rather than as an error, because this is the ordinary answer on
|
||||||
|
// most nodes and not a fault on any of them.
|
||||||
|
return Verdict{
|
||||||
|
Name: CapSeat, Present: false,
|
||||||
|
Detail: "no graphics devices are present (" + SeatSource + " is not readable)",
|
||||||
|
How: how,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var connected, disconnected []string
|
||||||
|
for _, e := range entries {
|
||||||
|
status, err := os.ReadFile(filepath.Join(SeatSource, e.Name(), "status"))
|
||||||
|
if err != nil {
|
||||||
|
// Cards themselves have no status file; only connectors do. Skipping is right and
|
||||||
|
// not a failure to report.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch strings.TrimSpace(string(status)) {
|
||||||
|
case "connected":
|
||||||
|
connected = append(connected, e.Name())
|
||||||
|
default:
|
||||||
|
disconnected = append(disconnected, e.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(connected)
|
||||||
|
|
||||||
|
if len(connected) > 0 {
|
||||||
|
return Verdict{
|
||||||
|
Name: CapSeat, Present: true,
|
||||||
|
Detail: strings.Join(connected, ", "),
|
||||||
|
How: how,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(disconnected) > 0 {
|
||||||
|
// The distinction worth drawing: this machine has graphics hardware and nothing plugged
|
||||||
|
// into it. A server with a GPU and no monitor, which is a different thing from a machine
|
||||||
|
// with no graphics at all — and a person deciding where a desktop goes wants to know
|
||||||
|
// which they are looking at.
|
||||||
|
return Verdict{
|
||||||
|
Name: CapSeat, Present: false,
|
||||||
|
Detail: "a graphics device with no display attached",
|
||||||
|
How: how,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Verdict{
|
||||||
|
Name: CapSeat, Present: false,
|
||||||
|
Detail: "no display connectors are present",
|
||||||
|
How: how,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package profile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// drm builds what the kernel would expose, so the cases below are the real ones rather than a
|
||||||
|
// fake's idea of them: a workstation, a server with a card and no monitor, a machine with no
|
||||||
|
// graphics at all.
|
||||||
|
func drm(t *testing.T, connectors map[string]string) {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
for name, status := range connectors {
|
||||||
|
if err := os.MkdirAll(filepath.Join(dir, name), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, name, "status"), []byte(status+"\n"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The card itself has no status file, and skipping it must not be mistaken for an answer.
|
||||||
|
if err := os.MkdirAll(filepath.Join(dir, "card0"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
old := SeatSource
|
||||||
|
SeatSource = dir
|
||||||
|
t.Cleanup(func() { SeatSource = old })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAMachineWithAMonitorHasASeat(t *testing.T) {
|
||||||
|
drm(t, map[string]string{"card0-DP-1": "connected", "card0-HDMI-A-1": "disconnected"})
|
||||||
|
|
||||||
|
got := seat{}.Detect(context.Background())
|
||||||
|
if !got.Present {
|
||||||
|
t.Fatalf("a machine with a connected display has no seat: %s", got.Detail)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got.Detail, "card0-DP-1") {
|
||||||
|
t.Errorf("the answer does not say which connector: %q", got.Detail)
|
||||||
|
}
|
||||||
|
if strings.Contains(got.Detail, "HDMI") {
|
||||||
|
t.Errorf("a disconnected connector was counted: %q", got.Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAServerWithAGraphicsCardAndNoMonitorHasNoSeat(t *testing.T) {
|
||||||
|
// The case this capability exists for. A display server assigned here would install, start,
|
||||||
|
// and have nowhere to draw — and the mesh would have thought it succeeded.
|
||||||
|
drm(t, map[string]string{"card0-HDMI-A-1": "disconnected", "card0-DP-1": "disconnected"})
|
||||||
|
|
||||||
|
got := seat{}.Detect(context.Background())
|
||||||
|
if got.Present {
|
||||||
|
t.Fatal("a machine with nothing plugged in reported a seat")
|
||||||
|
}
|
||||||
|
// And it says which of the two "no" answers this is, because a person deciding where a
|
||||||
|
// desktop goes wants to know whether the hardware is there.
|
||||||
|
if !strings.Contains(got.Detail, "no display attached") {
|
||||||
|
t.Errorf("the answer does not distinguish this from having no graphics at all: %q", got.Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAMachineWithNoGraphicsAtAllHasNoSeat(t *testing.T) {
|
||||||
|
// A container, or a phone, where the DRM subsystem is not there to read. The ordinary answer
|
||||||
|
// on most nodes and a fault on none of them, so it is a plain "no" rather than an error.
|
||||||
|
old := SeatSource
|
||||||
|
SeatSource = filepath.Join(t.TempDir(), "not-here")
|
||||||
|
t.Cleanup(func() { SeatSource = old })
|
||||||
|
|
||||||
|
got := seat{}.Detect(context.Background())
|
||||||
|
if got.Present {
|
||||||
|
t.Fatal("a machine with no graphics subsystem reported a seat")
|
||||||
|
}
|
||||||
|
if got.Detail == "" || strings.Contains(strings.ToLower(got.Detail), "error") {
|
||||||
|
t.Errorf("absence was reported as a failure: %q", got.Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnUnknownConnectorIsNotASeat(t *testing.T) {
|
||||||
|
// Writeback connectors and some virtual devices report "unknown". Counting those would give
|
||||||
|
// a seat to machines that have none — and this workstation has one, so it is not theoretical.
|
||||||
|
drm(t, map[string]string{"card0-Writeback-1": "unknown"})
|
||||||
|
|
||||||
|
if (seat{}).Detect(context.Background()).Present {
|
||||||
|
t.Fatal("a connector reporting \"unknown\" was counted as a display")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTheAnswerSaysHowItKnows(t *testing.T) {
|
||||||
|
// Every detector says how, so a wrong answer can be found rather than argued about.
|
||||||
|
drm(t, map[string]string{"card0-DP-1": "connected"})
|
||||||
|
if !strings.Contains((seat{}).Detect(context.Background()).How, "status") {
|
||||||
|
t.Error("the seat detector does not say what it looked at")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestASeatIsNotAGraphicalSession(t *testing.T) {
|
||||||
|
// The two are different questions and the whole point of adding one was that they had been
|
||||||
|
// answered by the same thing. A machine can have a seat and no session — that is every
|
||||||
|
// workstation before anything is installed on it, and exactly where a display server should
|
||||||
|
// be assigned.
|
||||||
|
drm(t, map[string]string{"card0-DP-1": "connected"})
|
||||||
|
t.Setenv("DISPLAY", "")
|
||||||
|
t.Setenv("WAYLAND_DISPLAY", "")
|
||||||
|
|
||||||
|
if !(seat{}).Detect(context.Background()).Present {
|
||||||
|
t.Fatal("a machine with a monitor and no session has no seat")
|
||||||
|
}
|
||||||
|
if (graphicalSession{}).Detect(context.Background()).Present {
|
||||||
|
t.Fatal("a machine with no session reported one")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user