]> git.r.bdr.sh - rbdr/mobius/blame - cmd/mobius-hotline-server/main.go
Add initial support for resource and info forks
[rbdr/mobius] / cmd / mobius-hotline-server / main.go
CommitLineData
6988a057
JH
1package main
2
3import (
4 "context"
8796b449 5 "embed"
23411fc2 6 "encoding/json"
6988a057
JH
7 "flag"
8 "fmt"
22c599ab 9 "github.com/jhalter/mobius/hotline"
6988a057
JH
10 "go.uber.org/zap"
11 "go.uber.org/zap/zapcore"
23411fc2
JH
12 "io"
13 "log"
93df4153 14 "math/rand"
23411fc2 15 "net/http"
6988a057 16 "os"
18a8614d 17 "runtime"
fd01ba0e 18 "strings"
93df4153 19 "time"
6988a057
JH
20)
21
8796b449
JH
22//go:embed mobius/config
23var cfgTemplate embed.FS
24
6988a057 25const (
18a8614d 26 defaultPort = 5500
6988a057
JH
27)
28
29func main() {
93df4153
JH
30 rand.Seed(time.Now().UnixNano())
31
7cd900d6
JH
32 ctx, cancel := context.WithCancel(context.Background())
33
34 // TODO: implement graceful shutdown by closing context
35 // c := make(chan os.Signal, 1)
36 // signal.Notify(c, os.Interrupt)
37 // defer func() {
38 // signal.Stop(c)
39 // cancel()
40 // }()
41 // go func() {
42 // select {
43 // case <-c:
44 // cancel()
45 // case <-ctx.Done():
46 // }
47 // }()
6988a057
JH
48
49 basePort := flag.Int("bind", defaultPort, "Bind address and port")
23411fc2 50 statsPort := flag.String("stats-port", "", "Enable stats HTTP endpoint on address and port")
18a8614d 51 configDir := flag.String("config", defaultConfigPath(), "Path to config root")
6988a057
JH
52 version := flag.Bool("version", false, "print version and exit")
53 logLevel := flag.String("log-level", "info", "Log level")
8796b449
JH
54 init := flag.Bool("init", false, "Populate the config dir with default configuration")
55
6988a057
JH
56 flag.Parse()
57
58 if *version {
59 fmt.Printf("v%s\n", hotline.VERSION)
60 os.Exit(0)
61 }
62
63 zapLvl, ok := zapLogLevel[*logLevel]
64 if !ok {
65 fmt.Printf("Invalid log level %s. Must be debug, info, warn, or error.\n", *logLevel)
66 os.Exit(0)
67 }
68
69 cores := []zapcore.Core{newStdoutCore(zapLvl)}
70 l := zap.New(zapcore.NewTee(cores...))
71 defer func() { _ = l.Sync() }()
72 logger := l.Sugar()
73
fd01ba0e
BA
74 if !(strings.HasSuffix(*configDir, "/") || strings.HasSuffix(*configDir, "\\")) {
75 *configDir = *configDir + "/"
76 }
77
8796b449 78 if *init {
6936ce91
JH
79 if _, err := os.Stat(*configDir + "/config.yaml"); os.IsNotExist(err) {
80 if err := os.MkdirAll(*configDir, 0750); err != nil {
81 logger.Fatal(err)
82 }
8796b449 83
6936ce91
JH
84 if err := copyDir("mobius/config", *configDir); err != nil {
85 logger.Fatal(err)
86 }
87 logger.Infow("Config dir initialized at " + *configDir)
8796b449 88
6936ce91
JH
89 } else {
90 logger.Infow("Existing config dir found. Skipping initialization.")
8796b449
JH
91 }
92 }
93
6988a057
JH
94 if _, err := os.Stat(*configDir); os.IsNotExist(err) {
95 logger.Fatalw("Configuration directory not found", "path", configDir)
96 }
97
7cd900d6 98 srv, err := hotline.NewServer(*configDir, *basePort, logger, &hotline.OSFileStore{})
6988a057
JH
99 if err != nil {
100 logger.Fatal(err)
101 }
102
23411fc2
JH
103 sh := statHandler{hlServer: srv}
104 if *statsPort != "" {
105 http.HandleFunc("/", sh.RenderStats)
106
107 go func(srv *hotline.Server) {
108 // Use the default DefaultServeMux.
109 err = http.ListenAndServe(":"+*statsPort, nil)
110 if err != nil {
111 log.Fatal(err)
112 }
113 }(srv)
114 }
115
6988a057 116 // Serve Hotline requests until program exit
7cd900d6 117 logger.Fatal(srv.ListenAndServe(ctx, cancel))
6988a057
JH
118}
119
23411fc2
JH
120type statHandler struct {
121 hlServer *hotline.Server
122}
123
124func (sh *statHandler) RenderStats(w http.ResponseWriter, _ *http.Request) {
125 u, err := json.Marshal(sh.hlServer.Stats)
126 if err != nil {
127 panic(err)
128 }
129
130 _, _ = io.WriteString(w, string(u))
131}
132
6988a057
JH
133func newStdoutCore(level zapcore.Level) zapcore.Core {
134 encoderCfg := zap.NewProductionEncoderConfig()
135 encoderCfg.TimeKey = "timestamp"
136 encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder
137
138 return zapcore.NewCore(
139 zapcore.NewConsoleEncoder(encoderCfg),
140 zapcore.Lock(os.Stdout),
141 level,
142 )
143}
144
145var zapLogLevel = map[string]zapcore.Level{
146 "debug": zap.DebugLevel,
147 "info": zap.InfoLevel,
148 "warn": zap.WarnLevel,
149 "error": zap.ErrorLevel,
150}
18a8614d
JH
151
152func defaultConfigPath() (cfgPath string) {
153 switch runtime.GOOS {
154 case "windows":
fd01ba0e 155 cfgPath = "config/"
18a8614d
JH
156 case "darwin":
157 if _, err := os.Stat("/usr/local/var/mobius/config/"); err == nil {
158 cfgPath = "/usr/local/var/mobius/config/"
159 } else if _, err := os.Stat("/opt/homebrew/var/mobius/config"); err == nil {
160 cfgPath = "/opt/homebrew/var/mobius/config/"
161 }
162 case "linux":
163 cfgPath = "/usr/local/var/mobius/config/"
164 default:
165 fmt.Printf("unsupported OS")
166 }
167
168 return cfgPath
169}
8796b449
JH
170
171// TODO: Simplify this mess. Why is it so difficult to recursively copy a directory?
172func copyDir(src, dst string) error {
173 entries, err := cfgTemplate.ReadDir(src)
174 if err != nil {
175 return err
176 }
177 for _, dirEntry := range entries {
178 if dirEntry.IsDir() {
179 if err := os.MkdirAll(dst+"/"+dirEntry.Name(), 0777); err != nil {
180 panic(err)
181 }
182 subdirEntries, _ := cfgTemplate.ReadDir(src + "/" + dirEntry.Name())
183 for _, subDirEntry := range subdirEntries {
184 f, err := os.Create(dst + "/" + dirEntry.Name() + "/" + subDirEntry.Name())
185 if err != nil {
186 return err
187 }
188
189 srcFile, err := cfgTemplate.Open(src + "/" + dirEntry.Name() + "/" + subDirEntry.Name())
190 if err != nil {
191 return err
192 }
193 _, err = io.Copy(f, srcFile)
194 if err != nil {
195 return err
196 }
197 f.Close()
198 }
199 } else {
200 f, err := os.Create(dst + "/" + dirEntry.Name())
201 if err != nil {
202 return err
203 }
204
205 srcFile, err := cfgTemplate.Open(src + "/" + dirEntry.Name())
206 if err != nil {
207 return err
208 }
209 _, err = io.Copy(f, srcFile)
210 if err != nil {
211 return err
212 }
213 f.Close()
214 }
215
216 }
217
218 return nil
219}