返回 DeepSeek-Reasonix
maintenance.go
根目录 / internal / session / maintenance.go
1 package session
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9
10 "reasonix/internal/fileutil"
11 filelock "reasonix/internal/identitylock"
12 )
13
14 // PurgeWithTombstone keeps directory ownership across writer-lock release and
15 // rename (required on Windows). The callback durably withdraws the identity.
16 // The deterministic staging directory makes interrupted removal replayable.
17 func (p *FilesystemPersistence) PurgeWithTombstone(ctx context.Context, id string, prepare func() error) error {
18 return p.purgeWithTombstone(ctx, id, prepare, func(string) {})
19 }
20
21 // checkpoint belongs to one invocation; tests terminate a child process here
22 // to exercise real lock release and restart, without process-global hooks.
23 func (p *FilesystemPersistence) purgeWithTombstone(ctx context.Context, id string, prepare func() error, checkpoint func(string)) error {
24 source, err := p.sessionDir(id, false)
25 if err != nil {
26 return err
27 }
28 if err := ctx.Err(); err != nil {
29 return err
30 }
31 releaseDirectory, err := filelock.TryAcquire(directoryOwnershipPath(source))
32 if err != nil {
33 return err
34 }
35 defer releaseDirectory()
36 staged := filepath.Join(p.Root, ".purging", id)
37 receipt := staged + ".receipt"
38 proof := "reasonix-session-purge-v1\n" + id + "\n"
39 if err := validatePurgeDirectories(p.Root, source, staged, receipt, proof); err != nil {
40 return err
41 }
42 if _, err := os.Lstat(source); err == nil {
43 if _, err := os.Lstat(staged); err == nil {
44 return errors.New("session purge staging collision")
45 } else if !os.IsNotExist(err) {
46 return err
47 }
48 release, err := filelock.TryAcquire(filepath.Join(source, "writer.lock"))
49 if err != nil {
50 return err
51 }
52 checkpoint("before-tombstone")
53 if err := prepare(); err != nil {
54 release()
55 return err
56 }
57 checkpoint("after-tombstone")
58 if err := os.MkdirAll(filepath.Dir(staged), 0700); err != nil {
59 release()
60 return err
61 }
62 if err := fileutil.AtomicCreateFile(receipt, []byte(proof), 0600); err != nil && !errors.Is(err, os.ErrExist) {
63 release()
64 return err
65 }
66 release()
67 checkpoint("before-rename")
68 if err := os.Rename(source, staged); err != nil {
69 return err
70 }
71 checkpoint("after-rename")
72 } else if !os.IsNotExist(err) {
73 return err
74 } else {
75 if _, err := os.Lstat(staged); err == nil {
76 if _, err := os.Lstat(receipt); err != nil {
77 return errors.New("unowned session purge staging directory")
78 }
79 } else if !os.IsNotExist(err) {
80 return err
81 }
82 if err := prepare(); err != nil {
83 return err
84 }
85 }
86 if err := os.RemoveAll(staged); err != nil {
87 return err
88 }
89 checkpoint("after-content-removal")
90 if err := os.RemoveAll(filepath.Join(p.Root, ".query-cache", id)); err != nil {
91 return err
92 }
93 if err := os.Remove(receipt); err != nil && !os.IsNotExist(err) {
94 return err
95 }
96 checkpoint("after-cleanup")
97 return nil
98 }
99
100 // AcquireMaintenance excludes an external writer without opening, recovering,
101 // or changing a cold session. The caller must not already own its runtime.
102 func (p *FilesystemPersistence) AcquireMaintenance(sessionID string) (func(), error) {
103 dir, err := p.sessionDir(sessionID, true)
104 if err != nil {
105 return nil, err
106 }
107 release, err := acquireSessionWriter(dir)
108 if errors.Is(err, filelock.ErrHeld) {
109 return nil, fmt.Errorf("%w", ErrWriterOwned)
110 }
111 return release, err
112 }
113
114 func validatePurgeDirectories(root, source, staged, receipt, proof string) error {
115 for _, path := range []string{root, filepath.Dir(staged), staged, source, filepath.Join(root, ".query-cache")} {
116 info, err := os.Lstat(path)
117 if err != nil && !os.IsNotExist(err) {
118 return err
119 }
120 if err == nil && (!info.IsDir() || info.Mode()&os.ModeSymlink != 0) {
121 return errors.New("unsafe session maintenance directory")
122 }
123 }
124 if info, err := os.Lstat(receipt); err == nil {
125 if !info.Mode().IsRegular() {
126 return errors.New("unsafe session purge receipt")
127 }
128 body, err := os.ReadFile(receipt)
129 if err != nil || string(body) != proof {
130 return errors.New("session purge receipt mismatch")
131 }
132 } else if !os.IsNotExist(err) {
133 return err
134 }
135 return nil
136 }
137
137 lines GO