gitlab.com/Raven-IO/raven-delve@v1.22.4/pkg/terminal/starbind/starlark.go (about)

     1  package starbind
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"runtime"
     9  	"sort"
    10  	"strings"
    11  	"sync"
    12  
    13  	startime "go.starlark.net/lib/time"
    14  	"go.starlark.net/resolve"
    15  	"go.starlark.net/starlark"
    16  
    17  	"gitlab.com/Raven-IO/raven-delve/service"
    18  	"gitlab.com/Raven-IO/raven-delve/service/api"
    19  )
    20  
    21  //go:generate go run ../../../_scripts/gen-starlark-bindings.go go ./starlark_mapping.go
    22  //go:generate go run ../../../_scripts/gen-starlark-bindings.go doc ../../../Documentation/cli/starlark.md
    23  
    24  const (
    25  	dlvCommandBuiltinName        = "dlv_command"
    26  	readFileBuiltinName          = "read_file"
    27  	writeFileBuiltinName         = "write_file"
    28  	commandPrefix                = "command_"
    29  	dlvContextName               = "dlv_context"
    30  	curScopeBuiltinName          = "cur_scope"
    31  	defaultLoadConfigBuiltinName = "default_load_config"
    32  	helpBuiltinName              = "help"
    33  )
    34  
    35  func init() {
    36  	resolve.AllowNestedDef = true
    37  	resolve.AllowLambda = true
    38  	resolve.AllowFloat = true
    39  	resolve.AllowSet = true
    40  	resolve.AllowBitwise = true
    41  	resolve.AllowRecursion = true
    42  	resolve.AllowGlobalReassign = true
    43  }
    44  
    45  // Context is the context in which starlark scripts are evaluated.
    46  // It contains methods to call API functions, command line commands, etc.
    47  type Context interface {
    48  	Client() service.Client
    49  	RegisterCommand(name, helpMsg string, cmdfn func(args string) error)
    50  	CallCommand(cmdstr string) error
    51  	Scope() api.EvalScope
    52  	LoadConfig() api.LoadConfig
    53  }
    54  
    55  // Env is the environment used to evaluate starlark scripts.
    56  type Env struct {
    57  	env       starlark.StringDict
    58  	contextMu sync.Mutex
    59  	thread    *starlark.Thread
    60  	cancelfn  context.CancelFunc
    61  
    62  	ctx Context
    63  	out EchoWriter
    64  }
    65  
    66  // New creates a new starlark binding environment.
    67  func New(ctx Context, out EchoWriter) *Env {
    68  	env := &Env{}
    69  
    70  	env.ctx = ctx
    71  	env.out = out
    72  
    73  	// Make the "time" module available to Starlark scripts.
    74  	starlark.Universe["time"] = startime.Module
    75  
    76  	var doc map[string]string
    77  	env.env, doc = env.starlarkPredeclare()
    78  
    79  	builtindoc := func(name, args, descr string) {
    80  		doc[name] = name + args + "\n\n" + name + " " + descr
    81  	}
    82  
    83  	env.env[dlvCommandBuiltinName] = starlark.NewBuiltin(dlvCommandBuiltinName, func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
    84  		if err := isCancelled(thread); err != nil {
    85  			return starlark.None, err
    86  		}
    87  		argstrs := make([]string, len(args))
    88  		for i := range args {
    89  			a, ok := args[i].(starlark.String)
    90  			if !ok {
    91  				return nil, fmt.Errorf("argument of dlv_command is not a string")
    92  			}
    93  			argstrs[i] = string(a)
    94  		}
    95  		err := env.ctx.CallCommand(strings.Join(argstrs, " "))
    96  		if err != nil && strings.Contains(err.Error(), " has exited with status ") {
    97  			return env.interfaceToStarlarkValue(err), nil
    98  		}
    99  		return starlark.None, decorateError(thread, err)
   100  	})
   101  	builtindoc(dlvCommandBuiltinName, "(Command)", "interrupts, continues and steps through the program.")
   102  
   103  	env.env[readFileBuiltinName] = starlark.NewBuiltin(readFileBuiltinName, func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   104  		if len(args) != 1 {
   105  			return nil, decorateError(thread, fmt.Errorf("wrong number of arguments"))
   106  		}
   107  		path, ok := args[0].(starlark.String)
   108  		if !ok {
   109  			return nil, decorateError(thread, fmt.Errorf("argument of read_file was not a string"))
   110  		}
   111  		buf, err := os.ReadFile(string(path))
   112  		if err != nil {
   113  			return nil, decorateError(thread, err)
   114  		}
   115  		return starlark.String(string(buf)), nil
   116  	})
   117  	builtindoc(readFileBuiltinName, "(Path)", "reads a file.")
   118  
   119  	env.env[writeFileBuiltinName] = starlark.NewBuiltin(writeFileBuiltinName, func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   120  		if len(args) != 2 {
   121  			return nil, decorateError(thread, fmt.Errorf("wrong number of arguments"))
   122  		}
   123  		path, ok := args[0].(starlark.String)
   124  		if !ok {
   125  			return nil, decorateError(thread, fmt.Errorf("first argument of write_file was not a string"))
   126  		}
   127  		err := os.WriteFile(string(path), []byte(args[1].String()), 0o640)
   128  		return starlark.None, decorateError(thread, err)
   129  	})
   130  	builtindoc(writeFileBuiltinName, "(Path, Text)", "writes text to the specified file.")
   131  
   132  	env.env[curScopeBuiltinName] = starlark.NewBuiltin(curScopeBuiltinName, func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   133  		return env.interfaceToStarlarkValue(env.ctx.Scope()), nil
   134  	})
   135  	builtindoc(curScopeBuiltinName, "()", "returns the current scope.")
   136  
   137  	env.env[defaultLoadConfigBuiltinName] = starlark.NewBuiltin(defaultLoadConfigBuiltinName, func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   138  		return env.interfaceToStarlarkValue(env.ctx.LoadConfig()), nil
   139  	})
   140  	builtindoc(defaultLoadConfigBuiltinName, "()", "returns the default load configuration.")
   141  
   142  	env.env[helpBuiltinName] = starlark.NewBuiltin(helpBuiltinName, func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   143  		switch len(args) {
   144  		case 0:
   145  			fmt.Fprintln(env.out, "Available builtins:")
   146  			bins := make([]string, 0, len(env.env))
   147  			for name, value := range env.env {
   148  				switch value.(type) {
   149  				case *starlark.Builtin:
   150  					bins = append(bins, name)
   151  				}
   152  			}
   153  			sort.Strings(bins)
   154  			for _, bin := range bins {
   155  				fmt.Fprintf(env.out, "\t%s\n", bin)
   156  			}
   157  		case 1:
   158  			switch x := args[0].(type) {
   159  			case *starlark.Builtin:
   160  				if doc[x.Name()] != "" {
   161  					fmt.Fprintf(env.out, "%s\n", doc[x.Name()])
   162  				} else {
   163  					fmt.Fprintf(env.out, "no help for builtin %s\n", x.Name())
   164  				}
   165  			case *starlark.Function:
   166  				fmt.Fprintf(env.out, "user defined function %s\n", x.Name())
   167  				if doc := x.Doc(); doc != "" {
   168  					fmt.Fprintln(env.out, doc)
   169  				}
   170  			default:
   171  				fmt.Fprintf(env.out, "no help for object of type %T\n", args[0])
   172  			}
   173  		default:
   174  			fmt.Fprintln(env.out, "wrong number of arguments ", len(args))
   175  		}
   176  		return starlark.None, nil
   177  	})
   178  	builtindoc(helpBuiltinName, "(Object)", "prints help for Object.")
   179  
   180  	return env
   181  }
   182  
   183  // Redirect redirects starlark output to out.
   184  func (env *Env) Redirect(out EchoWriter) {
   185  	env.out = out
   186  	if env.thread != nil {
   187  		env.thread.Print = env.printFunc()
   188  	}
   189  }
   190  
   191  func (env *Env) printFunc() func(_ *starlark.Thread, msg string) {
   192  	return func(_ *starlark.Thread, msg string) { fmt.Fprintln(env.out, msg) }
   193  }
   194  
   195  // Execute executes a script. Path is the name of the file to execute and
   196  // source is the source code to execute.
   197  // Source can be either a []byte, a string or a io.Reader. If source is nil
   198  // Execute will execute the file specified by 'path'.
   199  // After the file is executed if a function named mainFnName exists it will be called, passing args to it.
   200  func (env *Env) Execute(path string, source interface{}, mainFnName string, args []interface{}) (_ starlark.Value, _err error) {
   201  	defer func() {
   202  		err := recover()
   203  		if err == nil {
   204  			return
   205  		}
   206  		_err = fmt.Errorf("panic executing starlark script: %v", err)
   207  		fmt.Fprintf(env.out, "panic executing starlark script: %v\n", err)
   208  		for i := 0; ; i++ {
   209  			pc, file, line, ok := runtime.Caller(i)
   210  			if !ok {
   211  				break
   212  			}
   213  			fname := "<unknown>"
   214  			fn := runtime.FuncForPC(pc)
   215  			if fn != nil {
   216  				fname = fn.Name()
   217  			}
   218  			fmt.Fprintf(env.out, "%s\n\tin %s:%d\n", fname, file, line)
   219  		}
   220  	}()
   221  
   222  	thread := env.newThread()
   223  	globals, err := starlark.ExecFile(thread, path, source, env.env)
   224  	if err != nil {
   225  		return starlark.None, err
   226  	}
   227  
   228  	err = env.exportGlobals(globals)
   229  	if err != nil {
   230  		return starlark.None, err
   231  	}
   232  
   233  	return env.callMain(thread, globals, mainFnName, args)
   234  }
   235  
   236  // exportGlobals saves globals with a name starting with a capital letter
   237  // into the environment and creates commands from globals with a name
   238  // starting with "command_"
   239  func (env *Env) exportGlobals(globals starlark.StringDict) error {
   240  	for name, val := range globals {
   241  		switch {
   242  		case strings.HasPrefix(name, commandPrefix):
   243  			err := env.createCommand(name, val)
   244  			if err != nil {
   245  				return err
   246  			}
   247  		case name[0] >= 'A' && name[0] <= 'Z':
   248  			env.env[name] = val
   249  		}
   250  	}
   251  	return nil
   252  }
   253  
   254  // Cancel cancels the execution of a currently running script or function.
   255  func (env *Env) Cancel() {
   256  	if env == nil {
   257  		return
   258  	}
   259  	env.contextMu.Lock()
   260  	if env.cancelfn != nil {
   261  		env.cancelfn()
   262  		env.cancelfn = nil
   263  	}
   264  	if env.thread != nil {
   265  		env.thread.Cancel("user interrupt")
   266  	}
   267  	env.contextMu.Unlock()
   268  }
   269  
   270  func (env *Env) newThread() *starlark.Thread {
   271  	thread := &starlark.Thread{
   272  		Print: env.printFunc(),
   273  	}
   274  	env.contextMu.Lock()
   275  	var ctx context.Context
   276  	ctx, env.cancelfn = context.WithCancel(context.Background())
   277  	env.thread = thread
   278  	env.contextMu.Unlock()
   279  	thread.SetLocal(dlvContextName, ctx)
   280  	return thread
   281  }
   282  
   283  func (env *Env) createCommand(name string, val starlark.Value) error {
   284  	fnval, ok := val.(*starlark.Function)
   285  	if !ok {
   286  		return nil
   287  	}
   288  
   289  	name = name[len(commandPrefix):]
   290  
   291  	helpMsg := fnval.Doc()
   292  	if helpMsg == "" {
   293  		helpMsg = "user defined"
   294  	}
   295  
   296  	if fnval.NumParams() == 1 {
   297  		if p0, _ := fnval.Param(0); p0 == "args" {
   298  			env.ctx.RegisterCommand(name, helpMsg, func(args string) error {
   299  				_, err := starlark.Call(env.newThread(), fnval, starlark.Tuple{starlark.String(args)}, nil)
   300  				return err
   301  			})
   302  			return nil
   303  		}
   304  	}
   305  
   306  	env.ctx.RegisterCommand(name, helpMsg, func(args string) error {
   307  		thread := env.newThread()
   308  		argval, err := starlark.Eval(thread, "<input>", "("+args+")", env.env)
   309  		if err != nil {
   310  			return err
   311  		}
   312  		argtuple, ok := argval.(starlark.Tuple)
   313  		if !ok {
   314  			argtuple = starlark.Tuple{argval}
   315  		}
   316  		_, err = starlark.Call(thread, fnval, argtuple, nil)
   317  		return err
   318  	})
   319  	return nil
   320  }
   321  
   322  // callMain calls the main function in globals, if one was defined.
   323  func (env *Env) callMain(thread *starlark.Thread, globals starlark.StringDict, mainFnName string, args []interface{}) (starlark.Value, error) {
   324  	if mainFnName == "" {
   325  		return starlark.None, nil
   326  	}
   327  	mainval := globals[mainFnName]
   328  	if mainval == nil {
   329  		return starlark.None, nil
   330  	}
   331  	mainfn, ok := mainval.(*starlark.Function)
   332  	if !ok {
   333  		return starlark.None, fmt.Errorf("%s is not a function", mainFnName)
   334  	}
   335  	if mainfn.NumParams() != len(args) {
   336  		return starlark.None, fmt.Errorf("wrong number of arguments for %s", mainFnName)
   337  	}
   338  	argtuple := make(starlark.Tuple, len(args))
   339  	for i := range args {
   340  		argtuple[i] = env.interfaceToStarlarkValue(args[i])
   341  	}
   342  	return starlark.Call(thread, mainfn, argtuple, nil)
   343  }
   344  
   345  func isCancelled(thread *starlark.Thread) error {
   346  	if ctx, ok := thread.Local(dlvContextName).(context.Context); ok {
   347  		select {
   348  		case <-ctx.Done():
   349  			return ctx.Err()
   350  		default:
   351  		}
   352  	}
   353  	return nil
   354  }
   355  
   356  func decorateError(thread *starlark.Thread, err error) error {
   357  	if err == nil {
   358  		return nil
   359  	}
   360  	pos := thread.CallFrame(1).Pos
   361  	if pos.Col > 0 {
   362  		return fmt.Errorf("%s:%d:%d: %v", pos.Filename(), pos.Line, pos.Col, err)
   363  	}
   364  	return fmt.Errorf("%s:%d: %v", pos.Filename(), pos.Line, err)
   365  }
   366  
   367  type EchoWriter interface {
   368  	io.Writer
   369  	Echo(string)
   370  	Flush()
   371  }