// Command todo-go is the CARLOS bakeoff's TodoMVC, in Go. // // todo-go --socket --db on Carloku // todo-go --listen 127.0.0.1: --db locally // // It serves until SIGTERM or SIGINT, then finishes in-flight requests, // closes the database and exits 0. package main import ( "context" "errors" "flag" "fmt" "log/slog" "net" "net/http" "os" "os/signal" "syscall" "time" "todo-go/internal/todo" "todo-go/web" ) // version is the build version, set with -ldflags "-X main.version=". var version = "dev" func main() { log := slog.New(slog.NewTextHandler(os.Stderr, nil)) if err := run(os.Args[1:], log); err != nil { log.Error("exiting", "err", err) os.Exit(1) } } func run(args []string, log *slog.Logger) error { fset := flag.NewFlagSet("todo-go", flag.ContinueOnError) socket := fset.String("socket", "", "unix socket to listen on (Carloku)") listen := fset.String("listen", "", "127.0.0.1: to listen on instead of a socket (local runs)") dbPath := fset.String("db", "", "SQLite database file") if err := fset.Parse(args); err != nil { return err } if *dbPath == "" || (*socket == "") == (*listen == "") { return errors.New("usage: todo-go (--socket | --listen 127.0.0.1:) --db ") } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt) defer stop() cfg := web.Config{Version: version, Log: log} var ln net.Listener var err error if *socket != "" { // The platform hands us our hostname; never derive it from the socket. if host := os.Getenv("CARLOS_HOST"); host != "" { cfg.Origin = "https://" + host cfg.Secure = true } os.Remove(*socket) // a stale socket from a previous run ln, err = net.Listen("unix", *socket) } else { if err := checkLoopback(*listen); err != nil { return err } ln, err = net.Listen("tcp", *listen) } if err != nil { return err } if *listen != "" { cfg.Origin = "http://" + ln.Addr().String() // the real port, even for :0 } store, err := todo.Open(ctx, *dbPath) if err != nil { ln.Close() return err } defer store.Close() srv := &http.Server{ Handler: web.New(store, cfg), ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 2 * time.Minute, ErrorLog: slog.NewLogLogger(log.Handler(), slog.LevelWarn), } served := make(chan error, 1) go func() { served <- srv.Serve(ln) }() log.Info("serving", "addr", ln.Addr().String(), "version", version) select { case err := <-served: return err case <-ctx.Done(): } log.Info("shutting down") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return srv.Shutdown(shutdownCtx) } // checkLoopback refuses any --listen address that isn't a loopback IP, // so a local run can never be reached from the network. func checkLoopback(addr string) error { host, _, err := net.SplitHostPort(addr) if err != nil { return fmt.Errorf("--listen %q: %w", addr, err) } if ip := net.ParseIP(host); ip == nil || !ip.IsLoopback() { return fmt.Errorf("--listen %q: only a loopback address such as 127.0.0.1 is allowed", addr) } return nil }