| 1 | // Command todo is the bakeoff's Go + Rastrillo TodoMVC: resolve the |
| 2 | // platform's activation argv, open the database, build the app, serve. |
| 3 | // |
| 4 | // todo --socket <path> --db <path> on Carloku |
| 5 | // todo --listen 127.0.0.1:<port> --db <path> locally |
| 6 | package main |
| 7 | |
| 8 | import ( |
| 9 | "fmt" |
| 10 | "log/slog" |
| 11 | "net" |
| 12 | "os" |
| 13 | "strings" |
| 14 | "time" |
| 15 | |
| 16 | "amadan.net/rastrillo/rastrillo" |
| 17 | "amadan.net/rastrillo/rastrillo/db" |
| 18 | |
| 19 | todo "todo/internal/todo" |
| 20 | ) |
| 21 | |
| 22 | // localAddr is where the app listens when it is given neither a socket |
| 23 | // nor an address: loopback only, never every interface. |
| 24 | const localAddr = "127.0.0.1:8080" |
| 25 | |
| 26 | func main() { |
| 27 | // First, so perf's cold budget counts the whole start: a hibernated |
| 28 | // instance pays all of it on someone's click. |
| 29 | started := time.Now() |
| 30 | logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) |
| 31 | |
| 32 | // The bakeoff's local flag is --listen; Rastrillo's is --addr. |
| 33 | os.Args = listenAsAddr(os.Args) |
| 34 | |
| 35 | // Resolve, not Run: this app opens its own database handle via |
| 36 | // db.Open, so Options.DBPath must be empty again by the Serve call |
| 37 | // below, or Serve would open a second connection to the same file. |
| 38 | opts, err := rastrillo.Resolve(rastrillo.Options{DBPath: "todo.db", Logger: logger}) |
| 39 | if err != nil { |
| 40 | logger.Error("resolve activation", "err", err) |
| 41 | os.Exit(1) |
| 42 | } |
| 43 | if opts.Socket == "" && opts.Addr == "" && os.Getenv("LISTEN_FDS") == "" { |
| 44 | opts.Addr = localAddr |
| 45 | } |
| 46 | if opts.Addr != "" { |
| 47 | if err := loopbackOnly(opts.Addr); err != nil { |
| 48 | logger.Error("listen address", "err", err) |
| 49 | os.Exit(1) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | d, err := db.Open(opts.DBPath, logger) |
| 54 | if err != nil { |
| 55 | logger.Error("open database", "err", err) |
| 56 | os.Exit(1) |
| 57 | } |
| 58 | defer d.Close() |
| 59 | |
| 60 | o := origin(opts.Addr) |
| 61 | mux, err := todo.App(d, o, logger) |
| 62 | if err != nil { |
| 63 | logger.Error("build app", "err", err) |
| 64 | os.Exit(1) |
| 65 | } |
| 66 | |
| 67 | todo.Configure(&opts, mux, started) |
| 68 | opts.DBPath = "" |
| 69 | logger.Info("serving", "origin", o, "socket", opts.Socket, "addr", opts.Addr) |
| 70 | if err := rastrillo.Serve(opts); err != nil { |
| 71 | logger.Error("serve failed", "err", err) |
| 72 | os.Exit(1) |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | // loopbackOnly refuses a TCP address that isn't on 127.0.0.1: the app |
| 77 | // never listens on another interface ("0.0.0.0:8080" or ":8080" would |
| 78 | // expose it to the whole network). |
| 79 | func loopbackOnly(addr string) error { |
| 80 | host, _, err := net.SplitHostPort(addr) |
| 81 | if err != nil { |
| 82 | return fmt.Errorf("%q: %w", addr, err) |
| 83 | } |
| 84 | if host != "127.0.0.1" { |
| 85 | return fmt.Errorf("%q: only 127.0.0.1 may be listened on", addr) |
| 86 | } |
| 87 | return nil |
| 88 | } |
| 89 | |
| 90 | // origin is the app's own origin: https://$CARLOS_HOST on Carloku, and |
| 91 | // http://<listen address> when run locally. |
| 92 | func origin(addr string) string { |
| 93 | if host := os.Getenv("CARLOS_HOST"); host != "" { |
| 94 | return "https://" + host |
| 95 | } |
| 96 | if host, port, err := net.SplitHostPort(addr); err == nil && host != "" { |
| 97 | return "http://" + net.JoinHostPort(host, port) |
| 98 | } |
| 99 | return "http://" + localAddr |
| 100 | } |
| 101 | |
| 102 | // listenAsAddr rewrites --listen (and -listen, --listen=x) to Rastrillo's |
| 103 | // --addr, leaving every other argument alone. |
| 104 | func listenAsAddr(args []string) []string { |
| 105 | out := make([]string, 0, len(args)) |
| 106 | for _, a := range args { |
| 107 | name, value, hasValue := strings.Cut(a, "=") |
| 108 | if name == "--listen" || name == "-listen" { |
| 109 | a = "--addr" |
| 110 | if hasValue { |
| 111 | a += "=" + value |
| 112 | } |
| 113 | } |
| 114 | out = append(out, a) |
| 115 | } |
| 116 | return out |
| 117 | } |
| 118 | |