Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
313
backend/internal/pluginhost/platform.go
Normal file
313
backend/internal/pluginhost/platform.go
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
package pluginhost
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
pluginIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`)
|
||||
pluginVersionPattern = regexp.MustCompile(`^[0-9][0-9A-Za-z.+-]*$`)
|
||||
)
|
||||
|
||||
type pluginFile struct {
|
||||
ID string
|
||||
Path string
|
||||
Version string
|
||||
}
|
||||
|
||||
// PluginFileInfo describes a plugin binary selected by the host discovery rules.
|
||||
type PluginFileInfo struct {
|
||||
ID string
|
||||
Path string
|
||||
Version string
|
||||
}
|
||||
|
||||
// ValidatePluginID reports whether id can be used as a plugin configuration key.
|
||||
func ValidatePluginID(id string) bool {
|
||||
return validPluginID(id)
|
||||
}
|
||||
|
||||
func validPluginID(id string) bool {
|
||||
return pluginIDPattern.MatchString(id)
|
||||
}
|
||||
|
||||
func validPluginVersion(version string) bool {
|
||||
return version != "" && !strings.HasPrefix(version, "v") && pluginVersionPattern.MatchString(version)
|
||||
}
|
||||
|
||||
func pluginIDFromPath(path string) string {
|
||||
file, ok := pluginFileFromPath(path, "")
|
||||
if ok {
|
||||
return file.ID
|
||||
}
|
||||
base := filepath.Base(path)
|
||||
lowerBase := strings.ToLower(base)
|
||||
for _, extension := range []string{".so", ".dylib", ".dll"} {
|
||||
if strings.HasSuffix(lowerBase, extension) {
|
||||
return base[:len(base)-len(extension)]
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func pluginFileFromPath(filePath string, requiredExtension string) (pluginFile, bool) {
|
||||
base := filepath.Base(filePath)
|
||||
lowerBase := strings.ToLower(base)
|
||||
extension := strings.TrimSpace(requiredExtension)
|
||||
if extension != "" {
|
||||
if !strings.HasSuffix(lowerBase, strings.ToLower(extension)) {
|
||||
return pluginFile{}, false
|
||||
}
|
||||
} else {
|
||||
for _, candidateExtension := range []string{".so", ".dylib", ".dll"} {
|
||||
if strings.HasSuffix(lowerBase, candidateExtension) {
|
||||
extension = candidateExtension
|
||||
break
|
||||
}
|
||||
}
|
||||
if extension == "" {
|
||||
return pluginFile{}, false
|
||||
}
|
||||
}
|
||||
name := base[:len(base)-len(extension)]
|
||||
id := name
|
||||
version := ""
|
||||
if versionIndex := strings.LastIndex(name, "-v"); versionIndex > 0 {
|
||||
candidateID := name[:versionIndex]
|
||||
candidateVersion := name[versionIndex+2:]
|
||||
if validPluginID(candidateID) && validPluginVersion(candidateVersion) {
|
||||
id = candidateID
|
||||
version = candidateVersion
|
||||
}
|
||||
}
|
||||
if !validPluginID(id) {
|
||||
return pluginFile{}, false
|
||||
}
|
||||
return pluginFile{ID: id, Path: filePath, Version: version}, true
|
||||
}
|
||||
|
||||
// PluginExtension returns the dynamic library file extension used for goos.
|
||||
func PluginExtension(goos string) string {
|
||||
return pluginExtension(goos)
|
||||
}
|
||||
|
||||
func pluginExtension(goos string) string {
|
||||
switch goos {
|
||||
case "darwin":
|
||||
return ".dylib"
|
||||
case "windows":
|
||||
return ".dll"
|
||||
default:
|
||||
return ".so"
|
||||
}
|
||||
}
|
||||
|
||||
func selectPluginFiles(root string, desiredVersions ...map[string]string) ([]pluginFile, error) {
|
||||
selected, _, errSelect := selectPluginFilesWithCandidates(root, desiredVersions...)
|
||||
return selected, errSelect
|
||||
}
|
||||
|
||||
func selectPluginFilesWithCandidates(root string, desiredVersions ...map[string]string) ([]pluginFile, []pluginFile, error) {
|
||||
root = strings.TrimSpace(root)
|
||||
if root == "" {
|
||||
root = "plugins"
|
||||
}
|
||||
desired := normalizeDesiredPluginVersions(desiredVersions...)
|
||||
|
||||
candidates := candidateDirs(root, runtime.GOOS, runtime.GOARCH)
|
||||
extension := pluginExtension(runtime.GOOS)
|
||||
selectedByID := make(map[string]pluginFile)
|
||||
order := make([]string, 0)
|
||||
all := make([]pluginFile, 0)
|
||||
for _, dir := range candidates {
|
||||
entries, errReadDir := os.ReadDir(dir)
|
||||
if errReadDir != nil {
|
||||
if os.IsNotExist(errReadDir) {
|
||||
continue
|
||||
}
|
||||
return nil, nil, errReadDir
|
||||
}
|
||||
files := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry == nil || !entry.Type().IsRegular() {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(entry.Name()), extension) {
|
||||
files = append(files, filepath.Join(dir, entry.Name()))
|
||||
}
|
||||
}
|
||||
sort.Strings(files)
|
||||
for _, path := range files {
|
||||
file, okFile := pluginFileFromPath(path, extension)
|
||||
if !okFile {
|
||||
continue
|
||||
}
|
||||
all = append(all, file)
|
||||
current, exists := selectedByID[file.ID]
|
||||
if !exists {
|
||||
selectedByID[file.ID] = file
|
||||
order = append(order, file.ID)
|
||||
continue
|
||||
}
|
||||
if pluginFilePreferredForDesired(file, current, desired[file.ID]) {
|
||||
selectedByID[file.ID] = file
|
||||
}
|
||||
}
|
||||
}
|
||||
selected := make([]pluginFile, 0, len(order))
|
||||
for _, id := range order {
|
||||
file := selectedByID[id]
|
||||
if desiredVersion := desired[id]; desiredVersion != "" && file.Version != desiredVersion {
|
||||
continue
|
||||
}
|
||||
selected = append(selected, file)
|
||||
}
|
||||
return selected, all, nil
|
||||
}
|
||||
|
||||
func normalizeDesiredPluginVersions(sources ...map[string]string) map[string]string {
|
||||
out := make(map[string]string)
|
||||
for _, source := range sources {
|
||||
for id, version := range source {
|
||||
id = strings.TrimSpace(id)
|
||||
version = normalizePluginDesiredVersion(version)
|
||||
if id == "" || version == "" {
|
||||
continue
|
||||
}
|
||||
out[id] = version
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func pluginFilePreferredForDesired(candidate pluginFile, current pluginFile, desiredVersion string) bool {
|
||||
desiredVersion = normalizePluginDesiredVersion(desiredVersion)
|
||||
if desiredVersion != "" {
|
||||
candidateMatches := candidate.Version == desiredVersion
|
||||
currentMatches := current.Version == desiredVersion
|
||||
if candidateMatches != currentMatches {
|
||||
return candidateMatches
|
||||
}
|
||||
}
|
||||
return pluginFilePreferred(candidate, current)
|
||||
}
|
||||
|
||||
func pluginFilePreferred(candidate pluginFile, current pluginFile) bool {
|
||||
if candidate.Version == "" {
|
||||
return false
|
||||
}
|
||||
if current.Version == "" {
|
||||
return true
|
||||
}
|
||||
comparison, comparable := comparePluginVersions(candidate.Version, current.Version)
|
||||
if !comparable {
|
||||
return candidate.Version > current.Version
|
||||
}
|
||||
return comparison > 0
|
||||
}
|
||||
|
||||
func comparePluginVersions(a, b string) (int, bool) {
|
||||
segmentsA := strings.Split(a, ".")
|
||||
segmentsB := strings.Split(b, ".")
|
||||
length := len(segmentsA)
|
||||
if len(segmentsB) > length {
|
||||
length = len(segmentsB)
|
||||
}
|
||||
for index := 0; index < length; index++ {
|
||||
numberA, okA := pluginVersionSegment(segmentsA, index)
|
||||
numberB, okB := pluginVersionSegment(segmentsB, index)
|
||||
if !okA || !okB {
|
||||
return 0, false
|
||||
}
|
||||
if numberA != numberB {
|
||||
if numberA < numberB {
|
||||
return -1, true
|
||||
}
|
||||
return 1, true
|
||||
}
|
||||
}
|
||||
return 0, true
|
||||
}
|
||||
|
||||
func pluginVersionSegment(segments []string, index int) (int64, bool) {
|
||||
if index >= len(segments) {
|
||||
return 0, true
|
||||
}
|
||||
number, errParse := strconv.ParseInt(segments[index], 10, 64)
|
||||
if errParse != nil || number < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return number, true
|
||||
}
|
||||
|
||||
func cleanupUnselectedPluginFiles(root string, loaded []pluginFile) error {
|
||||
if len(loaded) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, candidates, errSelect := selectPluginFilesWithCandidates(root)
|
||||
if errSelect != nil {
|
||||
return errSelect
|
||||
}
|
||||
loadedByID := make(map[string]map[string]struct{}, len(loaded))
|
||||
for _, file := range loaded {
|
||||
if strings.TrimSpace(file.ID) == "" || strings.TrimSpace(file.Path) == "" {
|
||||
continue
|
||||
}
|
||||
paths := loadedByID[file.ID]
|
||||
if paths == nil {
|
||||
paths = make(map[string]struct{})
|
||||
loadedByID[file.ID] = paths
|
||||
}
|
||||
paths[filepath.Clean(file.Path)] = struct{}{}
|
||||
}
|
||||
var errs []error
|
||||
for _, candidate := range candidates {
|
||||
paths := loadedByID[candidate.ID]
|
||||
if len(paths) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, selected := paths[filepath.Clean(candidate.Path)]; selected {
|
||||
continue
|
||||
}
|
||||
if errRemove := os.Remove(candidate.Path); errRemove != nil && !errors.Is(errRemove, os.ErrNotExist) {
|
||||
errs = append(errs, errRemove)
|
||||
log.WithError(errRemove).Warnf("pluginhost: failed to remove old plugin file %s", candidate.Path)
|
||||
continue
|
||||
}
|
||||
log.WithFields(pluginLogFields(candidate.ID, "", candidate.Version, candidate.Path)).Info("pluginhost: old plugin file removed")
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// DiscoverPluginFiles returns plugin binaries selected by the current host discovery rules.
|
||||
func DiscoverPluginFiles(root string, desiredVersions ...map[string]string) ([]PluginFileInfo, error) {
|
||||
files, errSelect := selectPluginFiles(root, desiredVersions...)
|
||||
if errSelect != nil {
|
||||
return nil, errSelect
|
||||
}
|
||||
out := make([]PluginFileInfo, 0, len(files))
|
||||
for _, file := range files {
|
||||
out = append(out, PluginFileInfo{
|
||||
ID: file.ID,
|
||||
Path: file.Path,
|
||||
Version: file.Version,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func candidateDirs(root, goos, goarch string) []string {
|
||||
dirs := make([]string, 0, 2)
|
||||
dirs = append(dirs, filepath.Join(root, goos, goarch))
|
||||
dirs = append(dirs, root)
|
||||
return dirs
|
||||
}
|
||||
Loading…
Reference in a new issue