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. Load more
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) }