cmd/restic: Add command `unlock`, improve error message

This commit is contained in:
Alexander Neumann 2015-06-27 15:50:36 +02:00
parent 0ad3d71f01
commit e657287eac
4 changed files with 136 additions and 20 deletions

43
cmd/restic/cmd_unlock.go Normal file
View File

@ -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
}

View File

@ -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
}

49
lock.go
View File

@ -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())
})
}

View File

@ -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)