|
| 1 | +/* |
| 2 | + Copyright The containerd Authors. |
| 3 | +
|
| 4 | + Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + you may not use this file except in compliance with the License. |
| 6 | + You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | + Unless required by applicable law or agreed to in writing, software |
| 11 | + distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + See the License for the specific language governing permissions and |
| 14 | + limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package stack |
| 18 | + |
| 19 | +import ( |
| 20 | + "context" |
| 21 | + "encoding/json" |
| 22 | + "errors" |
| 23 | + "fmt" |
| 24 | + "os" |
| 25 | + "path" |
| 26 | + "runtime" |
| 27 | + "strings" |
| 28 | + "sync/atomic" |
| 29 | + "unsafe" |
| 30 | + |
| 31 | + "github.com/containerd/typeurl/v2" |
| 32 | + |
| 33 | + "github.com/containerd/errdefs/internal/types" |
| 34 | +) |
| 35 | + |
| 36 | +func init() { |
| 37 | + typeurl.Register((*stack)(nil), "github.com/containerd/errdefs", "stack+json") |
| 38 | +} |
| 39 | + |
| 40 | +var ( |
| 41 | + // Version is version of running process |
| 42 | + Version string = "dev" |
| 43 | + |
| 44 | + // Revision is the specific revision of the running process |
| 45 | + Revision string = "dirty" |
| 46 | +) |
| 47 | + |
| 48 | +type stack struct { |
| 49 | + decoded *Trace |
| 50 | + |
| 51 | + callers []uintptr |
| 52 | + helpers []uintptr |
| 53 | +} |
| 54 | + |
| 55 | +// Trace is a stack trace along with process information about the source |
| 56 | +type Trace struct { |
| 57 | + Version string `json:"version,omitempty"` |
| 58 | + Revision string `json:"revision,omitempty"` |
| 59 | + Cmdline []string `json:"cmdline,omitempty"` |
| 60 | + Frames []Frame `json:"frames,omitempty"` |
| 61 | + Pid int32 `json:"pid,omitempty"` |
| 62 | +} |
| 63 | + |
| 64 | +// Frame is a single frame of the trace representing a line of code |
| 65 | +type Frame struct { |
| 66 | + Name string `json:"Name,omitempty"` |
| 67 | + File string `json:"File,omitempty"` |
| 68 | + Line int32 `json:"Line,omitempty"` |
| 69 | +} |
| 70 | + |
| 71 | +func (f Frame) Format(s fmt.State, verb rune) { |
| 72 | + switch verb { |
| 73 | + case 'v': |
| 74 | + switch { |
| 75 | + case s.Flag('+'): |
| 76 | + fmt.Fprintf(s, "%s\n\t%s:%d\n", f.Name, f.File, f.Line) |
| 77 | + default: |
| 78 | + fmt.Fprint(s, f.Name) |
| 79 | + } |
| 80 | + case 's': |
| 81 | + fmt.Fprint(s, path.Base(f.Name)) |
| 82 | + case 'q': |
| 83 | + fmt.Fprintf(s, "%q", path.Base(f.Name)) |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +// callers returns the current stack, skipping over the number of frames mentioned |
| 88 | +// Frames with skip=0: |
| 89 | +// |
| 90 | +// frame[0] runtime.Callers |
| 91 | +// frame[1] <this function> github.com/containerd/errdefs/stack.callers |
| 92 | +// frame[2] <caller> (Use skip=2 to have this be first frame) |
| 93 | +func callers(skip int) *stack { |
| 94 | + const depth = 32 |
| 95 | + var pcs [depth]uintptr |
| 96 | + n := runtime.Callers(skip, pcs[:]) |
| 97 | + return &stack{ |
| 98 | + callers: pcs[0:n], |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +func (s *stack) getDecoded() *Trace { |
| 103 | + if s.decoded == nil { |
| 104 | + var unsafeDecoded = (*unsafe.Pointer)(unsafe.Pointer(&s.decoded)) |
| 105 | + |
| 106 | + var helpers map[string]struct{} |
| 107 | + if len(s.helpers) > 0 { |
| 108 | + helpers = make(map[string]struct{}) |
| 109 | + frames := runtime.CallersFrames(s.helpers) |
| 110 | + for { |
| 111 | + frame, more := frames.Next() |
| 112 | + helpers[frame.Function] = struct{}{} |
| 113 | + if !more { |
| 114 | + break |
| 115 | + } |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + f := make([]Frame, 0, len(s.callers)) |
| 120 | + if len(s.callers) > 0 { |
| 121 | + frames := runtime.CallersFrames(s.callers) |
| 122 | + for { |
| 123 | + frame, more := frames.Next() |
| 124 | + if _, ok := helpers[frame.Function]; !ok { |
| 125 | + f = append(f, Frame{ |
| 126 | + Name: frame.Function, |
| 127 | + File: frame.File, |
| 128 | + Line: int32(frame.Line), |
| 129 | + }) |
| 130 | + } |
| 131 | + if !more { |
| 132 | + break |
| 133 | + } |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + t := Trace{ |
| 138 | + Version: Version, |
| 139 | + Revision: Revision, |
| 140 | + Cmdline: os.Args, |
| 141 | + Frames: f, |
| 142 | + Pid: int32(os.Getpid()), |
| 143 | + } |
| 144 | + |
| 145 | + atomic.StorePointer(unsafeDecoded, unsafe.Pointer(&t)) |
| 146 | + } |
| 147 | + |
| 148 | + return s.decoded |
| 149 | +} |
| 150 | + |
| 151 | +func (s *stack) Error() string { |
| 152 | + return fmt.Sprintf("%+v", s.getDecoded()) |
| 153 | +} |
| 154 | + |
| 155 | +func (s *stack) MarshalJSON() ([]byte, error) { |
| 156 | + return json.Marshal(s.getDecoded()) |
| 157 | +} |
| 158 | + |
| 159 | +func (s *stack) UnmarshalJSON(b []byte) error { |
| 160 | + var unsafeDecoded = (*unsafe.Pointer)(unsafe.Pointer(&s.decoded)) |
| 161 | + var t Trace |
| 162 | + |
| 163 | + if err := json.Unmarshal(b, &t); err != nil { |
| 164 | + return err |
| 165 | + } |
| 166 | + |
| 167 | + atomic.StorePointer(unsafeDecoded, unsafe.Pointer(&t)) |
| 168 | + |
| 169 | + return nil |
| 170 | +} |
| 171 | + |
| 172 | +func (s *stack) Format(st fmt.State, verb rune) { |
| 173 | + switch verb { |
| 174 | + case 'v': |
| 175 | + if st.Flag('+') { |
| 176 | + t := s.getDecoded() |
| 177 | + fmt.Fprintf(st, "%d %s %s\n", t.Pid, t.Version, strings.Join(t.Cmdline, " ")) |
| 178 | + for _, f := range t.Frames { |
| 179 | + f.Format(st, verb) |
| 180 | + } |
| 181 | + fmt.Fprintln(st) |
| 182 | + return |
| 183 | + } |
| 184 | + } |
| 185 | +} |
| 186 | + |
| 187 | +func (s *stack) StackTrace() Trace { |
| 188 | + return *s.getDecoded() |
| 189 | +} |
| 190 | + |
| 191 | +func (s *stack) CollapseError() {} |
| 192 | + |
| 193 | +// ErrStack returns a new error for the callers stack, |
| 194 | +// this can be wrapped or joined into an existing error. |
| 195 | +// NOTE: When joined with errors.Join, the stack |
| 196 | +// will show up in the error string output. |
| 197 | +// Use with `stack.Join` to force addition of the |
| 198 | +// error stack. |
| 199 | +func ErrStack() error { |
| 200 | + return callers(3) |
| 201 | +} |
| 202 | + |
| 203 | +// Join adds a stack if there is no stack included to the errors |
| 204 | +// and returns a joined error with the stack hidden from the error |
| 205 | +// output. The stack error shows up when Unwrapped or formatted |
| 206 | +// with `%+v`. |
| 207 | +func Join(errs ...error) error { |
| 208 | + return joinErrors(nil, errs) |
| 209 | +} |
| 210 | + |
| 211 | +// WithStack will check if the error already has a stack otherwise |
| 212 | +// return a new error with the error joined with a stack error |
| 213 | +// Any helpers will be skipped. |
| 214 | +func WithStack(ctx context.Context, errs ...error) error { |
| 215 | + return joinErrors(ctx.Value(helperKey{}), errs) |
| 216 | +} |
| 217 | + |
| 218 | +func joinErrors(helperVal any, errs []error) error { |
| 219 | + var filtered []error |
| 220 | + var collapsible []error |
| 221 | + var hasStack bool |
| 222 | + for _, err := range errs { |
| 223 | + if err != nil { |
| 224 | + if !hasStack && hasLocalStackTrace(err) { |
| 225 | + hasStack = true |
| 226 | + } |
| 227 | + if _, ok := err.(types.CollapsibleError); ok { |
| 228 | + collapsible = append(collapsible, err) |
| 229 | + } else { |
| 230 | + filtered = append(filtered, err) |
| 231 | + } |
| 232 | + |
| 233 | + } |
| 234 | + } |
| 235 | + if len(filtered) == 0 { |
| 236 | + return nil |
| 237 | + } |
| 238 | + if !hasStack { |
| 239 | + s := callers(4) |
| 240 | + if helpers, ok := helperVal.([]uintptr); ok { |
| 241 | + s.helpers = helpers |
| 242 | + } |
| 243 | + collapsible = append(collapsible, s) |
| 244 | + } |
| 245 | + var err error |
| 246 | + if len(filtered) > 1 { |
| 247 | + err = errors.Join(filtered...) |
| 248 | + } else { |
| 249 | + err = filtered[0] |
| 250 | + } |
| 251 | + if len(collapsible) == 0 { |
| 252 | + return err |
| 253 | + } |
| 254 | + |
| 255 | + return types.CollapsedError(err, collapsible...) |
| 256 | +} |
| 257 | + |
| 258 | +func hasLocalStackTrace(err error) bool { |
| 259 | + switch e := err.(type) { |
| 260 | + case *stack: |
| 261 | + return true |
| 262 | + case interface{ Unwrap() error }: |
| 263 | + if hasLocalStackTrace(e.Unwrap()) { |
| 264 | + return true |
| 265 | + } |
| 266 | + case interface{ Unwrap() []error }: |
| 267 | + for _, ue := range e.Unwrap() { |
| 268 | + if hasLocalStackTrace(ue) { |
| 269 | + return true |
| 270 | + } |
| 271 | + } |
| 272 | + } |
| 273 | + |
| 274 | + // TODO: Consider if pkg/errors compatibility is needed |
| 275 | + // NOTE: This was implemented before the standard error package |
| 276 | + // so it may unwrap and have this interface. |
| 277 | + //if _, ok := err.(interface{ StackTrace() pkgerrors.StackTrace }); ok { |
| 278 | + // return true |
| 279 | + //} |
| 280 | + |
| 281 | + return false |
| 282 | +} |
| 283 | + |
| 284 | +type helperKey struct{} |
| 285 | + |
| 286 | +// WithHelper marks the context as from a helper function |
| 287 | +// This will add an additional skip to the error stack trace |
| 288 | +func WithHelper(ctx context.Context) context.Context { |
| 289 | + helpers, _ := ctx.Value(helperKey{}).([]uintptr) |
| 290 | + var pcs [1]uintptr |
| 291 | + n := runtime.Callers(2, pcs[:]) |
| 292 | + if n == 1 { |
| 293 | + ctx = context.WithValue(ctx, helperKey{}, append(helpers, pcs[0])) |
| 294 | + } |
| 295 | + return ctx |
| 296 | +} |
0 commit comments