feat: implement plugin loading system
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Lint (push) Failing after 4m23s
Build and Release / Build Binaries (amd64, darwin, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux, linux-latest) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Has been cancelled
Build and Release / Unit Tests (push) Has been cancelled
Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Lint (push) Failing after 4m23s
Build and Release / Build Binaries (amd64, darwin, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux, linux-latest) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Has been cancelled
Build and Release / Unit Tests (push) Has been cancelled
- Add modules/plugins/loader.go to load .so plugins from plugins directory - Add modules/setting/plugins.go for [plugins] configuration section - Wire up plugin loading before DB init so plugins can register models - Wire up plugin migrations and initialization after DB init - Register plugin routes in web.go Plugins can now be loaded at runtime by placing .so files in the plugins directory and enabling [plugins] ENABLED = true in app.ini. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
107
modules/plugins/loader.go
Normal file
107
modules/plugins/loader.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
|
||||
"code.gitcaddy.com/server/modules/log"
|
||||
"code.gitcaddy.com/server/modules/setting"
|
||||
)
|
||||
|
||||
// LoadAll loads all plugins from the configured plugin directory
|
||||
func LoadAll() error {
|
||||
if !setting.Plugins.Enabled {
|
||||
log.Info("Plugin system is disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
pluginDir := setting.Plugins.Path
|
||||
if pluginDir == "" {
|
||||
pluginDir = filepath.Join(setting.AppDataPath, "plugins")
|
||||
}
|
||||
|
||||
// Check if plugin directory exists
|
||||
info, err := os.Stat(pluginDir)
|
||||
if os.IsNotExist(err) {
|
||||
log.Info("Plugin directory does not exist: %s", pluginDir)
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat plugin directory: %w", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("plugin path is not a directory: %s", pluginDir)
|
||||
}
|
||||
|
||||
log.Info("Loading plugins from: %s", pluginDir)
|
||||
|
||||
// Find all .so files in the plugin directory
|
||||
entries, err := os.ReadDir(pluginDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read plugin directory: %w", err)
|
||||
}
|
||||
|
||||
loadedCount := 0
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := entry.Name()
|
||||
if filepath.Ext(name) != ".so" {
|
||||
continue
|
||||
}
|
||||
|
||||
pluginPath := filepath.Join(pluginDir, name)
|
||||
if err := loadPlugin(pluginPath); err != nil {
|
||||
log.Error("Failed to load plugin %s: %v", name, err)
|
||||
// Continue loading other plugins
|
||||
continue
|
||||
}
|
||||
loadedCount++
|
||||
}
|
||||
|
||||
log.Info("Loaded %d plugin(s)", loadedCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadPlugin loads a single plugin from a .so file
|
||||
func loadPlugin(path string) error {
|
||||
log.Info("Loading plugin: %s", path)
|
||||
|
||||
// Open the plugin
|
||||
p, err := plugin.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open plugin: %w", err)
|
||||
}
|
||||
|
||||
// Look for the Plugin symbol
|
||||
sym, err := p.Lookup("Plugin")
|
||||
if err != nil {
|
||||
return fmt.Errorf("plugin does not export 'Plugin' symbol: %w", err)
|
||||
}
|
||||
|
||||
// The Plugin symbol should be a pointer to a Plugin interface implementation
|
||||
plug, ok := sym.(*Plugin)
|
||||
if !ok {
|
||||
// Try to see if it's the interface directly
|
||||
plugDirect, ok := sym.(Plugin)
|
||||
if !ok {
|
||||
return fmt.Errorf("Plugin symbol is not a plugins.Plugin (got %T)", sym)
|
||||
}
|
||||
Register(plugDirect)
|
||||
return nil
|
||||
}
|
||||
|
||||
if plug == nil || *plug == nil {
|
||||
return fmt.Errorf("Plugin symbol is nil")
|
||||
}
|
||||
|
||||
Register(*plug)
|
||||
return nil
|
||||
}
|
||||
@@ -6,7 +6,6 @@ package plugins
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@@ -44,7 +43,8 @@ type WebRoutesPlugin interface {
|
||||
Plugin
|
||||
|
||||
// RegisterWebRoutes adds routes to the web UI router
|
||||
RegisterWebRoutes(m chi.Router)
|
||||
// The router is a *web.Router from the gitcaddy server
|
||||
RegisterWebRoutes(m any)
|
||||
}
|
||||
|
||||
// APIRoutesPlugin is implemented by plugins that add API routes
|
||||
@@ -52,7 +52,8 @@ type APIRoutesPlugin interface {
|
||||
Plugin
|
||||
|
||||
// RegisterAPIRoutes adds routes to the API router
|
||||
RegisterAPIRoutes(m chi.Router)
|
||||
// The router is a *web.Router from the gitcaddy server
|
||||
RegisterAPIRoutes(m any)
|
||||
}
|
||||
|
||||
// RepoRoutesPlugin is implemented by plugins that add per-repository routes
|
||||
@@ -60,10 +61,12 @@ type RepoRoutesPlugin interface {
|
||||
Plugin
|
||||
|
||||
// RegisterRepoWebRoutes adds routes under /{owner}/{repo}/
|
||||
RegisterRepoWebRoutes(m chi.Router)
|
||||
// The router is a *web.Router from the gitcaddy server
|
||||
RegisterRepoWebRoutes(m any)
|
||||
|
||||
// RegisterRepoAPIRoutes adds routes under /api/v1/repos/{owner}/{repo}/
|
||||
RegisterRepoAPIRoutes(m chi.Router)
|
||||
// The router is a *web.Router from the gitcaddy server
|
||||
RegisterRepoAPIRoutes(m any)
|
||||
}
|
||||
|
||||
// LicensedPlugin is implemented by plugins that require a license
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"code.gitcaddy.com/server/modules/log"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@@ -126,7 +125,7 @@ func MigrateAll(ctx context.Context, x *xorm.Engine) error {
|
||||
}
|
||||
|
||||
// RegisterWebRoutes registers web UI routes from all plugins
|
||||
func RegisterWebRoutes(m chi.Router) {
|
||||
func RegisterWebRoutes(m any) {
|
||||
registryLock.RLock()
|
||||
defer registryLock.RUnlock()
|
||||
|
||||
@@ -139,7 +138,7 @@ func RegisterWebRoutes(m chi.Router) {
|
||||
}
|
||||
|
||||
// RegisterAPIRoutes registers API routes from all plugins
|
||||
func RegisterAPIRoutes(m chi.Router) {
|
||||
func RegisterAPIRoutes(m any) {
|
||||
registryLock.RLock()
|
||||
defer registryLock.RUnlock()
|
||||
|
||||
@@ -152,7 +151,7 @@ func RegisterAPIRoutes(m chi.Router) {
|
||||
}
|
||||
|
||||
// RegisterRepoWebRoutes registers per-repository web routes from all plugins
|
||||
func RegisterRepoWebRoutes(m chi.Router) {
|
||||
func RegisterRepoWebRoutes(m any) {
|
||||
registryLock.RLock()
|
||||
defer registryLock.RUnlock()
|
||||
|
||||
@@ -165,7 +164,7 @@ func RegisterRepoWebRoutes(m chi.Router) {
|
||||
}
|
||||
|
||||
// RegisterRepoAPIRoutes registers per-repository API routes from all plugins
|
||||
func RegisterRepoAPIRoutes(m chi.Router) {
|
||||
func RegisterRepoAPIRoutes(m any) {
|
||||
registryLock.RLock()
|
||||
defer registryLock.RUnlock()
|
||||
|
||||
|
||||
21
modules/setting/plugins.go
Normal file
21
modules/setting/plugins.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
// Plugins settings
|
||||
var (
|
||||
Plugins = struct {
|
||||
Enabled bool
|
||||
Path string
|
||||
}{
|
||||
Enabled: false, // Disabled by default
|
||||
Path: "", // Empty means default to APP_DATA_PATH/plugins
|
||||
}
|
||||
)
|
||||
|
||||
func loadPluginsFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("plugins")
|
||||
Plugins.Enabled = sec.Key("ENABLED").MustBool(false)
|
||||
Plugins.Path = sec.Key("PATH").MustString("")
|
||||
}
|
||||
@@ -150,6 +150,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
||||
loadMarkupFrom(cfg)
|
||||
loadGlobalLockFrom(cfg)
|
||||
loadOtherFrom(cfg)
|
||||
loadPluginsFrom(cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"code.gitcaddy.com/server/models"
|
||||
ai_model "code.gitcaddy.com/server/models/ai"
|
||||
authmodel "code.gitcaddy.com/server/models/auth"
|
||||
"code.gitcaddy.com/server/models/db"
|
||||
"code.gitcaddy.com/server/modules/cache"
|
||||
"code.gitcaddy.com/server/modules/eventsource"
|
||||
"code.gitcaddy.com/server/modules/git"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"code.gitcaddy.com/server/modules/log"
|
||||
"code.gitcaddy.com/server/modules/markup"
|
||||
"code.gitcaddy.com/server/modules/markup/external"
|
||||
"code.gitcaddy.com/server/modules/plugins"
|
||||
"code.gitcaddy.com/server/modules/setting"
|
||||
"code.gitcaddy.com/server/modules/ssh"
|
||||
"code.gitcaddy.com/server/modules/storage"
|
||||
@@ -58,6 +60,8 @@ import (
|
||||
"code.gitcaddy.com/server/services/task"
|
||||
"code.gitcaddy.com/server/services/uinotification"
|
||||
"code.gitcaddy.com/server/services/webhook"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func mustInit(fn func() error) {
|
||||
@@ -144,8 +148,22 @@ func InitWebInstalled(ctx context.Context) {
|
||||
log.Fatal("SQLite3 support is disabled, but it is used for database setting. Please get or build a Gitea release with SQLite3 support.")
|
||||
}
|
||||
|
||||
// Load plugins before DB init so they can register their models
|
||||
mustInit(plugins.LoadAll)
|
||||
|
||||
mustInitCtx(ctx, common.InitDBEngine)
|
||||
log.Info("ORM engine initialization successful!")
|
||||
|
||||
// Run plugin migrations after DB init
|
||||
if err := plugins.MigrateAll(ctx, db.GetEngine(ctx).(*xorm.Engine)); err != nil {
|
||||
log.Fatal("Plugin migrations failed: %v", err)
|
||||
}
|
||||
|
||||
// Initialize all plugins
|
||||
if err := plugins.InitAll(ctx); err != nil {
|
||||
log.Fatal("Plugin initialization failed: %v", err)
|
||||
}
|
||||
|
||||
mustInit(system.Init)
|
||||
mustInitCtx(ctx, oauth2.Init)
|
||||
mustInitCtx(ctx, oauth2_provider.Init)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"code.gitcaddy.com/server/modules/graceful"
|
||||
"code.gitcaddy.com/server/modules/log"
|
||||
"code.gitcaddy.com/server/modules/metrics"
|
||||
"code.gitcaddy.com/server/modules/plugins"
|
||||
"code.gitcaddy.com/server/modules/public"
|
||||
"code.gitcaddy.com/server/modules/setting"
|
||||
"code.gitcaddy.com/server/modules/storage"
|
||||
@@ -1799,6 +1800,14 @@ func registerWebRoutes(m *web.Router) {
|
||||
})
|
||||
}
|
||||
|
||||
// Register plugin web routes
|
||||
plugins.RegisterWebRoutes(m)
|
||||
|
||||
// Register plugin repo routes (mounted under /{username}/{reponame})
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
plugins.RegisterRepoWebRoutes(m)
|
||||
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty)
|
||||
|
||||
m.NotFound(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := context.GetWebContext(req.Context())
|
||||
defer routing.RecordFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))()
|
||||
|
||||
Reference in New Issue
Block a user