diff --git a/cmd/flamenco-manager/main.go b/cmd/flamenco-manager/main.go index ba87a201..b5bdd96a 100644 --- a/cmd/flamenco-manager/main.go +++ b/cmd/flamenco-manager/main.go @@ -73,7 +73,7 @@ func main() { // Construct the services. persist := openDB(*configService) flamenco := buildFlamencoAPI(configService, persist) - e := buildWebService(flamenco, persist) + e := buildWebService(flamenco, persist, ssdp) installSignalHandler(mainCtxCancel) @@ -121,7 +121,11 @@ func buildFlamencoAPI(configService *config.Service, persist *persistence.DB) ap return flamenco } -func buildWebService(flamenco api.ServerInterface, persist api_impl.PersistenceService) *echo.Echo { +func buildWebService( + flamenco api.ServerInterface, + persist api_impl.PersistenceService, + ssdp *upnp_ssdp.Server, +) *echo.Echo { e := echo.New() e.HideBanner = true @@ -154,6 +158,13 @@ func buildWebService(flamenco api.ServerInterface, persist api_impl.PersistenceS return c.Redirect(http.StatusTemporaryRedirect, "/api/swagger-ui/") }) + // Serve UPnP service descriptions. + if ssdp != nil { + e.GET(ssdp.DescriptionPath(), func(c echo.Context) error { + return c.XMLPretty(http.StatusOK, ssdp.Description(), " ") + }) + } + // Log available routes routeLogger := log.Level(zerolog.DebugLevel) routeLogger.Debug().Msg("available routes:") diff --git a/internal/upnp_ssdp/server.go b/internal/upnp_ssdp/server.go index 7d2fc5d5..a2b9b9da 100644 --- a/internal/upnp_ssdp/server.go +++ b/internal/upnp_ssdp/server.go @@ -22,8 +22,11 @@ package upnp_ssdp import ( "context" + "fmt" "net/url" + "path" + "git.blender.org/flamenco/internal/appinfo" "github.com/fromkeith/gossdp" "github.com/rs/zerolog" ) @@ -60,8 +63,9 @@ func (s *Server) AddAdvertisement(serviceLocation string) { // AddAdvertisementURLs constructs a service location from the given URLs, and // adds the advertisement for it. -func (s *Server) AddAdvertisementURLs(urls []url.URL) { - for _, url := range urls { +func (s *Server) AddAdvertisementURLs(baseURLs []url.URL) { + for _, url := range baseURLs { + url.Path = path.Join(url.Path, serviceDescriptionPath) s.AddAdvertisement(url.String()) } } @@ -99,3 +103,26 @@ func (s *Server) Run(ctx context.Context) { s.log.Info().Msg("UPnP/SSDP advertisement stopped") } + +func (s *Server) Description() Description { + return Description{ + SpecVersion: SpecVersion{1, 0}, + URLBase: "/", + Device: Device{ + DeviceType: FlamencoServiceType, + FriendlyName: appinfo.FormattedApplicationInfo(), + ModelName: appinfo.ApplicationName, + ModelDescription: "Flamenco render farm, Manager component", + ModelURL: "https://flamenco.io/", + UDN: fmt.Sprintf("uuid:%s", FlamencoUUID), + Manufacturer: "Blender", + ManufacturerURL: "https://www.blender.org/", + ServiceList: []string{}, + PresentationURL: "/", + }, + } +} + +func (s *Server) DescriptionPath() string { + return serviceDescriptionPath +} diff --git a/internal/upnp_ssdp/upnp_ssdp.go b/internal/upnp_ssdp/upnp_ssdp.go index 210afcd6..6dd6a8b8 100644 --- a/internal/upnp_ssdp/upnp_ssdp.go +++ b/internal/upnp_ssdp/upnp_ssdp.go @@ -24,4 +24,30 @@ package upnp_ssdp const ( FlamencoUUID = "aa80bc5f-d0af-46b8-8630-23bd7e80ec4d" FlamencoServiceType = "urn:flamenco:manager:0" + + serviceDescriptionPath = "/upnp/description.xml" ) + +// SSDP Service description, usually served on some URL ending in `/description.xml`. +type Description struct { + XMLName string `xml:"urn:schemas-upnp-org:device-1-0 root"` + SpecVersion SpecVersion `xml:"specVersion"` + URLBase string `xml:"URLBase"` + Device Device `xml:"device"` +} +type SpecVersion struct { + Major int `xml:"major"` + Minor int `xml:"minor"` +} +type Device struct { + DeviceType string `xml:"deviceType"` + FriendlyName string `xml:"friendlyName"` + Manufacturer string `xml:"manufacturer"` + ManufacturerURL string `xml:"manufacturerURL"` + ModelDescription string `xml:"modelDescription"` + ModelName string `xml:"modelName"` + ModelURL string `xml:"modelURL"` + UDN string `xml:"UDN"` + ServiceList []string `xml:"serviceList"` // not []string, but since the list is empty, it doesn't matter. + PresentationURL string `xml:"presentationURL"` +} diff --git a/internal/worker/autodiscovery.go b/internal/worker/autodiscovery.go index d62f2cd9..44ab00f6 100644 --- a/internal/worker/autodiscovery.go +++ b/internal/worker/autodiscovery.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "net/url" "sync" "time" @@ -76,14 +77,28 @@ func autodiscoverManager(ctx context.Context) (string, error) { return usableURLs[0], nil } -// pingManager connects to a Manager and returns true if it responds. -func pingManager(ctx context.Context, url string) bool { - logger := log.With().Str("manager", url).Logger() +// pingManager connects to a Manager and returns the service URL if it responds. +func pingManager(ctx context.Context, descriptionURL string) string { + var err error + parsedURL, err := url.Parse(descriptionURL) + if err != nil { + log.Warn(). + Str("description", descriptionURL). + Err(err). + Msg("received invalid URL from autodiscovery, ignoring") + return "" + } - client, err := api.NewClientWithResponses(url) + // TODO: actually get the description XML from Flamenco Manager and use the path in there. + // For now, just assume it's on the root. + parsedURL.Path = "/" + serviceURL := parsedURL.String() + + logger := log.With().Str("url", serviceURL).Logger() + client, err := api.NewClientWithResponses(serviceURL) if err != nil { logger.Warn().Err(err).Msg("unable to create API client with this URL") - return false + return "" } resp, err := client.GetVersionWithResponse(ctx) @@ -91,21 +106,21 @@ func pingManager(ctx context.Context, url string) bool { // It is expected that some URLs will not work. Showing the error message at // info/warn level will likely confuse people, so leave it at debug level. logger.Debug().Err(err).Msg("unable to get Flamenco version from Manager") - return false + return "" } if resp.JSON200 == nil { logger.Warn(). Int("httpStatus", resp.StatusCode()). Msg("unable to get Flamenco version, unexpected reply") - return false + return "" } logger.Info(). Str("version", resp.JSON200.Version). Str("name", resp.JSON200.Name). Msg("found Flamenco Manager") - return true + return serviceURL } // pingManagers pings all URLs in parallel, returning only those that responded. @@ -116,19 +131,16 @@ func pingManagers(ctx context.Context, urls []string) []string { wg.Add(len(urls)) mutex := new(sync.Mutex) - pingURL := func(idx int, url string) { + pingURL := func(idx int, descriptionURL string) { defer wg.Done() - ok := pingManager(ctx, url) + serviceURL := pingManager(ctx, descriptionURL) mutex.Lock() defer mutex.Unlock() - if !ok { - // Erase the URL from the usable list. - // Modifying the original slice instead of appending to a new one ensures - // the original order is maintained. - urls[idx] = "" - } + // This is either the actual service URL, or an empty string if the + // description URL couldn't be reached/parsed. + urls[idx] = serviceURL } for idx, url := range urls {