mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-26 09:34:17 +08:00
182 lines
4.8 KiB
Go
182 lines
4.8 KiB
Go
//go:build windows || (cgo && (linux || darwin || freebsd))
|
|
|
|
package pluginhost
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Plugin dynamic libraries are loaded from content-addressed shadow copies.
|
|
// A loaded Go plugin image must stay mapped for the lifetime of the host
|
|
// process (its runtime cannot be unloaded), so loading from a shadow copy
|
|
// keeps the original artifact replaceable and gives every distinct plugin
|
|
// build its own path, which dynamic loaders map as a fresh image.
|
|
|
|
const (
|
|
shadowPluginPrefix = "cliproxy-plugin-"
|
|
shadowPluginTempPrefix = ".cliproxy-plugin-"
|
|
shadowPluginProcessDirPrefix = "pid-"
|
|
shadowPluginDigestLength = 32
|
|
)
|
|
|
|
var shadowPluginCleanupOnce sync.Once
|
|
|
|
func shadowCopyPlugin(file pluginFile) (string, error) {
|
|
dir, errDir := shadowPluginDir()
|
|
if errDir != nil {
|
|
return "", errDir
|
|
}
|
|
shadowPluginCleanupOnce.Do(func() {
|
|
removeStaleShadowPlugins(dir)
|
|
removeStaleShadowPluginDirs(filepath.Dir(dir), os.Getpid())
|
|
})
|
|
return shadowCopyPluginToDir(file, dir)
|
|
}
|
|
|
|
// removeStaleShadowPluginDirs removes shadow directories left behind by
|
|
// host processes that already exited. Directories whose owner process is
|
|
// still alive are kept, because that process may reuse the resident plugin
|
|
// images inside them.
|
|
func removeStaleShadowPluginDirs(baseDir string, currentPID int) {
|
|
entries, errRead := os.ReadDir(baseDir)
|
|
if errRead != nil {
|
|
return
|
|
}
|
|
for _, entry := range entries {
|
|
if entry == nil || !entry.IsDir() {
|
|
continue
|
|
}
|
|
name := entry.Name()
|
|
if !strings.HasPrefix(name, shadowPluginProcessDirPrefix) {
|
|
continue
|
|
}
|
|
pid, errParse := strconv.Atoi(strings.TrimPrefix(name, shadowPluginProcessDirPrefix))
|
|
if errParse != nil || pid <= 0 || pid == currentPID {
|
|
continue
|
|
}
|
|
if shadowPluginProcessAlive(pid) {
|
|
continue
|
|
}
|
|
removeShadowPluginDir(filepath.Join(baseDir, name))
|
|
}
|
|
}
|
|
|
|
func shadowCopyPluginToDir(file pluginFile, dir string) (string, error) {
|
|
source := filepath.Clean(file.Path)
|
|
tmp, errTemp := os.CreateTemp(dir, shadowPluginTempPrefix+file.ID+"-*"+filepath.Ext(source))
|
|
if errTemp != nil {
|
|
return "", errTemp
|
|
}
|
|
tmpName := tmp.Name()
|
|
removeTemp := true
|
|
defer func() {
|
|
if removeTemp {
|
|
removeShadowPlugin(tmpName)
|
|
}
|
|
}()
|
|
|
|
in, errOpen := os.Open(source)
|
|
if errOpen != nil {
|
|
_ = tmp.Close()
|
|
return "", errOpen
|
|
}
|
|
defer func() {
|
|
_ = in.Close()
|
|
}()
|
|
hasher := sha256.New()
|
|
size, errCopy := io.Copy(io.MultiWriter(tmp, hasher), in)
|
|
if errCopy != nil {
|
|
_ = tmp.Close()
|
|
return "", errCopy
|
|
}
|
|
if errClose := tmp.Close(); errClose != nil {
|
|
return "", errClose
|
|
}
|
|
digest := hex.EncodeToString(hasher.Sum(nil))
|
|
target := shadowPluginPath(dir, file.ID, digest, filepath.Ext(source))
|
|
if shadowPluginMatches(target, size, digest) {
|
|
return target, nil
|
|
}
|
|
if errRemove := os.Remove(target); errRemove != nil && !errors.Is(errRemove, os.ErrNotExist) {
|
|
if shadowPluginMatches(target, size, digest) {
|
|
return target, nil
|
|
}
|
|
removeShadowPlugin(target)
|
|
return "", fmt.Errorf("remove stale shadow plugin: %w", errRemove)
|
|
}
|
|
if errRename := os.Rename(tmpName, target); errRename != nil {
|
|
if shadowPluginMatches(target, size, digest) {
|
|
return target, nil
|
|
}
|
|
return "", fmt.Errorf("move shadow plugin: %w", errRename)
|
|
}
|
|
removeTemp = false
|
|
return target, nil
|
|
}
|
|
|
|
func shadowPluginDir() (string, error) {
|
|
dir := filepath.Join(os.TempDir(), "cliproxy-pluginhost", shadowPluginProcessDirName(os.Getpid()))
|
|
if errMkdir := os.MkdirAll(dir, 0o700); errMkdir != nil {
|
|
return "", errMkdir
|
|
}
|
|
return dir, nil
|
|
}
|
|
|
|
func shadowPluginProcessDirName(pid int) string {
|
|
return fmt.Sprintf("%s%d", shadowPluginProcessDirPrefix, pid)
|
|
}
|
|
|
|
func removeStaleShadowPlugins(dir string) {
|
|
entries, errRead := os.ReadDir(dir)
|
|
if errRead != nil {
|
|
return
|
|
}
|
|
for _, entry := range entries {
|
|
if entry == nil || entry.IsDir() {
|
|
continue
|
|
}
|
|
name := entry.Name()
|
|
if strings.HasPrefix(name, shadowPluginPrefix) || strings.HasPrefix(name, shadowPluginTempPrefix) {
|
|
removeShadowPlugin(filepath.Join(dir, name))
|
|
}
|
|
}
|
|
}
|
|
|
|
func shadowPluginPath(dir string, id string, digest string, extension string) string {
|
|
if len(digest) > shadowPluginDigestLength {
|
|
digest = digest[:shadowPluginDigestLength]
|
|
}
|
|
return filepath.Join(dir, shadowPluginPrefix+id+"-"+digest+extension)
|
|
}
|
|
|
|
func shadowPluginMatches(path string, size int64, digest string) bool {
|
|
info, errStat := os.Stat(path)
|
|
if errStat != nil {
|
|
return false
|
|
}
|
|
if !info.Mode().IsRegular() || info.Size() != size {
|
|
return false
|
|
}
|
|
file, errOpen := os.Open(path)
|
|
if errOpen != nil {
|
|
return false
|
|
}
|
|
defer func() {
|
|
_ = file.Close()
|
|
}()
|
|
hasher := sha256.New()
|
|
if _, errCopy := io.Copy(hasher, file); errCopy != nil {
|
|
return false
|
|
}
|
|
return hex.EncodeToString(hasher.Sum(nil)) == digest
|
|
}
|