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 var globalLocks []*restic.Lock
func lockRepo(repo *repository.Repository) (*restic.Lock, error) { func lockRepo(repo *repository.Repository) (*restic.Lock, error) {
lock, err := restic.NewLock(repo) return lockRepository(repo, false)
if err != nil {
return nil, err
}
globalLocks = append(globalLocks, lock)
return lock, err
} }
func lockRepoExclusive(repo *repository.Repository) (*restic.Lock, error) { 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 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 return nil, err
} }

49
lock.go
View file

@ -1,7 +1,7 @@
package restic package restic
import ( import (
"errors" "fmt"
"os" "os"
"os/signal" "os/signal"
"os/user" "os/user"
@ -33,10 +33,24 @@ type Lock struct {
lockID backend.ID lockID backend.ID
} }
var ( // ErrAlreadyLocked is returned when NewLock or NewExclusiveLock are unable to
ErrAlreadyLocked = errors.New("already locked") // acquire the desired lock.
ErrStaleLockFound = errors.New("stale lock found") 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 // NewLock returns a new, non-exclusive lock for the repository. If an
// exclusive lock is already held by another process, ErrAlreadyLocked is // 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 { if err = lock.checkForOtherLocks(); err != nil {
lock.Unlock() lock.Unlock()
return nil, ErrAlreadyLocked return nil, err
} }
return lock, nil return lock, nil
@ -130,11 +144,11 @@ func (l *Lock) checkForOtherLocks() error {
} }
if l.Exclusive { if l.Exclusive {
return ErrAlreadyLocked return ErrAlreadyLocked{otherLock: lock}
} }
if !l.Exclusive && lock.Exclusive { if !l.Exclusive && lock.Exclusive {
return ErrAlreadyLocked return ErrAlreadyLocked{otherLock: lock}
} }
return nil return nil
@ -206,6 +220,19 @@ func (l *Lock) Stale() bool {
return false 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 // listen for incoming SIGHUP and ignore
var ignoreSIGHUP sync.Once var ignoreSIGHUP sync.Once
@ -247,3 +274,9 @@ func RemoveStaleLocks(repo *repository.Repository) error {
return nil 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) OK(t, err)
lock, err := restic.NewLock(repo) 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") "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, lock.Unlock())
OK(t, elock.Unlock()) OK(t, elock.Unlock())
@ -82,8 +84,10 @@ func TestExclusiveLockOnLockedRepo(t *testing.T) {
OK(t, err) OK(t, err)
lock, err := restic.NewExclusiveLock(repo) lock, err := restic.NewExclusiveLock(repo)
Assert(t, err == restic.ErrAlreadyLocked, Assert(t, err != nil,
"create exclusive lock with locked repo didn't return an error") "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, lock.Unlock())
OK(t, elock.Unlock()) OK(t, elock.Unlock())
@ -170,6 +174,29 @@ func TestLockWithStaleLock(t *testing.T) {
OK(t, removeLock(repo, id2)) 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) { func TestLockConflictingExclusiveLocks(t *testing.T) {
repo := SetupRepo() repo := SetupRepo()
defer TeardownRepo(repo) defer TeardownRepo(repo)