Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ type ScheduleBaseSection struct {
ScheduleIgnoreOnBatteryLessThan int `mapstructure:"schedule-ignore-on-battery-less-than" show:"noshow" default:"" examples:"20;33;50;75" description:"Don't start this schedule when running on battery and the state of charge is less than this percentage"`
ScheduleAfterNetworkOnline maybe.Bool `mapstructure:"schedule-after-network-online" show:"noshow" description:"Don't start this schedule when the network is offline (supported in \"systemd\")"`
ScheduleHideWindow maybe.Bool `mapstructure:"schedule-hide-window" show:"noshow" default:"false" description:"Hide schedule window when running in foreground (Windows only)"`
ScheduleStartWhenAvailable maybe.Bool `mapstructure:"schedule-start-when-available" show:"noshow" default:"false" description:"Start the task as soon as possible after a scheduled start is missed (Windows only)"`
}

func (s *ScheduleBaseSection) setRootPath(_ *Profile, _ string) {
Expand Down
5 changes: 5 additions & 0 deletions config/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type ScheduleBaseConfig struct {
AfterNetworkOnline maybe.Bool `mapstructure:"after-network-online" description:"Don't start this schedule when the network is offline (supported in \"systemd\")"`
SystemdDropInFiles []string `mapstructure:"systemd-drop-in-files" default:"" description:"Files containing systemd drop-in (override) files - see https://creativeprojects.github.io/resticprofile/schedules/systemd/"`
HideWindow maybe.Bool `mapstructure:"hide-window" default:"false" description:"Hide schedule window when running in foreground (Windows only)"`
StartWhenAvailable maybe.Bool `mapstructure:"start-when-available" default:"false" description:"Start the task as soon as possible after a scheduled start is missed (Windows only)"`
}

// scheduleBaseConfigDefaults declares built-in scheduling defaults
Expand Down Expand Up @@ -100,6 +101,9 @@ func (s *ScheduleBaseConfig) init(defaults *ScheduleBaseConfig) {
if !s.HideWindow.HasValue() {
s.HideWindow = defaults.HideWindow
}
if !s.StartWhenAvailable.HasValue() {
s.StartWhenAvailable = defaults.StartWhenAvailable
}
}

func (s *ScheduleBaseConfig) applyOverrides(section *ScheduleBaseSection) {
Expand All @@ -116,6 +120,7 @@ func (s *ScheduleBaseConfig) applyOverrides(section *ScheduleBaseSection) {
s.IgnoreOnBattery = section.ScheduleIgnoreOnBattery
s.AfterNetworkOnline = section.ScheduleAfterNetworkOnline
s.HideWindow = section.ScheduleHideWindow
s.StartWhenAvailable = section.ScheduleStartWhenAvailable
// re-init with defaults
s.init(&defaults)
}
Expand Down
8 changes: 8 additions & 0 deletions docs/content/schedules/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ Note: It works only on Windows and makes sense only with `user_logged_on` permis

Note: The behavior of `conhost.exe` varies between Windows versions. It has been confirmed to work on Windows 11 (24H2) but not on Windows 10 (1607).

## schedule-start-when-available

When set to `true`, Windows Task Scheduler will start the task as soon as possible after a scheduled start is missed. This is useful when the computer might be asleep or off during the scheduled time.

For example, if a backup is scheduled for 3:00 AM but the computer is off, enabling this option will run the backup when the computer is next available.

Note: This option only works on Windows.

## Example

Here's an example of a scheduling configuration:
Expand Down
13 changes: 13 additions & 0 deletions docs/content/schedules/task_scheduler/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,16 @@ It's easy to spot a terminal window opened with Administrator privileges:

> [!IMPORTANT]
> Running the schedule command might cause Windows to delete _resticprofile.exe_, treating it as a threat.

## Start when available

If your computer might be asleep or off during a scheduled backup time, you can enable `schedule-start-when-available` to run the task as soon as the computer becomes available.

```yaml
profile:
backup:
schedule: "03:00"
schedule-start-when-available: true
```

This sets the "Start the task as soon as possible after a scheduled start is missed" option in Windows Task Scheduler.
1 change: 1 addition & 0 deletions schedule/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Config struct {
AfterNetworkOnline bool
SystemdDropInFiles []string
HideWindow bool
StartWhenAvailable bool
removeOnly bool
}

Expand Down
15 changes: 8 additions & 7 deletions schedule/handler_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,14 @@ func (h *HandlerWindows) CreateJob(job *Config, schedules []*calendar.Event, per
}

jobConfig := &schtasks.Config{
ProfileName: job.ProfileName,
CommandName: job.CommandName,
Command: command,
Arguments: arguments.String(),
WorkingDirectory: job.WorkingDirectory,
JobDescription: job.JobDescription,
RunLevel: job.RunLevel,
ProfileName: job.ProfileName,
CommandName: job.CommandName,
Command: command,
Arguments: arguments.String(),
WorkingDirectory: job.WorkingDirectory,
JobDescription: job.JobDescription,
RunLevel: job.RunLevel,
StartWhenAvailable: job.StartWhenAvailable,
}
err := schtasks.Create(jobConfig, schedules, perm)
if err != nil {
Expand Down
28 changes: 28 additions & 0 deletions schedule/handler_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,31 @@ func TestHideWindowOption(t *testing.T) {
assert.Equal(t, scheduledJobs[0].Command, "conhost.exe")
assert.Equal(t, scheduledJobs[0].Arguments.String(), "--headless echo hello there")
}

func TestStartWhenAvailableOption(t *testing.T) {
job := Config{
ProfileName: "TestStartWhenAvailableOption",
CommandName: "backup",
Command: "echo",
Arguments: NewCommandArguments([]string{"hello", "there"}),
WorkingDirectory: "C:\\",
JobDescription: "TestStartWhenAvailableOption",
StartWhenAvailable: true,
}

handler := NewHandler(SchedulerWindows{}).(*HandlerWindows)

event := calendar.NewEvent()
err := event.Parse("2020-01-02 03:04") // will never get triggered
require.NoError(t, err)

err = handler.CreateJob(&job, []*calendar.Event{event}, PermissionUserLoggedOn)
assert.NoError(t, err)
defer func() {
_ = handler.RemoveJob(&job, PermissionUserLoggedOn)
}()

scheduledJobs, err := handler.Scheduled(job.ProfileName)
assert.NoError(t, err)
assert.Equal(t, 1, len(scheduledJobs))
}
1 change: 1 addition & 0 deletions schedule_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,5 +239,6 @@ func scheduleToConfig(sched *config.Schedule) *schedule.Config {
AfterNetworkOnline: sched.AfterNetworkOnline.IsTrue(),
SystemdDropInFiles: sched.SystemdDropInFiles,
HideWindow: sched.HideWindow.IsTrue(),
StartWhenAvailable: sched.StartWhenAvailable.IsTrue(),
}
}
15 changes: 8 additions & 7 deletions schtasks/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
package schtasks

type Config struct {
ProfileName string
CommandName string
Command string
Arguments string
WorkingDirectory string
JobDescription string
RunLevel string
ProfileName string
CommandName string
Command string
Arguments string
WorkingDirectory string
JobDescription string
RunLevel string
StartWhenAvailable bool
}
1 change: 1 addition & 0 deletions schtasks/taskscheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ func getTaskPath(profileName, commandName string) string {
func createTaskDefinition(config *Config, schedules []*calendar.Event) Task {
task := NewTask()
task.RegistrationInfo.Description = config.JobDescription
task.Settings.StartWhenAvailable = config.StartWhenAvailable
task.AddExecAction(ExecAction{
Command: config.Command,
Arguments: config.Arguments,
Expand Down
54 changes: 54 additions & 0 deletions schtasks/taskscheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,57 @@ func TestRunLevelOption(t *testing.T) {
// see related: https://github.com/creativeprojects/resticprofile/issues/545
// TODO: implement test when possible
}

func TestStartWhenAvailableOption(t *testing.T) {
config := &Config{
ProfileName: "test-start-when-available",
CommandName: "backup",
Command: "echo",
Arguments: "hello",
WorkingDirectory: "C:\\",
JobDescription: "test StartWhenAvailable option",
StartWhenAvailable: true,
}

event := calendar.NewEvent()
err := event.Parse("2099-01-02 03:04") // far future, will never trigger
require.NoError(t, err)
schedules := []*calendar.Event{event}

file, err := os.CreateTemp(t.TempDir(), "*.xml")
require.NoError(t, err)
defer file.Close()

taskPath := getTaskPath(config.ProfileName, config.CommandName)
sourceTask := createTaskDefinition(config, schedules)
sourceTask.RegistrationInfo.URI = taskPath

// Verify StartWhenAvailable is set in source task
assert.True(t, sourceTask.Settings.StartWhenAvailable)

err = createTaskFile(sourceTask, file)
require.NoError(t, err)
file.Close()

result, err := createTask(taskPath, file.Name(), "", "")
t.Log(result)
require.NoError(t, err)
defer func() {
_, _ = deleteTask(taskPath)
}()

// Export and verify the task was created with StartWhenAvailable
taskXML, err := exportTaskDefinition(taskPath)
require.NoError(t, err)

buffer := bytes.NewBuffer(taskXML)
decoder := xml.NewDecoder(buffer)
decoder.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
return input, nil
}
readTask := &Task{}
err = decoder.Decode(&readTask)
require.NoError(t, err)

assert.True(t, readTask.Settings.StartWhenAvailable, "StartWhenAvailable should be true in the created task")
}
Loading