Files
SpotiFLAC-Mobile/go_backend/extension_manager.go
T
2026-01-17 10:04:21 +07:00

962 lines
28 KiB
Go

// Package gobackend provides extension management functionality
package gobackend
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/dop251/goja"
)
// compareVersions compares two semantic version strings
// Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
func compareVersions(v1, v2 string) int {
parts1 := strings.Split(strings.TrimPrefix(v1, "v"), ".")
parts2 := strings.Split(strings.TrimPrefix(v2, "v"), ".")
maxLen := len(parts1)
if len(parts2) > maxLen {
maxLen = len(parts2)
}
for i := 0; i < maxLen; i++ {
var n1, n2 int
if i < len(parts1) {
n1, _ = strconv.Atoi(parts1[i])
}
if i < len(parts2) {
n2, _ = strconv.Atoi(parts2[i])
}
if n1 < n2 {
return -1
}
if n1 > n2 {
return 1
}
}
return 0
}
// LoadedExtension represents an extension that has been loaded into memory
type LoadedExtension struct {
ID string `json:"id"`
Manifest *ExtensionManifest `json:"manifest"`
VM *goja.Runtime `json:"-"`
Enabled bool `json:"enabled"`
Error string `json:"error,omitempty"`
DataDir string `json:"data_dir"`
SourceDir string `json:"source_dir"`
IconPath string `json:"icon_path"`
}
// ExtensionManager manages all loaded extensions
type ExtensionManager struct {
mu sync.RWMutex
extensions map[string]*LoadedExtension
extensionsDir string // Base directory for extensions
dataDir string // Base directory for extension data
}
var (
globalExtManager *ExtensionManager
globalExtManagerOnce sync.Once
)
// GetExtensionManager returns the global extension manager instance
func GetExtensionManager() *ExtensionManager {
globalExtManagerOnce.Do(func() {
globalExtManager = &ExtensionManager{
extensions: make(map[string]*LoadedExtension),
}
})
return globalExtManager
}
// SetDirectories sets the extensions and data directories
func (m *ExtensionManager) SetDirectories(extensionsDir, dataDir string) error {
m.mu.Lock()
defer m.mu.Unlock()
m.extensionsDir = extensionsDir
m.dataDir = dataDir
if err := os.MkdirAll(extensionsDir, 0755); err != nil {
return fmt.Errorf("failed to create extensions directory: %w", err)
}
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
return nil
}
// LoadExtensionFromFile loads an extension from a .spotiflac-ext file
func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtension, error) {
// Validate file extension
if !strings.HasSuffix(strings.ToLower(filePath), ".spotiflac-ext") {
return nil, fmt.Errorf("Invalid file format. Please select a .spotiflac-ext file")
}
// Open the zip file
zipReader, err := zip.OpenReader(filePath)
if err != nil {
return nil, fmt.Errorf("Cannot open extension file. The file may be corrupted or not a valid extension package")
}
defer zipReader.Close()
var manifestData []byte
var hasIndexJS bool
for _, file := range zipReader.File {
name := filepath.Base(file.Name)
if name == "manifest.json" {
rc, err := file.Open()
if err != nil {
return nil, fmt.Errorf("failed to open manifest.json: %w", err)
}
manifestData, err = io.ReadAll(rc)
rc.Close()
if err != nil {
return nil, fmt.Errorf("failed to read manifest.json: %w", err)
}
}
if name == "index.js" {
hasIndexJS = true
}
}
if manifestData == nil {
return nil, fmt.Errorf("Invalid extension package: manifest.json not found")
}
if !hasIndexJS {
return nil, fmt.Errorf("Invalid extension package: index.js not found")
}
manifest, err := ParseManifest(manifestData)
if err != nil {
return nil, fmt.Errorf("Invalid extension manifest: %w", err)
}
m.mu.RLock()
existing, exists := m.extensions[manifest.Name]
var existingVersion string
var existingDisplayName string
if exists {
existingVersion = existing.Manifest.Version
existingDisplayName = existing.Manifest.DisplayName
}
m.mu.RUnlock()
if exists {
versionCompare := compareVersions(manifest.Version, existingVersion)
if versionCompare > 0 {
// This is an upgrade - call UpgradeExtension
return m.UpgradeExtension(filePath)
} else if versionCompare == 0 {
return nil, fmt.Errorf("Extension '%s' v%s is already installed", existingDisplayName, existingVersion)
} else {
return nil, fmt.Errorf("Cannot downgrade '%s' from v%s to v%s", existingDisplayName, existingVersion, manifest.Version)
}
}
m.mu.Lock()
defer m.mu.Unlock()
if _, exists := m.extensions[manifest.Name]; exists {
return nil, fmt.Errorf("Extension '%s' was installed by another process", manifest.DisplayName)
}
extDir := filepath.Join(m.extensionsDir, manifest.Name)
if err := os.MkdirAll(extDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create extension directory: %w", err)
}
// Extract all files (preserving directory structure)
for _, file := range zipReader.File {
if file.FileInfo().IsDir() {
continue
}
// Preserve relative path within the zip (support subdirectories)
// Clean the path to prevent path traversal attacks
relPath := filepath.Clean(file.Name)
if strings.HasPrefix(relPath, "..") || filepath.IsAbs(relPath) {
GoLog("[Extension] Skipping unsafe path in archive: %s\n", file.Name)
continue
}
destPath := filepath.Join(extDir, relPath)
destDir := filepath.Dir(destPath)
if err := os.MkdirAll(destDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create directory %s: %w", destDir, err)
}
destFile, err := os.Create(destPath)
if err != nil {
return nil, fmt.Errorf("failed to create file %s: %w", destPath, err)
}
srcFile, err := file.Open()
if err != nil {
destFile.Close()
return nil, fmt.Errorf("failed to open file in archive: %w", err)
}
_, err = io.Copy(destFile, srcFile)
srcFile.Close()
destFile.Close()
if err != nil {
return nil, fmt.Errorf("failed to extract file: %w", err)
}
}
extDataDir := filepath.Join(m.dataDir, manifest.Name)
if err := os.MkdirAll(extDataDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create extension data directory: %w", err)
}
ext := &LoadedExtension{
ID: manifest.Name,
Manifest: manifest,
Enabled: false, // New extensions start disabled
DataDir: extDataDir,
SourceDir: extDir,
}
// Initialize Goja VM
if err := m.initializeVM(ext); err != nil {
ext.Error = err.Error()
ext.Enabled = false
GoLog("[Extension] Failed to initialize VM for %s: %v\n", manifest.Name, err)
}
m.extensions[manifest.Name] = ext
GoLog("[Extension] Loaded extension: %s v%s\n", manifest.DisplayName, manifest.Version)
return ext, nil
}
// initializeVM creates and initializes the Goja VM for an extension
func (m *ExtensionManager) initializeVM(ext *LoadedExtension) error {
vm := goja.New()
ext.VM = vm
indexPath := filepath.Join(ext.SourceDir, "index.js")
jsCode, err := os.ReadFile(indexPath)
if err != nil {
return fmt.Errorf("failed to read index.js: %w", err)
}
runtime := NewExtensionRuntime(ext)
runtime.RegisterAPIs(vm)
runtime.RegisterGoBackendAPIs(vm)
console := vm.NewObject()
console.Set("log", func(call goja.FunctionCall) goja.Value {
args := make([]interface{}, len(call.Arguments))
for i, arg := range call.Arguments {
args[i] = arg.Export()
}
GoLog("[Extension:%s] %v\n", ext.ID, args)
return goja.Undefined()
})
vm.Set("console", console)
var registeredExtension goja.Value
vm.Set("registerExtension", func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) > 0 {
registeredExtension = call.Arguments[0]
vm.Set("extension", call.Arguments[0])
}
return goja.Undefined()
})
// Run the extension code
_, err = vm.RunString(string(jsCode))
if err != nil {
return fmt.Errorf("failed to execute extension code: %w", err)
}
// Verify extension was registered
if registeredExtension == nil || goja.IsUndefined(registeredExtension) {
return fmt.Errorf("extension did not call registerExtension()")
}
return nil
}
// UnloadExtension unloads an extension by ID
func (m *ExtensionManager) UnloadExtension(extensionID string) error {
m.mu.Lock()
defer m.mu.Unlock()
ext, exists := m.extensions[extensionID]
if !exists {
return fmt.Errorf("Extension not found")
}
// Call cleanup if VM is initialized
if ext.VM != nil {
// Try to call cleanup function
cleanup, err := ext.VM.RunString("typeof extension !== 'undefined' && typeof extension.cleanup === 'function' ? extension.cleanup() : null")
if err != nil {
GoLog("[Extension] Error calling cleanup for %s: %v\n", extensionID, err)
} else if cleanup != nil && !goja.IsUndefined(cleanup) && !goja.IsNull(cleanup) {
GoLog("[Extension] Cleanup called for %s\n", extensionID)
}
}
// Remove from registry
delete(m.extensions, extensionID)
GoLog("[Extension] Unloaded extension: %s\n", extensionID)
return nil
}
// GetExtension returns a loaded extension by ID
// Returns error if extension not found (gomobile compatible)
func (m *ExtensionManager) GetExtension(extensionID string) (*LoadedExtension, error) {
m.mu.RLock()
defer m.mu.RUnlock()
ext, exists := m.extensions[extensionID]
if !exists {
return nil, fmt.Errorf("Extension not found")
}
return ext, nil
}
// GetAllExtensions returns all loaded extensions
func (m *ExtensionManager) GetAllExtensions() []*LoadedExtension {
m.mu.RLock()
defer m.mu.RUnlock()
result := make([]*LoadedExtension, 0, len(m.extensions))
for _, ext := range m.extensions {
result = append(result, ext)
}
return result
}
// SetExtensionEnabled enables or disables an extension
func (m *ExtensionManager) SetExtensionEnabled(extensionID string, enabled bool) error {
m.mu.Lock()
defer m.mu.Unlock()
ext, exists := m.extensions[extensionID]
if !exists {
return fmt.Errorf("Extension not found")
}
ext.Enabled = enabled
GoLog("[Extension] %s %s\n", extensionID, map[bool]string{true: "enabled", false: "disabled"}[enabled])
// Persist enabled state to settings store
store := GetExtensionSettingsStore()
if err := store.Set(extensionID, "_enabled", enabled); err != nil {
GoLog("[Extension] Failed to persist enabled state for %s: %v\n", extensionID, err)
}
return nil
}
// LoadExtensionsFromDirectory scans a directory and loads all valid extensions
func (m *ExtensionManager) LoadExtensionsFromDirectory(dirPath string) ([]string, []error) {
var loaded []string
var errors []error
entries, err := os.ReadDir(dirPath)
if err != nil {
if os.IsNotExist(err) {
return loaded, errors
}
return nil, []error{fmt.Errorf("failed to read extensions directory: %w", err)}
}
for _, entry := range entries {
if entry.IsDir() {
manifestPath := filepath.Join(dirPath, entry.Name(), "manifest.json")
if _, err := os.Stat(manifestPath); err == nil {
ext, err := m.loadExtensionFromDirectory(filepath.Join(dirPath, entry.Name()))
if err != nil {
GoLog("[Extension] Failed to load %s: %v\n", entry.Name(), err)
errors = append(errors, fmt.Errorf("%s: %w", entry.Name(), err))
} else {
loaded = append(loaded, ext.ID)
}
}
} else if strings.HasSuffix(strings.ToLower(entry.Name()), ".spotiflac-ext") {
ext, err := m.LoadExtensionFromFile(filepath.Join(dirPath, entry.Name()))
if err != nil {
GoLog("[Extension] Failed to load %s: %v\n", entry.Name(), err)
errors = append(errors, fmt.Errorf("%s: %w", entry.Name(), err))
} else {
loaded = append(loaded, ext.ID)
}
}
}
return loaded, errors
}
// loadExtensionFromDirectory loads an extension from an already extracted directory
func (m *ExtensionManager) loadExtensionFromDirectory(dirPath string) (*LoadedExtension, error) {
m.mu.Lock()
defer m.mu.Unlock()
manifestPath := filepath.Join(dirPath, "manifest.json")
manifestData, err := os.ReadFile(manifestPath)
if err != nil {
return nil, fmt.Errorf("failed to read manifest.json: %w", err)
}
// Parse and validate manifest
manifest, err := ParseManifest(manifestData)
if err != nil {
return nil, fmt.Errorf("Invalid extension manifest: %w", err)
}
indexPath := filepath.Join(dirPath, "index.js")
if _, err := os.Stat(indexPath); os.IsNotExist(err) {
return nil, fmt.Errorf("Extension is missing index.js file")
}
if existing, exists := m.extensions[manifest.Name]; exists {
GoLog("[Extension] Extension '%s' already loaded, skipping\n", manifest.DisplayName)
return existing, nil
}
extDataDir := filepath.Join(m.dataDir, manifest.Name)
if err := os.MkdirAll(extDataDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create extension data directory: %w", err)
}
ext := &LoadedExtension{
ID: manifest.Name,
Manifest: manifest,
Enabled: false, // Will be restored from settings store
DataDir: extDataDir,
SourceDir: dirPath,
}
// Restore enabled state from settings store
store := GetExtensionSettingsStore()
if enabledVal, err := store.Get(manifest.Name, "_enabled"); err == nil {
if enabled, ok := enabledVal.(bool); ok {
ext.Enabled = enabled
GoLog("[Extension] Restored enabled state for %s: %v\n", manifest.Name, enabled)
}
}
// Initialize Goja VM
if err := m.initializeVM(ext); err != nil {
ext.Error = err.Error()
ext.Enabled = false
GoLog("[Extension] Failed to initialize VM for %s: %v\n", manifest.Name, err)
}
m.extensions[manifest.Name] = ext
GoLog("[Extension] Loaded extension: %s v%s\n", manifest.DisplayName, manifest.Version)
return ext, nil
}
// RemoveExtension completely removes an extension (unload + delete files)
func (m *ExtensionManager) RemoveExtension(extensionID string) error {
ext, err := m.GetExtension(extensionID)
if err != nil {
return err
}
// Unload first
if err := m.UnloadExtension(extensionID); err != nil {
return err
}
// Remove source directory
if ext.SourceDir != "" {
if err := os.RemoveAll(ext.SourceDir); err != nil {
GoLog("[Extension] Warning: failed to remove source dir: %v\n", err)
}
}
// Optionally remove data directory (keep for now to preserve settings)
// if ext.DataDir != "" {
// os.RemoveAll(ext.DataDir)
// }
return nil
}
// UpgradeExtension upgrades an existing extension from a new package file
// Only allows upgrades (new version > current version), not downgrades
func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension, error) {
// Validate file extension
if !strings.HasSuffix(strings.ToLower(filePath), ".spotiflac-ext") {
return nil, fmt.Errorf("Invalid file format. Please select a .spotiflac-ext file")
}
// Open the zip file
zipReader, err := zip.OpenReader(filePath)
if err != nil {
return nil, fmt.Errorf("Cannot open extension file. The file may be corrupted or not a valid extension package")
}
defer zipReader.Close()
var manifestData []byte
var hasIndexJS bool
for _, file := range zipReader.File {
name := filepath.Base(file.Name)
if name == "manifest.json" {
rc, err := file.Open()
if err != nil {
return nil, fmt.Errorf("failed to open manifest.json: %w", err)
}
manifestData, err = io.ReadAll(rc)
rc.Close()
if err != nil {
return nil, fmt.Errorf("failed to read manifest.json: %w", err)
}
}
if name == "index.js" {
hasIndexJS = true
}
}
if manifestData == nil {
return nil, fmt.Errorf("Invalid extension package: manifest.json not found")
}
if !hasIndexJS {
return nil, fmt.Errorf("Invalid extension package: index.js not found")
}
newManifest, err := ParseManifest(manifestData)
if err != nil {
return nil, fmt.Errorf("Invalid extension manifest: %w", err)
}
m.mu.RLock()
existing, exists := m.extensions[newManifest.Name]
m.mu.RUnlock()
if !exists {
return nil, fmt.Errorf("Extension '%s' is not installed. Use install instead of upgrade.", newManifest.DisplayName)
}
// Compare versions - only allow upgrade, not downgrade
versionCompare := compareVersions(newManifest.Version, existing.Manifest.Version)
if versionCompare < 0 {
return nil, fmt.Errorf("Cannot downgrade extension. Current version: %s, New version: %s", existing.Manifest.Version, newManifest.Version)
}
if versionCompare == 0 {
return nil, fmt.Errorf("Extension is already at version %s", existing.Manifest.Version)
}
GoLog("[Extension] Upgrading %s from v%s to v%s\n", newManifest.DisplayName, existing.Manifest.Version, newManifest.Version)
// Save data directory path and enabled state (we want to preserve them)
extDataDir := existing.DataDir
extDir := existing.SourceDir
wasEnabled := existing.Enabled
// Cleanup and unload existing extension
m.CleanupExtension(existing.ID)
m.UnloadExtension(existing.ID)
// Remove old source files but keep data directory
if extDir != "" {
if err := os.RemoveAll(extDir); err != nil {
GoLog("[Extension] Warning: failed to remove old source dir: %v\n", err)
}
}
if err := os.MkdirAll(extDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create extension directory: %w", err)
}
for _, file := range zipReader.File {
if file.FileInfo().IsDir() {
continue
}
relPath := filepath.Clean(file.Name)
if strings.HasPrefix(relPath, "..") || filepath.IsAbs(relPath) {
GoLog("[Extension] Skipping unsafe path in archive: %s\n", file.Name)
continue
}
destPath := filepath.Join(extDir, relPath)
destDir := filepath.Dir(destPath)
if err := os.MkdirAll(destDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create directory %s: %w", destDir, err)
}
destFile, err := os.Create(destPath)
if err != nil {
return nil, fmt.Errorf("failed to create file %s: %w", destPath, err)
}
srcFile, err := file.Open()
if err != nil {
destFile.Close()
return nil, fmt.Errorf("failed to open file in archive: %w", err)
}
_, err = io.Copy(destFile, srcFile)
srcFile.Close()
destFile.Close()
if err != nil {
return nil, fmt.Errorf("failed to extract file: %w", err)
}
}
ext := &LoadedExtension{
ID: newManifest.Name,
Manifest: newManifest,
Enabled: wasEnabled, // Preserve enabled state from before upgrade
DataDir: extDataDir,
SourceDir: extDir,
}
// Initialize Goja VM
if err := m.initializeVM(ext); err != nil {
ext.Error = err.Error()
ext.Enabled = false
GoLog("[Extension] Failed to initialize VM for %s: %v\n", newManifest.Name, err)
}
m.mu.Lock()
m.extensions[newManifest.Name] = ext
m.mu.Unlock()
GoLog("[Extension] Upgraded extension: %s to v%s\n", newManifest.DisplayName, newManifest.Version)
return ext, nil
}
// ExtensionUpgradeInfo holds information about extension upgrade check
type ExtensionUpgradeInfo struct {
ExtensionID string `json:"extension_id"`
CurrentVersion string `json:"current_version"`
NewVersion string `json:"new_version"`
CanUpgrade bool `json:"can_upgrade"`
IsInstalled bool `json:"is_installed"`
}
// checkExtensionUpgradeInternal checks if a package file is an upgrade for an existing extension
// Internal function that returns struct
func (m *ExtensionManager) checkExtensionUpgradeInternal(filePath string) (*ExtensionUpgradeInfo, error) {
// Validate file extension
if !strings.HasSuffix(strings.ToLower(filePath), ".spotiflac-ext") {
return nil, fmt.Errorf("Invalid file format")
}
// Open the zip file
zipReader, err := zip.OpenReader(filePath)
if err != nil {
return nil, fmt.Errorf("Cannot open extension file")
}
defer zipReader.Close()
var manifestData []byte
for _, file := range zipReader.File {
name := filepath.Base(file.Name)
if name == "manifest.json" {
rc, err := file.Open()
if err != nil {
return nil, fmt.Errorf("failed to open manifest.json")
}
manifestData, err = io.ReadAll(rc)
rc.Close()
if err != nil {
return nil, fmt.Errorf("failed to read manifest.json")
}
break
}
}
if manifestData == nil {
return nil, fmt.Errorf("manifest.json not found")
}
newManifest, err := ParseManifest(manifestData)
if err != nil {
return nil, fmt.Errorf("Invalid manifest: %w", err)
}
m.mu.RLock()
existing, exists := m.extensions[newManifest.Name]
m.mu.RUnlock()
info := &ExtensionUpgradeInfo{
ExtensionID: newManifest.Name,
NewVersion: newManifest.Version,
IsInstalled: exists,
}
if !exists {
// Not installed - this is a new install, not upgrade
info.CurrentVersion = ""
info.CanUpgrade = false
} else {
info.CurrentVersion = existing.Manifest.Version
info.CanUpgrade = compareVersions(newManifest.Version, existing.Manifest.Version) > 0
}
return info, nil
}
// CheckExtensionUpgradeJSON checks if a package file is an upgrade and returns JSON
func (m *ExtensionManager) CheckExtensionUpgradeJSON(filePath string) (string, error) {
info, err := m.checkExtensionUpgradeInternal(filePath)
if err != nil {
return "", err
}
jsonBytes, err := json.Marshal(info)
if err != nil {
return "", err
}
return string(jsonBytes), nil
}
// GetInstalledExtensionsJSON returns all extensions as JSON for Flutter
func (m *ExtensionManager) GetInstalledExtensionsJSON() (string, error) {
extensions := m.GetAllExtensions()
type ExtensionInfo struct {
ID string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
Version string `json:"version"`
Author string `json:"author"`
Description string `json:"description"`
Homepage string `json:"homepage,omitempty"`
IconPath string `json:"icon_path,omitempty"`
Types []ExtensionType `json:"types"`
Enabled bool `json:"enabled"`
Status string `json:"status"`
Error string `json:"error_message,omitempty"`
Settings []ExtensionSetting `json:"settings,omitempty"`
QualityOptions []QualityOption `json:"quality_options,omitempty"`
Permissions []string `json:"permissions"`
HasMetadataProvider bool `json:"has_metadata_provider"`
HasDownloadProvider bool `json:"has_download_provider"`
SkipMetadataEnrichment bool `json:"skip_metadata_enrichment"`
SearchBehavior *SearchBehaviorConfig `json:"search_behavior,omitempty"`
TrackMatching *TrackMatchingConfig `json:"track_matching,omitempty"`
PostProcessing *PostProcessingConfig `json:"post_processing,omitempty"`
}
infos := make([]ExtensionInfo, len(extensions))
for i, ext := range extensions {
permissions := []string{}
for _, domain := range ext.Manifest.Permissions.Network {
permissions = append(permissions, "network:"+domain)
}
if ext.Manifest.Permissions.Storage {
permissions = append(permissions, "storage:enabled")
}
// Determine status
status := "loaded"
if ext.Error != "" {
status = "error"
} else if !ext.Enabled {
status = "disabled"
}
iconPath := ""
if ext.Manifest.Icon != "" && ext.SourceDir != "" {
possibleIcon := filepath.Join(ext.SourceDir, ext.Manifest.Icon)
if _, err := os.Stat(possibleIcon); err == nil {
iconPath = possibleIcon
}
}
if iconPath == "" && ext.SourceDir != "" {
possibleIcon := filepath.Join(ext.SourceDir, "icon.png")
if _, err := os.Stat(possibleIcon); err == nil {
iconPath = possibleIcon
}
}
infos[i] = ExtensionInfo{
ID: ext.ID,
Name: ext.Manifest.Name,
DisplayName: ext.Manifest.DisplayName,
Version: ext.Manifest.Version,
Author: ext.Manifest.Author,
Description: ext.Manifest.Description,
Homepage: ext.Manifest.Homepage,
IconPath: iconPath,
Types: ext.Manifest.Types,
Enabled: ext.Enabled,
Status: status,
Error: ext.Error,
Settings: ext.Manifest.Settings,
QualityOptions: ext.Manifest.QualityOptions,
Permissions: permissions,
HasMetadataProvider: ext.Manifest.IsMetadataProvider(),
HasDownloadProvider: ext.Manifest.IsDownloadProvider(),
SkipMetadataEnrichment: ext.Manifest.SkipMetadataEnrichment,
SearchBehavior: ext.Manifest.SearchBehavior,
TrackMatching: ext.Manifest.TrackMatching,
PostProcessing: ext.Manifest.PostProcessing,
}
}
jsonBytes, err := json.Marshal(infos)
if err != nil {
return "", err
}
return string(jsonBytes), nil
}
// ==================== Extension Lifecycle ====================
// InitializeExtension calls the extension's initialize method with settings
func (m *ExtensionManager) InitializeExtension(extensionID string, settings map[string]interface{}) error {
m.mu.Lock()
defer m.mu.Unlock()
ext, exists := m.extensions[extensionID]
if !exists {
return fmt.Errorf("Extension not found")
}
if ext.VM == nil {
return fmt.Errorf("Extension failed to load. Please reinstall the extension")
}
settingsJSON, err := json.Marshal(settings)
if err != nil {
return fmt.Errorf("Failed to save settings")
}
script := fmt.Sprintf(`
(function() {
var settings = %s;
if (typeof extension !== 'undefined' && typeof extension.initialize === 'function') {
try {
extension.initialize(settings);
return { success: true };
} catch (e) {
return { success: false, error: e.toString() };
}
}
return { success: true, message: 'no initialize function' };
})()
`, string(settingsJSON))
result, err := ext.VM.RunString(script)
if err != nil {
ext.Error = fmt.Sprintf("initialize failed: %v", err)
ext.Enabled = false
GoLog("[Extension] Initialize error for %s: %v\n", extensionID, err)
return err
}
if result != nil && !goja.IsUndefined(result) {
exported := result.Export()
if resultMap, ok := exported.(map[string]interface{}); ok {
if success, ok := resultMap["success"].(bool); ok && !success {
errMsg := "unknown error"
if e, ok := resultMap["error"].(string); ok {
errMsg = e
}
ext.Error = errMsg
ext.Enabled = false
GoLog("[Extension] Initialize failed for %s: %s\n", extensionID, errMsg)
return fmt.Errorf("initialize failed: %s", errMsg)
}
}
}
GoLog("[Extension] Initialized %s\n", extensionID)
return nil
}
// CleanupExtension calls the extension's cleanup method
func (m *ExtensionManager) CleanupExtension(extensionID string) error {
m.mu.Lock()
defer m.mu.Unlock()
ext, exists := m.extensions[extensionID]
if !exists {
return fmt.Errorf("Extension not found")
}
if ext.VM == nil {
return nil // No VM, nothing to cleanup
}
// Call cleanup function
script := `
(function() {
if (typeof extension !== 'undefined' && typeof extension.cleanup === 'function') {
try {
extension.cleanup();
return { success: true };
} catch (e) {
return { success: false, error: e.toString() };
}
}
return { success: true, message: 'no cleanup function' };
})()
`
result, err := ext.VM.RunString(script)
if err != nil {
GoLog("[Extension] Cleanup error for %s: %v\n", extensionID, err)
return err
}
if result != nil && !goja.IsUndefined(result) {
exported := result.Export()
if resultMap, ok := exported.(map[string]interface{}); ok {
if success, ok := resultMap["success"].(bool); ok && !success {
errMsg := "unknown error"
if e, ok := resultMap["error"].(string); ok {
errMsg = e
}
GoLog("[Extension] Cleanup failed for %s: %s\n", extensionID, errMsg)
return fmt.Errorf("cleanup failed: %s", errMsg)
}
}
}
GoLog("[Extension] Cleaned up %s\n", extensionID)
return nil
}
// UnloadAllExtensions unloads all extensions gracefully
func (m *ExtensionManager) UnloadAllExtensions() {
m.mu.Lock()
extensionIDs := make([]string, 0, len(m.extensions))
for id := range m.extensions {
extensionIDs = append(extensionIDs, id)
}
m.mu.Unlock()
for _, id := range extensionIDs {
// Call cleanup first
m.CleanupExtension(id)
// Then unload
m.UnloadExtension(id)
}
GoLog("[Extension] All extensions unloaded\n")
}