mirror of
https://github.com/rclone/rclone.git
synced 2025-11-23 21:44:49 +02:00
Before this change, bisync used some global variables, which could cause errors if running multiple concurrent bisync runs through the rc. (Running normally from the command line was not affected.) This change deglobalizes those variables so that multiple bisync runs can be safely run at once, from the same rclone instance.
158 lines
5.1 KiB
Go
158 lines
5.1 KiB
Go
package bisync
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/cmd/bisync/bilib"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/lib/terminal"
|
|
)
|
|
|
|
const basicallyforever = fs.Duration(200 * 365 * 24 * time.Hour)
|
|
|
|
type lockFileOpt struct {
|
|
stopRenewal func()
|
|
data struct {
|
|
Session string
|
|
PID string
|
|
TimeRenewed time.Time
|
|
TimeExpires time.Time
|
|
}
|
|
}
|
|
|
|
func (b *bisyncRun) setLockFile() (err error) {
|
|
b.lockFile = ""
|
|
b.setLockFileExpiration()
|
|
if !b.opt.DryRun {
|
|
b.lockFile = b.basePath + ".lck"
|
|
if bilib.FileExists(b.lockFile) {
|
|
if !b.lockFileIsExpired() {
|
|
errTip := Color(terminal.MagentaFg, "Tip: this indicates that another bisync run (of these same paths) either is still running or was interrupted before completion. \n")
|
|
errTip += Color(terminal.MagentaFg, "If you're SURE you want to override this safety feature, you can delete the lock file with the following command, then run bisync again: \n")
|
|
errTip += fmt.Sprintf(Color(terminal.HiRedFg, "rclone deletefile \"%s\""), b.lockFile)
|
|
return fmt.Errorf(Color(terminal.RedFg, "prior lock file found: %s \n")+errTip, Color(terminal.HiYellowFg, b.lockFile))
|
|
}
|
|
}
|
|
|
|
pidStr := []byte(strconv.Itoa(os.Getpid()))
|
|
if err = os.WriteFile(b.lockFile, pidStr, bilib.PermSecure); err != nil {
|
|
return fmt.Errorf(Color(terminal.RedFg, "cannot create lock file: %s: %w"), b.lockFile, err)
|
|
}
|
|
fs.Debugf(nil, "Lock file created: %s", b.lockFile)
|
|
b.renewLockFile()
|
|
b.lockFileOpt.stopRenewal = b.startLockRenewal()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *bisyncRun) removeLockFile() (err error) {
|
|
if b.lockFile != "" {
|
|
b.lockFileOpt.stopRenewal()
|
|
err = os.Remove(b.lockFile)
|
|
if err == nil {
|
|
fs.Debugf(nil, "Lock file removed: %s", b.lockFile)
|
|
} else {
|
|
fs.Errorf(nil, "cannot remove lockfile %s: %v", b.lockFile, err)
|
|
}
|
|
b.lockFile = "" // block removing it again
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (b *bisyncRun) setLockFileExpiration() {
|
|
if b.opt.MaxLock > 0 && b.opt.MaxLock < fs.Duration(2*time.Minute) {
|
|
fs.Logf(nil, Color(terminal.YellowFg, "--max-lock cannot be shorter than 2 minutes (unless 0.) Changing --max-lock from %v to %v"), b.opt.MaxLock, 2*time.Minute)
|
|
b.opt.MaxLock = fs.Duration(2 * time.Minute)
|
|
} else if b.opt.MaxLock <= 0 {
|
|
b.opt.MaxLock = basicallyforever
|
|
}
|
|
}
|
|
|
|
func (b *bisyncRun) renewLockFile() {
|
|
if b.lockFile != "" && bilib.FileExists(b.lockFile) {
|
|
|
|
b.lockFileOpt.data.Session = b.basePath
|
|
b.lockFileOpt.data.PID = strconv.Itoa(os.Getpid())
|
|
b.lockFileOpt.data.TimeRenewed = time.Now()
|
|
b.lockFileOpt.data.TimeExpires = time.Now().Add(time.Duration(b.opt.MaxLock))
|
|
|
|
// save data file
|
|
df, err := os.Create(b.lockFile)
|
|
b.handleErr(b.lockFile, "error renewing lock file", err, true, true)
|
|
b.handleErr(b.lockFile, "error encoding JSON to lock file", json.NewEncoder(df).Encode(b.lockFileOpt.data), true, true)
|
|
b.handleErr(b.lockFile, "error closing lock file", df.Close(), true, true)
|
|
if b.opt.MaxLock < basicallyforever {
|
|
fs.Infof(nil, Color(terminal.HiBlueFg, "lock file renewed for %v. New expiration: %v"), b.opt.MaxLock, b.lockFileOpt.data.TimeExpires)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *bisyncRun) lockFileIsExpired() bool {
|
|
if b.lockFile != "" && bilib.FileExists(b.lockFile) {
|
|
rdf, err := os.Open(b.lockFile)
|
|
b.handleErr(b.lockFile, "error reading lock file", err, true, true)
|
|
dec := json.NewDecoder(rdf)
|
|
for {
|
|
if err := dec.Decode(&b.lockFileOpt.data); err != nil {
|
|
if err != io.EOF {
|
|
fs.Errorf(b.lockFile, "err: %v", err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
b.handleErr(b.lockFile, "error closing file", rdf.Close(), true, true)
|
|
if !b.lockFileOpt.data.TimeExpires.IsZero() && b.lockFileOpt.data.TimeExpires.Before(time.Now()) {
|
|
fs.Infof(b.lockFile, Color(terminal.GreenFg, "Lock file found, but it expired at %v. Will delete it and proceed."), b.lockFileOpt.data.TimeExpires)
|
|
markFailed(b.listing1) // listing is untrusted so force revert to prior (if --recover) or create new ones (if --resync)
|
|
markFailed(b.listing2)
|
|
return true
|
|
}
|
|
fs.Infof(b.lockFile, Color(terminal.RedFg, "Valid lock file found. Expires at %v. (%v from now)"), b.lockFileOpt.data.TimeExpires, time.Since(b.lockFileOpt.data.TimeExpires).Abs().Round(time.Second))
|
|
prettyprint(b.lockFileOpt.data, "Lockfile info", fs.LogLevelInfo)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// StartLockRenewal renews the lockfile every --max-lock minus one minute.
|
|
//
|
|
// It returns a func which should be called to stop the renewal.
|
|
func (b *bisyncRun) startLockRenewal() func() {
|
|
if b.opt.MaxLock <= 0 || b.opt.MaxLock >= basicallyforever || b.lockFile == "" {
|
|
return func() {}
|
|
}
|
|
stopLockRenewal := make(chan struct{})
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
ticker := time.NewTicker(time.Duration(b.opt.MaxLock) - time.Minute)
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
b.renewLockFile()
|
|
case <-stopLockRenewal:
|
|
ticker.Stop()
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return func() {
|
|
close(stopLockRenewal)
|
|
wg.Wait()
|
|
}
|
|
}
|
|
|
|
func markFailed(file string) {
|
|
failFile := file + "-err"
|
|
if bilib.FileExists(file) {
|
|
_ = os.Remove(failFile)
|
|
_ = os.Rename(file, failFile)
|
|
}
|
|
}
|