| 1 | // Command todo-go is the CARLOS bakeoff's TodoMVC, in Go. |
| 2 | // |
| 3 | // todo-go --socket <path> --db <path> on Carloku |
| 4 | // todo-go --listen 127.0.0.1:<port> --db <path> locally |
| 5 | // |
| 6 | // It serves until SIGTERM or SIGINT, then finishes in-flight requests, |
| 7 | // closes the database and exits 0. |
| 8 | package main |
| 9 | |
| 10 | import ( |
| 11 | "context" |
| 12 | "errors" |
| 13 | "flag" |
| 14 | "fmt" |
| 15 | "log/slog" |
| 16 | "net" |
| 17 | "net/http" |
| 18 | "os" |
| 19 | "os/signal" |
| 20 | "syscall" |
| 21 | "time" |
| 22 | |
| 23 | "todo-go/internal/todo" |
| 24 | "todo-go/web" |
| 25 | ) |
| 26 | |
| 27 | // version is the build version, set with -ldflags "-X main.version=<sha>". |
| 28 | var version = "dev" |
| 29 | |
| 30 | func main() { |
| 31 | log := slog.New(slog.NewTextHandler(os.Stderr, nil)) |
| 32 | if err := run(os.Args[1:], log); err != nil { |
| 33 | log.Error("exiting", "err", err) |
| 34 | os.Exit(1) |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | func run(args []string, log *slog.Logger) error { |
| 39 | fset := flag.NewFlagSet("todo-go", flag.ContinueOnError) |
| 40 | socket := fset.String("socket", "", "unix socket to listen on (Carloku)") |
| 41 | listen := fset.String("listen", "", "127.0.0.1:<port> to listen on instead of a socket (local runs)") |
| 42 | dbPath := fset.String("db", "", "SQLite database file") |
| 43 | if err := fset.Parse(args); err != nil { |
| 44 | return err |
| 45 | } |
| 46 | if *dbPath == "" || (*socket == "") == (*listen == "") { |
| 47 | return errors.New("usage: todo-go (--socket <path> | --listen 127.0.0.1:<port>) --db <path>") |
| 48 | } |
| 49 | |
| 50 | ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt) |
| 51 | defer stop() |
| 52 | |
| 53 | cfg := web.Config{Version: version, Log: log} |
| 54 | var ln net.Listener |
| 55 | var err error |
| 56 | if *socket != "" { |
| 57 | // The platform hands us our hostname; never derive it from the socket. |
| 58 | if host := os.Getenv("CARLOS_HOST"); host != "" { |
| 59 | cfg.Origin = "https://" + host |
| 60 | cfg.Secure = true |
| 61 | } |
| 62 | os.Remove(*socket) // a stale socket from a previous run |
| 63 | ln, err = net.Listen("unix", *socket) |
| 64 | } else { |
| 65 | if err := checkLoopback(*listen); err != nil { |
| 66 | return err |
| 67 | } |
| 68 | ln, err = net.Listen("tcp", *listen) |
| 69 | } |
| 70 | if err != nil { |
| 71 | return err |
| 72 | } |
| 73 | if *listen != "" { |
| 74 | cfg.Origin = "http://" + ln.Addr().String() // the real port, even for :0 |
| 75 | } |
| 76 | |
| 77 | store, err := todo.Open(ctx, *dbPath) |
| 78 | if err != nil { |
| 79 | ln.Close() |
| 80 | return err |
| 81 | } |
| 82 | defer store.Close() |
| 83 | |
| 84 | srv := &http.Server{ |
| 85 | Handler: web.New(store, cfg), |
| 86 | ReadHeaderTimeout: 10 * time.Second, |
| 87 | ReadTimeout: 30 * time.Second, |
| 88 | WriteTimeout: 30 * time.Second, |
| 89 | IdleTimeout: 2 * time.Minute, |
| 90 | ErrorLog: slog.NewLogLogger(log.Handler(), slog.LevelWarn), |
| 91 | } |
| 92 | served := make(chan error, 1) |
| 93 | go func() { served <- srv.Serve(ln) }() |
| 94 | log.Info("serving", "addr", ln.Addr().String(), "version", version) |
| 95 | |
| 96 | select { |
| 97 | case err := <-served: |
| 98 | return err |
| 99 | case <-ctx.Done(): |
| 100 | } |
| 101 | log.Info("shutting down") |
| 102 | shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
| 103 | defer cancel() |
| 104 | return srv.Shutdown(shutdownCtx) |
| 105 | } |
| 106 | |
| 107 | // checkLoopback refuses any --listen address that isn't a loopback IP, |
| 108 | // so a local run can never be reached from the network. |
| 109 | func checkLoopback(addr string) error { |
| 110 | host, _, err := net.SplitHostPort(addr) |
| 111 | if err != nil { |
| 112 | return fmt.Errorf("--listen %q: %w", addr, err) |
| 113 | } |
| 114 | if ip := net.ParseIP(host); ip == nil || !ip.IsLoopback() { |
| 115 | return fmt.Errorf("--listen %q: only a loopback address such as 127.0.0.1 is allowed", addr) |
| 116 | } |
| 117 | return nil |
| 118 | } |
| 119 | |