831 lines · 25.0 KB · Go
RawDownload
package homeplugins import (	"context"	"errors"	"fmt"	"net/http"	"os"	"path/filepath"	"runtime"	"sort"	"strings"	"time" 	"github.com/router-for-me/CLIProxyAPI/v7/internal/config"	"github.com/router-for-me/CLIProxyAPI/v7/internal/util"	sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"	sdkpluginstore "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginstore"	"gopkg.in/yaml.v3") type Platform struct {	GOOS   string `json:"goos"`	GOARCH string `json:"goarch"`} type PluginRuntime interface {	PluginBusy(id string) bool	UnloadPlugin(id string) bool} type PluginLoadInspector interface {	PluginRegistered(id string) bool} type contextualPluginUnloader interface {	UnloadPluginContext(ctx context.Context, id string) bool} type SyncReport struct {	SchemaVersion int                   `json:"schema_version"`	TaskID        uint                  `json:"task_id,omitempty"`	Task          string                `json:"task"`	NodeID        string                `json:"node_id,omitempty"`	Status        string                `json:"status"`	Phase         string                `json:"phase"`	OK            bool                  `json:"ok"`	StartedAt     time.Time             `json:"started_at"`	FinishedAt    time.Time             `json:"finished_at,omitempty"`	UpdatedAt     time.Time             `json:"updated_at"`	Platform      Platform              `json:"platform"`	Plugins       []PluginInstallStatus `json:"plugins"`	Error         string                `json:"error,omitempty"`} type PluginInstallStatus struct {	ID            string `json:"id"`	Version       string `json:"version,omitempty"`	ReleaseTag    string `json:"release_tag,omitempty"`	Repository    string `json:"repository,omitempty"`	InstallType   string `json:"install_type,omitempty"`	InstallStatus string `json:"install_status"`	LoadStatus    string `json:"load_status,omitempty"`	Path          string `json:"path,omitempty"`	Skipped       bool   `json:"skipped,omitempty"`	Overwritten   bool   `json:"overwritten,omitempty"`	Error         string `json:"error,omitempty"`} const (	pluginTaskName         = "plugin-sync"	pluginDeleteTaskName   = "plugin-delete"	pluginTaskStatusOK     = "success"	pluginTaskStatusError  = "failed"	pluginTaskPhaseInstall = "install"	pluginTaskPhaseLoad    = "load"	pluginTaskPhaseDelete  = "delete" 	pluginInstallStatusInstalled = "installed"	pluginInstallStatusSkipped   = "skipped"	pluginInstallStatusFailed    = "failed"	pluginInstallStatusDeleted   = "deleted"	pluginInstallStatusMissing   = "missing"	pluginLoadStatusLoaded       = "loaded"	pluginLoadStatusFailed       = "failed") // CurrentPlatform reports the platform used by pluginhost discovery.func CurrentPlatform() Platform {	return Platform{		GOOS:   runtime.GOOS,		GOARCH: runtime.GOARCH,	}} func NormalizePlatform(platform Platform) Platform {	goos := strings.ToLower(strings.TrimSpace(platform.GOOS))	switch goos {	case "mac", "macos", "osx":		goos = "darwin"	}	goarch := strings.ToLower(strings.TrimSpace(platform.GOARCH))	switch goarch {	case "x64", "x86_64":		goarch = "amd64"	case "aarch64":		goarch = "arm64"	}	return Platform{GOOS: goos, GOARCH: goarch}} func Sync(ctx context.Context, cfg *config.Config, pluginRuntime PluginRuntime) error {	_, errSync := SyncPlatformWithReport(ctx, cfg, pluginRuntime, CurrentPlatform())	return errSync} func SyncPlatform(ctx context.Context, cfg *config.Config, pluginRuntime PluginRuntime, platform Platform) error {	_, errSync := SyncPlatformWithReport(ctx, cfg, pluginRuntime, platform)	return errSync} func SyncWithReport(ctx context.Context, cfg *config.Config, pluginRuntime PluginRuntime) (SyncReport, error) {	return SyncPlatformWithReport(ctx, cfg, pluginRuntime, CurrentPlatform())} func SyncPlatformWithReport(ctx context.Context, cfg *config.Config, pluginRuntime PluginRuntime, platform Platform) (SyncReport, error) {	if cfg == nil || !cfg.Home.Enabled || !cfg.Plugins.Enabled {		return newSyncReport(platform), nil	}	platform = NormalizePlatform(platform)	report := newSyncReport(platform)	if platform.GOOS == "" {		errPlatform := fmt.Errorf("home plugins: goos is required")		finishReport(&report, errPlatform)		return report, errPlatform	}	if platform.GOARCH == "" {		errPlatform := fmt.Errorf("home plugins: goarch is required")		finishReport(&report, errPlatform)		return report, errPlatform	}	report.Platform = platform	root, errResolvePluginsDir := config.ResolvePluginsDir(cfg.Plugins.Dir)	if errResolvePluginsDir != nil {		errPluginsDir := fmt.Errorf("home plugins: %w", errResolvePluginsDir)		finishReport(&report, errPluginsDir)		return report, errPluginsDir	}	client := newPluginStoreClient(cfg)	var syncErrors []error	ids := make([]string, 0, len(cfg.Plugins.Configs))	for id := range cfg.Plugins.Configs {		ids = append(ids, id)	}	sort.Strings(ids)	for _, id := range ids {		item := cfg.Plugins.Configs[id]		if !pluginConfigEnabled(item) {			continue		}		manifest, okManifest, errManifest := storeManifestFromPluginConfig(id, item)		if errManifest != nil {			status := PluginInstallStatus{				ID:            strings.TrimSpace(id),				InstallStatus: pluginInstallStatusFailed,				Error:         errManifest.Error(),			}			report.Plugins = append(report.Plugins, status)			syncErrors = append(syncErrors, errManifest)			continue		}		if !okManifest {			continue		}		status := pluginStatusFromManifest(manifest)		result, errSync := installManifest(ctx, client, manifest, root, platform, pluginRuntime)		if errSync != nil {			status.InstallStatus = pluginInstallStatusFailed			status.Error = errSync.Error()			report.Plugins = append(report.Plugins, status)			syncErrors = append(syncErrors, errSync)			continue		}		status.Path = strings.TrimSpace(result.Path)		status.Skipped = result.Skipped		status.Overwritten = result.Overwritten		if result.Skipped {			status.InstallStatus = pluginInstallStatusSkipped		} else {			status.InstallStatus = pluginInstallStatusInstalled		}		report.Plugins = append(report.Plugins, status)	}	errSync := errors.Join(syncErrors...)	finishReport(&report, errSync)	return report, errSync} func SyncResolvedWithReport(ctx context.Context, cfg *config.Config, items []sdkpluginstore.PluginSyncItem, expiresAt time.Time, installedVersions map[string]string, pluginRuntime PluginRuntime) (SyncReport, error) {	defer func() {		for index := range items {			items[index].Clear()		}	}()	platform := NormalizePlatform(CurrentPlatform())	report := newSyncReport(platform)	if cfg == nil || !cfg.Home.Enabled || !cfg.Plugins.Enabled {		finishReport(&report, nil)		return report, nil	}	root, errResolvePluginsDir := config.ResolvePluginsDir(cfg.Plugins.Dir)	if errResolvePluginsDir != nil {		errPluginsDir := fmt.Errorf("home plugins: %w", errResolvePluginsDir)		finishReport(&report, errPluginsDir)		return report, errPluginsDir	}	addInstalledVersionStatuses(&report, cfg, root, installedVersions)	var syncErrors []error	for index := range items {		if !time.Now().UTC().Before(expiresAt) {			errExpired := fmt.Errorf("home plugins: plugin sync response expired")			syncErrors = append(syncErrors, errExpired)			break		}		item := &items[index]		manifest := item.Manifest		status := pluginStatusFromManifest(manifest)		result, errInstall := installResolvedManifest(ctx, cfg, manifest, item.Auth, expiresAt, root, platform, pluginRuntime)		item.Clear()		if errInstall != nil {			status.InstallStatus = pluginInstallStatusFailed			status.Error = errInstall.Error()			upsertPluginInstallStatus(&report, status)			syncErrors = append(syncErrors, errInstall)			continue		}		status.Path = strings.TrimSpace(result.Path)		status.Skipped = result.Skipped		status.Overwritten = result.Overwritten		if result.Skipped {			status.InstallStatus = pluginInstallStatusSkipped		} else {			status.InstallStatus = pluginInstallStatusInstalled		}		upsertPluginInstallStatus(&report, status)	}	errSync := errors.Join(syncErrors...)	finishReport(&report, errSync)	return report, errSync} func addInstalledVersionStatuses(report *SyncReport, cfg *config.Config, root string, installedVersions map[string]string) {	if report == nil || cfg == nil || len(installedVersions) == 0 {		return	}	ids := make([]string, 0, len(cfg.Plugins.Configs))	for id := range cfg.Plugins.Configs {		ids = append(ids, id)	}	sort.Strings(ids)	for _, id := range ids {		item := cfg.Plugins.Configs[id]		if !pluginConfigEnabled(item) {			continue		}		id = strings.TrimSpace(id)		version, okVersion := installedVersions[id]		if !okVersion {			continue		}		status := PluginInstallStatus{			ID:            id,			Version:       strings.TrimSpace(version),			InstallStatus: pluginInstallStatusSkipped,			Skipped:       true,		}		files, errFiles := pluginFileInfos(root, id)		if errFiles == nil {			for _, file := range files {				if strings.TrimSpace(file.Version) == status.Version {					status.Path = strings.TrimSpace(file.Path)					break				}			}		}		manifest, okManifest, errManifest := storeManifestFromPluginConfig(id, item)		if errManifest == nil && okManifest && pluginVersionsEqual(status.Version, manifest.Version) {			status.ReleaseTag = strings.TrimSpace(manifest.ReleaseTag)			status.Repository = strings.TrimSpace(manifest.Repository)			status.InstallType = manifest.InstallType()		}		report.Plugins = append(report.Plugins, status)	}} func pluginVersionsEqual(left string, right string) bool {	left = strings.TrimSpace(left)	right = strings.TrimSpace(right)	if left == "" || right == "" {		return false	}	return !sdkpluginstore.UpdateAvailable(left, right) && !sdkpluginstore.UpdateAvailable(right, left)} func upsertPluginInstallStatus(report *SyncReport, status PluginInstallStatus) {	if report == nil {		return	}	id := strings.TrimSpace(status.ID)	for index := range report.Plugins {		if strings.TrimSpace(report.Plugins[index].ID) == id {			report.Plugins[index] = status			return		}	}	report.Plugins = append(report.Plugins, status)} func installResolvedManifest(ctx context.Context, cfg *config.Config, manifest sdkpluginstore.Manifest, auth []sdkpluginstore.ResolvedAuthConfig, expiresAt time.Time, root string, platform Platform, pluginRuntime PluginRuntime) (sdkpluginstore.InstallResult, error) {	client := newResolvedPluginStoreClient(cfg, auth, expiresAt)	defer client.ClearAuth()	return installManifest(ctx, client, manifest, root, platform, pluginRuntime)} func InstalledVersions(cfg *config.Config) (map[string]string, error) {	if cfg == nil {		return map[string]string{}, nil	}	root, errResolvePluginsDir := config.ResolvePluginsDir(cfg.Plugins.Dir)	if errResolvePluginsDir != nil {		return nil, fmt.Errorf("home plugins: %w", errResolvePluginsDir)	}	versions := make(map[string]string, len(cfg.Plugins.Configs))	for id := range cfg.Plugins.Configs {		files, errFiles := pluginFileInfos(root, id)		if errFiles != nil {			return nil, fmt.Errorf("home plugins: discover installed plugin %s: %w", id, errFiles)		}		if len(files) == 0 {			continue		}		version := strings.TrimSpace(files[0].Version)		if version != "" {			versions[strings.TrimSpace(id)] = version		}	}	return versions, nil} func installManifest(ctx context.Context, client sdkpluginstore.Client, manifest sdkpluginstore.Manifest, root string, platform Platform, pluginRuntime PluginRuntime) (sdkpluginstore.InstallResult, error) {	id := strings.TrimSpace(manifest.ID)	if id == "" {		return sdkpluginstore.InstallResult{}, fmt.Errorf("home plugins: manifest plugin id is empty")	}	pluginIsBusy := func() bool {		return pluginRuntime != nil && pluginRuntime.PluginBusy(id)	}	result, errInstall := client.InstallManifest(ctx, manifest, sdkpluginstore.InstallOptions{		PluginsDir:   root,		GOOS:         platform.GOOS,		GOARCH:       platform.GOARCH,		PluginLoaded: pluginIsBusy,	})	if errInstall != nil {		return sdkpluginstore.InstallResult{}, fmt.Errorf("home plugins: install %s: %w", id, errInstall)	}	return result, nil} func DeleteWithReport(ctx context.Context, cfg *config.Config, pluginRuntime PluginRuntime, taskID uint, pluginID string) SyncReport {	if ctx == nil {		ctx = context.Background()	}	platform := CurrentPlatform()	report := newSyncReport(platform)	report.TaskID = taskID	report.Task = pluginDeleteTaskName	report.Phase = pluginTaskPhaseDelete	pluginID = strings.TrimSpace(pluginID)	status := PluginInstallStatus{ID: pluginID}	if errContext := ctx.Err(); errContext != nil {		status.InstallStatus = pluginInstallStatusFailed		status.Error = errContext.Error()		report.Plugins = append(report.Plugins, status)		finishReport(&report, errContext)		return report	}	if cfg == nil {		status.InstallStatus = pluginInstallStatusFailed		status.Error = "home plugins: config is nil"		report.Plugins = append(report.Plugins, status)		finishReport(&report, errors.New(status.Error))		return report	}	root, errResolvePluginsDir := config.ResolvePluginsDir(cfg.Plugins.Dir)	if errResolvePluginsDir != nil {		errPluginsDir := fmt.Errorf("home plugins: %w", errResolvePluginsDir)		status.InstallStatus = pluginInstallStatusFailed		status.Error = errPluginsDir.Error()		report.Plugins = append(report.Plugins, status)		finishReport(&report, errPluginsDir)		return report	}	if errContext := ctx.Err(); errContext != nil {		status.InstallStatus = pluginInstallStatusFailed		status.Error = errContext.Error()		report.Plugins = append(report.Plugins, status)		finishReport(&report, errContext)		return report	}	path, deleted, errDelete := deletePluginArtifact(ctx, root, pluginID, pluginRuntime)	status.Path = strings.TrimSpace(path)	switch {	case errDelete != nil:		status.InstallStatus = pluginInstallStatusFailed		status.Error = errDelete.Error()	case deleted:		status.InstallStatus = pluginInstallStatusDeleted	default:		status.InstallStatus = pluginInstallStatusMissing	}	report.Plugins = append(report.Plugins, status)	finishReport(&report, errDelete)	return report} func deletePluginArtifact(ctx context.Context, root string, id string, pluginRuntime PluginRuntime) (string, bool, error) {	if ctx == nil {		ctx = context.Background()	}	if errContext := ctx.Err(); errContext != nil {		return "", false, errContext	}	id = strings.TrimSpace(id)	if !validPluginFileID(id) {		return "", false, fmt.Errorf("invalid plugin id %q", id)	}	paths, errPaths := pluginFilePaths(root, id)	if errPaths != nil {		return "", false, errPaths	}	if errContext := ctx.Err(); errContext != nil {		return "", false, errContext	}	if len(paths) == 0 {		return "", false, nil	}	if pluginRuntime != nil && pluginRuntime.PluginBusy(id) {		if errContext := ctx.Err(); errContext != nil {			return paths[0], false, errContext		}		unloaded := false		if contextual, ok := pluginRuntime.(contextualPluginUnloader); ok {			unloaded = contextual.UnloadPluginContext(ctx, id)		} else {			unloaded = pluginRuntime.UnloadPlugin(id)		}		if !unloaded && pluginRuntime.PluginBusy(id) {			return paths[0], false, sdkpluginstore.ErrLoadedPluginLocked		}	}	deleted := false	for _, path := range paths {		if errContext := ctx.Err(); errContext != nil {			return paths[0], deleted, errContext		}		if errRemove := os.Remove(path); errRemove != nil {			if errors.Is(errRemove, os.ErrNotExist) {				continue			}			return paths[0], deleted, errRemove		}		deleted = true		if errContext := ctx.Err(); errContext != nil {			return paths[0], deleted, errContext		}	}	return paths[0], deleted, nil} func currentPluginFilePath(root string, id string) (string, error) {	paths, errPaths := pluginFilePaths(root, id)	if errPaths != nil {		return "", errPaths	}	if len(paths) == 0 {		return "", nil	}	return paths[0], nil} func pluginFilePaths(root string, id string) ([]string, error) {	files, errFiles := pluginFileInfos(root, id)	if errFiles != nil {		return nil, errFiles	}	out := make([]string, 0, len(files))	for _, file := range files {		out = append(out, file.Path)	}

Lines 1–500 of 831.