// Command todo is the bakeoff's Go + Rastrillo TodoMVC: resolve the // platform's activation argv, open the database, build the app, serve. // // todo --socket --db on Carloku // todo --listen 127.0.0.1: --db locally package main import ( "fmt" "log/slog" "net" "os" "strings" "time" "amadan.net/rastrillo/rastrillo" "amadan.net/rastrillo/rastrillo/db" todo "todo/internal/todo" ) // localAddr is where the app listens when it is given neither a socket // nor an address: loopback only, never every interface. const localAddr = "127.0.0.1:8080" func main() { // First, so perf's cold budget counts the whole start: a hibernated // instance pays all of it on someone's click. started := time.Now() logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) // The bakeoff's local flag is --listen; Rastrillo's is --addr. os.Args = listenAsAddr(os.Args) // Resolve, not Run: this app opens its own database handle via // db.Open, so Options.DBPath must be empty again by the Serve call // below, or Serve would open a second connection to the same file. opts, err := rastrillo.Resolve(rastrillo.Options{DBPath: "todo.db", Logger: logger}) if err != nil { logger.Error("resolve activation", "err", err) os.Exit(1) } if opts.Socket == "" && opts.Addr == "" && os.Getenv("LISTEN_FDS") == "" { opts.Addr = localAddr } if opts.Addr != "" { if err := loopbackOnly(opts.Addr); err != nil { logger.Error("listen address", "err", err) os.Exit(1) } } d, err := db.Open(opts.DBPath, logger) if err != nil { logger.Error("open database", "err", err) os.Exit(1) } defer d.Close() o := origin(opts.Addr) mux, err := todo.App(d, o, logger) if err != nil { logger.Error("build app", "err", err) os.Exit(1) } todo.Configure(&opts, mux, started) opts.DBPath = "" logger.Info("serving", "origin", o, "socket", opts.Socket, "addr", opts.Addr) if err := rastrillo.Serve(opts); err != nil { logger.Error("serve failed", "err", err) os.Exit(1) } } // loopbackOnly refuses a TCP address that isn't on 127.0.0.1: the app // never listens on another interface ("0.0.0.0:8080" or ":8080" would // expose it to the whole network). func loopbackOnly(addr string) error { host, _, err := net.SplitHostPort(addr) if err != nil { return fmt.Errorf("%q: %w", addr, err) } if host != "127.0.0.1" { return fmt.Errorf("%q: only 127.0.0.1 may be listened on", addr) } return nil } // origin is the app's own origin: https://$CARLOS_HOST on Carloku, and // http:// when run locally. func origin(addr string) string { if host := os.Getenv("CARLOS_HOST"); host != "" { return "https://" + host } if host, port, err := net.SplitHostPort(addr); err == nil && host != "" { return "http://" + net.JoinHostPort(host, port) } return "http://" + localAddr } // listenAsAddr rewrites --listen (and -listen, --listen=x) to Rastrillo's // --addr, leaving every other argument alone. func listenAsAddr(args []string) []string { out := make([]string, 0, len(args)) for _, a := range args { name, value, hasValue := strings.Cut(a, "=") if name == "--listen" || name == "-listen" { a = "--addr" if hasValue { a += "=" + value } } out = append(out, a) } return out }