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  }