github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/cmd/wazero/wazero.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"errors"
     7  	"flag"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"path/filepath"
    12  	"runtime"
    13  	"runtime/pprof"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/tetratelabs/wazero"
    19  	"github.com/tetratelabs/wazero/api"
    20  	"github.com/tetratelabs/wazero/experimental"
    21  	"github.com/tetratelabs/wazero/experimental/logging"
    22  	"github.com/tetratelabs/wazero/experimental/sock"
    23  	"github.com/tetratelabs/wazero/experimental/sysfs"
    24  	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
    25  	internalsys "github.com/tetratelabs/wazero/internal/sys"
    26  	"github.com/tetratelabs/wazero/internal/version"
    27  	"github.com/tetratelabs/wazero/sys"
    28  )
    29  
    30  func main() {
    31  	os.Exit(doMain(os.Stdout, os.Stderr))
    32  }
    33  
    34  // doMain is separated out for the purpose of unit testing.
    35  func doMain(stdOut io.Writer, stdErr logging.Writer) int {
    36  	flag.CommandLine.SetOutput(stdErr)
    37  
    38  	var help bool
    39  	flag.BoolVar(&help, "h", false, "Prints usage.")
    40  
    41  	flag.Parse()
    42  
    43  	if help || flag.NArg() == 0 {
    44  		printUsage(stdErr)
    45  		return 0
    46  	}
    47  
    48  	if flag.NArg() < 1 {
    49  		fmt.Fprintln(stdErr, "missing path to wasm file")
    50  		printUsage(stdErr)
    51  		return 1
    52  	}
    53  
    54  	subCmd := flag.Arg(0)
    55  	switch subCmd {
    56  	case "compile":
    57  		return doCompile(flag.Args()[1:], stdErr)
    58  	case "run":
    59  		return doRun(flag.Args()[1:], stdOut, stdErr)
    60  	case "version":
    61  		fmt.Fprintln(stdOut, version.GetWazeroVersion())
    62  		return 0
    63  	default:
    64  		fmt.Fprintln(stdErr, "invalid command")
    65  		printUsage(stdErr)
    66  		return 1
    67  	}
    68  }
    69  
    70  func doCompile(args []string, stdErr io.Writer) int {
    71  	flags := flag.NewFlagSet("compile", flag.ExitOnError)
    72  	flags.SetOutput(stdErr)
    73  
    74  	var help bool
    75  	flags.BoolVar(&help, "h", false, "Prints usage.")
    76  
    77  	var count int
    78  	var cpuProfile string
    79  	var memProfile string
    80  	if version.GetWazeroVersion() != version.Default {
    81  		count = 1
    82  	} else {
    83  		flags.IntVar(&count, "count", 1,
    84  			"Number of times to perform the compilation. This is useful to benchmark performance of the wazero compiler.")
    85  
    86  		flags.StringVar(&cpuProfile, "cpuprofile", "",
    87  			"Enables cpu profiling and writes the profile at the given path.")
    88  
    89  		flags.StringVar(&memProfile, "memprofile", "",
    90  			"Enables memory profiling and writes the profile at the given path.")
    91  	}
    92  
    93  	cacheDir := cacheDirFlag(flags)
    94  
    95  	_ = flags.Parse(args)
    96  
    97  	if help {
    98  		printCompileUsage(stdErr, flags)
    99  		return 0
   100  	}
   101  
   102  	if flags.NArg() < 1 {
   103  		fmt.Fprintln(stdErr, "missing path to wasm file")
   104  		printCompileUsage(stdErr, flags)
   105  		return 1
   106  	}
   107  
   108  	if memProfile != "" {
   109  		defer writeHeapProfile(stdErr, memProfile)
   110  	}
   111  
   112  	if cpuProfile != "" {
   113  		stopCPUProfile := startCPUProfile(stdErr, cpuProfile)
   114  		defer stopCPUProfile()
   115  	}
   116  
   117  	wasmPath := flags.Arg(0)
   118  
   119  	wasm, err := os.ReadFile(wasmPath)
   120  	if err != nil {
   121  		fmt.Fprintf(stdErr, "error reading wasm binary: %v\n", err)
   122  		return 1
   123  	}
   124  
   125  	c := wazero.NewRuntimeConfig()
   126  	if rc, cache := maybeUseCacheDir(cacheDir, stdErr); rc != 0 {
   127  		return rc
   128  	} else if cache != nil {
   129  		c = c.WithCompilationCache(cache)
   130  	}
   131  
   132  	ctx := context.Background()
   133  	rt := wazero.NewRuntimeWithConfig(ctx, c)
   134  	defer rt.Close(ctx)
   135  
   136  	for count > 0 {
   137  		compiledModule, err := rt.CompileModule(ctx, wasm)
   138  		if err != nil {
   139  			fmt.Fprintf(stdErr, "error compiling wasm binary: %v\n", err)
   140  			return 1
   141  		}
   142  		if err := compiledModule.Close(ctx); err != nil {
   143  			fmt.Fprintf(stdErr, "error releasing compiled module: %v\n", err)
   144  			return 1
   145  		}
   146  		count--
   147  	}
   148  	return 0
   149  }
   150  
   151  func doRun(args []string, stdOut io.Writer, stdErr logging.Writer) int {
   152  	flags := flag.NewFlagSet("run", flag.ExitOnError)
   153  	flags.SetOutput(stdErr)
   154  
   155  	var help bool
   156  	flags.BoolVar(&help, "h", false, "Prints usage.")
   157  
   158  	var useInterpreter bool
   159  	flags.BoolVar(&useInterpreter, "interpreter", false,
   160  		"Interprets WebAssembly modules instead of compiling them into native code.")
   161  
   162  	var envs sliceFlag
   163  	flags.Var(&envs, "env", "key=value pair of environment variable to expose to the binary. "+
   164  		"Can be specified multiple times.")
   165  
   166  	var envInherit bool
   167  	flags.BoolVar(&envInherit, "env-inherit", false,
   168  		"Inherits any environment variables from the calling process. "+
   169  			"Variables specified with the <env> flag are appended to the inherited list.")
   170  
   171  	var mounts sliceFlag
   172  	flags.Var(&mounts, "mount",
   173  		"Filesystem path to expose to the binary in the form of <path>[:<wasm path>][:ro]. "+
   174  			"This may be specified multiple times. When <wasm path> is unset, <path> is used. "+
   175  			"For example, -mount=/:/ or c:\\:/ makes the entire host volume writeable by wasm. "+
   176  			"For read-only mounts, append the suffix ':ro'.")
   177  
   178  	var listens sliceFlag
   179  	flags.Var(&listens, "listen",
   180  		"Open a TCP socket on the specified address of the form <host:port>. "+
   181  			"This may be specified multiple times. Host is optional, and port may be 0 to "+
   182  			"indicate a random port.")
   183  
   184  	var timeout time.Duration
   185  	flags.DurationVar(&timeout, "timeout", 0*time.Second,
   186  		"If a wasm binary runs longer than the given duration string, then exit abruptly. "+
   187  			"The duration string is an unsigned sequence of decimal numbers, "+
   188  			"each with optional fraction and a unit suffix, such as \"300ms\", \"1.5h\" or \"2h45m\". "+
   189  			"Valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\". "+
   190  			"If the duration is 0, the timeout is disabled. The default is disabled.")
   191  
   192  	var hostlogging logScopesFlag
   193  	flags.Var(&hostlogging, "hostlogging",
   194  		"A comma-separated list of host function scopes to log to stderr. "+
   195  			"This may be specified multiple times. Supported values: all,clock,filesystem,memory,proc,poll,random,sock")
   196  
   197  	var cpuProfile string
   198  	var memProfile string
   199  	if version.GetWazeroVersion() == version.Default {
   200  		flags.StringVar(&cpuProfile, "cpuprofile", "",
   201  			"Enables cpu profiling and writes the profile at the given path.")
   202  
   203  		flags.StringVar(&memProfile, "memprofile", "",
   204  			"Enables memory profiling and writes the profile at the given path.")
   205  	}
   206  
   207  	cacheDir := cacheDirFlag(flags)
   208  
   209  	_ = flags.Parse(args)
   210  
   211  	if help {
   212  		printRunUsage(stdErr, flags)
   213  		return 0
   214  	}
   215  
   216  	if flags.NArg() < 1 {
   217  		fmt.Fprintln(stdErr, "missing path to wasm file")
   218  		printRunUsage(stdErr, flags)
   219  		return 1
   220  	}
   221  
   222  	if memProfile != "" {
   223  		defer writeHeapProfile(stdErr, memProfile)
   224  	}
   225  
   226  	if cpuProfile != "" {
   227  		stopCPUProfile := startCPUProfile(stdErr, cpuProfile)
   228  		defer stopCPUProfile()
   229  	}
   230  
   231  	wasmPath := flags.Arg(0)
   232  	wasmArgs := flags.Args()[1:]
   233  	if len(wasmArgs) > 1 {
   234  		// Skip "--" if provided
   235  		if wasmArgs[0] == "--" {
   236  			wasmArgs = wasmArgs[1:]
   237  		}
   238  	}
   239  
   240  	// Don't use map to preserve order
   241  	var env []string
   242  	if envInherit {
   243  		envs = append(os.Environ(), envs...)
   244  	}
   245  	for _, e := range envs {
   246  		fields := strings.SplitN(e, "=", 2)
   247  		if len(fields) != 2 {
   248  			fmt.Fprintf(stdErr, "invalid environment variable: %s\n", e)
   249  			return 1
   250  		}
   251  		env = append(env, fields[0], fields[1])
   252  	}
   253  
   254  	rc, _, fsConfig := validateMounts(mounts, stdErr)
   255  	if rc != 0 {
   256  		return rc
   257  	}
   258  
   259  	wasm, err := os.ReadFile(wasmPath)
   260  	if err != nil {
   261  		fmt.Fprintf(stdErr, "error reading wasm binary: %v\n", err)
   262  		return 1
   263  	}
   264  
   265  	wasmExe := filepath.Base(wasmPath)
   266  
   267  	var rtc wazero.RuntimeConfig
   268  	if useInterpreter {
   269  		rtc = wazero.NewRuntimeConfigInterpreter()
   270  	} else {
   271  		rtc = wazero.NewRuntimeConfig()
   272  	}
   273  
   274  	ctx := maybeHostLogging(context.Background(), logging.LogScopes(hostlogging), stdErr)
   275  
   276  	if rc, cache := maybeUseCacheDir(cacheDir, stdErr); rc != 0 {
   277  		return rc
   278  	} else if cache != nil {
   279  		rtc = rtc.WithCompilationCache(cache)
   280  	}
   281  
   282  	if timeout > 0 {
   283  		newCtx, cancel := context.WithTimeout(ctx, timeout)
   284  		ctx = newCtx
   285  		defer cancel()
   286  		rtc = rtc.WithCloseOnContextDone(true)
   287  	} else if timeout < 0 {
   288  		fmt.Fprintf(stdErr, "timeout duration may not be negative, %v given\n", timeout)
   289  		printRunUsage(stdErr, flags)
   290  		return 1
   291  	}
   292  
   293  	if rc, sockCfg := validateListens(listens, stdErr); rc != 0 {
   294  		return rc
   295  	} else {
   296  		ctx = sock.WithConfig(ctx, sockCfg)
   297  	}
   298  
   299  	rt := wazero.NewRuntimeWithConfig(ctx, rtc)
   300  	defer rt.Close(ctx)
   301  
   302  	// Because we are running a binary directly rather than embedding in an application,
   303  	// we default to wiring up commonly used OS functionality.
   304  	conf := wazero.NewModuleConfig().
   305  		WithStdout(stdOut).
   306  		WithStderr(stdErr).
   307  		WithStdin(os.Stdin).
   308  		WithRandSource(rand.Reader).
   309  		WithFSConfig(fsConfig).
   310  		WithSysNanosleep().
   311  		WithSysNanotime().
   312  		WithSysWalltime().
   313  		WithArgs(append([]string{wasmExe}, wasmArgs...)...)
   314  	for i := 0; i < len(env); i += 2 {
   315  		conf = conf.WithEnv(env[i], env[i+1])
   316  	}
   317  
   318  	guest, err := rt.CompileModule(ctx, wasm)
   319  	if err != nil {
   320  		fmt.Fprintf(stdErr, "error compiling wasm binary: %v\n", err)
   321  		return 1
   322  	}
   323  
   324  	switch detectImports(guest.ImportedFunctions()) {
   325  	case modeWasi:
   326  		wasi_snapshot_preview1.MustInstantiate(ctx, rt)
   327  		_, err = rt.InstantiateModule(ctx, guest, conf)
   328  	case modeWasiUnstable:
   329  		// Instantiate the current WASI functions under the wasi_unstable
   330  		// instead of wasi_snapshot_preview1.
   331  		wasiBuilder := rt.NewHostModuleBuilder("wasi_unstable")
   332  		wasi_snapshot_preview1.NewFunctionExporter().ExportFunctions(wasiBuilder)
   333  		_, err = wasiBuilder.Instantiate(ctx)
   334  		if err == nil {
   335  			// Instantiate our binary, but using the old import names.
   336  			_, err = rt.InstantiateModule(ctx, guest, conf)
   337  		}
   338  	case modeDefault:
   339  		_, err = rt.InstantiateModule(ctx, guest, conf)
   340  	}
   341  
   342  	if err != nil {
   343  		if exitErr, ok := err.(*sys.ExitError); ok {
   344  			exitCode := exitErr.ExitCode()
   345  			if exitCode == sys.ExitCodeDeadlineExceeded {
   346  				fmt.Fprintf(stdErr, "error: %v (timeout %v)\n", exitErr, timeout)
   347  			}
   348  			return int(exitCode)
   349  		}
   350  		fmt.Fprintf(stdErr, "error instantiating wasm binary: %v\n", err)
   351  		return 1
   352  	}
   353  
   354  	// We're done, _start was called as part of instantiating the module.
   355  	return 0
   356  }
   357  
   358  func validateMounts(mounts sliceFlag, stdErr logging.Writer) (rc int, rootPath string, config wazero.FSConfig) {
   359  	config = wazero.NewFSConfig()
   360  	for _, mount := range mounts {
   361  		if len(mount) == 0 {
   362  			fmt.Fprintln(stdErr, "invalid mount: empty string")
   363  			return 1, rootPath, config
   364  		}
   365  
   366  		readOnly := false
   367  		if trimmed := strings.TrimSuffix(mount, ":ro"); trimmed != mount {
   368  			mount = trimmed
   369  			readOnly = true
   370  		}
   371  
   372  		// TODO: Support wasm paths with colon in them.
   373  		var dir, guestPath string
   374  		if clnIdx := strings.LastIndexByte(mount, ':'); clnIdx != -1 {
   375  			dir, guestPath = mount[:clnIdx], mount[clnIdx+1:]
   376  		} else {
   377  			dir = mount
   378  			guestPath = dir
   379  		}
   380  
   381  		// Eagerly validate the mounts as we know they should be on the host.
   382  		if abs, err := filepath.Abs(dir); err != nil {
   383  			fmt.Fprintf(stdErr, "invalid mount: path %q invalid: %v\n", dir, err)
   384  			return 1, rootPath, config
   385  		} else {
   386  			dir = abs
   387  		}
   388  
   389  		if stat, err := os.Stat(dir); err != nil {
   390  			fmt.Fprintf(stdErr, "invalid mount: path %q error: %v\n", dir, err)
   391  			return 1, rootPath, config
   392  		} else if !stat.IsDir() {
   393  			fmt.Fprintf(stdErr, "invalid mount: path %q is not a directory\n", dir)
   394  		}
   395  
   396  		root := sysfs.DirFS(dir)
   397  		if readOnly {
   398  			root = &sysfs.ReadFS{FS: root}
   399  		}
   400  
   401  		config = config.(sysfs.FSConfig).WithSysFSMount(root, guestPath)
   402  
   403  		if internalsys.StripPrefixesAndTrailingSlash(guestPath) == "" {
   404  			rootPath = dir
   405  		}
   406  	}
   407  	return 0, rootPath, config
   408  }
   409  
   410  // validateListens returns a non-nil net.Config, if there were any listen flags.
   411  func validateListens(listens sliceFlag, stdErr logging.Writer) (rc int, config sock.Config) {
   412  	for _, listen := range listens {
   413  		idx := strings.LastIndexByte(listen, ':')
   414  		if idx < 0 {
   415  			fmt.Fprintln(stdErr, "invalid listen")
   416  			return rc, config
   417  		}
   418  		port, err := strconv.Atoi(listen[idx+1:])
   419  		if err != nil {
   420  			fmt.Fprintln(stdErr, "invalid listen port:", err)
   421  			return rc, config
   422  		}
   423  		if config == nil {
   424  			config = sock.NewConfig()
   425  		}
   426  		config = config.WithTCPListener(listen[:idx], port)
   427  	}
   428  	return
   429  }
   430  
   431  const (
   432  	modeDefault importMode = iota
   433  	modeWasi
   434  	modeWasiUnstable
   435  )
   436  
   437  type importMode uint
   438  
   439  func detectImports(imports []api.FunctionDefinition) importMode {
   440  	for _, f := range imports {
   441  		moduleName, _, _ := f.Import()
   442  		switch moduleName {
   443  		case wasi_snapshot_preview1.ModuleName:
   444  			return modeWasi
   445  		case "wasi_unstable":
   446  			return modeWasiUnstable
   447  		}
   448  	}
   449  	return modeDefault
   450  }
   451  
   452  func maybeHostLogging(ctx context.Context, scopes logging.LogScopes, stdErr logging.Writer) context.Context {
   453  	if scopes != 0 {
   454  		return experimental.WithFunctionListenerFactory(ctx, logging.NewHostLoggingListenerFactory(stdErr, scopes))
   455  	}
   456  	return ctx
   457  }
   458  
   459  func cacheDirFlag(flags *flag.FlagSet) *string {
   460  	return flags.String("cachedir", "", "Writeable directory for native code compiled from wasm. "+
   461  		"Contents are re-used for the same version of wazero.")
   462  }
   463  
   464  func maybeUseCacheDir(cacheDir *string, stdErr io.Writer) (int, wazero.CompilationCache) {
   465  	if dir := *cacheDir; dir != "" {
   466  		if cache, err := wazero.NewCompilationCacheWithDir(dir); err != nil {
   467  			fmt.Fprintf(stdErr, "invalid cachedir: %v\n", err)
   468  			return 1, cache
   469  		} else {
   470  			return 0, cache
   471  		}
   472  	}
   473  	return 0, nil
   474  }
   475  
   476  func printUsage(stdErr io.Writer) {
   477  	fmt.Fprintln(stdErr, "wazero CLI")
   478  	fmt.Fprintln(stdErr)
   479  	fmt.Fprintln(stdErr, "Usage:\n  wazero <command>")
   480  	fmt.Fprintln(stdErr)
   481  	fmt.Fprintln(stdErr, "Commands:")
   482  	fmt.Fprintln(stdErr, "  compile\tPre-compiles a WebAssembly binary")
   483  	fmt.Fprintln(stdErr, "  run\t\tRuns a WebAssembly binary")
   484  	fmt.Fprintln(stdErr, "  version\tDisplays the version of wazero CLI")
   485  }
   486  
   487  func printCompileUsage(stdErr io.Writer, flags *flag.FlagSet) {
   488  	fmt.Fprintln(stdErr, "wazero CLI")
   489  	fmt.Fprintln(stdErr)
   490  	fmt.Fprintln(stdErr, "Usage:\n  wazero compile <options> <path to wasm file>")
   491  	fmt.Fprintln(stdErr)
   492  	fmt.Fprintln(stdErr, "Options:")
   493  	flags.PrintDefaults()
   494  }
   495  
   496  func printRunUsage(stdErr io.Writer, flags *flag.FlagSet) {
   497  	fmt.Fprintln(stdErr, "wazero CLI")
   498  	fmt.Fprintln(stdErr)
   499  	fmt.Fprintln(stdErr, "Usage:\n  wazero run <options> <path to wasm file> [--] <wasm args>")
   500  	fmt.Fprintln(stdErr)
   501  	fmt.Fprintln(stdErr, "Options:")
   502  	flags.PrintDefaults()
   503  }
   504  
   505  func startCPUProfile(stdErr io.Writer, path string) (stopCPUProfile func()) {
   506  	f, err := os.Create(path)
   507  	if err != nil {
   508  		fmt.Fprintf(stdErr, "error creating cpu profile output: %v\n", err)
   509  		return func() {}
   510  	}
   511  
   512  	if err := pprof.StartCPUProfile(f); err != nil {
   513  		f.Close()
   514  		fmt.Fprintf(stdErr, "error starting cpu profile: %v\n", err)
   515  		return func() {}
   516  	}
   517  
   518  	return func() {
   519  		defer f.Close()
   520  		pprof.StopCPUProfile()
   521  	}
   522  }
   523  
   524  func writeHeapProfile(stdErr io.Writer, path string) {
   525  	f, err := os.Create(path)
   526  	if err != nil {
   527  		fmt.Fprintf(stdErr, "error creating memory profile output: %v\n", err)
   528  		return
   529  	}
   530  	defer f.Close()
   531  	runtime.GC()
   532  	if err := pprof.WriteHeapProfile(f); err != nil {
   533  		fmt.Fprintf(stdErr, "error writing memory profile: %v\n", err)
   534  	}
   535  }
   536  
   537  type sliceFlag []string
   538  
   539  func (f *sliceFlag) String() string {
   540  	return strings.Join(*f, ",")
   541  }
   542  
   543  func (f *sliceFlag) Set(s string) error {
   544  	*f = append(*f, s)
   545  	return nil
   546  }
   547  
   548  type logScopesFlag logging.LogScopes
   549  
   550  func (f *logScopesFlag) String() string {
   551  	return logging.LogScopes(*f).String()
   552  }
   553  
   554  func (f *logScopesFlag) Set(input string) error {
   555  	for _, s := range strings.Split(input, ",") {
   556  		switch s {
   557  		case "":
   558  			continue
   559  		case "all":
   560  			*f |= logScopesFlag(logging.LogScopeAll)
   561  		case "clock":
   562  			*f |= logScopesFlag(logging.LogScopeClock)
   563  		case "filesystem":
   564  			*f |= logScopesFlag(logging.LogScopeFilesystem)
   565  		case "memory":
   566  			*f |= logScopesFlag(logging.LogScopeMemory)
   567  		case "proc":
   568  			*f |= logScopesFlag(logging.LogScopeProc)
   569  		case "poll":
   570  			*f |= logScopesFlag(logging.LogScopePoll)
   571  		case "random":
   572  			*f |= logScopesFlag(logging.LogScopeRandom)
   573  		case "sock":
   574  			*f |= logScopesFlag(logging.LogScopeSock)
   575  		default:
   576  			return errors.New("not a log scope")
   577  		}
   578  	}
   579  	return nil
   580  }