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 }