2023-12-03 03:19:13 -05:00
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"
)
2025-07-08 12:08:14 +02:00
const basicallyforever = fs . Duration ( 200 * 365 * 24 * time . Hour )
2023-12-03 03:19:13 -05:00
2025-07-11 12:11:16 -04:00
type lockFileOpt struct {
stopRenewal func ()
data struct {
Session string
PID string
TimeRenewed time . Time
TimeExpires time . Time
}
}
2023-12-03 03:19:13 -05:00
2025-07-11 12:11:16 -04:00
func ( b * bisyncRun ) setLockFile () ( err error ) {
2023-12-03 03:19:13 -05:00
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 ()
2025-07-11 12:11:16 -04:00
b . lockFileOpt . stopRenewal = b . startLockRenewal ()
2023-12-03 03:19:13 -05:00
}
return nil
}
2025-07-11 12:11:16 -04:00
func ( b * bisyncRun ) removeLockFile () ( err error ) {
2023-12-03 03:19:13 -05:00
if b . lockFile != "" {
2025-07-11 12:11:16 -04:00
b . lockFileOpt . stopRenewal ()
err = os . Remove ( b . lockFile )
if err == nil {
2023-12-03 03:19:13 -05:00
fs . Debugf ( nil , "Lock file removed: %s" , b . lockFile )
} else {
2025-07-11 12:11:16 -04:00
fs . Errorf ( nil , "cannot remove lockfile %s: %v" , b . lockFile , err )
2023-12-03 03:19:13 -05:00
}
b . lockFile = "" // block removing it again
}
2025-07-11 12:11:16 -04:00
return err
2023-12-03 03:19:13 -05:00
}
func ( b * bisyncRun ) setLockFileExpiration () {
2025-07-08 12:08:14 +02:00
if b . opt . MaxLock > 0 && b . opt . MaxLock < fs . Duration ( 2 * time . Minute ) {
2023-12-03 03:19:13 -05:00
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 )
2025-07-08 12:08:14 +02:00
b . opt . MaxLock = fs . Duration ( 2 * time . Minute )
2023-12-03 03:19:13 -05:00
} else if b . opt . MaxLock <= 0 {
b . opt . MaxLock = basicallyforever
}
}
func ( b * bisyncRun ) renewLockFile () {
if b . lockFile != "" && bilib . FileExists ( b . lockFile ) {
2025-07-11 12:11:16 -04:00
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 ))
2023-12-03 03:19:13 -05:00
// save data file
df , err := os . Create ( b . lockFile )
b . handleErr ( b . lockFile , "error renewing lock file" , err , true , true )
2025-07-11 12:11:16 -04:00
b . handleErr ( b . lockFile , "error encoding JSON to lock file" , json . NewEncoder ( df ). Encode ( b . lockFileOpt . data ), true , true )
2023-12-03 03:19:13 -05:00
b . handleErr ( b . lockFile , "error closing lock file" , df . Close (), true , true )
if b . opt . MaxLock < basicallyforever {
2025-07-11 12:11:16 -04:00
fs . Infof ( nil , Color ( terminal . HiBlueFg , "lock file renewed for %v. New expiration: %v" ), b . opt . MaxLock , b . lockFileOpt . data . TimeExpires )
2023-12-03 03:19:13 -05:00
}
}
}
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 )
2026-03-31 17:56:28 +08:00
var decodeErr error
2023-12-03 03:19:13 -05:00
for {
2025-07-11 12:11:16 -04:00
if err := dec . Decode ( & b . lockFileOpt . data ); err != nil {
2024-04-09 07:17:00 -04:00
if err != io . EOF {
2026-03-31 17:56:28 +08:00
decodeErr = err
2024-04-09 07:17:00 -04:00
}
2023-12-03 03:19:13 -05:00
break
}
}
b . handleErr ( b . lockFile , "error closing file" , rdf . Close (), true , true )
2026-03-31 17:56:28 +08:00
if decodeErr != nil {
if b . opt . MaxLock < basicallyforever {
fs . Infof ( b . lockFile , Color ( terminal . YellowFg , "Lock file is unreadable (decode error: %v) and --max-lock is set. Treating as expired." ), decodeErr )
markFailed ( b . listing1 )
markFailed ( b . listing2 )
return true
}
fs . Errorf ( b . lockFile , Color ( terminal . RedFg , "Lock file exists, but contents are unreadable. (decode error: %v)" ), decodeErr )
return false
}
2025-07-11 12:11:16 -04:00
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 )
2023-12-03 03:19:13 -05:00
markFailed ( b . listing1 ) // listing is untrusted so force revert to prior (if --recover) or create new ones (if --resync)
markFailed ( b . listing2 )
return true
}
2025-07-11 12:11:16 -04:00
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 )
2023-12-03 03:19:13 -05:00
}
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
2026-02-18 12:11:52 +00:00
wg . Go ( func () {
2025-07-08 12:08:14 +02:00
ticker := time . NewTicker ( time . Duration ( b . opt . MaxLock ) - time . Minute )
2023-12-03 03:19:13 -05:00
for {
select {
case <- ticker . C :
b . renewLockFile ()
case <- stopLockRenewal :
ticker . Stop ()
return
}
}
2026-02-18 12:11:52 +00:00
})
2023-12-03 03:19:13 -05:00
return func () {
close ( stopLockRenewal )
wg . Wait ()
}
}
func markFailed ( file string ) {
failFile := file + "-err"
if bilib . FileExists ( file ) {
_ = os . Remove ( failFile )
_ = os . Rename ( file , failFile )
}
}