diff --git a/cmd/restic/cmd_unlock.go b/cmd/restic/cmd_unlock.go new file mode 100644 index 000000000..47345350c --- /dev/null +++ b/cmd/restic/cmd_unlock.go @@ -0,0 +1,43 @@ +package main + +import "github.com/restic/restic" + +type CmdUnlock struct { + RemoveAll bool `long:"remove-all" description:"Remove all locks, even stale ones"` + + global *GlobalOptions +} + +func init() { + _, err := parser.AddCommand("unlock", + "remove locks", + "The unlock command checks for stale locks and removes them", + &CmdUnlock{global: &globalOpts}) + if err != nil { + panic(err) + } +} + +func (cmd CmdUnlock) Usage() string { + return "[unlock-options]" +} + +func (cmd CmdUnlock) Execute(args []string) error { + repo, err := cmd.global.OpenRepository() + if err != nil { + return err + } + + fn := restic.RemoveStaleLocks + if cmd.RemoveAll { + fn = restic.RemoveAllLocks + } + + err = fn(repo) + if err != nil { + return err + } + + cmd.global.Verbosef("successfully removed locks\n") + return nil +} diff --git a/cmd/restic/lock.go b/cmd/restic/lock.go index e7d6b9976..5f6c18802 100644 --- a/cmd/restic/lock.go +++ b/cmd/restic/lock.go @@ -14,19 +14,32 @@ import ( var globalLocks []*restic.Lock func lockRepo(repo *repository.Repository) (*restic.Lock, error) { - lock, err := restic.NewLock(repo) - if err != nil { - return nil, err - } - - globalLocks = append(globalLocks, lock) - - return lock, err + return lockRepository(repo, false) } func lockRepoExclusive(repo *repository.Repository) (*restic.Lock, error) { - lock, err := restic.NewExclusiveLock(repo) + return lockRepository(repo, true) +} + +func lockRepository(repo *repository.Repository, exclusive bool) (*restic.Lock, error) { + lockFn := restic.NewLock + if exclusive { + lockFn = restic.NewExclusiveLock + } + + lock, err := lockFn(repo) if err != nil { + if restic.IsAlreadyLocked(err) { + tpe := "" + if exclusive { + tpe = " exclusive" + } + fmt.Fprintf(os.Stderr, "unable to acquire%s lock for operation:\n", tpe) + fmt.Fprintln(os.Stderr, err) + fmt.Fprintf(os.Stderr, "\nthe `unlock` command can be used to remove stale locks\n") + os.Exit(1) + } + return nil, err } diff --git a/lock.go b/lock.go index bf0725b02..65d102323 100644 --- a/lock.go +++ b/lock.go @@ -1,7 +1,7 @@ package restic import ( - "errors" + "fmt" "os" "os/signal" "os/user" @@ -33,10 +33,24 @@ type Lock struct { lockID backend.ID } -var ( - ErrAlreadyLocked = errors.New("already locked") - ErrStaleLockFound = errors.New("stale lock found") -) +// ErrAlreadyLocked is returned when NewLock or NewExclusiveLock are unable to +// acquire the desired lock. +type ErrAlreadyLocked struct { + otherLock *Lock +} + +func (e ErrAlreadyLocked) Error() string { + return fmt.Sprintf("repository is already locked by %v", e.otherLock) +} + +// IsAlreadyLocked returns true iff err is an instance of ErrAlreadyLocked. +func IsAlreadyLocked(err error) bool { + if _, ok := err.(ErrAlreadyLocked); ok { + return true + } + + return false +} // NewLock returns a new, non-exclusive lock for the repository. If an // exclusive lock is already held by another process, ErrAlreadyLocked is @@ -84,7 +98,7 @@ func newLock(repo *repository.Repository, excl bool) (*Lock, error) { if err = lock.checkForOtherLocks(); err != nil { lock.Unlock() - return nil, ErrAlreadyLocked + return nil, err } return lock, nil @@ -130,11 +144,11 @@ func (l *Lock) checkForOtherLocks() error { } if l.Exclusive { - return ErrAlreadyLocked + return ErrAlreadyLocked{otherLock: lock} } if !l.Exclusive && lock.Exclusive { - return ErrAlreadyLocked + return ErrAlreadyLocked{otherLock: lock} } return nil @@ -206,6 +220,19 @@ func (l *Lock) Stale() bool { return false } +func (l Lock) String() string { + text := fmt.Sprintf("PID %d on %s by %s (UID %d, GID %d)\nlock was created at %s (%s ago)\nstorage ID %v", + l.PID, l.Hostname, l.Username, l.UID, l.GID, + l.Time.Format("2006-01-02 15:04:05"), time.Since(l.Time), + l.lockID.Str()) + + if l.Stale() { + text += " (stale)" + } + + return text +} + // listen for incoming SIGHUP and ignore var ignoreSIGHUP sync.Once @@ -247,3 +274,9 @@ func RemoveStaleLocks(repo *repository.Repository) error { return nil }) } + +func RemoveAllLocks(repo *repository.Repository) error { + return eachLock(repo, func(id backend.ID, lock *Lock, err error) error { + return repo.Backend().Remove(backend.Lock, id.String()) + }) +} diff --git a/lock_test.go b/lock_test.go index 15e48623a..fa4c6bbc4 100644 --- a/lock_test.go +++ b/lock_test.go @@ -67,8 +67,10 @@ func TestLockOnExclusiveLockedRepo(t *testing.T) { OK(t, err) lock, err := restic.NewLock(repo) - Assert(t, err == restic.ErrAlreadyLocked, + Assert(t, err != nil, "create normal lock with exclusively locked repo didn't return an error") + Assert(t, restic.IsAlreadyLocked(err), + "create normal lock with exclusively locked repo didn't return the correct error") OK(t, lock.Unlock()) OK(t, elock.Unlock()) @@ -82,8 +84,10 @@ func TestExclusiveLockOnLockedRepo(t *testing.T) { OK(t, err) lock, err := restic.NewExclusiveLock(repo) - Assert(t, err == restic.ErrAlreadyLocked, - "create exclusive lock with locked repo didn't return an error") + Assert(t, err != nil, + "create normal lock with exclusively locked repo didn't return an error") + Assert(t, restic.IsAlreadyLocked(err), + "create normal lock with exclusively locked repo didn't return the correct error") OK(t, lock.Unlock()) OK(t, elock.Unlock()) @@ -170,6 +174,29 @@ func TestLockWithStaleLock(t *testing.T) { OK(t, removeLock(repo, id2)) } +func TestRemoveAllLocks(t *testing.T) { + repo := SetupRepo() + defer TeardownRepo(repo) + + id1, err := createFakeLock(repo, time.Now().Add(-time.Hour), os.Getpid()) + OK(t, err) + + id2, err := createFakeLock(repo, time.Now().Add(-time.Minute), os.Getpid()) + OK(t, err) + + id3, err := createFakeLock(repo, time.Now().Add(-time.Minute), os.Getpid()+500) + OK(t, err) + + OK(t, restic.RemoveAllLocks(repo)) + + Assert(t, lockExists(repo, t, id1) == false, + "lock still exists after RemoveAllLocks was called") + Assert(t, lockExists(repo, t, id2) == false, + "lock still exists after RemoveAllLocks was called") + Assert(t, lockExists(repo, t, id3) == false, + "lock still exists after RemoveAllLocks was called") +} + func TestLockConflictingExclusiveLocks(t *testing.T) { repo := SetupRepo() defer TeardownRepo(repo)