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-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.
8package main
9
10import (
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>".
28var version = "dev"
29
30func 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
38func 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.
109func 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