goshort/internal/server/devui/server.go
Gustavo Maronato 28b49fb78f
All checks were successful
Check / checks (push) Successful in 2m57s
use odc instead of oidc for url
2024-03-23 14:56:02 -04:00

147 lines
3.4 KiB
Go

package devuiserver
import (
"context"
"fmt"
"log/slog"
"net"
"net/url"
"os"
"os/exec"
"syscall"
"git.maronato.dev/maronato/goshort/internal/config"
"git.maronato.dev/maronato/goshort/internal/util/logging"
"golang.org/x/sync/errgroup"
)
type Server struct {
// apiUrl is API server's URL
apiURL string
// oidcURL is OIDC server's URL
oidcURL string
// uiPort is the port the UI server will listen on
uiPort string
// host is the host the UI server will listen on
host string
// Cancel function to stop the server
cancel context.CancelFunc
}
// NewServer creates a new dev server.
func NewServer(cfg *config.Config) *Server {
apiURL := url.URL{
Host: net.JoinHostPort(cfg.Host, cfg.Port),
Scheme: "http",
Path: "/api",
}
oidcURL := url.URL{
Host: net.JoinHostPort(cfg.Host, cfg.Port),
Scheme: "http",
Path: "/odc",
}
return &Server{
apiURL: apiURL.String(),
oidcURL: oidcURL.String(),
uiPort: cfg.UIPort,
host: cfg.Host,
cancel: func() {},
}
}
func (s *Server) ListenAndServe(ctx context.Context) error {
l := logging.FromCtx(ctx)
eg, egCtx := errgroup.WithContext(ctx)
eg.Go(func() error {
l.Info("Starting UI dev server", slog.String("addr", net.JoinHostPort(s.host, s.uiPort)))
return s.Start(egCtx)
})
eg.Go(func() error {
// Wait for the context to be done
<-egCtx.Done()
// Shutdown the server
l.Info("Shutting down UI dev server")
return s.Shutdown()
})
if err := eg.Wait(); err != nil {
return fmt.Errorf("UI server exited with error: %w", err)
}
return nil
}
func (s *Server) Start(ctx context.Context) error {
l := logging.FromCtx(ctx)
// Create a new context with a cancel function so we can stop the server
uiCtx, cancel := context.WithCancel(ctx)
defer cancel()
s.cancel = cancel
// Build args for the UI server command
args := []string{"run", "--prefix", "frontend", "dev", "--", "--port", s.uiPort, "--host", s.host}
// Create the command
cmd := exec.Command("npm", args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
// Create a new process group so we can kill the process and all its children
Setpgid: true,
}
// Set the API_URL env var
cmd.Env = append(os.Environ(), "VITE_API_URL="+s.apiURL, "VITE_OIDC_URL="+s.oidcURL)
// Use the current process's stdout
cmd.Stdout = os.Stdout
// Create an errgroup to run the command and wait for the context to be done
eg, egCtx := errgroup.WithContext(uiCtx)
eg.Go(func() error {
// Execute the command
if err := cmd.Run(); err != nil {
return fmt.Errorf("error starting the server: %w", err)
}
return cmd.Wait() //nolint:wrapcheck // We wrap the error when we return from the errgroup
})
eg.Go(func() error {
// Wait for the context to be done
<-egCtx.Done()
// Get process pid and manually send a SIGKILL signal
// to the process group
pgid, err := syscall.Getpgid(cmd.Process.Pid)
if err != nil {
return fmt.Errorf("error getting process group id: %w", err)
}
err = syscall.Kill(-pgid, syscall.SIGKILL)
if err != nil {
return fmt.Errorf("error killing process group: %w", err)
}
l.Info("UI dev server shutdown complete")
return nil
})
// Wait for the context to be done and report erros that are not
// caused by the context being canceled
if err := eg.Wait(); err != nil && ctx.Err() == nil {
return fmt.Errorf("UI server exited with error: %w", err)
}
return nil
}
func (s *Server) Shutdown() error {
// Call the cancel function to stop the server
s.cancel()
return nil
}