//go:build linux package rootparticipation import ( "context" "errors" "os" "os/exec" "path/filepath" "syscall" "testing" "time" ) type recoveryDisk struct{ testDisk } func (d *recoveryDisk) Write(name string, raw []byte) error { f, err := os.CreateTemp(d.dir, "write-") if err != nil { return err } tmp := f.Name() defer os.Remove(tmp) if _, err = f.Write(raw); err == nil { err = f.Sync() } err = errors.Join(err, f.Close()) if err != nil { return err } if err = os.Rename(tmp, filepath.Join(d.dir, name)); err != nil { return err } dir, err := os.Open(d.dir) if err != nil { return err } defer dir.Close() return dir.Sync() } func (d *recoveryDisk) Lock(ctx context.Context, name string) (func(), error) { f, err := os.OpenFile(filepath.Join(d.dir, name), os.O_CREATE|os.O_RDWR, 0600) if err != nil { return nil, err } for { err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) if err == nil { return func() { _ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN); _ = f.Close() }, nil } if err != syscall.EWOULDBLOCK { f.Close() return nil, err } select { case <-ctx.Done(): f.Close() return nil, ctx.Err() case <-time.After(time.Millisecond): } } } func TestPlan14ExecutionRecoveryAfterProcessCrash(t *testing.T) { if dir := os.Getenv("PLAN14_EXEC_CRASH_DIR"); dir != "" { d := &recoveryDisk{testDisk: testDisk{dir: dir}} l := Ledger{State: d, VerifyLaunches: func(context.Context, uint64) error { return nil }} crash := func() error { os.Exit(31); return nil } var err error if os.Getenv("PLAN14_EXEC_CRASH_AT") == "acquire" { err = l.Acquire(t.Context(), crash) } else { err = l.BeginUpdateWith(t.Context(), "update", crash) } t.Fatal("did not reach crash", err) } for _, at := range []string{"acquire", "update"} { t.Run(at, func(t *testing.T) { l := censusFixture(t) dir := l.State.(*testDisk).dir l.State = &recoveryDisk{testDisk: testDisk{dir: dir}} child := exec.Command(os.Args[0], "-test.run=^TestPlan14ExecutionRecoveryAfterProcessCrash$") child.Env = append(os.Environ(), "PLAN14_EXEC_CRASH_DIR="+dir, "PLAN14_EXEC_CRASH_AT="+at) child.Dir = dir out, err := child.CombinedOutput() var exit *exec.ExitError if !errors.As(err, &exit) || exit.ExitCode() != 31 { t.Fatalf("crash: %v %s", err, out) } if err := l.Acquire(t.Context(), func() error { t.Error("acquired across crash"); return nil }); err == nil { t.Fatal("crash grant") } if at == "acquire" { if _, err := l.Census(); err == nil { t.Fatal("pending acquisition accepted") } l.VerifyLaunches = nil if err := l.RecoverAcquisition(t.Context()); err == nil { t.Fatal("absence replaced kernel completeness") } l.VerifyLaunches = func(context.Context, uint64) error { return nil } if err := l.RecoverAcquisition(t.Context()); err != nil { t.Fatal(err) } } else { failed := errors.New("terminal write failed") if err := l.EndUpdateWith(t.Context(), "update", func() error { return failed }); !errors.Is(err, failed) { t.Fatal(err) } if _, err := l.UpdateID(); err != nil { t.Fatal("terminal failure lost exclusion", err) } if err := l.EndUpdate(t.Context(), "update"); err != nil { t.Fatal(err) } } if err := l.Acquire(t.Context(), func() error { return nil }); err != nil { t.Fatal("positive recovery did not reopen", err) } }) } }