-
-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathcmd_with_lock.go
135 lines (111 loc) · 3.11 KB
/
cmd_with_lock.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package main
import (
"bytes"
"crypto/sha1"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/nightlyone/lockfile"
)
// Structure for our options and state.
type withLockCommand struct {
// prefix is the directory-root beneath which we write our lockfile.
prefix string
// lockFile contains the name of the user-supplied lockfile to use,
// if this is set then one will not be constructed automatically
// and prefix will be ignored.
lockFile string
}
// Arguments adds per-command args to the object.
func (wl *withLockCommand) Arguments(f *flag.FlagSet) {
f.StringVar(&wl.prefix, "prefix", "/var/tmp", "The location beneath which to write our lockfile")
f.StringVar(&wl.lockFile, "lock", "", "Specify a lockfile here directly, fully-qualified, if you don't want an auto-constructed one.")
}
// Info returns the name of this subcommand.
func (wl *withLockCommand) Info() (string, string) {
return "with-lock", `Execute a process, with a lock.
Details:
This command allows you to execute a command, with arguments,
using a lockfile. This will prevent multiple concurrent executions
of the same command.
The expected use-case is to prevent overlapping executions of cronjobs,
etc.
Implementation:
A filename is constructed based upon the command to be executed, and
this is used to prevent the concurrent execution. The command, and
arguments, to be executed are passed through a SHA1 hash for consistency.
The -lock flag may be used to supply a fully-qualified lockfile path,
in the case where a lockfile collision might be expected - in that case
the -prefix argument is ignored.
`
}
// Execute is invoked if the user specifies `with-lock` as the subcommand.
func (wl *withLockCommand) Execute(args []string) int {
//
// Ensure we have an argument
//
if len(args) < 1 {
fmt.Printf("You must specify the command to execute\n")
return 1
}
//
// Generate a lockfile
//
h := sha1.New()
for i, arg := range args {
h.Write([]byte(fmt.Sprintf("%d:%s", i, arg)))
}
hash := fmt.Sprintf("%x", h.Sum(nil))
//
// The actual path will go here
//
path := filepath.Join(wl.prefix, string(hash))
//
// If the user specified a complete path then that will
// be used instead.
//
if wl.lockFile != "" {
path = wl.lockFile
}
//
// Create the lockfile
//
lock, err := lockfile.New(path)
if err != nil {
fmt.Printf("Cannot init lockfile (%s). reason: %v", path, err)
return 1
}
// Error handling is essential, as we only try to get the lock.
if err = lock.TryLock(); err != nil {
fmt.Printf("Cannot lock %q (%s), reason: %v", lock, path, err)
return 1
}
defer func() {
if errr := lock.Unlock(); errr != nil {
fmt.Printf("Cannot unlock %q (%s), reason: %v", lock, path, errr)
os.Exit(1)
}
}()
//
// Run the command.
//
cmd := exec.Command(args[0], args[1:]...)
var stdout bytes.Buffer
cmd.Stdout = &stdout
var stderr bytes.Buffer
cmd.Stderr = &stderr
err = cmd.Run()
if len(stdout.String()) > 0 {
fmt.Print(stdout.String())
}
if len(stderr.String()) > 0 {
fmt.Print(stderr.String())
}
if err != nil {
fmt.Printf("Error running command:%s\n", err.Error())
return 1
}
return 0
}