141 lines
3.3 KiB
Go
141 lines
3.3 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
|
|
// 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",
|
|
}
|
|
|
|
return &Server{
|
|
apiURL: apiURL.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)
|
|
|
|
// 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
|
|
|
|
// Start the command execution
|
|
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
|
|
}
|