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