Skip to content

Commit 80ff13f

Browse files
committed
Add timeout when for Rsync
1 parent c5b8f69 commit 80ff13f

File tree

2 files changed

+78
-0
lines changed

2 files changed

+78
-0
lines changed

rsync.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os/exec"
88
"strconv"
99
"strings"
10+
"time"
1011
)
1112

1213
// Rsync is wrapper under rsync
@@ -15,6 +16,8 @@ type Rsync struct {
1516
Destination string
1617

1718
cmd *exec.Cmd
19+
20+
timeout time.Duration
1821
}
1922

2023
// RsyncOptions for rsync
@@ -217,6 +220,10 @@ func (r Rsync) StderrPipe() (io.ReadCloser, error) {
217220
return r.cmd.StderrPipe()
218221
}
219222

223+
func (r *Rsync) WithTimeout(timeout time.Duration) {
224+
r.timeout = timeout
225+
}
226+
220227
// Run start rsync task
221228
func (r Rsync) Run() error {
222229
if !isExist(r.Destination) {
@@ -229,6 +236,10 @@ func (r Rsync) Run() error {
229236
return err
230237
}
231238

239+
if r.timeout != 0 {
240+
return WaitTimeout(r.cmd, r.timeout)
241+
}
242+
232243
return r.cmd.Wait()
233244
}
234245

@@ -245,6 +256,7 @@ func NewRsync(source, destination string, options RsyncOptions) *Rsync {
245256
Source: source,
246257
Destination: destination,
247258
cmd: exec.Command(binaryPath, arguments...),
259+
timeout: 0 * time.Second,
248260
}
249261
}
250262

timeout_cmd.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package grsync
2+
3+
import (
4+
"errors"
5+
"log"
6+
"os/exec"
7+
"syscall"
8+
"time"
9+
)
10+
11+
var ErrTimeout = errors.New("command timed out")
12+
13+
// KillGrace is the amount of time we allow a process to shutdown before
14+
// sending a SIGKILL.
15+
const KillGrace = 5 * time.Second
16+
17+
// WaitTimeout waits for the given command to finish with a timeout.
18+
// It assumes the command has already been started.
19+
// If the command times out, it attempts to kill the process and returns
20+
// a ErrTimeout error.
21+
func WaitTimeout(c *exec.Cmd, timeout time.Duration) error {
22+
var kill *time.Timer
23+
24+
term := time.AfterFunc(timeout, func() {
25+
err := c.Process.Signal(syscall.SIGTERM)
26+
if err != nil {
27+
log.Printf("Error terminating process: %s", err)
28+
return
29+
}
30+
31+
kill = time.AfterFunc(KillGrace, func() {
32+
err := c.Process.Kill()
33+
if err != nil {
34+
log.Printf("Error killing process: %s", err)
35+
return
36+
}
37+
})
38+
})
39+
40+
err := c.Wait()
41+
42+
// Shutdown all timers (the kill timer and the term timer) before checking cmd err,
43+
// otherwise there is no chance to turn off these timers that have not expired.
44+
if kill != nil {
45+
kill.Stop()
46+
}
47+
termSent := !term.Stop()
48+
// For a timer created with AfterFunc(d, f), if t.Stop returns false, then
49+
// the timer has already expired and the function f has been started in its own goroutine.
50+
// So if termSent is true, it means the cmd does not finished before the term timer expired.
51+
52+
// Now, we can check cmd err.
53+
// If the process exited without error treat it as success.
54+
// This allows a process to do a clean shutdown on signal.
55+
if err == nil {
56+
return nil
57+
}
58+
59+
// If SIGTERM was sent then treat any process error as a timeout.
60+
if termSent {
61+
return ErrTimeout
62+
}
63+
64+
// Otherwise there was an cmd error unrelated to termination.
65+
return err
66+
}

0 commit comments

Comments
 (0)