package hw3test import ( "bytes" "errors" "fmt" "io" "math/rand" "os" "os/exec" "path/filepath" "strings" "text/template" "unicode" "go.uber.org/zap" ) // RunOpts contains command line arguments for the solution. // Those are passed to the template. type RunOpts struct { // Solution options Port int WorkingDirectory string ListenAddr string // Host ServerDomain string // Full run config that will be provided to the solution. CommandLineArgs string Env []string // Full run config if solution will be running in docker. DockerCommandLineArgs string DockerEnvArgs string DockerPortArgs string DockerVolumeArgs string // Hack to get exitcode of the solution. ExitCode chan int } func (o *RunOpts) Address() string { host := os.Getenv("SOLUTION_HOST") if host == "" { host = "localhost" } return fmt.Sprintf("http://%s:%d", host, o.Port) } func (o *RunOpts) GenerateRunConfig(t *TC, r *rand.Rand, gen *EnvGen) { o.CommandLineArgs, o.Env = o.BuildConfig(r, gen) dirsInDocker := []string{ "/files", "/files0", "/files1", "/files2", "/files3", } dirInDocker := dirsInDocker[r.Intn(len(dirsInDocker))] if o.WorkingDirectory == "" { dirInDocker = "" } else { o.DockerVolumeArgs = fmt.Sprintf(`-v "%s:%s"`, o.WorkingDirectory, dirInDocker) } o.DockerPortArgs = fmt.Sprintf("-p %d:%d", o.Port, o.Port) dockerArgs, dockerEnv := RunOpts{ ListenAddr: o.ListenAddr, Port: o.Port, WorkingDirectory: dirInDocker, ServerDomain: o.ServerDomain, }.BuildConfig(r, gen) o.DockerEnvArgs = "" for _, env := range dockerEnv { o.DockerEnvArgs += "--env \"" + env + "\" " } o.DockerCommandLineArgs = dockerArgs } // BuildConfig uses ListenAddr, Port, WorkingDirectory, ServerDomain. func (o RunOpts) BuildConfig(r *rand.Rand, gen *EnvGen) (args string, env []string) { if o.ListenAddr == "0.0.0.0" && r.Intn(2) == 1 { // can omit default value } else if gen.AllowEnv && r.Intn(3) == 1 { // use env env = append(env, fmt.Sprintf("SERVER_HOST=%s", o.ListenAddr)) } else if o.ListenAddr != "" { // use plain cmdline args args += fmt.Sprintf(" \"--host=%s\"", o.ListenAddr) if r.Intn(2) == 1 { // pass dummy env env = append(env, fmt.Sprintf("SERVER_HOST=%s", "8.8.8.8")) } } if o.Port == 8080 && r.Intn(2) == 1 { // can omit default value } else if gen.AllowEnv && r.Intn(3) == 1 { // use env env = append(env, fmt.Sprintf("SERVER_PORT=%d", o.Port)) } else { // use plain cmdline args args += fmt.Sprintf(" \"--port=%d\"", o.Port) if r.Intn(2) == 1 { // pass dummy env env = append(env, fmt.Sprintf("SERVER_PORT=%d", 80)) } } if o.WorkingDirectory == "" && r.Intn(2) == 1 { // can omit default value } else if gen.AllowEnv && r.Intn(3) == 1 { // use env env = append(env, fmt.Sprintf("SERVER_WORKING_DIRECTORY=%s", o.WorkingDirectory)) } else if o.WorkingDirectory != "" { // use plain cmdline args args += fmt.Sprintf(" \"--working-directory=%s\"", o.WorkingDirectory) if r.Intn(2) == 1 { // pass dummy env env = append(env, fmt.Sprintf("SERVER_WORKING_DIRECTORY=%s", "/")) } } if o.ServerDomain == "localhost" && r.Intn(2) == 1 { // can omit default value } else if gen.AllowEnv && r.Intn(3) == 1 { // use env env = append(env, fmt.Sprintf("SERVER_DOMAIN=%s", o.ServerDomain)) } else if o.ServerDomain != "" { // use plain cmdline args args += fmt.Sprintf(" \"--server-domain=%s\"", o.ServerDomain) if r.Intn(2) == 1 { // pass dummy env env = append(env, fmt.Sprintf("SERVER_DOMAIN=%s", "example.com")) } } return args, env } // Runner is a helper for running HTTP server solution. type Runner interface { // Run the solution with the given options. // Returns a function that can be used to stop the solution. Run(t *TC, opts RunOpts) (stop func(), err error) } // CmdRunner runs command based on template. type CmdRunner struct { tmpl template.Template useDocker bool } func NewCmdRunner(tmpl *template.Template, useDocker bool) *CmdRunner { return &CmdRunner{ tmpl: *tmpl, useDocker: useDocker, } } // splitCommand preserves quoted arguments without invoking a shell. func splitCommand(command string) ([]string, error) { var args []string var arg strings.Builder quoted := false started := false for _, char := range command { switch { case char == '"': quoted = !quoted started = true case unicode.IsSpace(char) && !quoted: if started { args = append(args, arg.String()) arg.Reset() started = false } default: arg.WriteRune(char) started = true } } if quoted { return nil, fmt.Errorf("unclosed quote in launch command") } if started { args = append(args, arg.String()) } if len(args) == 0 { return nil, fmt.Errorf("empty launch command") } return args, nil } func (r *CmdRunner) Run(t *TC, opts RunOpts) (stop func(), err error) { var b bytes.Buffer err = r.tmpl.Execute(&b, opts) if err != nil { return nil, fmt.Errorf("failed to execute template: %v", err) } envOpts := opts.Env if r.useDocker { envOpts = nil } cmdParts, err := splitCommand(b.String()) if err != nil { return nil, err } var cidDir, cidFile string if r.useDocker { if len(cmdParts) < 2 || !strings.EqualFold(filepath.Base(cmdParts[0]), "docker") || cmdParts[1] != "run" { return nil, fmt.Errorf("Docker launch template must start with docker run") } cidDir, err = os.MkdirTemp("", "hw3-container-") if err != nil { return nil, fmt.Errorf("failed to create container ID directory: %w", err) } cidFile = filepath.Join(cidDir, "id") cmdParts = append(append([]string{}, cmdParts[:2]...), append([]string{"--cidfile", cidFile}, cmdParts[2:]...)...) } Info(t, "Running command", zap.Strings("command", cmdParts), zap.Strings("env", envOpts)) cmd := exec.Command(cmdParts[0], cmdParts[1:]...) cmd.Env = append(os.Environ(), envOpts...) cmd.Stdout = NewProxyWriter(os.Stderr) cmd.Stderr = NewProxyWriter(os.Stderr) err = cmd.Start() if err != nil { if cidDir != "" { _ = os.Remove(cidDir) } return nil, fmt.Errorf("failed to run command: %v", err) } go func() { err := cmd.Wait() if err != nil && err.Error() != "signal: killed" && err.Error() != "exit status 1" { Warn(t, "Command finished with error", zap.Error(err)) } if opts.ExitCode != nil { if e, ok := err.(*exec.ExitError); ok { opts.ExitCode <- e.ExitCode() } else { opts.ExitCode <- 0 } } }() return func() { if cmd.Process != nil { if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) { Error(t, "Failed to kill command", zap.Error(err)) } } if r.useDocker { id, err := os.ReadFile(cidFile) if err == nil && len(strings.TrimSpace(string(id))) > 0 { output, removeErr := exec.Command("docker", "rm", "-f", strings.TrimSpace(string(id))).CombinedOutput() if removeErr != nil && !strings.Contains(string(output), "No such container") && !strings.Contains(string(output), "is already in progress") { Error(t, "Failed to remove Docker container", zap.Error(removeErr), zap.ByteString("output", output)) } } else if err != nil && !errors.Is(err, os.ErrNotExist) { Error(t, "Failed to read container ID", zap.Error(err)) } _ = os.Remove(cidFile) _ = os.Remove(cidDir) } }, nil } // ProxyWriter is used to forward solution output to standard output. type ProxyWriter struct { w io.Writer disable bool } func NewProxyWriter(w io.Writer) *ProxyWriter { return &ProxyWriter{ w: w, disable: boolFromEnv("DISABLE_SOLUTION_OUTPUT", false), } } func (w *ProxyWriter) Write(b []byte) (n int, err error) { if w.disable { return len(b), nil } // TODO: if there will be sync problems, we can take a global lock // and read until \n, then flush and apply color return w.w.Write(b) }