mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat: extend plugin watcher with improved logging and debounce duration adjustment
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
3c5b244b7e
commit
b58493141c
@ -13,7 +13,7 @@ import (
|
||||
|
||||
// debounceDuration is the time to wait before acting on file events
|
||||
// to handle multiple rapid events for the same file.
|
||||
const debounceDuration = 500 * time.Millisecond
|
||||
const debounceDuration = 2 * time.Second
|
||||
|
||||
// startWatcher starts the file watcher for the plugins folder.
|
||||
// It watches for CREATE, WRITE, and REMOVE events on .wasm files.
|
||||
@ -156,16 +156,20 @@ func (m *Manager) processPluginEvent(pluginName string, eventType notify.Event)
|
||||
|
||||
// Determine and execute the appropriate action
|
||||
action := determinePluginAction(eventType, isLoaded)
|
||||
log.Debug("Plugin event action", "plugin", pluginName, "action", action)
|
||||
switch action {
|
||||
case actionLoad:
|
||||
log.Debug("Loading new Plugin", "plugin", pluginName)
|
||||
if err := m.LoadPlugin(pluginName); err != nil {
|
||||
log.Error(m.ctx, "Failed to load plugin", "plugin", pluginName, err)
|
||||
}
|
||||
case actionUnload:
|
||||
log.Debug("Unloading removed Plugin", "plugin", pluginName)
|
||||
if err := m.UnloadPlugin(pluginName); err != nil {
|
||||
log.Debug(m.ctx, "Plugin not loaded, skipping unload", "plugin", pluginName, err)
|
||||
}
|
||||
case actionReload:
|
||||
log.Debug("Reloading modified Plugin", "plugin", pluginName)
|
||||
if err := m.ReloadPlugin(pluginName); err != nil {
|
||||
log.Error(m.ctx, "Failed to reload plugin", "plugin", pluginName, err)
|
||||
}
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/rjeczalik/notify"
|
||||
)
|
||||
|
||||
var _ = Describe("Watcher Integration", Ordered, func() {
|
||||
// Uses testdataDir and createTestManager from BeforeSuite
|
||||
var (
|
||||
manager *Manager
|
||||
tmpDir string
|
||||
ctx context.Context
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
if testing.Short() {
|
||||
Skip("Skipping integration test in short mode")
|
||||
}
|
||||
ctx = GinkgoT().Context()
|
||||
|
||||
// Create manager for watcher lifecycle tests (no plugin preloaded - tests copy plugin as needed)
|
||||
manager, tmpDir = createTestManager(nil)
|
||||
|
||||
// Remove the auto-loaded plugin so tests can control loading
|
||||
_ = manager.UnloadPlugin("test-metadata-agent")
|
||||
_ = os.Remove(filepath.Join(tmpDir, "test-metadata-agent.wasm"))
|
||||
})
|
||||
|
||||
// Helper to copy test plugin into the temp folder
|
||||
copyTestPlugin := func() {
|
||||
srcPath := filepath.Join(testdataDir, "test-metadata-agent.wasm")
|
||||
destPath := filepath.Join(tmpDir, "test-metadata-agent.wasm")
|
||||
data, err := os.ReadFile(srcPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = os.WriteFile(destPath, data, 0600)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
Describe("Plugin event processing (integration)", func() {
|
||||
// These tests verify the full flow with actual WASM plugin loading.
|
||||
|
||||
AfterEach(func() {
|
||||
// Clean up: unload plugin if loaded, remove copied file
|
||||
_ = manager.UnloadPlugin("test-metadata-agent")
|
||||
_ = os.Remove(filepath.Join(tmpDir, "test-metadata-agent.wasm"))
|
||||
})
|
||||
|
||||
It("loads a plugin on CREATE event", func() {
|
||||
copyTestPlugin()
|
||||
manager.processPluginEvent("test-metadata-agent", notify.Create)
|
||||
Expect(manager.PluginNames(string(CapabilityMetadataAgent))).To(ContainElement("test-metadata-agent"))
|
||||
})
|
||||
|
||||
It("reloads a plugin on WRITE event", func() {
|
||||
copyTestPlugin()
|
||||
err := manager.LoadPlugin("test-metadata-agent")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
manager.processPluginEvent("test-metadata-agent", notify.Write)
|
||||
Expect(manager.PluginNames(string(CapabilityMetadataAgent))).To(ContainElement("test-metadata-agent"))
|
||||
})
|
||||
|
||||
It("unloads a plugin on REMOVE event", func() {
|
||||
copyTestPlugin()
|
||||
err := manager.LoadPlugin("test-metadata-agent")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
manager.processPluginEvent("test-metadata-agent", notify.Remove)
|
||||
Expect(manager.PluginNames(string(CapabilityMetadataAgent))).ToNot(ContainElement("test-metadata-agent"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Watcher lifecycle", func() {
|
||||
It("does not start file watcher when AutoReload is disabled", func() {
|
||||
Expect(manager.watcherEvents).To(BeNil())
|
||||
Expect(manager.watcherDone).To(BeNil())
|
||||
})
|
||||
|
||||
It("starts file watcher when AutoReload is enabled", func() {
|
||||
_ = manager.Stop()
|
||||
|
||||
conf.Server.Plugins.AutoReload = true
|
||||
autoReloadManager := &Manager{
|
||||
plugins: make(map[string]*plugin),
|
||||
}
|
||||
err := autoReloadManager.Start(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
DeferCleanup(autoReloadManager.Stop)
|
||||
|
||||
Expect(autoReloadManager.watcherEvents).ToNot(BeNil())
|
||||
Expect(autoReloadManager.watcherDone).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,12 +1,103 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/rjeczalik/notify"
|
||||
)
|
||||
|
||||
var _ = Describe("Watcher", func() {
|
||||
var _ = Describe("Plugin Watcher", func() {
|
||||
Describe("Integration Tests", Ordered, func() {
|
||||
// Uses testdataDir and createTestManager from BeforeSuite
|
||||
var (
|
||||
manager *Manager
|
||||
tmpDir string
|
||||
ctx context.Context
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
ctx = GinkgoT().Context()
|
||||
|
||||
// Create manager for watcher lifecycle tests (no plugin preloaded - tests copy plugin as needed)
|
||||
manager, tmpDir = createTestManager(nil)
|
||||
|
||||
// Remove the auto-loaded plugin so tests can control loading
|
||||
_ = manager.UnloadPlugin("test-metadata-agent")
|
||||
_ = os.Remove(filepath.Join(tmpDir, "test-metadata-agent.wasm"))
|
||||
})
|
||||
|
||||
// Helper to copy test plugin into the temp folder
|
||||
copyTestPlugin := func() {
|
||||
srcPath := filepath.Join(testdataDir, "test-metadata-agent.wasm")
|
||||
destPath := filepath.Join(tmpDir, "test-metadata-agent.wasm")
|
||||
data, err := os.ReadFile(srcPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = os.WriteFile(destPath, data, 0600)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
Describe("Plugin event processing (integration)", func() {
|
||||
// These tests verify the full flow with actual WASM plugin loading.
|
||||
|
||||
AfterEach(func() {
|
||||
// Clean up: unload plugin if loaded, remove copied file
|
||||
_ = manager.UnloadPlugin("test-metadata-agent")
|
||||
_ = os.Remove(filepath.Join(tmpDir, "test-metadata-agent.wasm"))
|
||||
})
|
||||
|
||||
It("loads a plugin on CREATE event", func() {
|
||||
copyTestPlugin()
|
||||
manager.processPluginEvent("test-metadata-agent", notify.Create)
|
||||
Expect(manager.PluginNames(string(CapabilityMetadataAgent))).To(ContainElement("test-metadata-agent"))
|
||||
})
|
||||
|
||||
It("reloads a plugin on WRITE event", func() {
|
||||
copyTestPlugin()
|
||||
err := manager.LoadPlugin("test-metadata-agent")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
manager.processPluginEvent("test-metadata-agent", notify.Write)
|
||||
Expect(manager.PluginNames(string(CapabilityMetadataAgent))).To(ContainElement("test-metadata-agent"))
|
||||
})
|
||||
|
||||
It("unloads a plugin on REMOVE event", func() {
|
||||
copyTestPlugin()
|
||||
err := manager.LoadPlugin("test-metadata-agent")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
manager.processPluginEvent("test-metadata-agent", notify.Remove)
|
||||
Expect(manager.PluginNames(string(CapabilityMetadataAgent))).ToNot(ContainElement("test-metadata-agent"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Watcher lifecycle", func() {
|
||||
It("does not start file watcher when AutoReload is disabled", func() {
|
||||
Expect(manager.watcherEvents).To(BeNil())
|
||||
Expect(manager.watcherDone).To(BeNil())
|
||||
})
|
||||
|
||||
It("starts file watcher when AutoReload is enabled", func() {
|
||||
_ = manager.Stop()
|
||||
|
||||
conf.Server.Plugins.AutoReload = true
|
||||
autoReloadManager := &Manager{
|
||||
plugins: make(map[string]*plugin),
|
||||
}
|
||||
err := autoReloadManager.Start(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
DeferCleanup(autoReloadManager.Stop)
|
||||
|
||||
Expect(autoReloadManager.watcherEvents).ToNot(BeNil())
|
||||
Expect(autoReloadManager.watcherDone).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("determinePluginAction", func() {
|
||||
// These are fast unit tests for the pure routing logic.
|
||||
// No WASM compilation, no file I/O - runs in microseconds.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user