github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/osutils/lockfile/test/integration/pidlock_test.go (about) 1 package integration 2 3 import ( 4 "context" 5 "errors" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "runtime" 10 "sync" 11 "testing" 12 "time" 13 14 "github.com/ActiveState/cli/internal/environment" 15 "github.com/ActiveState/cli/internal/osutils/lockfile" 16 "github.com/stretchr/testify/assert" 17 "github.com/stretchr/testify/require" 18 ) 19 20 func buildTestExecutable(t *testing.T, dir string) string { 21 root, err := environment.GetRootPath() 22 require.NoError(t, err) 23 lockerExe := filepath.Join(dir, "locker") 24 if runtime.GOOS == "windows" { 25 lockerExe += ".exe" 26 } 27 28 cmd := exec.Command( 29 "go", "build", "-o", lockerExe, 30 filepath.Join(root, "internal", "osutils", "testdata", "locker"), 31 ) 32 err = cmd.Run() 33 require.NoError(t, err) 34 35 return lockerExe 36 } 37 38 func Test_acquirePidLockProcesses(t *testing.T) { 39 tmpDir, err := os.MkdirTemp("", "") 40 require.NoError(t, err) 41 defer os.RemoveAll(tmpDir) 42 43 // build the test process that acquires a lock 44 lockerExe := buildTestExecutable(t, tmpDir) 45 46 cases := []struct { 47 name string 48 keep string 49 }{ 50 {"PID still running", "keep"}, 51 {"PID file deleted", "remove"}, 52 } 53 54 for _, tc := range cases { 55 t.Run("locked in other process with "+tc.name, func(tt *testing.T) { 56 lockFile := filepath.Join(tmpDir, "locked-"+tc.keep) 57 58 lockCmd := exec.Command(lockerExe, lockFile, tc.keep) 59 lockCmd = prepLockCmd(lockCmd) 60 61 stdout, err := lockCmd.StdoutPipe() 62 require.NoError(tt, err) 63 err = lockCmd.Start() 64 require.NoError(tt, err) 65 66 // wait for command to block 67 buf := make([]byte, 6) 68 n, err := stdout.Read(buf) 69 require.NoError(tt, err) 70 require.Equal(tt, 6, n) 71 assert.Equal(tt, "LOCKED", string(buf)) 72 73 pl, err := lockfile.NewPidLock(lockFile) 74 require.NoError(tt, err) 75 76 // trying to acquire the lock in this process should fail 77 err = pl.TryLock() 78 require.Error(tt, err) 79 alreadyErr := &lockfile.AlreadyLockedError{} 80 assert.True(tt, errors.As(err, &alreadyErr)) 81 82 err = pl.Close() 83 require.NoError(tt, err) 84 85 // stopping the other process 86 interruptProcess(tt, lockCmd.Process) 87 88 // waiting for the process to finish without error 89 err = lockCmd.Wait() 90 require.NoError(tt, err) 91 assert.True(tt, lockCmd.ProcessState.Exited()) 92 assert.Equal(tt, 0, lockCmd.ProcessState.ExitCode()) 93 }) 94 95 t.Run("stress-test with "+tc.name, func(tt *testing.T) { 96 // stress tests runs numProcesses in parallel, and only one should get the lock 97 numProcesses := 10 98 99 lockFile := filepath.Join(tmpDir, "stress-test-"+tc.keep) 100 101 done := make(chan string, numProcesses+1) 102 defer close(done) 103 var wg sync.WaitGroup 104 defer wg.Wait() 105 106 ctx, cancel := context.WithCancel(context.Background()) 107 defer cancel() 108 109 for i := 0; i < numProcesses; i++ { 110 wg.Add(1) 111 go func() { 112 defer wg.Done() 113 lockCmd := exec.Command(lockerExe, lockFile, tc.keep) 114 lockCmd = prepLockCmd(lockCmd) 115 stdout, err := lockCmd.StdoutPipe() 116 require.NoError(tt, err) 117 err = lockCmd.Start() 118 require.NoError(tt, err) 119 120 // wait for command to block 121 buf := make([]byte, 6) 122 n, err := stdout.Read(buf) 123 require.NoError(tt, err) 124 require.Equal(tt, 6, n) 125 126 sb := string(buf[:n]) 127 if sb == "DENIED" { 128 done <- "DENIED" 129 return 130 } 131 assert.Equal(tt, "LOCKED", string(buf[:6])) 132 133 done <- "LOCKED" 134 135 // wait for the signal to kill process and to release the lock 136 <-ctx.Done() 137 interruptProcess(tt, lockCmd.Process) 138 139 err = lockCmd.Wait() 140 require.NoError(tt, err) 141 assert.True(tt, lockCmd.ProcessState.Exited()) 142 assert.Equal(tt, 0, lockCmd.ProcessState.ExitCode()) 143 }() 144 } 145 146 // timeout if test does not finish after 60 seconds 147 go func() { 148 select { 149 case <-ctx.Done(): 150 return 151 case <-time.After(60 * time.Second): 152 done <- "TIMEOUT" 153 } 154 }() 155 156 // ensure that numProcesses-1 processes are denied access and only 1 got the lock 157 var denied int 158 var locked int 159 for d := range done { 160 if d == "TIMEOUT" { 161 tt.Fatalf("test timed out") 162 } 163 if d == "LOCKED" { 164 locked++ 165 } 166 if d == "DENIED" { 167 denied++ 168 } 169 if denied+locked == numProcesses { 170 break 171 } 172 } 173 assert.Equal(t, 1, locked, "only process should lock") 174 assert.Equal(t, numProcesses-1, denied, "all but on processes should have been denied lock-file access") 175 }) 176 } 177 } 178 179 func Test_acquirePidLock(t *testing.T) { 180 tmpDir, err := os.MkdirTemp("", "") 181 require.NoError(t, err) 182 defer os.RemoveAll(tmpDir) 183 184 lockFile := filepath.Join(tmpDir, "lockfile") 185 186 pl, err := lockfile.NewPidLock(lockFile) 187 require.NoError(t, err) 188 err = pl.TryLock() 189 require.NoError(t, err) 190 191 pl2, err := lockfile.NewPidLock(lockFile) 192 require.NoError(t, err) 193 err = pl2.TryLock() 194 assert.Error(t, err) 195 196 err = pl2.Close() 197 require.NoError(t, err) 198 199 err = pl.Close() 200 require.NoError(t, err) 201 f, err := os.Stat(lockFile) 202 assert.True(t, err != nil && f == nil) 203 204 pl, err = lockfile.NewPidLock(lockFile) 205 require.NoError(t, err) 206 err = pl.TryLock() 207 require.NoError(t, err) 208 209 err = pl.Close(true) 210 require.NoError(t, err) 211 f, err = os.Stat(lockFile) 212 assert.True(t, err == nil && !f.IsDir()) 213 214 pl, err = lockfile.NewPidLock(lockFile) 215 require.NoError(t, err) 216 217 err = pl.TryLock() 218 assert.Error(t, err) 219 220 err = pl.Close() 221 require.NoError(t, err) 222 } 223 224 func TestPidExists(t *testing.T) { 225 assert.True(t, lockfile.PidExists(os.Getpid())) 226 assert.False(t, lockfile.PidExists(99999999)) 227 }