-
Notifications
You must be signed in to change notification settings - Fork 2
/
rsync.go
155 lines (146 loc) · 4.4 KB
/
rsync.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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/* See the file "LICENSE.txt" for the full license governing this code. */
// Handle creation and cancellation of os level process to create snapshots
package main
import (
"bufio"
"errors"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"syscall"
"time"
)
// rsyncIgnoredErrors are rsync return values that are considered temporary
// errors. If rsync returns one of these error codes, snaprd will not fail and
// try again next time.
var rsyncIgnoredErrors = map[int]string{
6: "Daemon unable to append to log-file",
10: "Error in socket I/O",
11: "Error in file I/O",
12: "Error in rsync protocol data stream",
13: "Errors with program diagnostics",
14: "Error in IPC code",
20: "Received SIGUSR1 or SIGINT",
21: "Some error returned by waitpid()",
22: "Error allocating core memory buffers",
23: "Partial transfer due to error",
24: "Partial transfer due to vanished source files",
25: "The --max-delete limit stopped deletions",
30: "Timeout in data send/receive",
35: "Timeout waiting for daemon connection",
}
// createRsyncCommand returns an exec.Command structure that, when executed,
// creates a snapshot using rsync. Takes an optional (non-nil) base to be used
// with rsyncs --link-dest feature.
func createRsyncCommand(sn *snapshot, base *snapshot) *exec.Cmd {
cmd := exec.Command(config.RsyncPath)
args := make([]string, 0, 256)
args = append(args, config.RsyncPath)
args = append(args, "--delete")
args = append(args, "-a")
args = append(args, "--stats")
args = append(args, config.RsyncOpts...)
if base != nil {
args = append(args, "--link-dest="+base.FullName())
}
args = append(args, config.Origin, sn.FullName())
cmd.Args = args
cmd.Dir = filepath.Join(config.repository, dataSubdir)
log.Println("run:", args)
return cmd
}
// runRsyncCommand executes the given command. On sucessful startup return an
// error channel the caller can receive a return status from.
func runRsyncCommand(cmd *exec.Cmd) (chan error, error) {
var err error
cmdOutput, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
cmd.Stderr = cmd.Stdout
debugf("starting rsync command")
err = cmd.Start()
if err != nil {
return nil, err
}
in := bufio.NewScanner(cmdOutput)
for in.Scan() {
log.Printf("(rsync) %s", in.Text())
}
if err := in.Err(); err != nil {
log.Printf("error scanning rsync output: %s", err)
}
done := make(chan error)
go func() {
time.Sleep(time.Second)
done <- cmd.Wait()
return
}()
return done, nil
}
// createSnapshot starts a potentially long running rsync command and returns a
// Snapshot pointer on success.
// For non-zero return values of rsync potentially restart the process if the
// error was presumably volatile.
func createSnapshot(base *snapshot) (*snapshot, error) {
cl := new(realClock)
newSn := lastReusableFromDisk(cl)
if newSn == nil {
newSn = newIncompleteSnapshot(cl)
} else {
newSn.transIncomplete(cl)
}
cmd := createRsyncCommand(newSn, base)
done, err := runRsyncCommand(cmd)
if err != nil {
log.Println("could not start rsync command:", err)
return nil, err
}
debugf("rsync started")
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
for {
select {
case sig := <-sigc:
debugf("trying to kill rsync with signal %v", sig)
err := cmd.Process.Signal(sig)
if err != nil {
log.Fatal("failed to kill: ", err)
}
return nil, errors.New("rsync killed by request")
case err := <-done:
debugf("received something on done channel: %v", err)
if err != nil {
// At this stage rsync ran, but with errors.
failed := true
// First, get the error code
if exiterr, ok := err.(*exec.ExitError); ok { // The return code != 0)
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { // Finally get the actual status code
rsyncRet := status.ExitStatus()
debugf("The error code we got is: %v", rsyncRet)
if errmsg, ok := rsyncIgnoredErrors[rsyncRet]; ok == true {
log.Printf("ignoring rsync error %d: %s", rsyncRet, errmsg)
// 24 ("files vanished") happens too often and is usually harmless
if rsyncRet != 24 && config.Notify != "" {
RsyncIssueMail(err, rsyncRet)
}
failed = false
}
}
}
if failed {
return nil, fmt.Errorf("rsync failed: %s", err)
}
}
err = newSn.transComplete(cl)
if err != nil {
return nil, err
}
log.Println("finished:", newSn.Name())
return newSn, nil
}
}
}