Repository navigation
Expand file tree
/
Copy pathservices.go
More file actions
159 lines (150 loc) · 5.59 KB
/
Copy pathservices.go
File metadata and controls
159 lines (150 loc) · 5.59 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
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
package exec
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"time"
"github.com/Agent-Field/codeaf/internal/executor"
"github.com/Agent-Field/codeaf/internal/processgroup"
commandshell "github.com/Agent-Field/codeaf/internal/shell"
)
const (
serviceTerminateGrace = 2 * time.Second
serviceStopPoll = 20 * time.Millisecond
)
// ProcessStartTime is the platform identity helper used both when a service
// is recorded and when it is re-adopted. Darwin and the other Unix targets
// supported by codeaf expose lstart through ps; callers degrade safely when a
// platform does not.
func ProcessStartTime(pid int) (time.Time, error) {
if pid <= 0 {
return time.Time{}, errors.New("invalid pid")
}
//codeaf:plumbing ps read of a process start time
command := exec.Command("ps", "-o", "lstart=", "-p", fmt.Sprint(pid))
// ps renders lstart in the caller's locale, which reorders month and day.
// Asking for C makes the one format we depend on the one we get; the
// day-first layouts below keep a platform that ignores the request usable.
command.Env = append(os.Environ(), "LC_ALL=C", "LANG=C", "LC_TIME=C")
output, err := command.Output()
if err != nil {
return time.Time{}, err
}
raw := strings.Join(strings.Fields(string(output)), " ")
for _, layout := range []string{
"Mon Jan 2 15:04:05 2006", "Mon Jan _2 15:04:05 2006",
"Mon 2 Jan 15:04:05 2006", "Jan 2 15:04:05 2006", "2 Jan 15:04:05 2006",
} {
if parsed, parseErr := time.ParseInLocation(layout, raw, time.Local); parseErr == nil {
return parsed.UTC(), nil
}
}
return time.Time{}, fmt.Errorf("parse process start time %q", raw)
}
// ProcessIdentityMatches protects re-adoption from PID reuse. A platform that
// cannot expose start time reports an error; callers can preserve the process
// without pretending identity was proven.
func ProcessIdentityMatches(pid int, startedAt time.Time) (bool, error) {
actual, err := ProcessStartTime(pid)
if err != nil {
return false, err
}
return actual.Equal(startedAt.UTC().Truncate(time.Second)), nil
}
// StartDetachedService uses the same shell, log, session/process-group shape
// as background jobs. No pipe or resident goroutine is created.
func StartDetachedService(command, dir, logPath string) (int, time.Time, error) {
logFile, err := commandshell.OpenLog(logPath, 0o644)
if err != nil {
return 0, time.Time{}, err
}
cmd, err := detachedCommand(executor.Host, context.Background(), dir, command, nil, logFile, 0)
if err == nil {
err = cmd.Start()
}
if err != nil {
_ = logFile.Close()
return 0, time.Time{}, err
}
_ = logFile.Close()
started := time.Now().UTC().Truncate(time.Second)
if identity, identityErr := ProcessStartTime(cmd.Process.Pid); identityErr == nil {
started = identity
}
pid := cmd.Process.Pid
// The supervisor reaps runtime-started children on its ordinary tick;
// releasing the Go handle avoids retaining one handle per restart.
_ = cmd.Process.Release()
return pid, started, nil
}
// detachedCommand builds the shell command a background job or service runs
// as: a login bash in dir, in a session of its own, writing to one shared
// output. A new session is also a new process group; it preserves group-wide
// job teardown and lets an adopted service survive the chat terminal closing.
func detachedCommand(
seat executor.Seat, ctx context.Context, dir, command string, environment []string, output io.Writer, wait time.Duration,
) (*exec.Cmd, error) {
bash, err := commandshell.Bash()
if err != nil {
return nil, err
}
cmd, err := seat.In(dir).Command(ctx, executor.ExecRequest{
Argv: []string{bash, "-lc", command}, Env: environment,
Group: executor.GroupSession, WaitDelay: wait,
})
if err != nil {
return nil, err
}
cmd.Stdout, cmd.Stderr = output, output
return cmd, nil
}
// StopServiceProcess terminates the whole detached session, preserving the
// job registry's TERM-then-KILL contract without requiring its waiter channel.
//
// IT REFUSES A RECYCLED PID. A service is addressed by a pid recorded at start;
// under pid pressure that number can name somebody else by the time a stop runs,
// and signalling it would tear down an unrelated process group. So the recorded
// start time is checked against the live process first, and a process that is no
// longer the one that was started — or whose identity cannot be read — is left
// alone. A missed stop leaks one service; a wrong stop destroys someone else's
// work.
func StopServiceProcess(pid int, startedAt time.Time) error {
if pid <= 0 {
return nil
}
if matched, err := ProcessIdentityMatches(pid, startedAt); err != nil || !matched {
return nil
}
if err := processgroup.Terminate(pid); err != nil {
return err
}
deadline := time.Now().Add(serviceTerminateGrace)
for serviceLeaderAlive(pid) && time.Now().Before(deadline) {
time.Sleep(serviceStopPoll)
}
if serviceLeaderAlive(pid) {
if err := processgroup.Kill(pid); err != nil {
return err
}
}
// One last sweep of anything the session leader left behind. Best effort:
// the group is commonly already gone, and a stop must not fail for saying
// so twice.
_ = processgroup.Kill(pid)
return nil
}
// serviceLeaderAlive asks about the leader rather than the group: a group
// probe can be refused while the process is plainly still running. A leader
// this process spawned is reaped here first, because an unreaped zombie still
// answers a liveness signal and would otherwise read as running forever. A
// re-adopted orphan is not our child, so the signal probe decides it.
func serviceLeaderAlive(pid int) bool {
if processgroup.ReapExitedChild(pid) {
return false
}
return processgroup.ProcessAlive(pid)
}