github.com/tetratelabs/wazero@v1.7.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/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 }