package loader
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/c4pt0r/agfs/agfs-server/pkg/plugin"
"github.com/c4pt0r/agfs/agfs-server/pkg/plugin/api"
"github.com/ebitengine/purego"
log "github.com/sirupsen/logrus"
)
type PluginType int
const (
PluginTypeUnknown PluginType = iota
PluginTypeNative
PluginTypeWASM
)
func (pt PluginType) String() string {
switch pt {
case PluginTypeNative:
return "native"
case PluginTypeWASM:
return "wasm"
default:
return "unknown"
}
}
type LoadedPlugin struct {
Path string
Plugin plugin.ServicePlugin
LibHandle uintptr
RefCount int
mu sync.Mutex
}
type PluginLoader struct {
loadedPlugins map[string]*LoadedPlugin
wasmLoader *WASMPluginLoader
poolConfig api.PoolConfig
mu sync.RWMutex
}
func NewPluginLoader(poolConfig api.PoolConfig) *PluginLoader {
return &PluginLoader{
loadedPlugins: make(map[string]*LoadedPlugin),
wasmLoader: NewWASMPluginLoader(),
poolConfig: poolConfig,
}
}
func DetectPluginType(libraryPath string) (PluginType, error) {
if _, err := os.Stat(libraryPath); err != nil {
return PluginTypeUnknown, fmt.Errorf("plugin file not found: %w", err)
}
file, err := os.Open(libraryPath)
if err != nil {
return PluginTypeUnknown, fmt.Errorf("failed to open plugin file: %w", err)
}
defer file.Close()
magic := make([]byte, 4)
n, err := file.Read(magic)
if err != nil || n < 4 {
return detectPluginTypeByExtension(libraryPath), nil
}
if magic[0] == 0x00 && magic[1] == 0x61 && magic[2] == 0x73 && magic[3] == 0x6D {
return PluginTypeWASM, nil
}
if magic[0] == 0x7F && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F' {
return PluginTypeNative, nil
}
if (magic[0] == 0xFE && magic[1] == 0xED && magic[2] == 0xFA && (magic[3] == 0xCE || magic[3] == 0xCF)) ||
(magic[0] == 0xCE && magic[1] == 0xFA && magic[2] == 0xED && magic[3] == 0xFE) ||
(magic[0] == 0xCF && magic[1] == 0xFA && magic[2] == 0xED && magic[3] == 0xFE) ||
(magic[0] == 0xCA && magic[1] == 0xFE && magic[2] == 0xBA && magic[3] == 0xBE) ||
(magic[0] == 0xBE && magic[1] == 0xBA && magic[2] == 0xFE && magic[3] == 0xCA) {
return PluginTypeNative, nil
}
if magic[0] == 'M' && magic[1] == 'Z' {
return PluginTypeNative, nil
}
return detectPluginTypeByExtension(libraryPath), nil
}
func detectPluginTypeByExtension(libraryPath string) PluginType {
ext := strings.ToLower(filepath.Ext(libraryPath))
switch ext {
case ".wasm":
return PluginTypeWASM
case ".so", ".dylib", ".dll":
return PluginTypeNative
default:
return PluginTypeUnknown
}
}
func (pl *PluginLoader) LoadPluginWithType(libraryPath string, pluginType PluginType, hostFS ...interface{}) (plugin.ServicePlugin, error) {
log.Debugf("Loading plugin with type %s: %s", pluginType, libraryPath)
switch pluginType {
case PluginTypeWASM:
return pl.wasmLoader.LoadWASMPlugin(libraryPath, pl.poolConfig, hostFS...)
case PluginTypeNative:
return pl.loadNativePlugin(libraryPath)
default:
return nil, fmt.Errorf("unsupported plugin type: %s", pluginType)
}
}
func (pl *PluginLoader) LoadPlugin(libraryPath string) (plugin.ServicePlugin, error) {
pluginType, err := DetectPluginType(libraryPath)
if err != nil {
return nil, fmt.Errorf("failed to detect plugin type: %w", err)
}
log.Debugf("Auto-detected plugin type: %s for %s", pluginType, libraryPath)
return pl.LoadPluginWithType(libraryPath, pluginType)
}
func (pl *PluginLoader) loadNativePlugin(libraryPath string) (plugin.ServicePlugin, error) {
pl.mu.Lock()
defer pl.mu.Unlock()
absPath, err := filepath.Abs(libraryPath)
if err != nil {
return nil, fmt.Errorf("failed to resolve path: %w", err)
}
if _, exists := pl.loadedPlugins[absPath]; exists {
log.Infof("Native plugin %s already loaded, creating new instance from copy", absPath)
tempDir := os.TempDir()
baseName := filepath.Base(libraryPath)
ext := filepath.Ext(baseName)
nameWithoutExt := strings.TrimSuffix(baseName, ext)
counter := 1
var tempLibPath string
for {
tempLibPath = filepath.Join(tempDir, fmt.Sprintf("%s.%d%s", nameWithoutExt, counter, ext))
if _, err := os.Stat(tempLibPath); os.IsNotExist(err) {
break
}
counter++
}
if err := copyFile(libraryPath, tempLibPath); err != nil {
return nil, fmt.Errorf("failed to create temp copy: %w", err)
}
absPath = tempLibPath
log.Infof("Created temp copy at: %s", absPath)
}
libHandle, err := openLibrary(absPath)
if err != nil {
return nil, fmt.Errorf("failed to open library %s: %w", absPath, err)
}
log.Infof("Loaded library: %s (handle: %v)", absPath, libHandle)
vtable, err := loadPluginVTable(libHandle)
if err != nil {
return nil, fmt.Errorf("failed to load plugin vtable: %w", err)
}
externalPlugin, err := api.NewExternalPlugin(libHandle, vtable)
if err != nil {
return nil, fmt.Errorf("failed to create plugin wrapper: %w", err)
}
loaded := &LoadedPlugin{
Path: absPath,
Plugin: externalPlugin,
LibHandle: libHandle,
RefCount: 1,
}
pl.loadedPlugins[absPath] = loaded
log.Infof("Successfully loaded plugin: %s (name: %s)", absPath, externalPlugin.Name())
return externalPlugin, nil
}
func (pl *PluginLoader) UnloadPluginWithType(libraryPath string, pluginType PluginType) error {
log.Debugf("Unloading plugin with type %s: %s", pluginType, libraryPath)
switch pluginType {
case PluginTypeWASM:
return pl.wasmLoader.UnloadWASMPlugin(libraryPath)
case PluginTypeNative:
return pl.unloadNativePlugin(libraryPath)
default:
return fmt.Errorf("unsupported plugin type: %s", pluginType)
}
}
func (pl *PluginLoader) UnloadPlugin(libraryPath string) error {
pluginType, err := DetectPluginType(libraryPath)
if err != nil {
return fmt.Errorf("failed to detect plugin type: %w", err)
}
return pl.UnloadPluginWithType(libraryPath, pluginType)
}
func (pl *PluginLoader) unloadNativePlugin(libraryPath string) error {
pl.mu.Lock()
defer pl.mu.Unlock()
absPath, err := filepath.Abs(libraryPath)
if err != nil {
return fmt.Errorf("failed to resolve path: %w", err)
}
loaded, exists := pl.loadedPlugins[absPath]
if !exists {
return fmt.Errorf("plugin not loaded: %s", absPath)
}
loaded.mu.Lock()
loaded.RefCount--
refCount := loaded.RefCount
loaded.mu.Unlock()
if refCount <= 0 {
if err := loaded.Plugin.Shutdown(); err != nil {
log.Warnf("Error shutting down plugin %s: %v", absPath, err)
}
delete(pl.loadedPlugins, absPath)
log.Infof("Unloaded plugin: %s (library remains in memory)", absPath)
} else {
log.Infof("Decremented plugin ref count: %s (refCount: %d)", absPath, refCount)
}
return nil
}
func (pl *PluginLoader) GetLoadedPlugins() []string {
pl.mu.RLock()
defer pl.mu.RUnlock()
paths := make([]string, 0, len(pl.loadedPlugins))
for path := range pl.loadedPlugins {
paths = append(paths, path)
}
wasmPaths := pl.wasmLoader.GetLoadedPlugins()
paths = append(paths, wasmPaths...)
return paths
}
func (pl *PluginLoader) GetPluginNameToPathMap() map[string]string {
pl.mu.RLock()
defer pl.mu.RUnlock()
nameToPath := make(map[string]string)
for path, loaded := range pl.loadedPlugins {
if loaded.Plugin != nil {
nameToPath[loaded.Plugin.Name()] = path
}
}
wasmNameToPath := pl.wasmLoader.GetPluginNameToPathMap()
for name, path := range wasmNameToPath {
nameToPath[name] = path
}
return nameToPath
}
func (pl *PluginLoader) IsLoadedWithType(libraryPath string, pluginType PluginType) bool {
switch pluginType {
case PluginTypeWASM:
return pl.wasmLoader.IsLoaded(libraryPath)
case PluginTypeNative:
return pl.isNativePluginLoaded(libraryPath)
default:
return false
}
}
func (pl *PluginLoader) IsLoaded(libraryPath string) bool {
pluginType, err := DetectPluginType(libraryPath)
if err != nil {
log.Debugf("Failed to detect plugin type for %s: %v", libraryPath, err)
return false
}
return pl.IsLoadedWithType(libraryPath, pluginType)
}
func (pl *PluginLoader) isNativePluginLoaded(libraryPath string) bool {
pl.mu.RLock()
defer pl.mu.RUnlock()
absPath, err := filepath.Abs(libraryPath)
if err != nil {
return false
}
_, exists := pl.loadedPlugins[absPath]
return exists
}
func loadPluginVTable(libHandle uintptr) (*api.PluginVTable, error) {
vtable := &api.PluginVTable{}
if err := loadFunc(libHandle, "PluginNew", &vtable.PluginNew); err != nil {
return nil, fmt.Errorf("missing required function PluginNew: %w", err)
}
loadFunc(libHandle, "PluginFree", &vtable.PluginFree)
loadFunc(libHandle, "PluginName", &vtable.PluginName)
loadFunc(libHandle, "PluginValidate", &vtable.PluginValidate)
loadFunc(libHandle, "PluginInitialize", &vtable.PluginInitialize)
loadFunc(libHandle, "PluginShutdown", &vtable.PluginShutdown)
loadFunc(libHandle, "PluginGetReadme", &vtable.PluginGetReadme)
loadFunc(libHandle, "FSCreate", &vtable.FSCreate)
loadFunc(libHandle, "FSMkdir", &vtable.FSMkdir)
loadFunc(libHandle, "FSRemove", &vtable.FSRemove)
loadFunc(libHandle, "FSRemoveAll", &vtable.FSRemoveAll)
loadFunc(libHandle, "FSRead", &vtable.FSRead)
loadFunc(libHandle, "FSWrite", &vtable.FSWrite)
loadFunc(libHandle, "FSReadDir", &vtable.FSReadDir)
loadFunc(libHandle, "FSStat", &vtable.FSStat)
loadFunc(libHandle, "FSRename", &vtable.FSRename)
loadFunc(libHandle, "FSChmod", &vtable.FSChmod)
return vtable, nil
}
func loadFunc(libHandle uintptr, name string, fptr interface{}) error {
defer func() {
if r := recover(); r != nil {
log.Debugf("Function %s not found in library (this may be ok if optional)", name)
}
}()
purego.RegisterLibFunc(fptr, libHandle, name)
return nil
}
func copyFile(src, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
err = os.WriteFile(dst, data, 0755)
if err != nil {
return err
}
return nil
}