diff --git a/cmd/aquareum/aquareum.go b/cmd/aquareum/aquareum.go index b6aec86c..e46a41e9 100644 --- a/cmd/aquareum/aquareum.go +++ b/cmd/aquareum/aquareum.go @@ -1,52 +1,20 @@ package main import ( - "fmt" - "log" - "net/http" - "os" + "context" - "aquareum.tv/aquareum/packages/app" - "github.com/adrg/xdg" + "aquareum.tv/aquareum/pkg/log" + + "aquareum.tv/aquareum/pkg/cmd" ) var Version = "unknown" func main() { - err := start() - if err != nil { - log.Fatal(err) - } -} - -func start() error { - if xdg.Home == "/" { - home, err := os.UserHomeDir() - if err != nil { - return err - } - os.Setenv("HOME", home) - xdg.Reload() - } - if xdg.Home == "/" { - return fmt.Errorf("couldn't find users home directory") - } - tlsCrtFile, err := xdg.ConfigFile("aquareum/tls/tls.crt") - if err != nil { - log.Fatal(err) - } - tlsKeyFile, err := xdg.ConfigFile("aquareum/tls/tls.key") - if err != nil { - log.Fatal(err) - } - files, err := app.Files() - if err != nil { - return err - } - http.Handle("/", http.FileServer(http.FS(files))) - err = http.ListenAndServeTLS(":443", tlsCrtFile, tlsKeyFile, nil) + err := cmd.Start(&cmd.BuildFlags{ + Version: Version, + }) if err != nil { - return err + log.Log(context.Background(), "exited uncleanly", "error", err) } - return nil } diff --git a/go.mod b/go.mod index 430d959c..d8eaa6e8 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,16 @@ module aquareum.tv/aquareum go 1.22.2 -require github.com/adrg/xdg v0.4.0 +require ( + github.com/adrg/xdg v0.4.0 + github.com/fatih/color v1.17.0 + github.com/golang/glog v1.2.0 + github.com/peterbourgon/ff/v3 v3.3.1 + golang.org/x/sync v0.6.0 +) -require golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/sys v0.18.0 // indirect +) diff --git a/go.sum b/go.sum index fccdf6cf..f01ca02e 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,31 @@ github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/peterbourgon/ff/v3 v3.3.1 h1:XSWvXxeNdgeppLNGGJEAOiXRdX2YMF/LuZfdnqQ1SNc= +github.com/peterbourgon/ff/v3 v3.3.1/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/api/api.go b/pkg/api/api.go new file mode 100644 index 00000000..92b3b813 --- /dev/null +++ b/pkg/api/api.go @@ -0,0 +1,64 @@ +package api + +import ( + "context" + "fmt" + "net/http" + "time" + + "aquareum.tv/aquareum/packages/app" + "aquareum.tv/aquareum/pkg/config" + "aquareum.tv/aquareum/pkg/log" +) + +func Handler() (http.Handler, error) { + mux := http.NewServeMux() + files, err := app.Files() + if err != nil { + return nil, err + } + mux.Handle("/", http.FileServer(http.FS(files))) + return mux, nil +} + +func ServeHTTP(ctx context.Context, cli config.CLI) error { + return ServerWithShutdown(ctx, cli, func(s *http.Server) error { + s.Addr = cli.HttpAddr + log.Log(ctx, "http server starting", "addr", s.Addr) + return s.ListenAndServe() + }) +} + +func ServeHTTPS(ctx context.Context, cli config.CLI) error { + return ServerWithShutdown(ctx, cli, func(s *http.Server) error { + s.Addr = cli.HttpsAddr + log.Log(ctx, "https server starting", + "addr", s.Addr, + "certPath", cli.TLSCertPath, + "keyPath", cli.TLSKeyPath, + ) + return s.ListenAndServeTLS(cli.TLSCertPath, cli.TLSKeyPath) + }) +} + +func ServerWithShutdown(ctx context.Context, cli config.CLI, serve func(*http.Server) error) error { + handler, err := Handler() + if err != nil { + return err + } + ctx, cancel := context.WithCancel(ctx) + server := http.Server{Handler: handler} + var serveErr error + go func() { + serveErr = serve(&server) + cancel() + }() + <-ctx.Done() + if serveErr != nil { + return fmt.Errorf("error in http server: %w", serveErr) + } + + ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return server.Shutdown(ctx) +} diff --git a/pkg/cmd/aquareum.go b/pkg/cmd/aquareum.go new file mode 100644 index 00000000..bc7e3ff6 --- /dev/null +++ b/pkg/cmd/aquareum.go @@ -0,0 +1,103 @@ +package cmd + +import ( + "context" + "flag" + "fmt" + "os" + "os/signal" + "syscall" + + "log" + + "aquareum.tv/aquareum/pkg/api" + "aquareum.tv/aquareum/pkg/config" + "github.com/adrg/xdg" + "github.com/peterbourgon/ff/v3" + "golang.org/x/sync/errgroup" +) + +var Version = "unknown" + +type BuildFlags struct { + Version string +} + +// parse the CLI and fire up an aquareum node! +func Start(build *BuildFlags) error { + err := normalizeXDG() + if err != nil { + return err + } + + tlsCertFile, err := xdg.ConfigFile("aquareum/tls/tls.crt") + if err != nil { + return err + } + tlsKeyFile, err := xdg.ConfigFile("aquareum/tls/tls.key") + if err != nil { + return err + } + + fs := flag.NewFlagSet("aquareum", flag.ExitOnError) + cli := config.CLI{} + fs.StringVar(&cli.HttpAddr, "http-addr", ":8080", "Public HTTP address") + fs.StringVar(&cli.HttpsAddr, "https-addr", ":8443", "Public HTTPS address") + fs.BoolVar(&cli.Insecure, "insecure", false, "Run without HTTPS. not recomended, as WebRTC support requires HTTPS") + fs.StringVar(&cli.TLSCertPath, "tls-cert", tlsCertFile, "Path to TLS certificate") + fs.StringVar(&cli.TLSKeyPath, "tls-key", tlsKeyFile, "Path to TLS key") + + ff.Parse( + fs, os.Args[1:], + ff.WithEnvVarPrefix("AQ"), + ff.WithEnvVarSplit(","), + ) + + group, ctx := errgroup.WithContext(context.Background()) + + group.Go(func() error { + return handleSignals(ctx) + }) + + group.Go(func() error { + return api.ServeHTTP(ctx, cli) + }) + + if !cli.Insecure { + group.Go(func() error { + return api.ServeHTTPS(ctx, cli) + }) + } + + return group.Wait() +} + +// xdg sometimes gets confused in systemd, give it a default +func normalizeXDG() error { + if xdg.Home == "/" { + home, err := os.UserHomeDir() + if err != nil { + return err + } + os.Setenv("HOME", home) + xdg.Reload() + } + if xdg.Home == "/" { + return fmt.Errorf("couldn't find home directory") + } + return nil +} + +func handleSignals(ctx context.Context) error { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) + for { + select { + case s := <-c: + log.Printf("caught signal=%v, attempting clean shutdown", s) + return fmt.Errorf("caught signal=%v", s) + case <-ctx.Done(): + return nil + } + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 00000000..1ce57301 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,9 @@ +package config + +type CLI struct { + TLSCertPath string + TLSKeyPath string + Insecure bool + HttpAddr string + HttpsAddr string +} diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 00000000..410bc083 --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,122 @@ +/* +Package clog provides Context with logging metadata, as well as logging helper functions. +*/ +package log + +import ( + "context" + "flag" + "fmt" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/fatih/color" + "github.com/golang/glog" +) + +// unique type to prevent assignment. +type clogContextKeyType struct{} + +// singleton value to identify our logging metadata in context +var clogContextKey = clogContextKeyType{} + +var defaultLogLevel glog.Level = 3 + +// basic type to represent logging container. logging context is immutable after +// creation, so we don't have to worry about locking. +type metadata map[string]any + +func init() { + // Set default v level to 3; this is overridden in main() but is useful for tests + vFlag := flag.Lookup("v") + // nolint:errcheck + vFlag.Value.Set(fmt.Sprintf("%d", defaultLogLevel)) +} + +type VerboseLogger struct { + level glog.Level +} + +// implementation of our logger aware of glog -v=[0-9] levels +func V(level glog.Level) *VerboseLogger { + return &VerboseLogger{level: level} +} + +func (m metadata) Flat() []any { + out := []any{} + for k, v := range m { + out = append(out, k) + out = append(out, v) + } + return out +} + +// Return a new context, adding in the provided values to the logging metadata +func WithLogValues(ctx context.Context, args ...string) context.Context { + oldMetadata, _ := ctx.Value(clogContextKey).(metadata) + // No previous logging found, set up a new map + if oldMetadata == nil { + oldMetadata = metadata{} + } + var newMetadata = metadata{} + for k, v := range oldMetadata { + newMetadata[k] = v + } + for i := range args { + if i%2 == 0 { + continue + } + newMetadata[args[i-1]] = args[i] + } + return context.WithValue(ctx, clogContextKey, newMetadata) +} + +var badGlyphs = " \n" + +// Actual log handler; the others have wrappers to properly handle stack depth +func (v *VerboseLogger) log(ctx context.Context, message string, args ...any) { + if !glog.V(v.level) { + return + } + keyCol := color.New(color.FgMagenta).SprintFunc() + valCol := color.New(color.FgGreen).SprintFunc() + messageCol := color.New(color.FgCyan).SprintFunc() + meta, _ := ctx.Value(clogContextKey).(metadata) + allArgs := append([]any{}, meta.Flat()...) + allArgs = append(allArgs, args...) + allArgs = append(allArgs, "caller", caller(3)) + str := messageCol(message) + for i := range allArgs { + if i%2 == 0 { + continue + } + safeVal := fmt.Sprintf("%s", allArgs[i]) + if strings.ContainsAny(safeVal, badGlyphs) { + safeVal = fmt.Sprintf("%q", allArgs[i]) + } + str = fmt.Sprintf("%s %s=%s", str, keyCol(allArgs[i-1]), valCol(safeVal)) + } + fmt.Println(str) +} + +func (v *VerboseLogger) Log(ctx context.Context, message string, args ...any) { + v.log(ctx, message, args...) +} + +func Log(ctx context.Context, message string, args ...any) { + V(defaultLogLevel).log(ctx, message, args...) +} + +// returns filenames relative to aquareum root +// e.g. handlers/misttriggers/triggers.go:58 +func caller(depth int) string { + _, myfile, _, _ := runtime.Caller(0) + // This assumes that the root directory of aquareum is two levels above this folder. + // If that changes, please update this rootDir resolution. + rootDir := filepath.Join(filepath.Dir(myfile), "..", "..") + _, file, line, _ := runtime.Caller(depth) + rel, _ := filepath.Rel(rootDir, file) + return rel + ":" + strconv.Itoa(line) +}