github.com/tetratelabs/wazero@v1.2.1/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/gojs" 22 "github.com/tetratelabs/wazero/experimental/logging" 23 "github.com/tetratelabs/wazero/experimental/sock" 24 "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" 25 "github.com/tetratelabs/wazero/internal/platform" 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, rootPath, 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 code, 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(code.ImportedFunctions()) { 325 case modeWasi: 326 wasi_snapshot_preview1.MustInstantiate(ctx, rt) 327 _, err = rt.InstantiateModule(ctx, code, 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, code, conf) 337 } 338 case modeGo: 339 gojs.MustInstantiate(ctx, rt) 340 341 config := gojs.NewConfig(conf).WithOSUser() 342 343 // Strip the volume of the path, for example C:\ 344 rootDir := rootPath[len(filepath.VolumeName(rootPath)):] 345 346 // If the user mounted the entire filesystem, try to inherit the CWD. 347 // This is better than introducing a flag just for GOOS=js, especially 348 // as removing flags breaks syntax compat. 349 if platform.ToPosixPath(rootDir) == "/" { 350 config = config.WithOSWorkdir() 351 } 352 353 err = gojs.Run(ctx, rt, code, config) 354 case modeDefault: 355 _, err = rt.InstantiateModule(ctx, code, conf) 356 } 357 358 if err != nil { 359 if exitErr, ok := err.(*sys.ExitError); ok { 360 exitCode := exitErr.ExitCode() 361 if exitCode == sys.ExitCodeDeadlineExceeded { 362 fmt.Fprintf(stdErr, "error: %v (timeout %v)\n", exitErr, timeout) 363 } 364 return int(exitCode) 365 } 366 fmt.Fprintf(stdErr, "error instantiating wasm binary: %v\n", err) 367 return 1 368 } 369 370 // We're done, _start was called as part of instantiating the module. 371 return 0 372 } 373 374 func validateMounts(mounts sliceFlag, stdErr logging.Writer) (rc int, rootPath string, config wazero.FSConfig) { 375 config = wazero.NewFSConfig() 376 for _, mount := range mounts { 377 if len(mount) == 0 { 378 fmt.Fprintln(stdErr, "invalid mount: empty string") 379 return 1, rootPath, config 380 } 381 382 readOnly := false 383 if trimmed := strings.TrimSuffix(mount, ":ro"); trimmed != mount { 384 mount = trimmed 385 readOnly = true 386 } 387 388 // TODO(anuraaga): Support wasm paths with colon in them. 389 var dir, guestPath string 390 if clnIdx := strings.LastIndexByte(mount, ':'); clnIdx != -1 { 391 dir, guestPath = mount[:clnIdx], mount[clnIdx+1:] 392 } else { 393 dir = mount 394 guestPath = dir 395 } 396 397 // Provide a better experience if duplicates are found later. 398 if guestPath == "" { 399 guestPath = "/" 400 } 401 402 // Eagerly validate the mounts as we know they should be on the host. 403 if abs, err := filepath.Abs(dir); err != nil { 404 fmt.Fprintf(stdErr, "invalid mount: path %q invalid: %v\n", dir, err) 405 return 1, rootPath, config 406 } else { 407 dir = abs 408 } 409 410 if stat, err := os.Stat(dir); err != nil { 411 fmt.Fprintf(stdErr, "invalid mount: path %q error: %v\n", dir, err) 412 return 1, rootPath, config 413 } else if !stat.IsDir() { 414 fmt.Fprintf(stdErr, "invalid mount: path %q is not a directory\n", dir) 415 } 416 417 if readOnly { 418 config = config.WithReadOnlyDirMount(dir, guestPath) 419 } else { 420 config = config.WithDirMount(dir, guestPath) 421 } 422 423 if guestPath == "/" { 424 rootPath = dir 425 } 426 } 427 return 0, rootPath, config 428 } 429 430 // validateListens returns a non-nil net.Config, if there were any listen flags. 431 func validateListens(listens sliceFlag, stdErr logging.Writer) (rc int, config sock.Config) { 432 for _, listen := range listens { 433 idx := strings.LastIndexByte(listen, ':') 434 if idx < 0 { 435 fmt.Fprintln(stdErr, "invalid listen") 436 return rc, config 437 } 438 port, err := strconv.Atoi(listen[idx+1:]) 439 if err != nil { 440 fmt.Fprintln(stdErr, "invalid listen port:", err) 441 return rc, config 442 } 443 if config == nil { 444 config = sock.NewConfig() 445 } 446 config = config.WithTCPListener(listen[:idx], port) 447 } 448 return 449 } 450 451 const ( 452 modeDefault importMode = iota 453 modeWasi 454 modeWasiUnstable 455 modeGo 456 ) 457 458 type importMode uint 459 460 func detectImports(imports []api.FunctionDefinition) importMode { 461 for _, f := range imports { 462 moduleName, _, _ := f.Import() 463 switch moduleName { 464 case wasi_snapshot_preview1.ModuleName: 465 return modeWasi 466 case "wasi_unstable": 467 return modeWasiUnstable 468 case "go": 469 return modeGo 470 } 471 } 472 return modeDefault 473 } 474 475 func maybeHostLogging(ctx context.Context, scopes logging.LogScopes, stdErr logging.Writer) context.Context { 476 if scopes != 0 { 477 return context.WithValue(ctx, experimental.FunctionListenerFactoryKey{}, logging.NewHostLoggingListenerFactory(stdErr, scopes)) 478 } 479 return ctx 480 } 481 482 func cacheDirFlag(flags *flag.FlagSet) *string { 483 return flags.String("cachedir", "", "Writeable directory for native code compiled from wasm. "+ 484 "Contents are re-used for the same version of wazero.") 485 } 486 487 func maybeUseCacheDir(cacheDir *string, stdErr io.Writer) (int, wazero.CompilationCache) { 488 if dir := *cacheDir; dir != "" { 489 if cache, err := wazero.NewCompilationCacheWithDir(dir); err != nil { 490 fmt.Fprintf(stdErr, "invalid cachedir: %v\n", err) 491 return 1, cache 492 } else { 493 return 0, cache 494 } 495 } 496 return 0, nil 497 } 498 499 func printUsage(stdErr io.Writer) { 500 fmt.Fprintln(stdErr, "wazero CLI") 501 fmt.Fprintln(stdErr) 502 fmt.Fprintln(stdErr, "Usage:\n wazero <command>") 503 fmt.Fprintln(stdErr) 504 fmt.Fprintln(stdErr, "Commands:") 505 fmt.Fprintln(stdErr, " compile\tPre-compiles a WebAssembly binary") 506 fmt.Fprintln(stdErr, " run\t\tRuns a WebAssembly binary") 507 fmt.Fprintln(stdErr, " version\tDisplays the version of wazero CLI") 508 } 509 510 func printCompileUsage(stdErr io.Writer, flags *flag.FlagSet) { 511 fmt.Fprintln(stdErr, "wazero CLI") 512 fmt.Fprintln(stdErr) 513 fmt.Fprintln(stdErr, "Usage:\n wazero compile <options> <path to wasm file>") 514 fmt.Fprintln(stdErr) 515 fmt.Fprintln(stdErr, "Options:") 516 flags.PrintDefaults() 517 } 518 519 func printRunUsage(stdErr io.Writer, flags *flag.FlagSet) { 520 fmt.Fprintln(stdErr, "wazero CLI") 521 fmt.Fprintln(stdErr) 522 fmt.Fprintln(stdErr, "Usage:\n wazero run <options> <path to wasm file> [--] <wasm args>") 523 fmt.Fprintln(stdErr) 524 fmt.Fprintln(stdErr, "Options:") 525 flags.PrintDefaults() 526 } 527 528 func startCPUProfile(stdErr io.Writer, path string) (stopCPUProfile func()) { 529 f, err := os.Create(path) 530 if err != nil { 531 fmt.Fprintf(stdErr, "error creating cpu profile output: %v\n", err) 532 return func() {} 533 } 534 535 if err := pprof.StartCPUProfile(f); err != nil { 536 f.Close() 537 fmt.Fprintf(stdErr, "error starting cpu profile: %v\n", err) 538 return func() {} 539 } 540 541 return func() { 542 defer f.Close() 543 pprof.StopCPUProfile() 544 } 545 } 546 547 func writeHeapProfile(stdErr io.Writer, path string) { 548 f, err := os.Create(path) 549 if err != nil { 550 fmt.Fprintf(stdErr, "error creating memory profile output: %v\n", err) 551 return 552 } 553 defer f.Close() 554 runtime.GC() 555 if err := pprof.WriteHeapProfile(f); err != nil { 556 fmt.Fprintf(stdErr, "error writing memory profile: %v\n", err) 557 } 558 } 559 560 type sliceFlag []string 561 562 func (f *sliceFlag) String() string { 563 return strings.Join(*f, ",") 564 } 565 566 func (f *sliceFlag) Set(s string) error { 567 *f = append(*f, s) 568 return nil 569 } 570 571 type logScopesFlag logging.LogScopes 572 573 func (f *logScopesFlag) String() string { 574 return logging.LogScopes(*f).String() 575 } 576 577 func (f *logScopesFlag) Set(input string) error { 578 for _, s := range strings.Split(input, ",") { 579 switch s { 580 case "": 581 continue 582 case "all": 583 *f |= logScopesFlag(logging.LogScopeAll) 584 case "clock": 585 *f |= logScopesFlag(logging.LogScopeClock) 586 case "filesystem": 587 *f |= logScopesFlag(logging.LogScopeFilesystem) 588 case "memory": 589 *f |= logScopesFlag(logging.LogScopeMemory) 590 case "proc": 591 *f |= logScopesFlag(logging.LogScopeProc) 592 case "poll": 593 *f |= logScopesFlag(logging.LogScopePoll) 594 case "random": 595 *f |= logScopesFlag(logging.LogScopeRandom) 596 case "sock": 597 *f |= logScopesFlag(logging.LogScopeSock) 598 default: 599 return errors.New("not a log scope") 600 } 601 } 602 return nil 603 }