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  }