Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 95 additions & 3 deletions internal/cli/wake_lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -33,6 +34,7 @@ type wakeProcessInfo struct {
Running bool
StartToken string
BootID string
LegacyBootID string
Executable string
Args []string
InspectError error
Expand Down Expand Up @@ -200,7 +202,7 @@ func classifyWakeLock(root, me string, inspection *wakeLockInspection) {
inspection.Reason = inspectionReason("process start time unavailable", proc.InspectError)
return
}
if lock.BootID != "" && proc.BootID != "" && lock.BootID != proc.BootID {
if compareWakeBootID(lock.BootID, proc) != bootIDMatch {
inspection.Status = wakeLockUnverified
if wakeProcessProvenNotWake(proc) {
inspection.Status = wakeLockStale
Expand Down Expand Up @@ -322,7 +324,7 @@ func currentWakeLockMatches(lock wakeLock) bool {
if !proc.Running || proc.StartToken == "" {
return false
}
if lock.BootID != "" && proc.BootID != "" && lock.BootID != proc.BootID {
if compareWakeBootID(lock.BootID, proc) != bootIDMatch {
return false
}
return lock.ProcessStart == proc.StartToken
Expand Down Expand Up @@ -413,7 +415,7 @@ func wakeProcessStillMatches(inspection wakeLockInspection) bool {
if proc.StartToken == "" || inspection.Lock.ProcessStart != proc.StartToken {
return false
}
if inspection.Lock.BootID != "" && proc.BootID != "" && inspection.Lock.BootID != proc.BootID {
if compareWakeBootID(inspection.Lock.BootID, proc) == bootIDMismatch {
return false
}
}
Expand All @@ -428,3 +430,93 @@ func wakeProcessStillMatches(inspection wakeLockInspection) bool {
}
return true
}

type bootIDComparison int

const (
bootIDMatch bootIDComparison = iota
bootIDMismatch
bootIDUnknown
)

func compareWakeBootID(recorded string, proc wakeProcessInfo) bootIDComparison {
if recorded == "" {
return bootIDMatch
}
if proc.BootID == "" && proc.LegacyBootID == "" {
return bootIDUnknown
}
if recorded == proc.BootID || recorded == proc.LegacyBootID {
return bootIDMatch
}
for _, current := range []string{proc.BootID, proc.LegacyBootID} {
if legacyDarwinBootIDsMatch(recorded, current) {
return bootIDMatch
}
}
// Only unlike boots of the same identity representation are conclusive.
// A UUID cannot be disproved by a readable boottime, and vice versa.
if isDarwinBootUUID(recorded) && isDarwinBootUUID(proc.BootID) {
return bootIDMismatch
}
if _, ok := parseLegacyDarwinBootID(recorded); ok {
if _, ok := parseLegacyDarwinBootID(proc.LegacyBootID); ok {
return bootIDMismatch
}
}
return bootIDUnknown
}

func wakeBootIDMismatch(recorded string, proc wakeProcessInfo) bool {
return compareWakeBootID(recorded, proc) == bootIDMismatch
}

func isDarwinBootUUID(value string) bool {
if len(value) != 36 {
return false
}
for i, r := range value {
if i == 8 || i == 13 || i == 18 || i == 23 {
if r != '-' {
return false
}
continue
}
if (r < '0' || r > '9') && (r < 'a' || r > 'f') && (r < 'A' || r > 'F') {
return false
}
}
return true
}

// Legacy Darwin boot IDs came from kern.boottime, which can move slightly as
// macOS corrects wall-clock time. A one-second migration tolerance preserves
// old live wake locks without making two realistically distinct boots equal.
func legacyDarwinBootIDsMatch(first, second string) bool {
firstTime, firstOK := parseLegacyDarwinBootID(first)
secondTime, secondOK := parseLegacyDarwinBootID(second)
if !firstOK || !secondOK {
return false
}
delta := firstTime.Sub(secondTime)
if delta < 0 {
delta = -delta
}
return delta <= time.Second
}

func parseLegacyDarwinBootID(value string) (time.Time, bool) {
seconds, nanos, ok := strings.Cut(value, ".")
if !ok || seconds == "" || len(nanos) != 9 || strings.Contains(nanos, ".") {
return time.Time{}, false
}
sec, err := strconv.ParseInt(seconds, 10, 64)
if err != nil {
return time.Time{}, false
}
nsec, err := strconv.ParseInt(nanos, 10, 64)
if err != nil || nsec < 0 || nsec >= int64(time.Second) {
return time.Time{}, false
}
return time.Unix(sec, nsec), true
}
23 changes: 23 additions & 0 deletions internal/cli/wake_lock_boot_regression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package cli

import "testing"

func TestStillMatchesLiveWakeUncheckableBootIsStillPresent(t *testing.T) {
insp := wakeLockInspection{PID: 4343, Lock: wakeLock{PID: 4343, ProcessStart: "start-1", BootID: "recorded-boot", Executable: "/opt/homebrew/bin/amq"}, Root: "/tmp/x", Agent: "codex"}
stubInspectWakeProcess(t, func(pid int) wakeProcessInfo {
return wakeProcessInfo{PID: pid, Running: true, StartToken: "start-1", Executable: "/opt/homebrew/bin/amq", Args: []string{"/opt/homebrew/bin/amq", "wake", "--root", "/tmp/x", "--me", "codex"}}
})
if !wakeProcessStillMatches(insp) {
t.Fatal("live wake with unknown boot identity must remain present")
}
}

func TestStillMatchesDifferentUUIDBootIsGone(t *testing.T) {
insp := wakeLockInspection{PID: 4343, Lock: wakeLock{PID: 4343, ProcessStart: "start-1", BootID: "9C0682F4-901B-4243-8B5C-287FAFB9AD0E", Executable: "/opt/homebrew/bin/amq"}, Root: "/tmp/x", Agent: "codex"}
stubInspectWakeProcess(t, func(pid int) wakeProcessInfo {
return wakeProcessInfo{PID: pid, Running: true, StartToken: "start-1", BootID: "AAAAAAAA-901B-4243-8B5C-287FAFB9AD0E", Executable: "/opt/homebrew/bin/amq", Args: []string{"/opt/homebrew/bin/amq", "wake", "--root", "/tmp/x", "--me", "codex"}}
})
if wakeProcessStillMatches(insp) {
t.Fatal("different UUID boot must be gone")
}
}
114 changes: 114 additions & 0 deletions internal/cli/wake_lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,117 @@ func bindWakeLockToTarget(lock wakeLock, target wakeTarget) wakeLock {
lock.TargetDigest = wakeTargetDigest(target)
return lock
}

func TestWakeBootIDMismatchAcceptsDarwinLegacyMigration(t *testing.T) {
tests := []struct {
name string
recorded string
process wakeProcessInfo
mismatch bool
}{
{
name: "current boot session uuid",
recorded: "9C0682F4-901B-4243-8B5C-287FAFB9AD0E",
process: wakeProcessInfo{BootID: "9C0682F4-901B-4243-8B5C-287FAFB9AD0E"},
},
{
name: "legacy boot time with macOS clock correction",
recorded: "1783327533.465308000",
process: wakeProcessInfo{
BootID: "9C0682F4-901B-4243-8B5C-287FAFB9AD0E",
LegacyBootID: "1783327533.407566000",
},
},
{
name: "boot time fallback with macOS clock correction",
recorded: "1783327533.465308000",
process: wakeProcessInfo{BootID: "1783327533.407566000"},
},
{
name: "different legacy boot",
recorded: "1783327533.465308000",
process: wakeProcessInfo{
BootID: "9C0682F4-901B-4243-8B5C-287FAFB9AD0E",
LegacyBootID: "1783327535.407566000",
},
mismatch: true,
},
{
name: "different boot session uuid",
recorded: "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA",
process: wakeProcessInfo{BootID: "BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB"},
mismatch: true,
},
{
name: "recorded boot with unavailable current identity",
recorded: "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA",
process: wakeProcessInfo{},
mismatch: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := wakeBootIDMismatch(tc.recorded, tc.process); got != tc.mismatch {
t.Fatalf("wakeBootIDMismatch() = %v, want %v", got, tc.mismatch)
}
})
}
}

func TestInspectWakeLockAcceptsLegacyDarwinBootIDForProvenWake(t *testing.T) {
const wakePID = 4242
root := secureTempDirForTest(t)
writeWakeLockForTest(t, root, "codex", wakeLock{
PID: wakePID,
TTY: "tty",
ProcessStart: "start-1",
BootID: "1783327533.465308000",
Executable: "/opt/homebrew/bin/amq",
})
stubInspectWakeProcess(t, func(pid int) wakeProcessInfo {
if pid == wakePID {
return wakeProcessInfo{
PID: pid,
Running: true,
StartToken: "start-1",
BootID: "9C0682F4-901B-4243-8B5C-287FAFB9AD0E",
LegacyBootID: "1783327533.407566000",
Executable: "/opt/homebrew/bin/amq",
Args: []string{"/opt/homebrew/bin/amq", "wake", "--root", root, "--me", "codex"},
}
}
return wakeProcessInfo{PID: pid}
})

inspection := inspectWakeLock(root, "codex")
if inspection.Status != wakeLockValid || !inspection.IdentityConfirmed {
t.Fatalf("inspection = status %q reason %q confirmed %v", inspection.Status, inspection.Reason, inspection.IdentityConfirmed)
}
}

func TestInspectWakeLockTreatsUnavailableCurrentBootIdentityAsUnverified(t *testing.T) {
const wakePID = 4343
root := secureTempDirForTest(t)
writeWakeLockForTest(t, root, "codex", wakeLock{
PID: wakePID,
TTY: "tty",
ProcessStart: "start-1",
BootID: "recorded-boot",
Executable: "/opt/homebrew/bin/amq",
})
stubInspectWakeProcess(t, func(pid int) wakeProcessInfo {
return wakeProcessInfo{
PID: pid,
Running: true,
StartToken: "start-1",
Executable: "/opt/homebrew/bin/amq",
Args: []string{"/opt/homebrew/bin/amq", "wake", "--root", root, "--me", "codex"},
}
})

inspection := inspectWakeLock(root, "codex")
if inspection.Status != wakeLockUnverified {
t.Fatalf("inspection status = %q, want unverified (reason %q)", inspection.Status, inspection.Reason)
}
}
44 changes: 39 additions & 5 deletions internal/cli/wake_process_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@ package cli

import (
"fmt"
"strings"

"golang.org/x/sys/unix"
)

// SZOMB from <sys/proc.h>: the process has exited and is waiting for its
// parent to reap it. kill(pid, 0) still succeeds for this state.
const darwinProcessStateZombie int8 = 5

var (
readDarwinKinfoProc = unix.SysctlKinfoProc
readDarwinBootSessionUUID = func() (string, error) {
return unix.Sysctl("kern.bootsessionuuid")
}
readDarwinBootTime = func() (*unix.Timeval, error) {
return unix.SysctlTimeval("kern.boottime")
}
)

func inspectWakeProcessPlatform(pid int) wakeProcessInfo {
info := wakeProcessInfo{PID: pid}
if !processAlive(pid) {
return info
}
info.Running = true

kp, err := unix.SysctlKinfoProc("kern.proc.pid", pid)
kp, err := readDarwinKinfoProc("kern.proc.pid", pid)
if err != nil {
info.InspectError = err
return info
Expand All @@ -24,19 +39,38 @@ func inspectWakeProcessPlatform(pid int) wakeProcessInfo {
info.Running = false
return info
}
if kp.Proc.P_stat == darwinProcessStateZombie {
info.Running = false
return info
}

sec, nsec := kp.Proc.P_starttime.Unix()
info.StartToken = fmt.Sprintf("%d.%09d", sec, nsec)
info.Executable = nulTerminatedString(kp.Proc.P_comm[:])

if boot, err := unix.SysctlTimeval("kern.boottime"); err == nil && boot != nil {
bsec, bnsec := boot.Unix()
info.BootID = fmt.Sprintf("%d.%09d", bsec, bnsec)
}
info.BootID, info.LegacyBootID = darwinBootIdentity()

return info
}

func darwinBootIdentity() (bootID, legacyBootID string) {
if boot, err := readDarwinBootTime(); err == nil && boot != nil {
sec, nsec := boot.Unix()
legacyBootID = fmt.Sprintf("%d.%09d", sec, nsec)
}

if sessionUUID, err := readDarwinBootSessionUUID(); err == nil {
sessionUUID = strings.TrimSpace(sessionUUID)
if sessionUUID != "" && !strings.ContainsRune(sessionUUID, 0) {
return sessionUUID, legacyBootID
}
}

// kern.boottime was AMQ's Darwin boot identity before v0.42. Keep it as a
// best-effort fallback for macOS versions where bootsessionuuid is absent.
return legacyBootID, ""
}

func nulTerminatedString(raw []byte) string {
for i, b := range raw {
if b == 0 {
Expand Down
Loading