github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/src/go.chromium.org/tast/core/internal/usercode/usercode.go (about) 1 // Copyright 2020 The ChromiumOS Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 // Package usercode provides utilities to interact with user-defined code. 6 package usercode 7 8 import ( 9 "context" 10 "sync/atomic" 11 "time" 12 13 "go.chromium.org/tast/core/errors" 14 ) 15 16 // PanicHandler specifies how to handle panics in SafeCall. 17 type PanicHandler func(val interface{}) 18 19 // ErrorReporter is the interface for reporting errors. It is implemented by 20 // testing.State and its sibling types. 21 type ErrorReporter interface { 22 Error(args ...interface{}) 23 } 24 25 // ErrorOnPanic returns a PanicHandler that reports a panic via e. 26 func ErrorOnPanic(e ErrorReporter) PanicHandler { 27 return func(val interface{}) { 28 e.Error("Panic: ", val) 29 } 30 } 31 32 // SafeCall runs a function f on a goroutine to protect callers from its 33 // possible bad behavior. 34 // 35 // SafeCall calls f with a context having a specified timeout. If f does not 36 // return before the timeout, SafeCall further waits for gracePeriod to allow 37 // some clean up. If f does not return after timeout + gracePeriod or ctx is 38 // canceled before f finishes, SafeCall abandons the goroutine and immediately 39 // returns an error. name is included in an error message to explain which user 40 // code did not return. 41 // 42 // If f panics, SafeCall calls a panic handler ph to handle it. SafeCall will 43 // not call ph if it decides to abandon f, even if f panics later. 44 // 45 // If f calls runtime.Goexit, it is handled just like the function returns 46 // normally. 47 // 48 // SafeCall returns an error only if execution of f was abandoned for some 49 // reasons (e.g. f ignored the timeout, ctx was canceled). In other cases, it 50 // returns nil. 51 func SafeCall(ctx context.Context, name string, timeout, gracePeriod time.Duration, ph PanicHandler, f func(ctx context.Context)) error { 52 // Two goroutines race for a token below. 53 // The main goroutine attempts to take a token when it sees timeout 54 // or context cancellation. If it successfully takes a token, SafeCall 55 // returns immediately without waiting for f to finish, and ph will 56 // never be called. 57 // A background goroutine attempts to take a token when it finishes 58 // calling f. If it successfully takes a token, it calls recover and 59 // ph (if it recovered from a panic). Until the goroutine finishes 60 // SafeCall will not return. 61 62 var token uintptr 63 // takeToken returns true if it is called first time. 64 takeToken := func() bool { 65 return atomic.CompareAndSwapUintptr(&token, 0, 1) 66 } 67 68 done := make(chan struct{}) // closed when the background goroutine finishes 69 var callErr error // an error to be returned by a user function call 70 71 // Start a background goroutine that calls into the user code. 72 go func() { 73 defer close(done) 74 75 defer func() { 76 // Always call recover to avoid crashing the process. 77 val := recover() 78 79 // Declare that the user function finished. 80 // If the timeout was already reached, return immediately. 81 if finishWins := takeToken(); !finishWins { 82 return 83 } 84 85 // If the timeout is not reached yet, proceed with panic handling. 86 // The main goroutine waits this handling to complete. 87 callErr = func() error { 88 // If the user code didn't panic, return success. 89 if val == nil { 90 return nil 91 } 92 93 // Handle forced errors. 94 if fe, ok := val.(forcedError); ok { 95 return fe.err 96 } 97 98 // Call ph to handle panic. Note that we must call ph on 99 // this goroutine to include the panic location in the 100 // stack trace. 101 ph(val) 102 103 // Always returning nil here is a bit weird, but we don't 104 // have a use case of PanicHandler returning errors as of 105 // today. 106 return nil 107 }() 108 }() 109 110 ctx, cancel := context.WithTimeout(ctx, timeout) 111 defer cancel() 112 f(ctx) 113 }() 114 115 // Allow f to clean up after timeout for gracePeriod. 116 tm := time.NewTimer(timeout + gracePeriod) 117 defer tm.Stop() 118 119 // Wait until the user function call finishes or the timeout is reached. 120 waitErr := func() error { 121 select { 122 case <-done: 123 return nil 124 case <-tm.C: 125 return errors.Errorf("%s did not return on timeout", name) 126 case <-ctx.Done(): 127 return ctx.Err() 128 } 129 }() 130 131 // Declare that the timeout was reached. 132 if timeoutWins := takeToken(); timeoutWins { 133 return waitErr 134 } 135 136 // If the user function call was already finished, wait for the panic 137 // handling to complete and return its result. 138 <-done 139 return callErr 140 } 141 142 type forcedError struct { 143 err error 144 } 145 146 // ForceErrorForTesting always panics. If the current function is called by 147 // SafeCall, it forces SafeCall to return an error. 148 // This function is to be used by unit tests which want to simulate SafeCall 149 // errors reliably. 150 func ForceErrorForTesting(err error) { 151 panic(forcedError{err}) 152 }