Repository navigation
Expand file tree
/
Copy pathbinary.go
More file actions
105 lines (96 loc) · 4.24 KB
/
Copy pathbinary.go
File metadata and controls
105 lines (96 loc) · 4.24 KB
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
package enginehost
// binary.go is the host's memory of the file it was started from.
//
// ── WHY A PROCESS WATCHES ITS OWN BINARY ────────────────────────────────────
//
// A host outlives the connection that started it, and that is the feature. It
// also outlives the BUILD that started it, and that is a trap: `rm bin/codeaf
// && make build` on a machine leaves the new binary answering `codeaf version`
// while the old process is still holding the socket, so every surface that
// dials in is spliced onto a build nobody has been running for an hour. When
// the protocol moved with that build, what the person got was the old host's
// refusal — telling them to update a machine they had just updated.
//
// The exchange in internal/remote fixes the connection that finds it. This
// fixes the one that never comes: a host whose binary was replaced RETIRES ON
// ITS OWN as soon as it is holding nothing, so a stale one drains away rather
// than waiting for somebody to trip over it. There is nothing to lose by
// leaving — the next connection starts a host from the binary that is actually
// on disk, which takes about a second and is invisible.
//
// THE TEST IS THE FILE'S IDENTITY AND NOT ITS CONTENT. os.SameFile is the
// standard library's own answer to "is this the same file", and an install that
// removes and replaces (the only install this tree allows — CLAUDE.md's own
// standing order, because a copy over a running binary is what macOS answers
// with `Killed: 9`) mints a new inode every time. The size and the modification
// time are checked beside it so that a copy in place, which keeps the inode, is
// caught too.
//
// A MACHINE THAT CANNOT ANSWER "WHICH FILE AM I" SIMPLY DOES NOT HAVE THIS.
// os.Executable fails on a few platforms and can be lied to on more; a host
// that could not learn its own path never claims to have been replaced, so the
// capability is absent rather than present and guessing.
import (
"os"
"time"
"github.com/Agent-Field/codeaf/internal/buildinfo"
)
// hostBinary is the file this host was started from, as it was at the moment it
// started.
type hostBinary struct {
path string
was os.FileInfo
}
// thisBinary reads the running file's identity. Both failures — no path, no
// stat — are answered the same way, with a value whose [hostBinary.replaced] is
// forever false.
func thisBinary() hostBinary {
path, err := os.Executable()
if err != nil {
return hostBinary{}
}
was, err := os.Stat(path)
if err != nil {
return hostBinary{}
}
return hostBinary{path: path, was: was}
}
// replaced reports that the file this host was started from is not the file
// that is there now: removed and rebuilt, copied over, or gone entirely.
func (b hostBinary) replaced() bool {
if b.path == "" || b.was == nil {
return false
}
now, err := os.Stat(b.path)
if err != nil {
// The path no longer resolves to a file at all, which on Linux is also
// what a running process whose binary was deleted reads as: os.Executable
// answered `/…/bin/codeaf (deleted)` and nothing stats that.
return true
}
return !os.SameFile(b.was, now) ||
!now.ModTime().Equal(b.was.ModTime()) ||
now.Size() != b.was.Size()
}
// builtAt is the moment this binary was built, which is what puts two builds in
// order: the stamp `make build` links in when there is one, and otherwise the
// file's own modification time as it was when the process started. A process
// that could not learn its own file and carries no stamp answers the zero time,
// and zero reads as OLDER than everything — a host that cannot say when it was
// built is never the one kept over a build that can.
func (b hostBinary) builtAt() time.Time {
if stamp := buildinfo.BuiltAt(); !stamp.IsZero() {
return stamp
}
if b.was == nil {
return time.Time{}
}
return b.was.ModTime()
}
// BuildMoment is [hostBinary.builtAt] for the process asking: the same rule on
// both sides of the comparison, so a host and the window deciding whether to
// replace it are measured with one ruler.
func BuildMoment() time.Time { return thisBinary().builtAt() }
// ThisBinary is the file this process was started from, "" when the platform
// cannot say.
func ThisBinary() string { return thisBinary().path }