-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherrors.go
More file actions
284 lines (246 loc) · 10.6 KB
/
Copy patherrors.go
File metadata and controls
284 lines (246 loc) · 10.6 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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
package processkit
import (
"context"
"errors"
"fmt"
"strings"
"time"
"unicode/utf8"
)
// Sentinel errors for use with errors.Is. The data-carrying error types below
// match the relevant sentinels through their Is methods.
var (
// ErrCancelled means the run was abandoned via its context. Unlike a timeout
// (which is captured in the [Result]), a cancellation is always an error and
// carries no output. It wins over a co-occurring timeout.
ErrCancelled = errors.New("processkit: run cancelled")
// ErrTimeout means the run exceeded its deadline and was killed. A timed-out
// [*ExitError] matches this via errors.Is.
ErrTimeout = errors.New("processkit: run timed out")
// ErrUnsupported means the operation is not available on this platform (e.g.
// a non-kill [Group.Signal], or [Group.Suspend] / [Group.Resume], on Windows).
// Never a silent skip.
ErrUnsupported = errors.New("processkit: operation not supported on this platform")
// ErrNotReady means a readiness probe did not pass within its deadline (or can
// no longer pass). Distinct from ErrTimeout, which is the run's own deadline.
ErrNotReady = errors.New("processkit: readiness probe did not pass")
// ErrResourceLimit means a requested whole-tree resource cap could not be
// enforced — never a silently-unbounded group.
ErrResourceLimit = errors.New("processkit: resource limit could not be enforced")
// ErrNotFound means the program could not be found. A [*NotFoundError] matches
// this via errors.Is.
ErrNotFound = errors.New("processkit: program not found")
// ErrStart means the process could not be started (spawned or contained). A
// [*StartError] matches this via errors.Is; unwrap it for the underlying OS cause.
ErrStart = errors.New("processkit: failed to start the process")
// ErrTooFewStages means a [Pipeline] was run with fewer than two stages. A
// pipeline needs at least two commands to chain.
ErrTooFewStages = errors.New("processkit: a pipeline needs at least two stages")
)
// ExitError reports a run that completed but was not a success — a non-zero exit
// code, a signal kill (Unix), or a timeout. It carries the captured output so the
// caller can diagnose the failure. Match it with errors.As; a timed-out ExitError
// additionally matches errors.Is(err, [ErrTimeout]).
type ExitError struct {
Program string
Outcome Outcome
Stdout string
Stderr string
Mechanism Mechanism
}
// Error renders a safe, bounded summary. Captured streams are previewed (not
// dumped in full) and sanitized so child-controlled bytes can't inject terminal
// escapes or bidi overrides (Trojan-Source, CVE-2021-42574).
func (e *ExitError) Error() string {
var b strings.Builder
b.WriteString("processkit: ")
b.WriteString(quoteProgram(e.Program))
switch {
case e.Outcome.TimedOut():
b.WriteString(" timed out")
default:
if s, ok := e.Outcome.Signal(); ok {
fmt.Fprintf(&b, " killed by signal %d", s)
} else if c, ok := e.Outcome.Code(); ok {
fmt.Fprintf(&b, " exited with code %d", c)
} else {
b.WriteString(" terminated abnormally")
}
}
if d := diagnostic(e.Stderr, e.Stdout); d != "" {
b.WriteString(": ")
b.WriteString(d)
}
return b.String()
}
// Is reports a match for ErrTimeout when this exit was a timeout, so
// errors.Is(err, ErrTimeout) works on a timed-out ExitError.
func (e *ExitError) Is(target error) bool {
return target == ErrTimeout && e.Outcome.TimedOut()
}
// CancelError reports that a run was ended by the caller's context — either
// cancelled or its deadline elapsed. It carries no captured output (the run was
// abandoned). Matches errors.Is(err, [ErrCancelled]); Cause is the underlying
// context error, so errors.Is(err, context.Canceled) / context.DeadlineExceeded
// also work. (A run's *own* [Cmd.WithTimeout] deadline is captured in the
// [Result] instead — see [Outcome.TimedOut].)
type CancelError struct {
Program string
Cause error // context.Canceled or context.DeadlineExceeded
}
// Error renders the cancellation, distinguishing a cancelled context from an
// elapsed parent deadline (the Is/Unwrap match against [ErrCancelled] and the
// underlying context error is unaffected either way).
func (e *CancelError) Error() string {
reason := "cancelled"
if errors.Is(e.Cause, context.DeadlineExceeded) {
reason = "context deadline exceeded"
}
return fmt.Sprintf("processkit: %s %s", quoteProgram(e.Program), reason)
}
// Is matches the ErrCancelled sentinel.
func (e *CancelError) Is(target error) bool { return target == ErrCancelled }
// Unwrap exposes the underlying context error to errors.Is / errors.As.
func (e *CancelError) Unwrap() error { return e.Cause }
// NotFoundError reports that a program could not be resolved. Searched holds the
// PATH directories that were checked, when known. Matches errors.Is(err, [ErrNotFound]).
type NotFoundError struct {
Program string
Searched []string
}
// Error renders the failure, naming how many PATH directories were searched (not
// their contents, to avoid leaking the environment).
func (e *NotFoundError) Error() string {
if len(e.Searched) > 0 {
return fmt.Sprintf("processkit: %s not found on PATH (searched %d director%s)",
quoteProgram(e.Program), len(e.Searched), plural(len(e.Searched), "y", "ies"))
}
return fmt.Sprintf("processkit: %s not found", quoteProgram(e.Program))
}
// Is matches the ErrNotFound sentinel.
func (e *NotFoundError) Is(target error) bool { return target == ErrNotFound }
// StartError reports a spawn failure that is not a not-found (e.g. a permission
// error, a bad working directory). It wraps the underlying cause.
type StartError struct {
Program string
Err error
}
// Error renders the spawn failure.
func (e *StartError) Error() string {
return fmt.Sprintf("processkit: failed to start %s: %v", quoteProgram(e.Program), e.Err)
}
// Is matches the ErrStart sentinel (in addition to the wrapped cause via Unwrap).
func (e *StartError) Is(target error) bool { return target == ErrStart }
// Unwrap exposes the underlying OS error to errors.Is / errors.As.
func (e *StartError) Unwrap() error { return e.Err }
// NotReadyError reports that a readiness probe ([RunningProcess.WaitForLine],
// [RunningProcess.WaitForPort], [RunningProcess.WaitFor]) did not pass — the line
// never appeared, the port never accepted, the predicate never held, or the
// process exited before becoming ready. Matches errors.Is(err, [ErrNotReady]).
//
// It is distinct from [ErrTimeout]: a probe deadline is the caller's own
// readiness budget, not the run's [Cmd.WithTimeout], and a failed probe does NOT
// kill the process — the caller decides what happens next.
type NotReadyError struct {
Program string // the process that did not become ready
Probe string // which probe: "line", "port", or "predicate"
Timeout time.Duration // the probe deadline that elapsed
Cause error // the last underlying failure (e.g. the last dial error), if any
}
// Error renders the readiness failure.
func (e *NotReadyError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("processkit: %s not ready (%s probe) after %s: %v",
quoteProgram(e.Program), e.Probe, e.Timeout, e.Cause)
}
return fmt.Sprintf("processkit: %s not ready (%s probe) after %s",
quoteProgram(e.Program), e.Probe, e.Timeout)
}
// Is matches the ErrNotReady sentinel.
func (e *NotReadyError) Is(target error) bool { return target == ErrNotReady }
// Unwrap exposes the last underlying failure (if any) to errors.Is / errors.As.
func (e *NotReadyError) Unwrap() error { return e.Cause }
// ResourceLimitError reports that a whole-tree resource cap requested via
// [NewGroup] — [WithMemoryMax], [WithMaxProcesses], or [WithCPUQuota] — could not
// be enforced. Either the value was invalid, or the active mechanism has no
// whole-tree limit primitive: a Windows Job Object enforces all three, but every
// Unix backend here does not (a Linux cgroup-v2 backend is planned), so a limit
// requested there fails fast. An unenforced limit is no protection, so this is
// raised rather than handing back a silently-unbounded group. Matches
// errors.Is(err, [ErrResourceLimit]).
type ResourceLimitError struct {
Limit string // which cap: "memory", "processes", "cpu", or "" for the whole request
Reason string // why it could not be enforced (always set)
Cause error // the underlying OS error, if any (nil for a rejected value)
}
// Error renders the limit failure.
func (e *ResourceLimitError) Error() string {
if e.Limit != "" {
return fmt.Sprintf("processkit: could not enforce %s limit: %s", e.Limit, sanitize(e.Reason))
}
return fmt.Sprintf("processkit: could not enforce resource limits: %s", sanitize(e.Reason))
}
// Is matches the ErrResourceLimit sentinel.
func (e *ResourceLimitError) Is(target error) bool { return target == ErrResourceLimit }
// Unwrap exposes the underlying OS error (if any) to errors.Is / errors.As.
func (e *ResourceLimitError) Unwrap() error { return e.Cause }
// --- safe rendering helpers (shared by the error types) ---
// previewLimit bounds how many bytes of a captured stream appear in an error
// string — enough to diagnose, not enough to dump a multi-megabyte capture.
const previewLimit = 200
// diagnostic picks the most useful captured stream (stderr, falling back to
// stdout) and returns a bounded, sanitized preview, or "" if both are empty.
func diagnostic(stderr, stdout string) string {
s := strings.TrimSpace(stderr)
if s == "" {
s = strings.TrimSpace(stdout)
}
if s == "" {
return ""
}
return sanitize(preview(s))
}
// preview truncates s to previewLimit bytes on a rune boundary, marking a cut.
func preview(s string) string {
if len(s) <= previewLimit {
return s
}
cut := previewLimit
for cut > 0 && !utf8.RuneStart(s[cut]) {
cut--
}
return s[:cut] + "…"
}
// sanitize escapes control characters (except \n and \t) and Unicode bidi /
// directional-override controls, defusing terminal-injection and Trojan-Source
// attacks (CVE-2021-42574) from child-controlled output.
func sanitize(s string) string {
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
switch {
case r == '\n' || r == '\t':
b.WriteRune(r)
case r < 0x20 || r == 0x7f:
fmt.Fprintf(&b, "\\x%02x", r)
case (r >= 0x202a && r <= 0x202e) || (r >= 0x2066 && r <= 0x2069) || r == 0x200e || r == 0x200f:
fmt.Fprintf(&b, "\\u%04x", r)
default:
b.WriteRune(r)
}
}
return b.String()
}
// quoteProgram renders a program name for an error, sanitized and backtick-quoted.
func quoteProgram(p string) string {
if p == "" {
return "`<command>`"
}
return "`" + sanitize(p) + "`"
}
func plural(n int, one, many string) string {
if n == 1 {
return one
}
return many
}