github.com/emcfarlane/larking@v0.0.0-20220605172417-1704b45ee6c3/cmd/lark/main.go (about) 1 // Copyright 2021 Edward McFarlane. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // lark 6 package main 7 8 import ( 9 "bytes" 10 "context" 11 "crypto/x509" 12 "flag" 13 "fmt" 14 "io" 15 "io/ioutil" 16 "log" 17 "os" 18 "path" 19 "path/filepath" 20 "regexp" 21 "runtime" 22 "strings" 23 "testing" 24 25 "github.com/emcfarlane/larking/apipb/workerpb" 26 _ "github.com/emcfarlane/larking/cmd/internal/bindings" 27 "github.com/emcfarlane/larking/control" 28 "github.com/emcfarlane/larking/starlib" 29 "github.com/emcfarlane/larking/starlib/starlarkthread" 30 "github.com/emcfarlane/larking/worker" 31 "github.com/emcfarlane/starlarkassert" 32 "github.com/peterh/liner" 33 "go.starlark.net/starlark" 34 "go.starlark.net/syntax" 35 "gocloud.dev/blob" 36 "google.golang.org/grpc" 37 "google.golang.org/grpc/credentials" 38 "google.golang.org/grpc/credentials/insecure" 39 ) 40 41 func env(key, def string) string { 42 if e := os.Getenv(key); e != "" { 43 return e 44 } 45 return def 46 } 47 48 var ( 49 flagRemoteAddr = flag.String("remote", env("LARK_REMOTE", ""), "Remote server address to execute on.") 50 flagCacheDir = flag.String("cache", env("LARK_CACHE", ""), "Cache directory.") 51 flagAutocomplete = flag.Bool("autocomplete", true, "Enable autocomplete, defaults to true.") 52 flagExecprog = flag.String("c", "", "Execute program `prog`.") 53 flagControlAddr = flag.String("control", "https://larking.io", "Control server for credentials.") 54 flagInsecure = flag.Bool("insecure", false, "Insecure, disable credentials.") 55 flagThread = flag.String("thread", "", "Thread to run on.") 56 flagCreds = flag.String("credentials", env("CREDENTIALS", ""), "Runtime variable for credentials.") 57 58 // TODO: relative/absolute pathing needs to be resolved... 59 flagDir = flag.String("dir", env("LARK_DIR", "file://./?metadata=skip"), "Set the module loading directory") 60 ) 61 62 type Options struct { 63 _ struct{} // pragma: no unkeyed literals 64 CacheDir string // Path to cache directory 65 HistoryFile string // Path to file for storing history 66 AutoComplete bool // Experimental autocompletion 67 Remote workerpb.WorkerClient // Remote thread execution 68 //RemoteAddr string // Remote worker address. 69 //CredentialsFile string // Path to file for remote credentials 70 //Creds map[string]string // Loaded credentials. 71 Filename string 72 Source string 73 } 74 75 func read(line *liner.State, buf *bytes.Buffer) (*syntax.File, error) { 76 buf.Reset() 77 78 // suggest 79 suggest := func(line string) string { 80 var noSpaces int 81 for _, c := range line { 82 if c == ' ' { 83 noSpaces += 1 84 } else { 85 break 86 } 87 } 88 if strings.HasSuffix(line, ":") { 89 noSpaces += 4 90 } 91 return strings.Repeat(" ", noSpaces) 92 } 93 94 var eof bool 95 var previous string 96 prompt := ">>> " 97 readline := func() ([]byte, error) { 98 text := suggest(previous) 99 s, err := line.PromptWithSuggestion(prompt, text, -1) 100 if err != nil { 101 switch err { 102 case io.EOF: 103 eof = true 104 case liner.ErrPromptAborted: 105 return []byte("\n"), nil 106 } 107 return nil, err 108 } 109 prompt = "... " 110 previous = s 111 line.AppendHistory(s) 112 out := []byte(s + "\n") 113 if _, err := buf.Write(out); err != nil { 114 return nil, err 115 } 116 return out, nil 117 } 118 119 f, err := syntax.ParseCompoundStmt("<stdin>", readline) 120 if err != nil { 121 if eof { 122 return nil, io.EOF 123 } 124 starlib.FprintErr(os.Stderr, err) 125 return nil, err 126 } 127 return f, nil 128 } 129 130 func remote(ctx context.Context, line *liner.State, client workerpb.WorkerClient, autocomplete bool) error { 131 stream, err := client.RunOnThread(ctx) 132 if err != nil { 133 return err 134 } 135 136 if autocomplete { 137 line.SetCompleter(func(line string) []string { 138 if err := stream.SendMsg(&workerpb.Command{ 139 Exec: &workerpb.Command_Complete{ 140 Complete: line, 141 }, 142 }); err != nil { 143 return nil 144 } 145 result, err := stream.Recv() 146 if err != nil { 147 return nil 148 } 149 if completion := result.GetCompletion(); completion != nil { 150 return completion.Completions 151 } 152 return nil 153 }) 154 } 155 156 var buf bytes.Buffer 157 for ctx.Err() == nil { 158 _, err := read(line, &buf) 159 if err != nil { 160 if err == io.EOF { 161 return err 162 } 163 continue 164 } 165 166 cmd := &workerpb.Command{ 167 Name: *flagThread, 168 Exec: &workerpb.Command_Input{ 169 Input: buf.String(), 170 }, 171 } 172 if err := stream.Send(cmd); err != nil { 173 if err == io.EOF { 174 fmt.Fprint(os.Stderr, "eof") 175 return err 176 } 177 starlib.FprintErr(os.Stderr, err) 178 continue 179 } 180 181 res, err := stream.Recv() 182 if err != nil { 183 if err == io.EOF { 184 return err 185 } 186 starlib.FprintErr(os.Stderr, err) 187 return err 188 } 189 if output := res.GetOutput(); output != nil { 190 if output.Output != "" { 191 fmt.Println(output.Output) 192 } 193 if output.Status != nil { 194 starlib.FprintStatus(os.Stderr, output.Status) 195 } 196 } 197 } 198 return ctx.Err() 199 } 200 201 func printer() func(*starlark.Thread, string) { 202 return func(_ *starlark.Thread, msg string) { 203 os.Stdout.WriteString(msg + "\n") 204 } 205 } 206 207 func local(ctx context.Context, line *liner.State, autocomplete bool) (err error) { 208 globals := starlib.NewGlobals() 209 loader := starlib.NewLoader(globals) 210 defer loader.Close() 211 212 thread := &starlark.Thread{ 213 Name: "<stdin>", 214 Load: loader.Load, 215 Print: printer(), 216 } 217 starlarkthread.SetContext(thread, ctx) 218 close := starlarkthread.WithResourceStore(thread) 219 defer func() { 220 if cerr := close(); err == nil { 221 err = cerr 222 } 223 }() 224 225 if autocomplete { 226 c := starlib.Completer{StringDict: globals} 227 line.SetCompleter(c.Complete) 228 } 229 230 soleExpr := func(f *syntax.File) syntax.Expr { 231 if len(f.Stmts) == 1 { 232 if stmt, ok := f.Stmts[0].(*syntax.ExprStmt); ok { 233 return stmt.X 234 } 235 } 236 return nil 237 } 238 239 var buf bytes.Buffer 240 for ctx.Err() == nil { 241 f, err := read(line, &buf) 242 if err != nil { 243 if err == io.EOF { 244 return err 245 } 246 continue 247 } 248 249 if expr := soleExpr(f); expr != nil { 250 // eval 251 v, err := starlark.EvalExpr(thread, expr, globals) 252 if err != nil { 253 starlib.FprintErr(os.Stderr, err) 254 continue 255 } 256 257 // print 258 if v != starlark.None { 259 fmt.Println(v) 260 } 261 } else if err := starlark.ExecREPLChunk(f, thread, globals); err != nil { 262 starlib.FprintErr(os.Stderr, err) 263 continue 264 } 265 } 266 return ctx.Err() 267 } 268 269 func loop(ctx context.Context, opts *Options) (err error) { 270 line := liner.NewLiner() 271 defer line.Close() 272 273 if opts.HistoryFile != "" { 274 if f, err := os.Open(opts.HistoryFile); err == nil { 275 if err != nil { 276 return nil 277 } 278 if _, err := line.ReadHistory(f); err != nil { 279 f.Close() //nolint 280 return err 281 } 282 if err := f.Close(); err != nil { 283 return err 284 } 285 } 286 } 287 288 if client := opts.Remote; client != nil { 289 err = remote(ctx, line, client, opts.AutoComplete) 290 } else { 291 err = local(ctx, line, opts.AutoComplete) 292 } 293 if opts.HistoryFile != "" { 294 f, err := os.Create(opts.HistoryFile) 295 if err != nil { 296 return err 297 } 298 if _, err := line.WriteHistory(f); err != nil { 299 f.Close() //nolint 300 return err 301 } 302 if err := f.Close(); err != nil { 303 return err 304 } 305 } 306 return 307 } 308 309 func loadTransportCredentials(ctx context.Context) (credentials.TransportCredentials, error) { 310 if *flagInsecure { 311 return insecure.NewCredentials(), nil 312 } 313 pool, err := x509.SystemCertPool() 314 if err != nil { 315 return nil, err 316 } 317 return credentials.NewClientTLSFromCert(pool, ""), nil 318 } 319 320 func createRemoteConn(ctx context.Context, addr string, ctrlClient *control.Client) (*grpc.ClientConn, error) { 321 var opts []grpc.DialOption 322 if !*flagInsecure { 323 if u := *flagCreds; u != "" { 324 perRPC, err := control.OpenRPCCredentials(ctx, *flagCreds) 325 if err != nil { 326 return nil, err 327 } 328 opts = append(opts, grpc.WithPerRPCCredentials(perRPC)) 329 330 } else { 331 perRPC, err := ctrlClient.OpenRPCCredentials(ctx) 332 if err != nil { 333 return nil, err 334 } 335 opts = append(opts, grpc.WithPerRPCCredentials(perRPC)) 336 } 337 } 338 339 creds, err := loadTransportCredentials(ctx) 340 if err != nil { 341 return nil, err 342 } 343 opts = append(opts, grpc.WithTransportCredentials(creds)) 344 345 cc, err := grpc.DialContext(ctx, addr, opts...) 346 if err != nil { 347 return nil, err 348 } 349 return cc, nil 350 } 351 352 func run(ctx context.Context, opts *Options) (err error) { 353 if err := loop(ctx, opts); err != io.EOF { 354 return err 355 } 356 os.Stdout.WriteString("\n") // break EOF 357 return err 358 } 359 360 func exec(ctx context.Context, opts *Options) (err error) { 361 src := opts.Source 362 if client := opts.Remote; client != nil { 363 stream, err := client.RunOnThread(ctx) 364 if err != nil { 365 return err 366 } 367 368 cmd := &workerpb.Command{ 369 Name: "default", // TODO: name? 370 Exec: &workerpb.Command_Input{ 371 Input: src, 372 }, 373 } 374 if err := stream.Send(cmd); err != nil { 375 return err 376 } 377 378 res, err := stream.Recv() 379 if err != nil { 380 return err 381 } 382 if output := res.GetOutput(); output != nil { 383 if output.Output != "" { 384 fmt.Println(output.Output) 385 } 386 if output.Status != nil { 387 starlib.FprintStatus(os.Stderr, output.Status) 388 } 389 } 390 return nil 391 } 392 393 globals := starlib.NewGlobals() 394 loader := starlib.NewLoader(globals) 395 defer loader.Close() 396 397 thread := &starlark.Thread{ 398 Name: opts.Filename, 399 Load: loader.Load, 400 Print: printer(), 401 } 402 starlarkthread.SetContext(thread, ctx) 403 close := starlarkthread.WithResourceStore(thread) 404 defer func() { 405 cerr := close() 406 if err == nil { 407 err = cerr 408 } 409 }() 410 411 module, err := starlark.ExecFile(thread, opts.Filename, src, globals) 412 if err != nil { 413 return err 414 } 415 mainFn, ok := module["main"] 416 if !ok { 417 return nil 418 } 419 if _, err := starlark.Call(thread, mainFn, nil, nil); err != nil { 420 return err 421 } 422 return nil 423 } 424 425 func start(ctx context.Context, filename, src string) error { 426 var dir string 427 if name := *flagCacheDir; name != "" { 428 if f, err := os.Stat(name); err != nil { 429 return fmt.Errorf("error: invalid cache dir: %w", err) 430 } else if !f.IsDir() { 431 return fmt.Errorf("error: invalid cache dir: %s", name) 432 } 433 dir = name 434 } 435 436 if dir == "" { 437 dirname, err := os.UserHomeDir() 438 if err != nil { 439 return err 440 } 441 dir = filepath.Join(dirname, ".cache", "larking") 442 if err := os.MkdirAll(dir, os.ModePerm); err != nil { 443 return err 444 } 445 } 446 447 ctrl, err := control.NewClient(*flagControlAddr, dir) 448 if err != nil { 449 return err 450 } 451 452 var client workerpb.WorkerClient 453 if remoteAddr := *flagRemoteAddr; remoteAddr != "" { 454 cc, err := createRemoteConn(ctx, remoteAddr, ctrl) 455 if err != nil { 456 return err 457 } 458 defer cc.Close() 459 460 log.Printf("remote: %s, status: %s", cc.Target(), cc.GetState()) 461 462 client = workerpb.NewWorkerClient(cc) 463 } 464 465 var historyFile string 466 if dir != "" { 467 historyFile = filepath.Join(dir, "history.txt") 468 } 469 autocomplete := *flagAutocomplete 470 471 opts := &Options{ 472 CacheDir: dir, 473 HistoryFile: historyFile, 474 AutoComplete: autocomplete, 475 Remote: client, 476 Filename: filename, 477 Source: src, 478 } 479 480 if opts.Source != "" { // TODO: flag better? 481 return exec(ctx, opts) 482 } 483 return run(ctx, opts) 484 } 485 486 func test(ctx context.Context, pattern string) error { 487 if _, err := path.Match(pattern, ""); err != nil { 488 return err // invalid pattern 489 } 490 491 globals := starlib.NewGlobals() 492 loader := starlib.NewLoader(globals) 493 defer loader.Close() 494 495 runner := func(t testing.TB, thread *starlark.Thread) func() { 496 thread.Load = loader.Load 497 498 starlarkthread.SetContext(thread, ctx) 499 500 close := starlarkthread.WithResourceStore(thread) 501 return func() { 502 if err := close(); err != nil { 503 t.Error(err) 504 } 505 } 506 } 507 508 bkt, err := blob.OpenBucket(ctx, *flagDir) 509 if err != nil { 510 return err 511 } 512 defer bkt.Close() 513 514 // Limit choice by prefix, path.Match the rest. 515 opts := &blob.ListOptions{ 516 Prefix: pattern, 517 } 518 if i := strings.IndexAny(pattern, "*?[\\"); i >= 0 { 519 opts.Prefix = pattern[:i] 520 } 521 522 var tests []testing.InternalTest 523 iter := bkt.List(opts) 524 for { 525 obj, err := iter.Next(ctx) 526 if err == io.EOF { 527 break 528 } 529 if err != nil { 530 return err 531 } 532 533 if ok, _ := path.Match(pattern, obj.Key); !ok { 534 continue 535 } 536 537 src, err := bkt.ReadAll(ctx, obj.Key) 538 if err != nil { 539 return err 540 } 541 542 tests = append(tests, testing.InternalTest{ 543 Name: obj.Key, 544 F: func(t *testing.T) { 545 starlarkassert.TestFile( 546 t, obj.Key, src, globals, runner) 547 }, 548 }) 549 } 550 551 var ( 552 matchPat string 553 matchRe *regexp.Regexp 554 ) 555 deps := starlarkassert.MatchStringOnly( 556 func(pat, str string) (result bool, err error) { 557 if matchRe == nil || matchPat != pat { 558 matchPat = pat 559 matchRe, err = regexp.Compile(matchPat) 560 if err != nil { 561 return 562 } 563 } 564 return matchRe.MatchString(str), nil 565 }, 566 ) 567 if testing.MainStart(deps, tests, nil, nil, nil).Run() > 0 { 568 return fmt.Errorf("failed") 569 } 570 571 return nil 572 } 573 574 func format(ctx context.Context, pattern string) error { 575 if _, err := path.Match(pattern, ""); err != nil { 576 return err // invalid pattern 577 } 578 579 bkt, err := blob.OpenBucket(ctx, *flagDir) 580 if err != nil { 581 return err 582 } 583 defer bkt.Close() 584 585 // Limit choice by prefix, path.Match the rest. 586 opts := &blob.ListOptions{ 587 Prefix: pattern, 588 } 589 if i := strings.IndexAny(pattern, "*?[\\"); i >= 0 { 590 opts.Prefix = pattern[:i] 591 } 592 593 iter := bkt.List(opts) 594 for { 595 obj, err := iter.Next(ctx) 596 if err == io.EOF { 597 break 598 } 599 if err != nil { 600 return err 601 } 602 603 if ok, _ := path.Match(pattern, obj.Key); !ok { 604 continue 605 } 606 607 src, err := bkt.ReadAll(ctx, obj.Key) 608 if err != nil { 609 return err 610 } 611 612 dst, err := worker.Format(ctx, obj.Key, src) 613 if err != nil { 614 return err 615 } 616 617 if bytes.Equal(src, dst) { 618 continue 619 } 620 621 log.Println("formatting", obj.Key) 622 if err := bkt.WriteAll(ctx, obj.Key, dst, nil); err != nil { 623 return err 624 } 625 } 626 627 return nil 628 629 } 630 631 func main() { 632 ctx := context.Background() 633 log.SetPrefix("") 634 log.SetFlags(0) 635 flag.Parse() 636 637 var arg0 string 638 if flag.NArg() >= 1 { 639 arg0 = flag.Arg(0) 640 } 641 642 const fileExt = ".star" 643 644 switch { 645 case arg0 == "fmt": 646 pattern := "*" + fileExt 647 if flag.NArg() == 2 { 648 pattern = flag.Arg(1) 649 } 650 if err := format(ctx, pattern); err != nil { 651 if err != io.EOF { 652 starlib.FprintErr(os.Stderr, err) 653 } 654 os.Exit(1) 655 } 656 657 case arg0 == "test": 658 pattern := "*_test" + fileExt 659 if flag.NArg() == 2 { 660 pattern = flag.Arg(1) 661 } 662 if err := test(ctx, pattern); err != nil { 663 if err != io.EOF { 664 starlib.FprintErr(os.Stderr, err) 665 } 666 os.Exit(1) 667 } 668 669 case flag.NArg() == 1 || *flagExecprog != "": 670 var ( 671 filename string 672 src string 673 ) 674 if *flagExecprog != "" { 675 // Execute provided program. 676 filename = "cmdline" 677 src = *flagExecprog 678 } else { 679 // Execute specified file. 680 filename = arg0 681 682 var err error 683 b, err := ioutil.ReadFile(filename) 684 if err != nil { 685 log.Fatal(err) 686 } 687 src = string(b) 688 } 689 if err := start(ctx, filename, src); err != nil { 690 starlib.FprintErr(os.Stderr, err) 691 os.Exit(1) 692 } 693 case flag.NArg() == 0: 694 text := ` _, 695 ( '> Welcome to lark 696 / ) ) (larking.io, %s) 697 /|^^ 698 ` 699 fmt.Printf(text, runtime.Version()) 700 if err := start(ctx, "<stdin>", ""); err != nil { 701 log.Fatal(err) 702 } 703 default: 704 log.Fatal("want at most one Starlark file name") 705 } 706 707 }