feat(plugins): add subsonicRouter to Manager and refactor host service registration

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-28 12:35:49 -05:00
parent 7c6c49c7a1
commit 6321dc1622
7 changed files with 111 additions and 53 deletions

View File

@ -7,6 +7,7 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -70,8 +71,9 @@ var _ = Describe("ArtworkService", Ordered, func() {
// Create and start manager // Create and start manager
manager = &Manager{ manager = &Manager{
plugins: make(map[string]*plugin), plugins: make(map[string]*plugin),
ds: dataStore, ds: dataStore,
subsonicRouter: http.NotFoundHandler(),
} }
err = manager.Start(GinkgoT().Context()) err = manager.Start(GinkgoT().Context())
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())

View File

@ -8,6 +8,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@ -359,8 +360,9 @@ var _ = Describe("CacheService Integration", Ordered, func() {
// Create and start manager // Create and start manager
manager = &Manager{ manager = &Manager{
plugins: make(map[string]*plugin), plugins: make(map[string]*plugin),
ds: dataStore, ds: dataStore,
subsonicRouter: http.NotFoundHandler(),
} }
err = manager.Start(GinkgoT().Context()) err = manager.Start(GinkgoT().Context())
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())

View File

@ -6,6 +6,7 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@ -75,8 +76,9 @@ var _ = Describe("SchedulerService", Ordered, func() {
// Create and start manager // Create and start manager
manager = &Manager{ manager = &Manager{
plugins: make(map[string]*plugin), plugins: make(map[string]*plugin),
ds: dataStore, ds: dataStore,
subsonicRouter: http.NotFoundHandler(),
} }
err = manager.Start(GinkgoT().Context()) err = manager.Start(GinkgoT().Context())
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())

View File

@ -68,8 +68,9 @@ var _ = Describe("WebSocketService", Ordered, func() {
// Create and start manager // Create and start manager
manager = &Manager{ manager = &Manager{
plugins: make(map[string]*plugin), plugins: make(map[string]*plugin),
ds: dataStore, ds: dataStore,
subsonicRouter: http.NotFoundHandler(),
} }
err = manager.Start(GinkgoT().Context()) err = manager.Start(GinkgoT().Context())
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())

View File

@ -136,6 +136,10 @@ func (m *Manager) Start(ctx context.Context) error {
return nil return nil
} }
if m.subsonicRouter == nil {
log.Fatal(ctx, "Plugin manager requires DataStore to be configured")
}
// Set extism log level based on plugin-specific config or global log level // Set extism log level based on plugin-specific config or global log level
pluginLogLevel := conf.Server.Plugins.LogLevel pluginLogLevel := conf.Server.Plugins.LogLevel
if pluginLogLevel == "" { if pluginLogLevel == "" {
@ -411,13 +415,79 @@ type compiledPluginInfo struct {
compiled *extism.CompiledPlugin compiled *extism.CompiledPlugin
} }
// serviceContext provides dependencies needed by host service factories.
type serviceContext struct {
pluginName string
manager *Manager
permissions *Permissions
}
// hostServiceEntry defines a host service for table-driven registration.
type hostServiceEntry struct {
name string
hasPermission func(*Permissions) bool
registerStubs func() []extism.HostFunction
create func(*serviceContext) ([]extism.HostFunction, io.Closer)
}
// hostServices defines all available host services.
// Adding a new host service only requires adding an entry here.
var hostServices = []hostServiceEntry{
{
name: "SubsonicAPI",
hasPermission: func(p *Permissions) bool { return p != nil && p.Subsonicapi != nil },
registerStubs: func() []extism.HostFunction { return host.RegisterSubsonicAPIHostFunctions(nil) },
create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) {
perm := ctx.permissions.Subsonicapi
service := newSubsonicAPIService(ctx.pluginName, ctx.manager.subsonicRouter, ctx.manager.ds, perm)
return host.RegisterSubsonicAPIHostFunctions(service), nil
},
},
{
name: "Scheduler",
hasPermission: func(p *Permissions) bool { return p != nil && p.Scheduler != nil },
registerStubs: func() []extism.HostFunction { return host.RegisterSchedulerHostFunctions(nil) },
create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) {
service := newSchedulerService(ctx.pluginName, ctx.manager, scheduler.GetInstance())
return host.RegisterSchedulerHostFunctions(service), service
},
},
{
name: "WebSocket",
hasPermission: func(p *Permissions) bool { return p != nil && p.Websocket != nil },
registerStubs: func() []extism.HostFunction { return host.RegisterWebSocketHostFunctions(nil) },
create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) {
perm := ctx.permissions.Websocket
service := newWebSocketService(ctx.pluginName, ctx.manager, perm.AllowedHosts)
return host.RegisterWebSocketHostFunctions(service), service
},
},
{
name: "Artwork",
hasPermission: func(p *Permissions) bool { return p != nil && p.Artwork != nil },
registerStubs: func() []extism.HostFunction { return host.RegisterArtworkHostFunctions(nil) },
create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) {
service := newArtworkService()
return host.RegisterArtworkHostFunctions(service), nil
},
},
{
name: "Cache",
hasPermission: func(p *Permissions) bool { return p != nil && p.Cache != nil },
registerStubs: func() []extism.HostFunction { return host.RegisterCacheHostFunctions(nil) },
create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) {
service := newCacheService(ctx.pluginName)
return host.RegisterCacheHostFunctions(service), service
},
},
}
// stubHostFunctions returns the list of stub host functions needed for initial plugin compilation. // stubHostFunctions returns the list of stub host functions needed for initial plugin compilation.
func stubHostFunctions() []extism.HostFunction { func stubHostFunctions() []extism.HostFunction {
stubs := host.RegisterSubsonicAPIHostFunctions(nil) var stubs []extism.HostFunction
stubs = append(stubs, host.RegisterSchedulerHostFunctions(nil)...) for _, entry := range hostServices {
stubs = append(stubs, host.RegisterWebSocketHostFunctions(nil)...) stubs = append(stubs, entry.registerStubs()...)
stubs = append(stubs, host.RegisterArtworkHostFunctions(nil)...) }
stubs = append(stubs, host.RegisterCacheHostFunctions(nil)...)
return stubs return stubs
} }
@ -748,45 +818,22 @@ func (m *Manager) loadPluginWithConfig(name, wasmPath, configJSON string) error
pluginManifest.AllowedHosts = hosts pluginManifest.AllowedHosts = hosts
} }
// Register SubsonicAPI host functions if permission is granted // Register host functions based on permissions using table-driven approach
if info.manifest.Permissions != nil && info.manifest.Permissions.Subsonicapi != nil { svcCtx := &serviceContext{
perm := info.manifest.Permissions.Subsonicapi pluginName: name,
if m.subsonicRouter != nil && m.ds != nil { manager: m,
service := newSubsonicAPIService(name, m.subsonicRouter, m.ds, perm) permissions: info.manifest.Permissions,
hostFunctions = append(hostFunctions, host.RegisterSubsonicAPIHostFunctions(service)...) }
} else { for _, entry := range hostServices {
log.Warn(m.ctx, "Plugin requires SubsonicAPI but router/datastore not available", "plugin", name) if entry.hasPermission(info.manifest.Permissions) {
funcs, closer := entry.create(svcCtx)
hostFunctions = append(hostFunctions, funcs...)
if closer != nil {
closers = append(closers, closer)
}
} }
} }
// Register Scheduler host functions if permission is granted
if info.manifest.Permissions != nil && info.manifest.Permissions.Scheduler != nil {
service := newSchedulerService(name, m, scheduler.GetInstance())
closers = append(closers, service)
hostFunctions = append(hostFunctions, host.RegisterSchedulerHostFunctions(service)...)
}
// Register WebSocket host functions if permission is granted
if info.manifest.Permissions != nil && info.manifest.Permissions.Websocket != nil {
perm := info.manifest.Permissions.Websocket
service := newWebSocketService(name, m, perm.AllowedHosts)
closers = append(closers, service)
hostFunctions = append(hostFunctions, host.RegisterWebSocketHostFunctions(service)...)
}
// Register Artwork host functions if permission is granted
if info.manifest.Permissions != nil && info.manifest.Permissions.Artwork != nil {
service := newArtworkService()
hostFunctions = append(hostFunctions, host.RegisterArtworkHostFunctions(service)...)
}
// Register Cache host functions if permission is granted
if info.manifest.Permissions != nil && info.manifest.Permissions.Cache != nil {
service := newCacheService(name)
closers = append(closers, service)
hostFunctions = append(hostFunctions, host.RegisterCacheHostFunctions(service)...)
}
// Check if the plugin needs to be recompiled with real host functions // Check if the plugin needs to be recompiled with real host functions
compiled := info.compiled compiled := info.compiled
needsRecompile := len(pluginManifest.AllowedHosts) > 0 || len(hostFunctions) > 0 needsRecompile := len(pluginManifest.AllowedHosts) > 0 || len(hostFunctions) > 0

View File

@ -6,6 +6,7 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -113,8 +114,9 @@ func createTestManagerWithPlugins(pluginConfig map[string]map[string]string, plu
// Create and start manager // Create and start manager
manager := &Manager{ manager := &Manager{
plugins: make(map[string]*plugin), plugins: make(map[string]*plugin),
ds: dataStore, ds: dataStore,
subsonicRouter: http.NotFoundHandler(), // Stub router for tests
} }
err = manager.Start(GinkgoT().Context()) err = manager.Start(GinkgoT().Context())
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())

View File

@ -2,6 +2,7 @@ package plugins
import ( import (
"context" "context"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -135,8 +136,9 @@ var _ = Describe("Plugin Watcher", func() {
dataStore := &tests.MockDataStore{MockedPlugin: mockPluginRepo} dataStore := &tests.MockDataStore{MockedPlugin: mockPluginRepo}
autoReloadManager := &Manager{ autoReloadManager := &Manager{
plugins: make(map[string]*plugin), plugins: make(map[string]*plugin),
ds: dataStore, ds: dataStore,
subsonicRouter: http.NotFoundHandler(),
} }
err := autoReloadManager.Start(ctx) err := autoReloadManager.Start(ctx)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())