screen-lock: the lock screen as a module, claiming node-lock-screen and serving lock (hq ADR 0208)
The distribution's i3lock behind a locker that releases xss-lock's sleep lock once it is up; timeouts and xss-lock from the session's xinitrc slot, ending with the session; i3lock-color and xscreensaver declared absent; Go tools lock, idle, inhibit and locked.
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const nobody = 4194400
|
||||
|
||||
// xset q as the laptop answered it on 2026-10-04 (keyboard lines shortened).
|
||||
const xsetQOutput = `Keyboard Control:
|
||||
auto repeat: on key click percent: 0 LED mask: 00000000
|
||||
Screen Saver:
|
||||
prefer blanking: yes allow exposures: yes
|
||||
timeout: 600 cycle: 600
|
||||
Colors:
|
||||
default colormap: 0x20 BlackPixel: 0x0 WhitePixel: 0xffffff
|
||||
DPMS (Display Power Management Signaling):
|
||||
Standby: 1800 Suspend: 1800 Off: 3600
|
||||
DPMS is Enabled
|
||||
Monitor is On
|
||||
`
|
||||
|
||||
func TestTheTimeoutsAreReadFromXset(t *testing.T) {
|
||||
st, err := parseXsetQ(xsetQOutput)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st.LockAfterSeconds != 600 || st.CycleSeconds != 600 || !st.DPMSEnabled || st.StandbySeconds != 1800 ||
|
||||
st.SuspendSeconds != 1800 || st.OffSeconds != 3600 || !st.MonitorOn {
|
||||
t.Fatalf("%+v", st)
|
||||
}
|
||||
if _, err := parseXsetQ("nothing"); err == nil {
|
||||
t.Fatal("an answer without a screensaver was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAProcessStartsWhenItsStatAndTheBootTimeSay(t *testing.T) {
|
||||
fakeMachine(t)
|
||||
fakeProcess(t, nobody, "i3lock")
|
||||
// A command name with a space and a parenthesis, which a naive split gets wrong.
|
||||
stat := strconv.Itoa(nobody) + " (i3 lock) x) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 12345 0 0\n"
|
||||
if err := os.WriteFile(filepath.Join(procRoot, strconv.Itoa(nobody), "stat"), []byte(stat), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(procRoot, "stat"), []byte("cpu 1 2 3\nbtime 1700000000\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
at, ok := startTime(nobody)
|
||||
if !ok || !at.Equal(time.Unix(1700000000, 0).Add(123450*time.Millisecond)) {
|
||||
t.Fatalf("%v %v", at, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// lockingMachine is a session whose loginctl, asked to lock, starts a (fake) i3lock.
|
||||
func lockingMachine(t *testing.T, watcher bool) string {
|
||||
t.Helper()
|
||||
fakeMachine(t)
|
||||
fakeProcess(t, nobody, "i3", "DISPLAY=:1", "XDG_SESSION_ID=4")
|
||||
if watcher {
|
||||
fakeProcess(t, nobody+1, "xss-lock", "DISPLAY=:1")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(runUserDir, strconv.Itoa(os.Getuid())), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lockNow := `mkdir -p "$PROC/` + strconv.Itoa(nobody+2) + `" && echo i3lock > "$PROC/` + strconv.Itoa(nobody+2) + `/comm"`
|
||||
bin := fakeBinaries(t, map[string]string{
|
||||
"loginctl": `echo "loginctl $*" >> "$LOG"
|
||||
case "$1" in lock-session) ` + lockNow + ` ;; show-session) echo yes ;; esac`,
|
||||
"systemd-run": `echo "systemd-run $*" >> "$LOG"; ` + lockNow,
|
||||
"systemctl": `echo "systemctl $*" >> "$LOG"; echo inactive`,
|
||||
})
|
||||
t.Setenv("LOG", filepath.Join(bin, "log"))
|
||||
t.Setenv("PROC", procRoot)
|
||||
return bin
|
||||
}
|
||||
|
||||
func TestLockGoesThroughLogindSoTheOneLockerAnswers(t *testing.T) {
|
||||
bin := lockingMachine(t, true)
|
||||
st, err := Lock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !st.Locked || !st.Watcher || st.LockedHint == nil || !*st.LockedHint || !strings.Contains(st.Via, "logind") {
|
||||
t.Fatalf("%+v", st)
|
||||
}
|
||||
log, _ := os.ReadFile(filepath.Join(bin, "log"))
|
||||
if !strings.Contains(string(log), "loginctl lock-session 4\n") || strings.Contains(string(log), "systemd-run") {
|
||||
t.Fatalf("asked:\n%s", log)
|
||||
}
|
||||
again, err := Lock()
|
||||
if err != nil || again.Via != "already locked" {
|
||||
t.Fatalf("locking a locked screen: %+v, %v", again, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithoutTheWatcherTheLockerRunsUnderTheAccountsServiceManager(t *testing.T) {
|
||||
bin := lockingMachine(t, false)
|
||||
st, err := Lock()
|
||||
if err != nil || !st.Locked || !strings.Contains(st.Via, "xss-lock is not running") {
|
||||
t.Fatalf("%+v, %v", st, err)
|
||||
}
|
||||
log, _ := os.ReadFile(filepath.Join(bin, "log"))
|
||||
if !strings.Contains(string(log), "--unit=screen-lock --setenv=DISPLAY=:1 --setenv=XDG_SESSION_ID=4 -- ") ||
|
||||
!strings.Contains(string(log), "/.local/bin/screen-lock") {
|
||||
t.Fatalf("asked:\n%s", log)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockedWithoutASessionIsAnAnswerNotAnError(t *testing.T) {
|
||||
fakeMachine(t)
|
||||
st, err := Locked()
|
||||
if err != nil || st.Locked || st.Watcher || st.PIDs == nil {
|
||||
t.Fatalf("%+v, %v", st, err)
|
||||
}
|
||||
if _, err := Lock(); !errors.Is(err, ErrNoSession) {
|
||||
t.Fatalf("lock without a session: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdleChangesOnlyWhatWasAskedAndSaysForHowLong(t *testing.T) {
|
||||
fakeMachine(t)
|
||||
fakeProcess(t, nobody, "i3", "DISPLAY=:1")
|
||||
bin := fakeBinaries(t, map[string]string{"xset": `echo "xset $*" >> "$LOG"; [ "$1" = q ] && cat "$Q"; true`})
|
||||
t.Setenv("LOG", filepath.Join(bin, "log"))
|
||||
q := filepath.Join(bin, "q")
|
||||
if err := os.WriteFile(q, []byte(xsetQOutput), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("Q", q)
|
||||
if st, err := Idle(IdleChange{}); err != nil || st.Note != "" || st.LockAfterSeconds != 600 {
|
||||
t.Fatalf("read: %+v, %v", st, err)
|
||||
}
|
||||
c, err := idleChangeOf(map[string]any{"lock_after_seconds": float64(900), "off_seconds": float64(7200)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := Idle(c)
|
||||
if err != nil || !strings.Contains(st.Note, "next login") {
|
||||
t.Fatalf("%+v, %v", st, err)
|
||||
}
|
||||
log, _ := os.ReadFile(filepath.Join(bin, "log"))
|
||||
if !strings.Contains(string(log), "xset s 900 600\n") || !strings.Contains(string(log), "xset dpms 1800 1800 7200\n") {
|
||||
t.Fatalf("asked:\n%s", log)
|
||||
}
|
||||
if _, err := idleChangeOf(map[string]any{"off_seconds": float64(-1)}); err == nil {
|
||||
t.Fatal("a negative timeout was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnInhibitionTurnsIdleOffAndRestoresWhatWasThereWhenItEndsOrIsStopped(t *testing.T) {
|
||||
was := IdleState{LockAfterSeconds: 1800, CycleSeconds: 1800, DPMSEnabled: true, StandbySeconds: 1800, SuspendSeconds: 1800, OffSeconds: 3600}
|
||||
script := inhibitScript(2, was)
|
||||
if !strings.Contains(script, "xset s off -dpms; sleep 120 & wait; restore") ||
|
||||
!strings.Contains(script, "restore() { xset s 1800 1800; xset dpms 1800 1800 3600; xset +dpms; }") ||
|
||||
!strings.Contains(script, "trap 'restore; exit 0' TERM") {
|
||||
t.Fatalf("%s", script)
|
||||
}
|
||||
// Run it for real with a fake xset, stopped early as systemctl stop would.
|
||||
dir := t.TempDir()
|
||||
bin := fakeBinaries(t, map[string]string{"xset": `echo "$*" >> "` + filepath.Join(dir, "log") + `"`})
|
||||
_ = bin
|
||||
cmd := exec.Command("/bin/sh", "-c", inhibitScript(1, was))
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
_ = cmd.Process.Signal(os.Interrupt)
|
||||
_ = cmd.Wait()
|
||||
got, _ := os.ReadFile(filepath.Join(dir, "log"))
|
||||
if string(got) != "s off -dpms\ns 1800 1800\ndpms 1800 1800 3600\n+dpms\n" {
|
||||
t.Fatalf("the timeouts were not restored on stop:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheDeclaredTimeoutsAreTheSessionStartsOwn(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
code := m.Shell[0].Code
|
||||
if !strings.Contains(code, "xset s "+strconv.Itoa(declaredLock)+" "+strconv.Itoa(declaredLock)+"\n") ||
|
||||
!strings.Contains(code, "xset dpms "+strconv.Itoa(declaredStandby)+" "+strconv.Itoa(declaredSuspend)+" "+strconv.Itoa(declaredOff)+"\n") {
|
||||
t.Fatalf("the tools' declared values and the session start's disagree:\n%s", code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user