forked from TrueCloudLab/restic
cmd/restic: Add command unlock
, improve error message
This commit is contained in:
parent
0ad3d71f01
commit
e657287eac
4 changed files with 136 additions and 20 deletions
43
cmd/restic/cmd_unlock.go
Normal file
43
cmd/restic/cmd_unlock.go
Normal 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
|
||||||
|
}
|
|
@ -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
49
lock.go
|
@ -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
33
lock_test.go
33
lock_test.go
|
@ -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)
|
||||||
|
|
Loading…
Add table
Reference in a new issue