mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
feat(plugins): add subsonicRouter to Manager and refactor host service registration
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
7c6c49c7a1
commit
6321dc1622
@ -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())
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user