carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

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
6package main
7
8import (
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.
24const localAddr = "127.0.0.1:8080"
25
26func 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).
79func 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.
92func 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.
104func 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