package app import ( "context" "errors" "fmt" "log" "net" "net/http" "sort" "time" "supervisor/internal/config" "supervisor/internal/httpserver" "supervisor/internal/session" "supervisor/internal/store/memory" "supervisor/internal/supervisor" "supervisor/internal/util" ) type App struct { cfg config.Config logger *log.Logger httpServer *http.Server SessionManager *session.Manager Supervisor *supervisor.Manager } func New(cfg config.Config) (*App, error) { logger := util.NewLogger() memStore := memory.NewStore() sessionManager := session.NewManager(memStore, nil) supervisorManager := supervisor.NewManager() router, err := httpserver.NewRouter(httpserver.Dependencies{ Logger: logger, Manager: sessionManager, }) if err != nil { return nil, fmt.Errorf("build router: %w", err) } srv := &http.Server{ Addr: cfg.Addr, Handler: router, ReadHeaderTimeout: 10 * time.Second, } return &App{ cfg: cfg, logger: logger, httpServer: srv, SessionManager: sessionManager, Supervisor: supervisorManager, }, nil } func (a *App) Run(ctx context.Context) error { errCh := make(chan error, 1) go func() { a.logServerURLs() errCh <- a.httpServer.ListenAndServe() }() select { case <-ctx.Done(): shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _ = a.httpServer.Shutdown(shutdownCtx) return nil case err := <-errCh: if errors.Is(err, http.ErrServerClosed) { return nil } return err } } func (a *App) logServerURLs() { urls, err := serverURLs(a.cfg.Addr) if err != nil { a.logger.Printf("warning: determine local server URLs: %v", err) } if len(urls) == 0 { a.logger.Printf("HTTP server listening on %s", a.cfg.Addr) return } a.logger.Print("HTTP server available at:") for _, url := range urls { a.logger.Printf(" %s", url) } } func serverURLs(addr string) ([]string, error) { return serverURLsWithLocalIPv4Addrs(addr, discoverLocalIPv4Addrs) } func serverURLsWithLocalIPv4Addrs(addr string, localIPv4Addrs func() ([]string, error)) ([]string, error) { host, port, err := net.SplitHostPort(addr) if err != nil { return nil, err } urls := make([]string, 0, 4) seen := make(map[string]struct{}) addURL := func(host string) { if host == "" { return } url := "http://" + net.JoinHostPort(host, port) if _, ok := seen[url]; ok { return } seen[url] = struct{}{} urls = append(urls, url) } if isWildcardHost(host) { addURL("localhost") addURL("127.0.0.1") addrs, err := localIPv4Addrs() for _, addr := range addrs { ip := net.ParseIP(addr) if !isUsableLocalIPv4(ip) { continue } addURL(ip.String()) } return urls, err } ip := net.ParseIP(host) if ip == nil { addURL(host) return urls, nil } if ip4 := ip.To4(); ip4 != nil && !ip4.IsUnspecified() { addURL(ip4.String()) } return urls, nil } func isWildcardHost(host string) bool { return host == "" || host == "0.0.0.0" || host == "::" } func discoverLocalIPv4Addrs() ([]string, error) { interfaces, err := net.Interfaces() if err != nil { return nil, err } addrs := make([]string, 0) seen := make(map[string]struct{}) for _, iface := range interfaces { if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { continue } ifaceAddrs, err := iface.Addrs() if err != nil { return addrs, err } for _, ifaceAddr := range ifaceAddrs { ip := ipFromAddr(ifaceAddr) if !isUsableLocalIPv4(ip) { continue } addr := ip.To4().String() if _, ok := seen[addr]; ok { continue } seen[addr] = struct{}{} addrs = append(addrs, addr) } } sort.Strings(addrs) return addrs, nil } func ipFromAddr(addr net.Addr) net.IP { switch v := addr.(type) { case *net.IPNet: return v.IP case *net.IPAddr: return v.IP default: return nil } } func isUsableLocalIPv4(ip net.IP) bool { if ip == nil { return false } ip4 := ip.To4() if ip4 == nil { return false } return !ip4.IsUnspecified() && !ip4.IsLoopback() && !ip4.IsMulticast() && !ip4.IsLinkLocalUnicast() }