diff --git a/cmd/restray/cli.go b/cmd/restray/cli.go index 9566cd0..7c624ca 100644 --- a/cmd/restray/cli.go +++ b/cmd/restray/cli.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log" "os" "os/exec" "os/signal" @@ -182,7 +183,7 @@ func cliSchedule(prof Profile) error { } if prof.PreHook != "" { - fmt.Fprintln(os.Stderr, "Running pre-hook...") + log.Printf("[%s] running pre-hook", prof.displayName()) if err := cliRunHook(prof.PreHook, prof, hookEnv...); err != nil { return cli.Exit("error: pre-hook failed", 1) } @@ -191,21 +192,21 @@ func cliSchedule(prof Profile) error { failed := false if !interrupted.Load() && prof.Schedule.BackupEnabled() { - fmt.Fprintln(os.Stderr, "Running backup...") + log.Printf("[%s] running backup", prof.displayName()) if err := cliBackup(prof, true); err != nil { failed = true } } if !interrupted.Load() && prof.Schedule.Prune && len(prof.Prune.Args) > 0 { - fmt.Fprintln(os.Stderr, "Running prune...") + log.Printf("[%s] running prune", prof.displayName()) if err := cliPrune(prof); err != nil { failed = true } } if !interrupted.Load() && prof.Schedule.Check { - fmt.Fprintln(os.Stderr, "Running check...") + log.Printf("[%s] running check", prof.displayName()) if err := cliCheck(prof); err != nil { failed = true } @@ -213,7 +214,7 @@ func cliSchedule(prof Profile) error { if prof.PostHook != "" { signal.Reset(os.Interrupt, syscall.SIGTERM) - fmt.Fprintln(os.Stderr, "Running post-hook...") + log.Printf("[%s] running post-hook", prof.displayName()) postEnv := hookEnv if failed || interrupted.Load() { errMsg := "operation failed" @@ -244,11 +245,11 @@ func buildDaemonSchedule(wg *sync.WaitGroup) (*cron.Cron, error) { current := loadConfig() _, prof, err := resolveProfile(current, name) if err != nil { - fmt.Fprintf(os.Stderr, "[%s] skipping scheduled run: %v\n", name, err) + log.Printf("[%s] skipping scheduled run: %v", name, err) return } if skipForBattery(prof) { - fmt.Fprintf(os.Stderr, "[%s] skipping scheduled run: on battery power\n", prof.displayName()) + log.Printf("[%s] skipping scheduled run: on battery power", prof.displayName()) return } wg.Add(1) @@ -256,10 +257,10 @@ func buildDaemonSchedule(wg *sync.WaitGroup) (*cron.Cron, error) { defer wg.Done() defer func() { if r := recover(); r != nil { - fmt.Fprintf(os.Stderr, "[%s] scheduled run panicked: %v\n", prof.displayName(), r) + log.Printf("[%s] scheduled run panicked: %v", prof.displayName(), r) } }() - fmt.Fprintf(os.Stderr, "[%s] running scheduled job\n", prof.displayName()) + log.Printf("[%s] running scheduled job", prof.displayName()) cliSchedule(prof) }() }) @@ -288,7 +289,7 @@ func cliDaemon(context.Context, *cli.Command) error { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) sched.Start() - fmt.Fprintln(os.Stderr, "Scheduler running, press Ctrl+C to stop") + log.Print("Scheduler running, press Ctrl+C to stop") for sig := range sigCh { if sig != syscall.SIGHUP { @@ -296,17 +297,17 @@ func cliDaemon(context.Context, *cli.Command) error { } next, err := buildDaemonSchedule(&wg) if err != nil { - fmt.Fprintln(os.Stderr, "error: reload failed, keeping previous schedule:", err) + log.Printf("error: reload failed, keeping previous schedule: %v", err) continue } <-sched.Stop().Done() sched = next sched.Start() - fmt.Fprintln(os.Stderr, "Scheduler reloaded") + log.Print("Scheduler reloaded") } <-sched.Stop().Done() - fmt.Fprintln(os.Stderr, "Waiting for running jobs to finish...") + log.Print("Waiting for running jobs to finish...") wg.Wait() return nil } diff --git a/cmd/restray/operations.go b/cmd/restray/operations.go index f2655de..d68852e 100644 --- a/cmd/restray/operations.go +++ b/cmd/restray/operations.go @@ -99,6 +99,9 @@ func runRestic(idx int, prof Profile, mStatus prefixedMenuItem, args ...string) log.Printf("[%s] %s killed by system, retrying once", prof.displayName(), args[0]) msg, err = runResticOnce(idx, prof, mStatus, args...) } + if err != nil && takeCancelled(idx) && retryLockEnabled(prof) { + retryUnlock(idx, prof, mStatus) + } return msg, err } @@ -191,6 +194,9 @@ func doBackup(idx int, mStatus prefixedMenuItem, prof Profile, scheduled bool) ( setProfileFailed(idx, "") ok, code = doBackupOnce(idx, mStatus, prof, args) } + if !ok && takeCancelled(idx) && retryLockEnabled(prof) { + retryUnlock(idx, prof, mStatus) + } if ok { setLastBackup(idx, time.Now()) } diff --git a/cmd/restray/restic.go b/cmd/restray/restic.go index 8dc2465..c26e8b4 100644 --- a/cmd/restray/restic.go +++ b/cmd/restray/restic.go @@ -128,18 +128,23 @@ type repoResult struct { } func repoStatus(prof Profile) repoResult { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() path, _ := findRestic() - cmd := exec.CommandContext(ctx, path, "cat", "config", "--no-lock", "-q") + args := []string{"cat", "config", "--no-lock", "-q"} + log.Printf("[%s] run: restic %s", prof.displayName(), strings.Join(args, " ")) + cmd := exec.CommandContext(ctx, path, args...) cmd.Env = resticEnv(prof) hideWindow(cmd) out, err := cmd.CombinedOutput() if err != nil { + trimmed := strings.TrimSpace(string(out)) if ctx.Err() != nil { + log.Printf("[%s] repo status timed out after 30s; last output: %s", prof.displayName(), trimmed) return repoResult{errMsg: "Repository unreachable"} } - lines := strings.Split(strings.TrimSpace(string(out)), "\n") + log.Printf("[%s] repo status error: %s", prof.displayName(), trimmed) + lines := strings.Split(trimmed, "\n") msg, code := classifyError(err, lines[len(lines)-1]) return repoResult{errMsg: msg, needsInit: code == 10} } @@ -151,7 +156,7 @@ func queryLastBackup(prof Profile) time.Time { if err != nil { return time.Time{} } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() path, _ := findRestic() cmd := exec.CommandContext(ctx, path, "snapshots", "--json", "--latest", "1", "--host", host) @@ -179,6 +184,7 @@ type appState struct { notifications string failStatus map[int]string lastBackup map[int]time.Time + cancelled map[int]bool } var state = appState{ @@ -187,6 +193,15 @@ var state = appState{ mountStopping: make(map[int]bool), failStatus: make(map[int]string), lastBackup: make(map[int]time.Time), + cancelled: make(map[int]bool), +} + +func takeCancelled(idx int) bool { + state.mu.Lock() + defer state.mu.Unlock() + was := state.cancelled[idx] + delete(state.cancelled, idx) + return was } func setLastBackup(idx int, t time.Time) { @@ -257,6 +272,7 @@ func cancelProfile(idx int) { state.mu.Lock() defer state.mu.Unlock() if cmd := state.busyProfiles[idx]; cmd != nil && cmd.Process != nil { + state.cancelled[idx] = true interruptProcess(cmd.Process) } } diff --git a/nix/nixos-module.nix b/nix/nixos-module.nix index 26d449c..f18eb50 100644 --- a/nix/nixos-module.nix +++ b/nix/nixos-module.nix @@ -38,6 +38,7 @@ in { Environment = [ "RESTRAY_CONFIG=/etc/restray" "RESTRAY_STATE=/var/lib/restray" + "HOME=/var/lib/restray" ]; }; }; diff --git a/packaging/linux/restray.service b/packaging/linux/restray.service index 6f53b04..e59daf1 100644 --- a/packaging/linux/restray.service +++ b/packaging/linux/restray.service @@ -6,6 +6,7 @@ Type=simple Environment=RESTRAY_CONFIG=/etc/restray StateDirectory=restray Environment=RESTRAY_STATE=/var/lib/restray +Environment=HOME=/var/lib/restray ExecStart=restray daemon ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure