diff --git a/cmd/process.go b/cmd/process.go new file mode 100644 index 0000000..9eb9b15 --- /dev/null +++ b/cmd/process.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "errors" + "fmt" + "io" + "os" + "strconv" + "strings" +) + +var ErrNoProcessFound = errors.New("no process found") + +// findProcessWithPrefix finds a process with the given prefix in its command line +func findProcessWithPrefix(prefix string) (int, error) { + d, err := os.Open("/proc") + if err != nil { + return 0, err + } + defer d.Close() + + for { + names, err := d.Readdirnames(10) + if err == io.EOF { + break + } + if err != nil { + return 0, err + } + + for _, name := range names { + // We only care if the name starts with a numeric + if name[0] < '0' || name[0] > '9' { + continue + } + + // From this point forward, any errors we just ignore, because + // it might simply be that the process doesn't exist anymore. + pid, err := strconv.ParseInt(name, 10, 0) + if err != nil { + continue + } + + cmdline, err := readCmdline(int(pid)) + if err != nil { + continue + } + if strings.HasPrefix(cmdline, prefix) { + return int(pid), nil + } + } + } + return 0, ErrNoProcessFound +} + +// readCmdline reads the command line of a process +func readCmdline(pid int) (string, error) { + cmdlinePath := fmt.Sprintf("/proc/%d/cmdline", pid) + dataBytes, err := os.ReadFile(cmdlinePath) + if err != nil { + return "", err + } + return string(dataBytes), nil +} diff --git a/cmd/script.go b/cmd/script.go index a2341e5..e6baf8f 100644 --- a/cmd/script.go +++ b/cmd/script.go @@ -9,10 +9,6 @@ import ( "strings" ) -// LockAcquireAttempts is the number of attempts to acquire the lock. Also -// correlates with the number of seconds to wait for the lock. -const LockAcquireAttempts = 180 - // Script represents a Python script type Script struct { AbsolutePath string // Full path to the script diff --git a/cmd/utils.go b/cmd/utils.go index c27f098..87a6670 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -17,6 +17,13 @@ import ( const CyanColor = "\033[1;36m" const ResetColor = "\033[0m" +// LockAcquireAttempts is the number of attempts to acquire the lock. Also +// correlates with the number of seconds to wait for the lock. +const LockAcquireAttempts = 300 + +// LockStaleTime is the time after which the lock is considered stale +const LockStaleTime = 15 * time.Minute + // getFileHash calculates the SHA256 hash of the file func getFileHash(filename string) (string, error) { // Check that the file exists @@ -80,12 +87,36 @@ func acquireLock(envDir string, attempt int) error { if err != nil { return err } - _, err = os.Stat(lockFileName) + stat, err := os.Stat(lockFileName) if err == nil { if attempt >= LockAcquireAttempts { return fmt.Errorf("failed to acquire lock") } - // Sleep for 1 second + + // Lockfile exists, check if it is stale by checking its age + if time.Since(stat.ModTime()) > LockStaleTime { + if flagDebug { + loggerErr.Printf("lockfile %s is stale, removing it\n", lockFileName) + } + err = os.Remove(lockFileName) + if err != nil { + return err + } + } + + // Lockfile is not stale, check if there is a process which uses the venv + _, err := findProcessWithPrefix(envDir) + if err == ErrNoProcessFound { + if flagDebug { + loggerErr.Printf("process which is using virtual environment not found, removing lockfile %s\n", lockFileName) + } + err = os.Remove(lockFileName) + if err != nil { + return err + } + } + + // Lockfile is not stale, wait for 1 second and try again time.Sleep(1 * time.Second) return acquireLock(envDir, attempt+1) }