1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
// SPDX-License-Identifier: GPL-3.0-or-later
package engine
import (
"context"
"fmt"
"krino/internal/journal"
"krino/internal/lock"
"krino/internal/plan"
)
// Session is one run of krino over one or more directories: the log it
// writes, the run id every entry carries, and the claims its directories
// share. Both the command line and the GUI go through it, so the order a
// directory is locked, planned, applied and released in - and what a later
// directory may still claim - is written once (GUI design §1.3).
//
// A dry session opens no log and takes no run id: journal.Open creates the
// state directory and an empty krino.log merely by being called, and a dry
// run must not (spec §11).
type Session struct {
e *Engine
j *journal.Writer
run string
claims *plan.Claims
landed []string // where this run's applied files ended up, in order
dry bool
}
// NewSession starts a run. The log is opened by OpenLog, or by the first
// Apply or ApplyUndo: sorting opens it before it plans anything, while undo
// opens it only once it knows it will reverse something, and neither must
// create a log a dry run would not. The caller closes the session when the
// run is over.
func (e *Engine) NewSession(dry bool) (*Session, error) {
return &Session{e: e, claims: plan.NewClaims(), dry: dry}, nil
}
// OpenLog opens the log and takes the run id, once. A dry session does
// neither: journal.Open creates the state directory and an empty krino.log
// merely by being called (spec §11).
func (s *Session) OpenLog() error {
if s.dry || s.j != nil {
return nil
}
j, err := journal.Open(s.e.Config.LogFile())
if err != nil {
return err
}
s.j, s.run = j, journal.NewRunID(s.e.Now())
return nil
}
// Run is the run id every entry of this session carries; "" for a dry one,
// and until the log is open.
func (s *Session) Run() string { return s.run }
// Journal is the log this session writes, nil for a dry one.
func (s *Session) Journal() *journal.Writer { return s.j }
// Lock takes d's lock (spec §3, §11). wait blocks until the holder is gone,
// or until ctx is cancelled; without it a held lock returns lock.ErrHeld at
// once, so a cron job never piles up behind a stuck run. The caller
// releases it.
func (s *Session) Lock(ctx context.Context, d *Dir, wait bool) (*lock.Lock, error) {
return lock.Acquire(ctx, s.e.Config.LockFile(d.Name), wait)
}
// LockDirs takes the locks of several directories, in the order given, and
// releases every one it took if any of them cannot be had - so an undo
// spanning directories never holds half of them (spec §10).
func (s *Session) LockDirs(ctx context.Context, names []string, wait bool) ([]*lock.Lock, error) {
var held []*lock.Lock
for _, name := range names {
l, err := lock.Acquire(ctx, s.e.Config.LockFile(name), wait)
if err != nil {
for _, h := range held {
h.Release()
}
return nil, fmt.Errorf("%s: %w", name, err)
}
held = append(held, l)
}
return held, nil
}
// Plan builds d's plan with the run's claims.
func (s *Session) Plan(ctx context.Context, d *Dir) (*DirPlan, error) {
return s.e.Plan(ctx, d, s.claims)
}
// Apply carries out the approved files of dp and logs the run's steps,
// remembering where they ended up for FinishDirectory.
func (s *Session) Apply(ctx context.Context, dp *DirPlan, approved map[string]bool) (*ApplyResult, error) {
if err := s.OpenLog(); err != nil {
return nil, err
}
res, err := s.e.Apply(ctx, dp, approved, s.j, s.run)
s.landed = append(s.landed, landedAt(res)...)
return res, err
}
// FinishDirectory ends one directory of a real run: the disk is now the
// truth for the next one, so the claims start again from where this run's
// files have actually ended up - every directory's, not just this one's
// (spec §7.4). A path this directory planned but did not apply is free
// again; a path it did apply stays protected from a later (on-conflict
// overwrite) for the rest of the run, even across a directory that applies
// nothing. A dry run applies nothing and keeps every claim.
func (s *Session) FinishDirectory() {
if s.dry {
return
}
s.claims = plan.NewClaims()
for _, p := range s.landed {
s.claims.Claim(p)
}
}
// landedAt is where res's files ended up: each copy, and the last place a
// move or rename put a file - not a path it passed through and left, and
// nothing at all for a file deleted for good.
func landedAt(res *ApplyResult) []string {
if res == nil {
return nil
}
var out []string
for _, fr := range res.Files {
final := ""
for _, sr := range fr.Steps {
switch {
case sr.Status != "ok":
case sr.Step.Kind == plan.DeletePermanent:
final = ""
case sr.Dst == "":
case sr.Step.Kind == plan.Copy:
out = append(out, sr.Dst)
default:
final = sr.Dst
}
}
if final != "" {
out = append(out, final)
}
}
return out
}
// PlanUndo builds the reversal of runID (spec §10).
func (s *Session) PlanUndo(runID string) (*UndoPlan, error) {
return s.e.PlanUndo(runID)
}
// ApplyUndo carries out up and logs it under this session's run id.
func (s *Session) ApplyUndo(ctx context.Context, up *UndoPlan) (*ApplyResult, error) {
if err := s.OpenLog(); err != nil {
return nil, err
}
return s.e.ApplyUndo(ctx, up, s.j, s.run)
}
// Close closes the log. A dry session has nothing to close.
func (s *Session) Close() error {
if s.j == nil {
return nil
}
err := s.j.Close()
s.j = nil
return err
}
|