power: a machine's power as a module holding node-power; the laptop's resume and lid move onto it (hq ADR 0211)
Code around sleep was written into the service manager's sleep units by the module that needed it, and the mesh could not tell a sleeping machine from a lost one. power runs every module's code for the six moments, each piece bounded, owns logind's power handling from its settings, and says booted, sleeping, woke, shutting-down and the power source on the bus, sleeping under logind's delay lock before the machine sleeps.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Run on a real machine with MESH_LIVE=1: logind gives this account a delay lock, and it shows.
|
||||
func TestLiveLogindGivesADelayLock(t *testing.T) {
|
||||
if os.Getenv("MESH_LIVE") == "" {
|
||||
t.Skip("set MESH_LIVE=1 on a machine with logind")
|
||||
}
|
||||
l, err := DialLogin1()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
f, err := l.Inhibit("sleep:shutdown", "mesh power", "live test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, _ := exec.Command("systemd-inhibit", "--list", "--no-pager").CombinedOutput()
|
||||
if !strings.Contains(string(out), "mesh power") {
|
||||
t.Fatalf("not listed:\n%s", out)
|
||||
}
|
||||
t.Logf("%s", out)
|
||||
f.Close()
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
// systemLogin1 is logind on the system bus.
|
||||
type systemLogin1 struct {
|
||||
conn *dbus.Conn
|
||||
out chan Signal
|
||||
}
|
||||
|
||||
// DialLogin1 connects to logind and listens for its sleep and shutdown announcements.
|
||||
func DialLogin1() (Login1, error) {
|
||||
conn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, member := range []string{"PrepareForSleep", "PrepareForShutdown"} {
|
||||
if err := conn.AddMatchSignal(dbus.WithMatchInterface("org.freedesktop.login1.Manager"),
|
||||
dbus.WithMatchMember(member)); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
raw := make(chan *dbus.Signal, 16)
|
||||
conn.Signal(raw)
|
||||
l := &systemLogin1{conn: conn, out: make(chan Signal, 16)}
|
||||
go func() {
|
||||
defer close(l.out)
|
||||
for s := range raw {
|
||||
if len(s.Body) != 1 {
|
||||
continue
|
||||
}
|
||||
start, ok := s.Body[0].(bool)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch s.Name {
|
||||
case "org.freedesktop.login1.Manager.PrepareForSleep":
|
||||
l.out <- Signal{Start: start}
|
||||
case "org.freedesktop.login1.Manager.PrepareForShutdown":
|
||||
l.out <- Signal{Shutdown: true, Start: start}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (l *systemLogin1) Inhibit(what, who, why string) (*os.File, error) {
|
||||
var fd dbus.UnixFD
|
||||
err := l.conn.Object("org.freedesktop.login1", "/org/freedesktop/login1").
|
||||
Call("org.freedesktop.login1.Manager.Inhibit", 0, what, who, why, "delay").Store(&fd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.NewFile(uintptr(fd), "logind-inhibitor"), nil
|
||||
}
|
||||
|
||||
func (l *systemLogin1) Signals() <-chan Signal { return l.out }
|
||||
|
||||
func (l *systemLogin1) Close() { l.conn.Close() }
|
||||
@@ -0,0 +1,24 @@
|
||||
// The power module's Go bundle (novox/hq ADR 0211, ADR 0198): one process the node's runtime
|
||||
// launches, serving the module's tools over MCP on stdio and running its watcher beside them, which
|
||||
// publishes the machine's power states on the bus.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
m := Here()
|
||||
w := NewWatcher(m, func(eventType string, body any) error { return stdio.Emit(eventType, body) }, DialLogin1)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go w.Run(ctx)
|
||||
if err := stdio.Serve("", Tools(m, w)); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// Tools are the module's tools over MCP (novox/hq ADR 0211): the machine's power now, the code
|
||||
// modules contributed for each moment, the recent power history, running one moment on demand, and
|
||||
// the module's own check.
|
||||
func Tools(m *Machine, w *Watcher) []stdio.Tool {
|
||||
return []stdio.Tool{
|
||||
{
|
||||
Name: "power_state",
|
||||
Description: "This machine's power now: since when it runs, mains or battery and the charge, the lid, " +
|
||||
"logind's power-key and lid settings in force, and what the watcher last saw: its delay lock, " +
|
||||
"the last sleep and wake, and events still waiting for the bus.",
|
||||
Run: func(map[string]any) (any, error) { return m.State(w), nil },
|
||||
},
|
||||
{
|
||||
Name: "power_hooks",
|
||||
Description: "The code every module contributed for each power moment (after boot, on mains, on battery, " +
|
||||
"before sleep, after waking, before shutdown), as placed on this machine, each piece with its module.",
|
||||
Run: func(map[string]any) (any, error) { return m.Hooks(), nil },
|
||||
},
|
||||
{
|
||||
Name: "power_history",
|
||||
Description: "The machine's recent power events from the journal: each boot, every sleep and wake, and " +
|
||||
"every moment's run with each module's outcome.",
|
||||
Input: map[string]any{
|
||||
"boots": map[string]any{"type": "integer", "description": "how many boots back (default 3, at most 20)"},
|
||||
},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
n := 3
|
||||
if v, ok := args["boots"].(float64); ok {
|
||||
n = int(v)
|
||||
}
|
||||
if n < 1 || n > 20 {
|
||||
return nil, fmt.Errorf("boots is 1 to 20")
|
||||
}
|
||||
return m.History(context.Background(), n)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "power_run",
|
||||
Description: "Run one moment's contributed code now, as the machine would, to test it: each module's " +
|
||||
"piece on its own, bounded. `supply` runs on-mains or on-battery for the source the machine is on. " +
|
||||
"Answers the runner's lines. Needs sudo -n.",
|
||||
Input: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"moment": map[string]any{"type": "string", "enum": append(append([]string(nil), Moments...), "supply"),
|
||||
"description": "the moment to run"},
|
||||
},
|
||||
"required": []string{"moment"},
|
||||
},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
moment, _ := args["moment"].(string)
|
||||
known := moment == "supply"
|
||||
for _, mo := range Moments {
|
||||
known = known || mo == moment
|
||||
}
|
||||
if !known {
|
||||
return nil, fmt.Errorf("no moment %q; the moments are %s and supply", moment, strings.Join(Moments, ", "))
|
||||
}
|
||||
out, err := m.privileged(context.Background(), RunnerPath, moment, "--force")
|
||||
return map[string]any{"moment": moment, "said": strings.Split(strings.TrimSpace(out), "\n")}, err
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "power_check",
|
||||
Description: "Whether this machine's power handling is as the mesh declares it: the module's units enabled, " +
|
||||
"the runner and every moment's file in place, logind's power file the mesh's and the only one, no " +
|
||||
"hand-placed sleep hooks, the watcher's delay lock held and no events stuck before the bus.",
|
||||
Run: func(map[string]any) (any, error) { return m.Check(context.Background(), w), nil },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The watcher publishes the machine's power states as this module's events (novox/hq ADR 0211 §4).
|
||||
//
|
||||
// **`sleeping` goes out before the machine sleeps.** It holds logind's delay lock on sleep and
|
||||
// shutdown; when logind announces a sleep it publishes, waits for the bus at most AnnounceWithin,
|
||||
// and only then lets go, so the lock never holds a machine awake for long. Whatever could not be
|
||||
// published waits in order and goes out as soon as the bus answers again — after waking, a `woke`
|
||||
// queued behind a `sleeping` that never left.
|
||||
|
||||
// Event types, as the module's manifest declares them in `emits`.
|
||||
const (
|
||||
Booted = "booted"
|
||||
Sleeping = "sleeping"
|
||||
Woke = "woke"
|
||||
ShuttingDown = "shutting-down"
|
||||
OnMains = "on-mains"
|
||||
OnBattery = "on-battery"
|
||||
BatteryLow = "battery-low"
|
||||
)
|
||||
|
||||
// AnnounceWithin bounds how long the lock holds a sleep or a shutdown for the bus: logind's own
|
||||
// InhibitDelayMaxSec is 5 s, and the machine must sleep when the bus is gone.
|
||||
const AnnounceWithin = 3 * time.Second
|
||||
|
||||
// LowBattery is the charge below which battery-low is said, once per discharge.
|
||||
const LowBattery = 10
|
||||
|
||||
// Signal is what logind says before a sleep or a shutdown (Start true) and after a wake (false).
|
||||
type Signal struct {
|
||||
Shutdown bool
|
||||
Start bool
|
||||
}
|
||||
|
||||
// Login1 is logind as the watcher uses it, behind an interface so it is tested without a bus.
|
||||
type Login1 interface {
|
||||
// Inhibit takes a delay lock; closing the file lets it go.
|
||||
Inhibit(what, who, why string) (*os.File, error)
|
||||
Signals() <-chan Signal
|
||||
Close()
|
||||
}
|
||||
|
||||
// Emitter publishes one event and returns once the bus has it.
|
||||
type Emitter func(eventType string, body any) error
|
||||
|
||||
type queued struct {
|
||||
Type string
|
||||
Body map[string]any
|
||||
}
|
||||
|
||||
// Watcher is the long-running half of the module.
|
||||
type Watcher struct {
|
||||
m *Machine
|
||||
emit Emitter
|
||||
dial func() (Login1, error)
|
||||
now func() time.Time
|
||||
state string // where the last announced boot id is kept
|
||||
poll time.Duration
|
||||
mu sync.Mutex
|
||||
lock *os.File
|
||||
queue []queued
|
||||
sleep time.Time
|
||||
wake time.Time
|
||||
source string
|
||||
low bool
|
||||
issue string
|
||||
}
|
||||
|
||||
// NewWatcher is a watcher for this machine, emitting through emit and reaching logind through dial.
|
||||
func NewWatcher(m *Machine, emit Emitter, dial func() (Login1, error)) *Watcher {
|
||||
home, _ := os.UserHomeDir()
|
||||
return &Watcher{m: m, emit: emit, dial: dial, now: time.Now, poll: 10 * time.Second,
|
||||
state: filepath.Join(home, ".local", "state", "mesh-power", "announced-boot")}
|
||||
}
|
||||
|
||||
// Snapshot is what power_state and power_check show of the watcher.
|
||||
type Snapshot struct {
|
||||
Inhibiting bool `json:"inhibiting"`
|
||||
Pending int `json:"pending"`
|
||||
Problem string `json:"problem,omitempty"`
|
||||
LastSleep string `json:"last_sleep,omitempty"`
|
||||
LastWake string `json:"last_wake,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
func (w *Watcher) Snapshot() Snapshot {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
s := Snapshot{Inhibiting: w.lock != nil, Pending: len(w.queue), Problem: w.issue, Source: w.source}
|
||||
if !w.sleep.IsZero() {
|
||||
s.LastSleep = w.sleep.Format(time.RFC3339)
|
||||
}
|
||||
if !w.wake.IsZero() {
|
||||
s.LastWake = w.wake.Format(time.RFC3339)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (w *Watcher) problem(s string) {
|
||||
w.mu.Lock()
|
||||
w.issue = s
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
// enqueue adds an event in order, stamped with when it happened.
|
||||
func (w *Watcher) enqueue(eventType string, body map[string]any) {
|
||||
if body == nil {
|
||||
body = map[string]any{}
|
||||
}
|
||||
body["at"] = w.now().UTC().Format(time.RFC3339)
|
||||
w.mu.Lock()
|
||||
w.queue = append(w.queue, queued{eventType, body})
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
// flush publishes what waits, in order, and stops at the first the bus does not take.
|
||||
func (w *Watcher) flush() {
|
||||
for {
|
||||
w.mu.Lock()
|
||||
if len(w.queue) == 0 {
|
||||
w.mu.Unlock()
|
||||
return
|
||||
}
|
||||
next := w.queue[0]
|
||||
w.mu.Unlock()
|
||||
if err := w.emit(next.Type, next.Body); err != nil {
|
||||
w.problem("the bus did not take " + next.Type + ": " + err.Error())
|
||||
return
|
||||
}
|
||||
w.mu.Lock()
|
||||
w.queue = w.queue[1:]
|
||||
if len(w.queue) == 0 && strings.HasPrefix(w.issue, "the bus") {
|
||||
w.issue = ""
|
||||
}
|
||||
w.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// flushWithin publishes what waits, giving up after d: a sleep must not wait for a bus that is gone.
|
||||
func (w *Watcher) flushWithin(d time.Duration) {
|
||||
done := make(chan struct{})
|
||||
go func() { w.flush(); close(done) }()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(d):
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) inhibit(l Login1) {
|
||||
f, err := l.Inhibit("sleep:shutdown", "mesh power", "say on the bus that this machine sleeps or stops")
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if err != nil {
|
||||
w.issue = "no delay lock: " + err.Error()
|
||||
return
|
||||
}
|
||||
w.lock = f
|
||||
}
|
||||
|
||||
func (w *Watcher) release() {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.lock != nil {
|
||||
w.lock.Close()
|
||||
w.lock = nil
|
||||
}
|
||||
}
|
||||
|
||||
// bootOnce queues `booted` the first time this boot is seen, remembered across restarts of the
|
||||
// runtime, so a restart is not mistaken for a boot.
|
||||
func (w *Watcher) bootOnce() {
|
||||
id := w.m.read("/proc/sys/kernel/random/boot_id")
|
||||
if id == "" {
|
||||
return
|
||||
}
|
||||
if last, _ := os.ReadFile(w.state); strings.TrimSpace(string(last)) == id {
|
||||
return
|
||||
}
|
||||
body := map[string]any{"boot_id": id}
|
||||
if bt, ok := w.m.BootTime(); ok {
|
||||
body["booted_at"] = bt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
w.enqueue(Booted, body)
|
||||
if err := os.MkdirAll(filepath.Dir(w.state), 0o755); err == nil {
|
||||
_ = os.WriteFile(w.state, []byte(id+"\n"), 0o644)
|
||||
}
|
||||
}
|
||||
|
||||
// supply queues on-mains, on-battery and battery-low as the machine's supplies change. The first
|
||||
// reading sets the baseline and says nothing; a machine with no Mains supply says nothing at all.
|
||||
func (w *Watcher) supply() {
|
||||
supplies := w.m.Supplies()
|
||||
src := Source(supplies)
|
||||
w.mu.Lock()
|
||||
was := w.source
|
||||
w.source = src
|
||||
w.mu.Unlock()
|
||||
if src != "none" && was != "" && was != src {
|
||||
w.enqueue(src, nil)
|
||||
}
|
||||
p, _ := Battery(supplies)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
w.mu.Lock()
|
||||
low := w.low
|
||||
w.mu.Unlock()
|
||||
switch {
|
||||
case src == "on-battery" && *p <= LowBattery && !low:
|
||||
w.mu.Lock()
|
||||
w.low = true
|
||||
w.mu.Unlock()
|
||||
w.enqueue(BatteryLow, map[string]any{"percent": *p})
|
||||
case src == "on-mains" || *p > LowBattery+5:
|
||||
w.mu.Lock()
|
||||
w.low = false
|
||||
w.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Run watches until ctx ends. Without logind it still says boots and supplies, and tries logind
|
||||
// again every minute.
|
||||
func (w *Watcher) Run(ctx context.Context) {
|
||||
w.bootOnce()
|
||||
w.supply()
|
||||
tick := time.NewTicker(w.poll)
|
||||
defer tick.Stop()
|
||||
var l Login1
|
||||
var signals <-chan Signal
|
||||
retry := time.NewTimer(0)
|
||||
defer retry.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
w.release()
|
||||
if l != nil {
|
||||
l.Close()
|
||||
}
|
||||
return
|
||||
case <-retry.C:
|
||||
got, err := w.dial()
|
||||
if err != nil {
|
||||
w.problem("logind: " + err.Error())
|
||||
retry.Reset(time.Minute)
|
||||
continue
|
||||
}
|
||||
l, signals = got, got.Signals()
|
||||
w.inhibit(l)
|
||||
case s, open := <-signals:
|
||||
if !open {
|
||||
w.release()
|
||||
l, signals = nil, nil
|
||||
retry.Reset(time.Minute)
|
||||
continue
|
||||
}
|
||||
w.handle(l, s)
|
||||
case <-tick.C:
|
||||
w.supply()
|
||||
w.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) handle(l Login1, s Signal) {
|
||||
switch {
|
||||
case s.Shutdown && s.Start:
|
||||
w.enqueue(ShuttingDown, nil)
|
||||
w.flushWithin(AnnounceWithin)
|
||||
w.release()
|
||||
case !s.Shutdown && s.Start:
|
||||
w.mu.Lock()
|
||||
w.sleep = w.now()
|
||||
w.mu.Unlock()
|
||||
w.enqueue(Sleeping, nil)
|
||||
w.flushWithin(AnnounceWithin)
|
||||
w.release()
|
||||
case !s.Shutdown && !s.Start:
|
||||
w.mu.Lock()
|
||||
w.wake = w.now()
|
||||
w.mu.Unlock()
|
||||
w.enqueue(Woke, nil)
|
||||
w.inhibit(l)
|
||||
w.supply()
|
||||
w.flush()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeLogin struct {
|
||||
mu sync.Mutex
|
||||
held int
|
||||
signals chan Signal
|
||||
}
|
||||
|
||||
func (f *fakeLogin) Inhibit(what, who, why string) (*os.File, error) {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Close()
|
||||
f.mu.Lock()
|
||||
f.held++
|
||||
f.mu.Unlock()
|
||||
return w, nil
|
||||
}
|
||||
func (f *fakeLogin) Signals() <-chan Signal { return f.signals }
|
||||
func (f *fakeLogin) Close() {}
|
||||
|
||||
type bus struct {
|
||||
mu sync.Mutex
|
||||
down bool
|
||||
got []string
|
||||
}
|
||||
|
||||
func (b *bus) emit(t string, _ any) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.down {
|
||||
return errors.New("no bus")
|
||||
}
|
||||
b.got = append(b.got, t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bus) seen() []string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return append([]string(nil), b.got...)
|
||||
}
|
||||
|
||||
func machine(t *testing.T, files map[string]string) *Machine {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
for p, c := range files {
|
||||
full := filepath.Join(root, p)
|
||||
os.MkdirAll(filepath.Dir(full), 0o755)
|
||||
os.WriteFile(full, []byte(c), 0o644)
|
||||
}
|
||||
return &Machine{Root: root}
|
||||
}
|
||||
|
||||
func watcher(t *testing.T, m *Machine, b *bus) *Watcher {
|
||||
w := NewWatcher(m, b.emit, nil)
|
||||
w.state = filepath.Join(t.TempDir(), "announced-boot")
|
||||
return w
|
||||
}
|
||||
|
||||
func TestSleepingIsSaidBeforeTheLockGoesAndWokeAfter(t *testing.T) {
|
||||
b := &bus{}
|
||||
l := &fakeLogin{}
|
||||
w := watcher(t, machine(t, nil), b)
|
||||
w.inhibit(l)
|
||||
w.handle(l, Signal{Start: true})
|
||||
if got := b.seen(); !reflect.DeepEqual(got, []string{Sleeping}) {
|
||||
t.Fatalf("before sleep the bus has %v", got)
|
||||
}
|
||||
if w.Snapshot().Inhibiting {
|
||||
t.Fatal("the delay lock is still held after the announcement: the machine would wait to sleep")
|
||||
}
|
||||
w.handle(l, Signal{Start: false})
|
||||
if got := b.seen(); !reflect.DeepEqual(got, []string{Sleeping, Woke}) {
|
||||
t.Fatalf("after waking the bus has %v", got)
|
||||
}
|
||||
if !w.Snapshot().Inhibiting || l.held != 2 {
|
||||
t.Fatalf("the lock is not taken again after waking: %+v, held %d", w.Snapshot(), l.held)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABusThatIsGoneDoesNotHoldTheSleepAndTheEventsFollowInOrder(t *testing.T) {
|
||||
b := &bus{down: true}
|
||||
l := &fakeLogin{}
|
||||
w := watcher(t, machine(t, nil), b)
|
||||
w.inhibit(l)
|
||||
start := time.Now()
|
||||
w.handle(l, Signal{Start: true})
|
||||
if time.Since(start) > AnnounceWithin+time.Second {
|
||||
t.Fatal("the sleep waited longer than its bound for a bus that is gone")
|
||||
}
|
||||
if w.Snapshot().Inhibiting {
|
||||
t.Fatal("the lock was kept while the bus was gone")
|
||||
}
|
||||
w.handle(l, Signal{Start: false})
|
||||
if s := w.Snapshot(); s.Pending != 2 || s.Problem == "" {
|
||||
t.Fatalf("what the bus did not take is not kept and said: %+v", s)
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.down = false
|
||||
b.mu.Unlock()
|
||||
w.flush()
|
||||
if got := b.seen(); !reflect.DeepEqual(got, []string{Sleeping, Woke}) {
|
||||
t.Fatalf("the late events arrived as %v", got)
|
||||
}
|
||||
if s := w.Snapshot(); s.Pending != 0 || s.Problem != "" {
|
||||
t.Fatalf("after the bus came back: %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShutdownIsSaidAndLetGo(t *testing.T) {
|
||||
b := &bus{}
|
||||
l := &fakeLogin{}
|
||||
w := watcher(t, machine(t, nil), b)
|
||||
w.inhibit(l)
|
||||
w.handle(l, Signal{Shutdown: true, Start: true})
|
||||
if got := b.seen(); !reflect.DeepEqual(got, []string{ShuttingDown}) || w.Snapshot().Inhibiting {
|
||||
t.Fatalf("%v, inhibiting %v", got, w.Snapshot().Inhibiting)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootIsSaidOncePerBootNotPerRestart(t *testing.T) {
|
||||
m := machine(t, map[string]string{"/proc/sys/kernel/random/boot_id": "abc\n", "/proc/stat": "btime 1790000000\n"})
|
||||
b := &bus{}
|
||||
w := watcher(t, m, b)
|
||||
w.bootOnce()
|
||||
w.flush()
|
||||
again := NewWatcher(m, b.emit, nil)
|
||||
again.state = w.state
|
||||
again.bootOnce()
|
||||
again.flush()
|
||||
if got := b.seen(); !reflect.DeepEqual(got, []string{Booted}) {
|
||||
t.Fatalf("a restart of the runtime was taken for a boot: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func supplies(online string, capacity string) map[string]string {
|
||||
return map[string]string{
|
||||
"/sys/class/power_supply/ACAD/type": "Mains", "/sys/class/power_supply/ACAD/online": online,
|
||||
"/sys/class/power_supply/BAT1/type": "Battery", "/sys/class/power_supply/BAT1/capacity": capacity,
|
||||
"/sys/class/power_supply/BAT1/status": "Discharging",
|
||||
}
|
||||
}
|
||||
|
||||
func TestASupplyChangeIsSaidAndALowBatteryOncePerDischarge(t *testing.T) {
|
||||
b := &bus{}
|
||||
m := machine(t, supplies("1", "50"))
|
||||
w := watcher(t, m, b)
|
||||
w.supply() // baseline: says nothing
|
||||
m2 := machine(t, supplies("0", "9"))
|
||||
w.m = m2
|
||||
w.supply()
|
||||
w.supply()
|
||||
w.flush()
|
||||
if got := b.seen(); !reflect.DeepEqual(got, []string{OnBattery, BatteryLow}) {
|
||||
t.Fatalf("%v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMachineWithoutMainsSaysNothingAboutSupplies(t *testing.T) {
|
||||
b := &bus{}
|
||||
w := watcher(t, machine(t, nil), b)
|
||||
w.supply()
|
||||
w.supply()
|
||||
w.flush()
|
||||
if got := b.seen(); len(got) != 0 {
|
||||
t.Fatalf("a server announced %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithoutLogindStillSaysTheBoot(t *testing.T) {
|
||||
m := machine(t, map[string]string{"/proc/sys/kernel/random/boot_id": "xyz\n"})
|
||||
b := &bus{}
|
||||
w := NewWatcher(m, b.emit, func() (Login1, error) { return nil, errors.New("no system bus") })
|
||||
w.state = filepath.Join(t.TempDir(), "b")
|
||||
w.poll = 10 * time.Millisecond
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
w.Run(ctx)
|
||||
if got := b.seen(); len(got) == 0 || got[0] != Booted || w.Snapshot().Problem == "" {
|
||||
t.Fatalf("%v %+v", got, w.Snapshot())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user