internal/pluginhost/shadow_test.go
Rama main · réplica de lectura del repositorio original.
//go:build windows || (cgo && (linux || darwin || freebsd))
package pluginhost
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
func testPluginLibraryExtension() string {
switch runtime.GOOS {
case "windows":
return ".dll"
case "darwin":
return ".dylib"
default:
return ".so"
}
}
func TestShadowPluginDirUsesSharedCache(t *testing.T) {
dir, errDir := shadowPluginDir()
if errDir != nil {
t.Fatalf("shadowPluginDir() error = %v", errDir)
}
want := filepath.Join(os.TempDir(), "cliproxy-pluginhost", "cache")
if dir != want {
t.Fatalf("shadowPluginDir() = %q, want %q", dir, want)
}
}
func TestShadowCopyPluginReusesAcrossProcessRestartAndUsesLocalShadowDir(t *testing.T) {
pluginDir := t.TempDir()
source := filepath.Join(pluginDir, "alpha-v1.0.0"+testPluginLibraryExtension())
content := []byte("plugin-v1")
if errWrite := os.WriteFile(source, content, 0o644); errWrite != nil {
t.Fatalf("WriteFile() error = %v", errWrite)
}
file := pluginFile{ID: "alpha", Path: source, Version: "1.0.0"}
first, errFirst := shadowCopyPlugin(file)
if errFirst != nil {
t.Fatalf("shadowCopyPlugin() first error = %v", errFirst)
}
wantShadowDir := filepath.Join(pluginDir, ".shadow")
if filepath.Dir(first) != wantShadowDir {
t.Fatalf("shadowCopyPlugin() dir = %q, want %q", filepath.Dir(first), wantShadowDir)
}
past := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)
if errCh := os.Chtimes(first, past, past); errCh != nil {
t.Fatalf("Chtimes(first) error = %v", errCh)
}
if errCh := os.Chtimes(wantShadowDir, past, past); errCh != nil {
t.Fatalf("Chtimes(shadowDir) error = %v", errCh)
}
childPath := runChildShadowCopyPlugin(t, file)
if childPath != first {
t.Fatalf("child process shadowCopyPlugin() = %q, want reused %q", childPath, first)
}
info, errStat := os.Stat(first)
if errStat != nil {
t.Fatalf("Stat(%s) after child restart error = %v", first, errStat)
}
if !info.ModTime().Equal(past) {
t.Fatalf("shadow file ModTime = %v, want unchanged %v", info.ModTime(), past)
}
dirInfo, errDirStat := os.Stat(wantShadowDir)
if errDirStat != nil {
t.Fatalf("Stat(%s) after child restart error = %v", wantShadowDir, errDirStat)
}
if !dirInfo.ModTime().Equal(past) {
t.Fatalf("shadow dir ModTime = %v, want unchanged %v (no temp file should be created on cache hit)", dirInfo.ModTime(), past)
}
}
func TestShadowCopyPluginToDirSkipsTempWriteWhenShadowMatches(t *testing.T) {
dir := t.TempDir()
source := filepath.Join(t.TempDir(), "alpha"+testPluginLibraryExtension())
if errWrite := os.WriteFile(source, []byte("plugin-v1"), 0o644); errWrite != nil {
t.Fatalf("WriteFile() error = %v", errWrite)
}
file := pluginFile{ID: "alpha", Path: source}
first, errFirst := shadowCopyPluginToDir(file, dir)
if errFirst != nil {
t.Fatalf("shadowCopyPluginToDir() first error = %v", errFirst)
}
past := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)
if errCh := os.Chtimes(first, past, past); errCh != nil {
t.Fatalf("Chtimes(first) error = %v", errCh)
}
if errCh := os.Chtimes(dir, past, past); errCh != nil {
t.Fatalf("Chtimes(dir) error = %v", errCh)
}
second, errSecond := shadowCopyPluginToDir(file, dir)
if errSecond != nil {
t.Fatalf("shadowCopyPluginToDir() second error = %v", errSecond)
}
if second != first {
t.Fatalf("second shadow path = %q, want %q", second, first)
}
dirInfo, errDirStat := os.Stat(dir)
if errDirStat != nil {
t.Fatalf("Stat(dir) error = %v", errDirStat)
}
if !dirInfo.ModTime().Equal(past) {
t.Fatalf("shadow dir ModTime = %v, want unchanged %v (temp file was written before checking shadowPluginMatches)", dirInfo.ModTime(), past)
}
}
func TestShadowCopyPluginFallsBackToSharedCacheWhenLocalShadowDirUnavailable(t *testing.T) {
pluginDir := t.TempDir()
// Create a regular file named ".shadow" so creating the local .shadow directory fails.
blockerPath := filepath.Join(pluginDir, shadowPluginLocalDirName)
if errWrite := os.WriteFile(blockerPath, []byte("blocker"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(blocker) error = %v", errWrite)
}
source := filepath.Join(pluginDir, "fallback-v1.0.0"+testPluginLibraryExtension())
if errWrite := os.WriteFile(source, []byte("plugin-fallback"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(source) error = %v", errWrite)
}
file := pluginFile{ID: "fallback", Path: source, Version: "1.0.0"}
got, errShadow := shadowCopyPlugin(file)
if errShadow != nil {
t.Fatalf("shadowCopyPlugin() error = %v", errShadow)
}
t.Cleanup(func() {
removeShadowPlugin(got)
})
wantDir, errDir := shadowPluginFallbackDir(source)
if errDir != nil {
t.Fatalf("shadowPluginFallbackDir() error = %v", errDir)
}
if filepath.Dir(got) != wantDir {
t.Fatalf("shadowCopyPlugin() fallback dir = %q, want %q", filepath.Dir(got), wantDir)
}
}
func TestShadowCopyPluginFallbackCacheIsolatesDistinctPluginDirs(t *testing.T) {
ext := testPluginLibraryExtension()
rootA := t.TempDir()
platformDirA := filepath.Join(rootA, runtime.GOOS, runtime.GOARCH)
if errMkdir := os.MkdirAll(platformDirA, 0o755); errMkdir != nil {
t.Fatalf("MkdirAll(platformDirA) error = %v", errMkdir)
}
if errWrite := os.WriteFile(filepath.Join(platformDirA, shadowPluginLocalDirName), []byte("blocker"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(blockerA) error = %v", errWrite)
}
sourceA := filepath.Join(platformDirA, "alpha-v1.0.0"+ext)
if errWrite := os.WriteFile(sourceA, []byte("alpha-install-A"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(sourceA) error = %v", errWrite)
}
fileA := pluginFile{ID: "alpha", Path: sourceA, Version: "1.0.0"}
rootB := t.TempDir()
platformDirB := filepath.Join(rootB, runtime.GOOS, runtime.GOARCH)
if errMkdir := os.MkdirAll(platformDirB, 0o755); errMkdir != nil {
t.Fatalf("MkdirAll(platformDirB) error = %v", errMkdir)
}
if errWrite := os.WriteFile(filepath.Join(platformDirB, shadowPluginLocalDirName), []byte("blocker"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(blockerB) error = %v", errWrite)
}
sourceB := filepath.Join(platformDirB, "alpha-v1.0.0"+ext)
if errWrite := os.WriteFile(sourceB, []byte("alpha-install-B"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(sourceB) error = %v", errWrite)
}
fileB := pluginFile{ID: "alpha", Path: sourceB, Version: "1.0.0"}
shadowA, errShadowA := shadowCopyPlugin(fileA)
if errShadowA != nil {
t.Fatalf("shadowCopyPlugin(fileA) error = %v", errShadowA)
}
t.Cleanup(func() { removeShadowPluginDir(filepath.Dir(shadowA)) })
shadowB, errShadowB := shadowCopyPlugin(fileB)
if errShadowB != nil {
t.Fatalf("shadowCopyPlugin(fileB) error = %v", errShadowB)
}
t.Cleanup(func() { removeShadowPluginDir(filepath.Dir(shadowB)) })
if filepath.Dir(shadowA) == filepath.Dir(shadowB) {
t.Fatalf("fallback cache dirs collided across installations: %q", filepath.Dir(shadowA))
}
if errCleanup := cleanupUnselectedPluginFiles(rootA, []pluginFile{fileA}); errCleanup != nil {
t.Fatalf("cleanupUnselectedPluginFiles(rootA) error = %v", errCleanup)
}
if _, errStat := os.Stat(shadowB); errStat != nil {
t.Fatalf("Stat(shadowB %s) after rootA cleanup error = %v, want kept", shadowB, errStat)
}
}
func TestShadowDirectoryDigestResolvesRelativePathsByWorkingDirectory(t *testing.T) {
origWD, errWD := os.Getwd()
if errWD != nil {
t.Fatalf("Getwd() error = %v", errWD)
}
t.Cleanup(func() {
_ = os.Chdir(origWD)
})
workA := t.TempDir()
workB := t.TempDir()
relDir := filepath.Join("plugins", runtime.GOOS, runtime.GOARCH)
if errChdir := os.Chdir(workA); errChdir != nil {
t.Fatalf("Chdir(workA) error = %v", errChdir)
}
digestA := shadowDirectoryDigest(relDir)
if errChdir := os.Chdir(workB); errChdir != nil {
t.Fatalf("Chdir(workB) error = %v", errChdir)
}
digestB := shadowDirectoryDigest(relDir)
if digestA == digestB {
t.Fatalf("shadowDirectoryDigest(%q) collided across working directories: %s", relDir, digestA)
}
}
func TestShadowCopyPluginConcurrentPublishNeverUnlinksExistingTarget(t *testing.T) {
dir := t.TempDir()
source := filepath.Join(t.TempDir(), "alpha"+testPluginLibraryExtension())
content := []byte("concurrent-plugin-content")
if errWrite := os.WriteFile(source, content, 0o644); errWrite != nil {
t.Fatalf("WriteFile() error = %v", errWrite)
}
file := pluginFile{ID: "alpha", Path: source}
first, errFirst := shadowCopyPluginToDir(file, dir)
if errFirst != nil {
t.Fatalf("shadowCopyPluginToDir() initial error = %v", errFirst)
}
infoBefore, errStatBefore := os.Stat(first)
if errStatBefore != nil {
t.Fatalf("Stat(first) error = %v", errStatBefore)
}
size, digest, errHash := hashPluginFile(source)
if errHash != nil {
t.Fatalf("hashPluginFile() error = %v", errHash)
}
// Simulate a racer that already wrote its temp file before winner published target,
// and now attempts publishShadowTempFile against the already-published target.
racerTmp := filepath.Join(dir, shadowPluginTempPrefix+"racer"+testPluginLibraryExtension())
if errWrite := os.WriteFile(racerTmp, content, 0o644); errWrite != nil {
t.Fatalf("WriteFile(racerTmp) error = %v", errWrite)
}
if errPublish := publishShadowTempFile(racerTmp, first, size, digest); errPublish != nil {
t.Fatalf("publishShadowTempFile() error = %v", errPublish)
}
if _, errStatTmp := os.Stat(racerTmp); !os.IsNotExist(errStatTmp) {
t.Fatalf("Stat(racerTmp) error = %v, want not exist", errStatTmp)
}
infoAfter, errStatAfter := os.Stat(first)
if errStatAfter != nil {
t.Fatalf("Stat(first) after race error = %v", errStatAfter)
}
if !os.SameFile(infoBefore, infoAfter) {
t.Fatal("existing target shadow file was unlinked/replaced by racer")
}
}
func TestCleanupUnselectedPluginFilesDoesNotConfusePrefixPluginIDs(t *testing.T) {
root := t.TempDir()
platformDir := filepath.Join(root, runtime.GOOS, runtime.GOARCH)
if errMkdir := os.MkdirAll(platformDir, 0o755); errMkdir != nil {
t.Fatalf("MkdirAll() error = %v", errMkdir)
}
ext := testPluginLibraryExtension()
alphaOldPath := filepath.Join(platformDir, "alpha-v1.0.0"+ext)
alphaNewPath := filepath.Join(platformDir, "alpha-v2.0.0"+ext)
alphaBetaPath := filepath.Join(platformDir, "alpha-beta-v1.0.0"+ext)
for path, content := range map[string]string{
alphaOldPath: "alpha-v1",
alphaNewPath: "alpha-v2",
alphaBetaPath: "alpha-beta-v1",
} {
if errWrite := os.WriteFile(path, []byte(content), 0o644); errWrite != nil {
t.Fatalf("WriteFile(%s) error = %v", path, errWrite)
}
}
alphaOldShadow, errOld := shadowCopyPlugin(pluginFile{ID: "alpha", Path: alphaOldPath, Version: "1.0.0"})
if errOld != nil {
t.Fatalf("shadowCopyPlugin(alpha old) error = %v", errOld)
}
alphaNewFile := pluginFile{ID: "alpha", Path: alphaNewPath, Version: "2.0.0"}
alphaNewShadow, errNew := shadowCopyPlugin(alphaNewFile)
if errNew != nil {
t.Fatalf("shadowCopyPlugin(alpha new) error = %v", errNew)
}
alphaBetaShadow, errBeta := shadowCopyPlugin(pluginFile{ID: "alpha-beta", Path: alphaBetaPath, Version: "1.0.0"})
if errBeta != nil {
t.Fatalf("shadowCopyPlugin(alpha-beta) error = %v", errBeta)
}
removedPath := filepath.Join(platformDir, "removed-v1.0.0"+ext)
if errWrite := os.WriteFile(removedPath, []byte("removed-v1"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(removedPath) error = %v", errWrite)
}
removedShadow, errRemoved := shadowCopyPlugin(pluginFile{ID: "removed", Path: removedPath, Version: "1.0.0"})
if errRemoved != nil {
t.Fatalf("shadowCopyPlugin(removed) error = %v", errRemoved)
}
if errRemove := os.Remove(removedPath); errRemove != nil {
t.Fatalf("Remove(removedPath) error = %v", errRemove)
}
if errCleanup := cleanupUnselectedPluginFiles(root, []pluginFile{alphaNewFile}); errCleanup != nil {
t.Fatalf("cleanupUnselectedPluginFiles() error = %v", errCleanup)
}
if _, errStat := os.Stat(alphaOldShadow); !os.IsNotExist(errStat) {
t.Fatalf("Stat(alphaOldShadow %s) error = %v, want not exist", alphaOldShadow, errStat)
}
if _, errStat := os.Stat(removedShadow); !os.IsNotExist(errStat) {
t.Fatalf("Stat(removedShadow %s) error = %v, want not exist", removedShadow, errStat)
}
if _, errStat := os.Stat(alphaNewShadow); errStat != nil {
t.Fatalf("Stat(alphaNewShadow %s) error = %v, want kept", alphaNewShadow, errStat)
}
if _, errStat := os.Stat(alphaBetaShadow); errStat != nil {
t.Fatalf("Stat(alphaBetaShadow %s) error = %v, want kept for sibling plugin", alphaBetaShadow, errStat)
}
}
func TestPruneShadowDirPreservesConcurrentlyInstalledPluginAndLiveGuard(t *testing.T) {
root := t.TempDir()
platformDir := filepath.Join(root, runtime.GOOS, runtime.GOARCH)
if errMkdir := os.MkdirAll(platformDir, 0o755); errMkdir != nil {
t.Fatalf("MkdirAll() error = %v", errMkdir)
}
ext := testPluginLibraryExtension()
v1Path := filepath.Join(platformDir, "alpha-v1.0.0"+ext)
if errWrite := os.WriteFile(v1Path, []byte("alpha-v1"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(v1Path) error = %v", errWrite)
}
v1File := pluginFile{ID: "alpha", Path: v1Path, Version: "1.0.0"}
v1Shadow, errV1 := shadowCopyPlugin(v1File)
if errV1 != nil {
t.Fatalf("shadowCopyPlugin(v1) error = %v", errV1)
}
// Simulate another process installing v2 and publishing its shadow file after keepNames was computed for v1.
v2Path := filepath.Join(platformDir, "alpha-v2.0.0"+ext)
if errWrite := os.WriteFile(v2Path, []byte("alpha-v2"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(v2Path) error = %v", errWrite)
}
v2File := pluginFile{ID: "alpha", Path: v2Path, Version: "2.0.0"}
v2Shadow, errV2 := shadowCopyPlugin(v2File)
if errV2 != nil {
t.Fatalf("shadowCopyPlugin(v2) error = %v", errV2)
}
shadowDir := filepath.Dir(v1Shadow)
pruneShadowDirForSourceDir(shadowDir, platformDir, []string{filepath.Base(v1Shadow)})
if _, errStat := os.Stat(v2Shadow); errStat != nil {
t.Fatalf("Stat(v2Shadow %s) error = %v, want preserved because v2 exists in sourceDir", v2Shadow, errStat)
}
// Write v2 content to a separate temp source before removing v2Path from platformDir so we can
// also test the interleaving where v2 was removed from platformDir while another process holds a guard.
detachedV2Path := filepath.Join(t.TempDir(), "alpha-v2.0.0"+ext)
if errWrite := os.WriteFile(detachedV2Path, []byte("alpha-v2"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(detachedV2Path) error = %v", errWrite)
}
v2File = pluginFile{ID: "alpha", Path: detachedV2Path, Version: "2.0.0"}
if errRemove := os.Remove(v2Path); errRemove != nil {
t.Fatalf("Remove(v2Path) error = %v", errRemove)
}
_, releaseGuard, errAcquire := acquireShadowPluginLoad(v1File)
if errAcquire != nil {
t.Fatalf("acquireShadowPluginLoad() error = %v", errAcquire)
}
pruneShadowDirForSourceDir(shadowDir, platformDir, []string{filepath.Base(v1Shadow)})
if _, errStat := os.Stat(v2Shadow); errStat != nil {
releaseGuard()
t.Fatalf("Stat(v2Shadow %s) while load guard active error = %v, want preserved", v2Shadow, errStat)
}
releaseGuard()
pruneShadowDirForSourceDir(shadowDir, platformDir, []string{filepath.Base(v1Shadow)})
if _, errStat := os.Stat(v2Shadow); !os.IsNotExist(errStat) {
t.Fatalf("Stat(v2Shadow %s) after guard released error = %v, want not exist", v2Shadow, errStat)
}
// Verify deterministic interleaving: pruneShadowDirForSourceDir has already read directory entries,
// and before it acquires the coord lock to delete a candidate shadow file, another caller acquires a load guard.
v2ShadowRe, errV2Re := shadowCopyPluginToDir(v2File, shadowDir)
if errV2Re != nil {
t.Fatalf("shadowCopyPluginToDir(v2) error = %v", errV2Re)
}
var releaseInterleavedGuard func()
origHook := pruneShadowBeforeLockHook
t.Cleanup(func() {
pruneShadowBeforeLockHook = origHook
if releaseInterleavedGuard != nil {
releaseInterleavedGuard()
}
})
pruneShadowBeforeLockHook = func(shadowFileName string) {
if shadowFileName == filepath.Base(v2ShadowRe) && releaseInterleavedGuard == nil {
_, release, errLoad := acquireShadowPluginLoad(v1File)
if errLoad != nil {
t.Errorf("interleaved acquireShadowPluginLoad() error = %v", errLoad)
return
}
releaseInterleavedGuard = release
}
}
pruneShadowDirForSourceDir(shadowDir, platformDir, []string{filepath.Base(v1Shadow)})
pruneShadowBeforeLockHook = origHook
if _, errStat := os.Stat(v2ShadowRe); errStat != nil {
t.Fatalf("Stat(v2ShadowRe %s) after interleaved guard acquisition error = %v, want preserved", v2ShadowRe, errStat)
}
// Even when keepNames is empty (e.g., all source files were removed or failed hashing),
// an active load guard must still prevent pruning shadow files.
pruneShadowDirForSourceDir(shadowDir, platformDir, nil)
if _, errStat := os.Stat(v2ShadowRe); errStat != nil {
t.Fatalf("Stat(v2ShadowRe %s) with empty keepNames while guard active error = %v, want preserved", v2ShadowRe, errStat)
}
if releaseInterleavedGuard != nil {
releaseInterleavedGuard()
releaseInterleavedGuard = nil
}
// Verify that if a process crashes/exits abnormally while holding the OS coordination lock,
// the kernel automatically releases the lock so subsequent loads and pruning recover immediately.
cmd := exec.Command(os.Args[0], "-test.run=^TestShadowHelperHoldLockAndExit$")
cmd.Env = append(
os.Environ(),
"GO_WANT_SHADOW_HOLD_LOCK_EXIT_HELPER=1",
"CLIPROXY_SHADOW_HELPER_SOURCE_DIR="+platformDir,
"CLIPROXY_SHADOW_HELPER_SHADOW_DIR="+shadowDir,
)
if output, errRun := cmd.CombinedOutput(); errRun == nil {
t.Fatalf("helper unexpectedly exited with status 0: %s", string(output))
}
_, releaseAfterCrash, errAfterCrash := acquireShadowPluginLoad(v1File)
if errAfterCrash != nil {
t.Fatalf("acquireShadowPluginLoad() after crashing lock holder error = %v", errAfterCrash)
}
releaseAfterCrash()
pruneShadowDirForSourceDir(shadowDir, platformDir, []string{filepath.Base(v1Shadow)})
if _, errStat := os.Stat(v2ShadowRe); !os.IsNotExist(errStat) {
t.Fatalf("Stat(v2ShadowRe %s) after crashing lock holder and prune error = %v, want not exist", v2ShadowRe, errStat)
}
}
func TestRemoveStaleShadowTempFilesPreservesLiveProcessTempFile(t *testing.T) {
dir := t.TempDir()
ext := testPluginLibraryExtension()
currentPID := os.Getpid()
deadPID := startExitedProcessPID(t)
if deadPID == currentPID {
t.Skip("exited helper process reused the test pid")
}
liveOther, liveOtherPID := startWaitingProcess(t)
if liveOtherPID == currentPID || liveOtherPID == deadPID {
t.Skip("live helper process pid collided with another test pid")
}
defer stopWaitingProcess(t, liveOther)
selfTemp := filepath.Join(dir, fmt.Sprintf("%s%s%d-alpha-self%s", shadowPluginTempPrefix, shadowPluginProcessDirPrefix, currentPID, ext))
liveTemp := filepath.Join(dir, fmt.Sprintf("%s%s%d-alpha-live%s", shadowPluginTempPrefix, shadowPluginProcessDirPrefix, liveOtherPID, ext))
deadTemp := filepath.Join(dir, fmt.Sprintf("%s%s%d-alpha-dead%s", shadowPluginTempPrefix, shadowPluginProcessDirPrefix, deadPID, ext))
legacyTemp := filepath.Join(dir, shadowPluginTempPrefix+"alpha-legacy"+ext)
for _, path := range []string{selfTemp, liveTemp, deadTemp, legacyTemp} {
if errWrite := os.WriteFile(path, []byte("temp"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(%s) error = %v", path, errWrite)
}
}
removeStaleShadowTempFiles(dir)
for _, deadPath := range []string{deadTemp, legacyTemp} {
if _, errStat := os.Stat(deadPath); !os.IsNotExist(errStat) {
t.Fatalf("Stat(%s) error = %v, want not exist", deadPath, errStat)
}
}
for _, livePath := range []string{selfTemp, liveTemp} {
if _, errStat := os.Stat(livePath); errStat != nil {
t.Fatalf("Stat(%s) error = %v, want kept for live process", livePath, errStat)
}
}
}
func TestCleanupUnselectedPluginFilesPrunesStaleShadowPlugins(t *testing.T) {
root := t.TempDir()
platformDir := filepath.Join(root, runtime.GOOS, runtime.GOARCH)
if errMkdir := os.MkdirAll(platformDir, 0o755); errMkdir != nil {
t.Fatalf("MkdirAll() error = %v", errMkdir)
}
ext := testPluginLibraryExtension()
oldPath := filepath.Join(platformDir, "alpha-v1.0.0"+ext)
newPath := filepath.Join(platformDir, "alpha-v2.0.0"+ext)
if errWrite := os.WriteFile(oldPath, []byte("alpha-v1"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(oldPath) error = %v", errWrite)
}
if errWrite := os.WriteFile(newPath, []byte("alpha-v2"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(newPath) error = %v", errWrite)
}
oldShadow, errOldShadow := shadowCopyPlugin(pluginFile{ID: "alpha", Path: oldPath, Version: "1.0.0"})
if errOldShadow != nil {
t.Fatalf("shadowCopyPlugin(old) error = %v", errOldShadow)
}
newFile := pluginFile{ID: "alpha", Path: newPath, Version: "2.0.0"}
newShadow, errNewShadow := shadowCopyPlugin(newFile)
if errNewShadow != nil {
t.Fatalf("shadowCopyPlugin(new) error = %v", errNewShadow)
}
tempShadow := filepath.Join(filepath.Dir(newShadow), shadowPluginTempPrefix+"alpha-stale"+ext)
if errWrite := os.WriteFile(tempShadow, []byte("partial"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(tempShadow) error = %v", errWrite)
}
if errCleanup := cleanupUnselectedPluginFiles(root, []pluginFile{newFile}); errCleanup != nil {
t.Fatalf("cleanupUnselectedPluginFiles() error = %v", errCleanup)
}
if _, errStat := os.Stat(oldShadow); !os.IsNotExist(errStat) {
t.Fatalf("Stat(oldShadow %s) error = %v, want not exist", oldShadow, errStat)
}
if _, errStat := os.Stat(tempShadow); !os.IsNotExist(errStat) {
t.Fatalf("Stat(tempShadow %s) error = %v, want not exist", tempShadow, errStat)
}
if _, errStat := os.Stat(newShadow); errStat != nil {
t.Fatalf("Stat(newShadow %s) error = %v, want kept", newShadow, errStat)
}
}
func TestShadowCopyPluginReusesContentAddressedShadow(t *testing.T) {
dir := t.TempDir()
source := filepath.Join(t.TempDir(), "alpha"+testPluginLibraryExtension())
content := []byte("plugin-v1")
if errWrite := os.WriteFile(source, content, 0o644); errWrite != nil {
t.Fatalf("WriteFile() error = %v", errWrite)
}
file := pluginFile{ID: "alpha", Path: source}
first, errFirst := shadowCopyPluginToDir(file, dir)
if errFirst != nil {
t.Fatalf("shadowCopyPluginToDir() first error = %v", errFirst)
}
second, errSecond := shadowCopyPluginToDir(file, dir)
if errSecond != nil {
t.Fatalf("shadowCopyPluginToDir() second error = %v", errSecond)
}
if second != first {
t.Fatalf("second shadow path = %q, want reused path %q", second, first)
}
gotContent, errRead := os.ReadFile(first)
if errRead != nil {
t.Fatalf("ReadFile(%s) error = %v", first, errRead)
}
if string(gotContent) != string(content) {
t.Fatalf("shadow content = %q, want %q", gotContent, content)
}
digest := sha256.Sum256(content)
wantDigest := hex.EncodeToString(digest[:])[:shadowPluginDigestLength]
name := filepath.Base(first)
if !strings.HasPrefix(name, shadowPluginPrefix+"alpha-") || !strings.Contains(name, wantDigest) {
t.Fatalf("shadow file name = %q, want alpha content digest %s", name, wantDigest)
}
if count := countShadowPluginFiles(t, dir); count != 1 {
t.Fatalf("shadow file count = %d, want 1", count)
}
}
func TestShadowCopyPluginCreatesNewPathForChangedContent(t *testing.T) {
dir := t.TempDir()
source := filepath.Join(t.TempDir(), "alpha"+testPluginLibraryExtension())
file := pluginFile{ID: "alpha", Path: source}
if errWrite := os.WriteFile(source, []byte("plugin-v1"), 0o644); errWrite != nil {
t.Fatalf("WriteFile() v1 error = %v", errWrite)
}
first, errFirst := shadowCopyPluginToDir(file, dir)
if errFirst != nil {
t.Fatalf("shadowCopyPluginToDir() v1 error = %v", errFirst)
}
if errWrite := os.WriteFile(source, []byte("plugin-v2"), 0o644); errWrite != nil {
t.Fatalf("WriteFile() v2 error = %v", errWrite)
}
second, errSecond := shadowCopyPluginToDir(file, dir)
if errSecond != nil {
t.Fatalf("shadowCopyPluginToDir() v2 error = %v", errSecond)
}
if second == first {
t.Fatalf("second shadow path reused %q after content changed", second)
}
if count := countShadowPluginFiles(t, dir); count != 2 {
t.Fatalf("shadow file count = %d, want 2 versions", count)
}
}
func TestShadowCopyPluginReplacesCorruptSameSizeShadow(t *testing.T) {
dir := t.TempDir()
source := filepath.Join(t.TempDir(), "alpha"+testPluginLibraryExtension())
content := []byte("plugin-v1")
if errWrite := os.WriteFile(source, content, 0o644); errWrite != nil {
t.Fatalf("WriteFile() source error = %v", errWrite)
}
digest := sha256.Sum256(content)
target := shadowPluginPath(dir, "alpha", hex.EncodeToString(digest[:]), testPluginLibraryExtension())
if errWrite := os.WriteFile(target, []byte("corrupt!!"), 0o644); errWrite != nil {
t.Fatalf("WriteFile() corrupt shadow error = %v", errWrite)
}
gotPath, errCopy := shadowCopyPluginToDir(pluginFile{ID: "alpha", Path: source}, dir)
if errCopy != nil {
t.Fatalf("shadowCopyPluginToDir() error = %v", errCopy)
}
if gotPath != target {
t.Fatalf("shadow path = %q, want %q", gotPath, target)
}
gotContent, errRead := os.ReadFile(target)
if errRead != nil {
t.Fatalf("ReadFile(%s) error = %v", target, errRead)
}
if string(gotContent) != string(content) {
t.Fatalf("shadow content = %q, want %q", gotContent, content)
}
if count := countShadowPluginFiles(t, dir); count != 1 {
t.Fatalf("shadow file count = %d, want 1", count)
}
}
func TestRemoveStaleShadowPluginsOnlyRemovesShadowFiles(t *testing.T) {
dir := t.TempDir()
stale := filepath.Join(dir, shadowPluginPrefix+"alpha-deadbeef"+testPluginLibraryExtension())
temp := filepath.Join(dir, shadowPluginTempPrefix+"alpha-temp"+testPluginLibraryExtension())
keep := filepath.Join(dir, "keep"+testPluginLibraryExtension())
for _, path := range []string{stale, temp, keep} {
if errWrite := os.WriteFile(path, []byte("x"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(%s) error = %v", path, errWrite)
}
}
removeStaleShadowPlugins(dir)
for _, path := range []string{stale, temp} {
if _, errStat := os.Stat(path); !os.IsNotExist(errStat) {
t.Fatalf("Stat(%s) error = %v, want not exist", path, errStat)
}
}
if _, errStat := os.Stat(keep); errStat != nil {
t.Fatalf("Stat(%s) error = %v, want kept", keep, errStat)
}
}
func TestRemoveStaleShadowPluginDirsOnlyRemovesDeadProcesses(t *testing.T) {
base := t.TempDir()
currentPID := os.Getpid()
deadPID := startExitedProcessPID(t)
if deadPID == currentPID {
t.Skip("exited helper process reused the test pid")
}
liveDir := filepath.Join(base, shadowPluginProcessDirName(currentPID))
deadDir := filepath.Join(base, shadowPluginProcessDirName(deadPID))
otherDir := filepath.Join(base, "unrelated")
// A directory owned by a different live process must survive the
// liveness check itself, not just the current-pid shortcut.
liveOther, liveOtherPID := startWaitingProcess(t)
if liveOtherPID == currentPID || liveOtherPID == deadPID {
t.Skip("live helper process pid collided with another test pid")
}
liveOtherDir := filepath.Join(base, shadowPluginProcessDirName(liveOtherPID))
for _, dir := range []string{liveDir, deadDir, otherDir, liveOtherDir} {
if errMkdir := os.MkdirAll(dir, 0o755); errMkdir != nil {
t.Fatalf("MkdirAll(%s) error = %v", dir, errMkdir)
}
shadowFile := filepath.Join(dir, shadowPluginPrefix+"alpha-deadbeef"+testPluginLibraryExtension())
if errWrite := os.WriteFile(shadowFile, []byte("x"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(%s) error = %v", shadowFile, errWrite)
}
}
removeStaleShadowPluginDirs(base, currentPID)
if _, errStat := os.Stat(deadDir); !os.IsNotExist(errStat) {
t.Fatalf("Stat(%s) error = %v, want not exist", deadDir, errStat)
}
for _, dir := range []string{liveDir, otherDir, liveOtherDir} {
if _, errStat := os.Stat(dir); errStat != nil {
t.Fatalf("Stat(%s) error = %v, want kept", dir, errStat)
}
}
stopWaitingProcess(t, liveOther)
}
func startExitedProcessPID(t *testing.T) int {
t.Helper()
cmd := exec.Command(os.Args[0], "-test.run=^TestShadowHelperProcessExits$")
cmd.Env = append(os.Environ(), "CLIPROXY_SHADOW_HELPER=1")
if errRun := cmd.Run(); errRun != nil {
t.Fatalf("helper process error = %v", errRun)
}
if cmd.Process == nil {
t.Fatal("helper process is nil")
}
return cmd.Process.Pid
}
func TestShadowHelperProcessExits(t *testing.T) {
if os.Getenv("CLIPROXY_SHADOW_HELPER") != "1" {
t.Skip("helper process entry point")
}
}
func runChildShadowCopyPlugin(t *testing.T, file pluginFile) string {
t.Helper()
cmd := exec.Command(os.Args[0], "-test.run=^TestShadowHelperCopyPlugin$")
cmd.Env = append(
os.Environ(),
"CLIPROXY_SHADOW_COPY_HELPER=1",
"CLIPROXY_SHADOW_COPY_ID="+file.ID,
"CLIPROXY_SHADOW_COPY_PATH="+file.Path,
"CLIPROXY_SHADOW_COPY_VERSION="+file.Version,
)
out, errRun := cmd.CombinedOutput()
if errRun != nil {
t.Fatalf("child shadowCopyPlugin helper error = %v\n%s", errRun, out)
}
for _, line := range strings.Split(string(out), "\n") {
if strings.HasPrefix(line, "SHADOW_PATH=") {
return strings.TrimSpace(strings.TrimPrefix(line, "SHADOW_PATH="))
}
}
t.Fatalf("child shadowCopyPlugin output missing SHADOW_PATH: %s", out)
return ""
}
func TestShadowHelperCopyPlugin(t *testing.T) {
if os.Getenv("CLIPROXY_SHADOW_COPY_HELPER") != "1" {
t.Skip("shadow copy helper process entry point")
}
got, errShadow := shadowCopyPlugin(pluginFile{
ID: os.Getenv("CLIPROXY_SHADOW_COPY_ID"),
Path: os.Getenv("CLIPROXY_SHADOW_COPY_PATH"),
Version: os.Getenv("CLIPROXY_SHADOW_COPY_VERSION"),
})
if errShadow != nil {
t.Fatalf("shadowCopyPlugin() error = %v", errShadow)
}
fmt.Printf("SHADOW_PATH=%s\n", got)
}
func startWaitingProcess(t *testing.T) (*exec.Cmd, int) {
t.Helper()
cmd := exec.Command(os.Args[0], "-test.run=^TestShadowHelperProcessWaits$")
cmd.Env = append(os.Environ(), "CLIPROXY_SHADOW_HELPER_WAIT=1")
stdin, errStdin := cmd.StdinPipe()
if errStdin != nil {
t.Fatalf("StdinPipe() error = %v", errStdin)
}
if errStart := cmd.Start(); errStart != nil {
t.Fatalf("start waiting helper error = %v", errStart)
}
t.Cleanup(func() {
_ = stdin.Close()
_ = cmd.Wait()
})
if cmd.Process == nil {
t.Fatal("waiting helper process is nil")
}
return cmd, cmd.Process.Pid
}
func stopWaitingProcess(t *testing.T, cmd *exec.Cmd) {
t.Helper()
if cmd == nil || cmd.Process == nil {
return
}
_ = cmd.Process.Kill()
_ = cmd.Wait()
}
func TestShadowHelperHoldLockAndExit(t *testing.T) {
if os.Getenv("GO_WANT_SHADOW_HOLD_LOCK_EXIT_HELPER") != "1" {
t.Skip("crash lock helper entry point")
}
sourceDir := os.Getenv("CLIPROXY_SHADOW_HELPER_SOURCE_DIR")
shadowDir := os.Getenv("CLIPROXY_SHADOW_HELPER_SHADOW_DIR")
if sourceDir == "" || shadowDir == "" {
os.Exit(2)
}
// Acquire an exclusive coordination lock and exit abnormally without unlocking.
if _, errLock := acquireShadowCoordLock(sourceDir, shadowDir, true, false); errLock != nil {
os.Exit(3)
}
os.Exit(1)
}
func TestShadowHelperProcessWaits(t *testing.T) {
if os.Getenv("CLIPROXY_SHADOW_HELPER_WAIT") != "1" {
t.Skip("waiting helper process entry point")
}
buf := make([]byte, 1)
_, _ = os.Stdin.Read(buf)
}
func countShadowPluginFiles(t *testing.T, dir string) int {
t.Helper()
entries, errRead := os.ReadDir(dir)
if errRead != nil {
t.Fatalf("ReadDir(%s) error = %v", dir, errRead)
}
count := 0
for _, entry := range entries {
if strings.HasPrefix(entry.Name(), shadowPluginPrefix) {
count++
}
if strings.HasPrefix(entry.Name(), shadowPluginTempPrefix) {
t.Fatalf("temporary shadow file was not cleaned up: %s", entry.Name())
}
}
return count
}