gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/runsc/cli/main.go (about) 1 // Copyright 2018 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package cli is the main entrypoint for runsc. 16 package cli 17 18 import ( 19 "context" 20 "fmt" 21 "io" 22 "io/ioutil" 23 "os" 24 "os/signal" 25 "runtime" 26 "strconv" 27 "strings" 28 "time" 29 30 "github.com/google/subcommands" 31 "golang.org/x/sys/unix" 32 "gvisor.dev/gvisor/pkg/coverage" 33 "gvisor.dev/gvisor/pkg/log" 34 "gvisor.dev/gvisor/pkg/refs" 35 "gvisor.dev/gvisor/pkg/sentry/platform" 36 "gvisor.dev/gvisor/pkg/sentry/syscalls/linux" 37 "gvisor.dev/gvisor/runsc/cmd" 38 "gvisor.dev/gvisor/runsc/cmd/nvproxy" 39 "gvisor.dev/gvisor/runsc/cmd/trace" 40 "gvisor.dev/gvisor/runsc/cmd/util" 41 "gvisor.dev/gvisor/runsc/config" 42 "gvisor.dev/gvisor/runsc/flag" 43 "gvisor.dev/gvisor/runsc/specutils" 44 "gvisor.dev/gvisor/runsc/version" 45 ) 46 47 // versionFlagName is the name of a flag that triggers printing the version. 48 // Although this flags is not part of the OCI spec, it is used by 49 // Docker, and thus should not be removed. 50 const versionFlagName = "version" 51 52 var ( 53 // These flags are unique to runsc, and are used to configure parts of the 54 // system that are not covered by the runtime spec. 55 56 // Debugging flags. 57 logFD = flag.Int("log-fd", -1, "file descriptor to log to. If set, the 'log' flag is ignored.") 58 debugLogFD = flag.Int("debug-log-fd", -1, "file descriptor to write debug logs to. If set, the 'debug-log-dir' flag is ignored.") 59 panicLogFD = flag.Int("panic-log-fd", -1, "file descriptor to write Go's runtime messages.") 60 coverageFD = flag.Int("coverage-fd", -1, "file descriptor to write Go coverage output.") 61 ) 62 63 // Main is the main entrypoint. 64 func Main() { 65 // Register all commands. 66 forEachCmd(subcommands.Register) 67 68 // Register with the main command line. 69 config.RegisterFlags(flag.CommandLine) 70 71 // Register version flag if it is not already defined. 72 if flag.Lookup(versionFlagName) == nil { 73 flag.Bool(versionFlagName, false, "show version and exit.") 74 } 75 76 // All subcommands must be registered before flag parsing. 77 flag.Parse() 78 79 // Are we showing the version? 80 if flag.Get(flag.Lookup(versionFlagName).Value).(bool) { 81 // The format here is the same as runc. 82 fmt.Fprintf(os.Stdout, "runsc version %s\n", version.Version()) 83 fmt.Fprintf(os.Stdout, "spec: %s\n", specutils.Version) 84 os.Exit(0) 85 } 86 87 // Create a new Config from the flags. 88 conf, err := config.NewFromFlags(flag.CommandLine) 89 if err != nil { 90 util.Fatalf(err.Error()) 91 } 92 93 var errorLogger io.Writer 94 if *logFD > -1 { 95 errorLogger = os.NewFile(uintptr(*logFD), "error log file") 96 97 } else if conf.LogFilename != "" { 98 // We must set O_APPEND and not O_TRUNC because Docker passes 99 // the same log file for all commands (and also parses these 100 // log files), so we can't destroy them on each command. 101 var err error 102 errorLogger, err = os.OpenFile(conf.LogFilename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 103 if err != nil { 104 util.Fatalf("error opening log file %q: %v", conf.LogFilename, err) 105 } 106 } 107 util.ErrorLogger = errorLogger 108 109 if _, err := platform.Lookup(conf.Platform); err != nil { 110 util.Fatalf("%v", err) 111 } 112 113 // Sets the reference leak check mode. Also set it in config below to 114 // propagate it to child processes. 115 refs.SetLeakMode(conf.ReferenceLeak) 116 117 subcommand := flag.CommandLine.Arg(0) 118 119 // Set up logging. 120 if conf.Debug && specutils.IsDebugCommand(conf, subcommand) { 121 log.SetLevel(log.Debug) 122 } 123 124 // Logging will include the local date and time via the time package. 125 // 126 // On first use, time.Local initializes the local time zone, which 127 // involves opening tzdata files on the host. Since this requires 128 // opening host files, it must be done before syscall filter 129 // installation. 130 // 131 // Generally there will be a log message before filter installation 132 // that will force initialization, but force initialization here in 133 // case that does not occur. 134 _ = time.Local.String() 135 136 var emitters log.MultiEmitter 137 if *debugLogFD > -1 { 138 f := os.NewFile(uintptr(*debugLogFD), "debug log file") 139 140 emitters = append(emitters, newEmitter(conf.DebugLogFormat, f)) 141 142 } else if len(conf.DebugLog) > 0 && specutils.IsDebugCommand(conf, subcommand) { 143 f, err := specutils.DebugLogFile(conf.DebugLog, subcommand, "" /* name */) 144 if err != nil { 145 util.Fatalf("error opening debug log file in %q: %v", conf.DebugLog, err) 146 } 147 emitters = append(emitters, newEmitter(conf.DebugLogFormat, f)) 148 149 } else { 150 // Stderr is reserved for the application, just discard the logs if no debug 151 // log is specified. 152 emitters = append(emitters, newEmitter("text", ioutil.Discard)) 153 } 154 155 if *panicLogFD > -1 || *debugLogFD > -1 { 156 fd := *panicLogFD 157 if fd < 0 { 158 fd = *debugLogFD 159 } 160 // Quick sanity check to make sure no other commands get passed 161 // a log fd (they should use log dir instead). 162 if subcommand != "boot" && subcommand != "gofer" { 163 util.Fatalf("flags --debug-log-fd and --panic-log-fd should only be passed to 'boot' and 'gofer' command, but was passed to %q", subcommand) 164 } 165 166 // If we are the boot process, then we own our stdio FDs and can do what we 167 // want with them. Since Docker and Containerd both eat boot's stderr, we 168 // dup our stderr to the provided log FD so that panics will appear in the 169 // logs, rather than just disappear. 170 if err := unix.Dup3(fd, int(os.Stderr.Fd()), 0); err != nil { 171 util.Fatalf("error dup'ing fd %d to stderr: %v", fd, err) 172 } 173 } else if conf.AlsoLogToStderr { 174 emitters = append(emitters, newEmitter(conf.DebugLogFormat, os.Stderr)) 175 } 176 if ulEmittter, add := userLogEmitter(conf, subcommand); add { 177 emitters = append(emitters, ulEmittter) 178 } 179 180 switch len(emitters) { 181 case 0: 182 // Do nothing. 183 case 1: 184 // Use the singular emitter to avoid needless 185 // `for` loop overhead when logging to a single place. 186 log.SetTarget(emitters[0]) 187 default: 188 log.SetTarget(&emitters) 189 } 190 191 const delimString = `**************** gVisor ****************` 192 log.Infof(delimString) 193 log.Infof("Version %s, %s, %s, %d CPUs, %s, PID %d, PPID %d, UID %d, GID %d", version.Version(), runtime.Version(), runtime.GOARCH, runtime.NumCPU(), runtime.GOOS, os.Getpid(), os.Getppid(), os.Getuid(), os.Getgid()) 194 log.Debugf("Page size: 0x%x (%d bytes)", os.Getpagesize(), os.Getpagesize()) 195 log.Infof("Args: %v", os.Args) 196 conf.Log() 197 log.Infof(delimString) 198 199 if *coverageFD >= 0 { 200 f := os.NewFile(uintptr(*coverageFD), "coverage file") 201 coverage.EnableReport(f) 202 } 203 if conf.TestOnlyAllowRunAsCurrentUserWithoutChroot { 204 // SIGTERM is sent to all processes if a test exceeds its 205 // timeout and this case is handled by syscall_test_runner. 206 log.Warningf("Block the TERM signal. This is only safe in tests!") 207 signal.Ignore(unix.SIGTERM) 208 } 209 linux.SetAFSSyscallPanic(conf.TestOnlyAFSSyscallPanic) 210 211 // Call the subcommand and pass in the configuration. 212 var ws unix.WaitStatus 213 subcmdCode := subcommands.Execute(context.Background(), conf, &ws) 214 // Check for leaks and write coverage report before os.Exit(). 215 refs.DoLeakCheck() 216 _ = coverage.Report() 217 if subcmdCode == subcommands.ExitSuccess { 218 log.Infof("Exiting with status: %v", ws) 219 if ws.Signaled() { 220 // No good way to return it, emulate what the shell does. Maybe raise 221 // signal to self? 222 os.Exit(128 + int(ws.Signal())) 223 } 224 os.Exit(ws.ExitStatus()) 225 } 226 // Return an error that is unlikely to be used by the application. 227 log.Warningf("Failure to execute command, err: %v", subcmdCode) 228 os.Exit(128) 229 } 230 231 // forEachCmd invokes the passed callback for each command supported by runsc. 232 func forEachCmd(cb func(cmd subcommands.Command, group string)) { 233 // Help and flags commands are generated automatically. 234 help := cmd.NewHelp(subcommands.DefaultCommander) 235 help.Register(new(cmd.Platforms)) 236 help.Register(new(cmd.Syscalls)) 237 cb(help, "") 238 cb(subcommands.FlagsCommand(), "") 239 240 // Register OCI user-facing runsc commands. 241 cb(new(cmd.Checkpoint), "") 242 cb(new(cmd.Create), "") 243 cb(new(cmd.Delete), "") 244 cb(new(cmd.Do), "") 245 cb(new(cmd.Events), "") 246 cb(new(cmd.Exec), "") 247 cb(new(cmd.Kill), "") 248 cb(new(cmd.List), "") 249 cb(new(cmd.PS), "") 250 cb(new(cmd.Pause), "") 251 cb(new(cmd.PortForward), "") 252 cb(new(cmd.Restore), "") 253 cb(new(cmd.Resume), "") 254 cb(new(cmd.Run), "") 255 cb(new(cmd.Spec), "") 256 cb(new(cmd.Start), "") 257 cb(new(cmd.State), "") 258 cb(new(cmd.Wait), "") 259 260 // Helpers. 261 const helperGroup = "helpers" 262 cb(new(cmd.Install), helperGroup) 263 cb(new(cmd.Mitigate), helperGroup) 264 cb(new(cmd.Uninstall), helperGroup) 265 cb(new(nvproxy.Nvproxy), helperGroup) 266 cb(new(trace.Trace), helperGroup) 267 268 const debugGroup = "debug" 269 cb(new(cmd.Debug), debugGroup) 270 cb(new(cmd.Statefile), debugGroup) 271 cb(new(cmd.Symbolize), debugGroup) 272 cb(new(cmd.Usage), debugGroup) 273 cb(new(cmd.ReadControl), debugGroup) 274 cb(new(cmd.WriteControl), debugGroup) 275 276 const metricGroup = "metrics" 277 cb(new(cmd.MetricMetadata), metricGroup) 278 cb(new(cmd.MetricExport), metricGroup) 279 cb(new(cmd.MetricServer), metricGroup) 280 281 // Internal commands. 282 const internalGroup = "internal use only" 283 cb(new(cmd.Boot), internalGroup) 284 cb(new(cmd.Gofer), internalGroup) 285 cb(new(cmd.Umount), internalGroup) 286 } 287 288 func newEmitter(format string, logFile io.Writer) log.Emitter { 289 switch format { 290 case "text": 291 return log.GoogleEmitter{&log.Writer{Next: logFile}} 292 case "json": 293 return log.JSONEmitter{&log.Writer{Next: logFile}} 294 case "json-k8s": 295 return log.K8sJSONEmitter{&log.Writer{Next: logFile}} 296 } 297 util.Fatalf("invalid log format %q, must be 'text', 'json', or 'json-k8s'", format) 298 panic("unreachable") 299 } 300 301 // userLogEmitter returns an emitter to add logs to user logs if requested. 302 func userLogEmitter(conf *config.Config, subcommand string) (log.Emitter, bool) { 303 if subcommand != "boot" || !conf.DebugToUserLog { 304 return nil, false 305 } 306 // We need to manually scan for `--user-log-fd` since it is a flag of the 307 // `boot` subcommand. We know it is in `--user-log-fd=FD` format because 308 // we control how arguments to `runsc boot` are formatted. 309 const userLogFDFlagPrefix = "--user-log-fd=" 310 var userLog *os.File 311 for _, arg := range os.Args[1:] { 312 if !strings.HasPrefix(arg, userLogFDFlagPrefix) { 313 continue 314 } 315 if userLog != nil { 316 util.Fatalf("duplicate %q flag", userLogFDFlagPrefix) 317 } 318 userLogFD, err := strconv.Atoi(arg[len(userLogFDFlagPrefix):]) 319 if err != nil { 320 util.Fatalf("invalid user log FD flag %q: %v", arg, err) 321 } 322 userLog = os.NewFile(uintptr(userLogFD), "user log file") 323 } 324 if userLog == nil { 325 return nil, false 326 } 327 return log.K8sJSONEmitter{&log.Writer{Next: userLog}}, true 328 }