Add HW 3
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user