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  }