aboutsummaryrefslogtreecommitdiff
path: root/internal/engine/session.go
blob: 8d52027e1200c5f464e7fedf2d59b148e4c7a45b (plain) (blame)
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
// 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
	dry    bool
}

// NewSession starts a run. A real one opens the log; the caller closes the
// session when the run is over.
func (e *Engine) NewSession(dry bool) (*Session, error) {
	s := &Session{e: e, claims: plan.NewClaims(), dry: dry}
	if dry {
		return s, nil
	}
	j, err := journal.Open(e.Config.LogFile())
	if err != nil {
		return nil, err
	}
	s.j, s.run = j, journal.NewRunID(e.Now())
	return s, nil
}

// Run is the run id every entry of this session carries; "" for a dry one.
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. A
// real run applies each directory before the next is planned, so afterwards
// the disk is the truth for the next one: only the paths this directory's
// files ended up at stay claimed, which keeps a later (on-conflict
// overwrite) from displacing this run's own result (spec §7.4). A dry
// session keeps every claim, since it applies nothing.
func (s *Session) Apply(ctx context.Context, dp *DirPlan, approved map[string]bool) (*ApplyResult, error) {
	res, err := s.e.Apply(ctx, dp, approved, s.j, s.run)
	if !s.dry {
		s.claims = plan.NewClaims()
		for _, p := range landedAt(res) {
			s.claims.Claim(p)
		}
	}
	return res, err
}

// 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) {
	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
}