Skip to content

Commit a64f02c

Browse files
Feature/589 windows start when available (#590)
* initial * docs: manual documentation --------- Co-authored-by: Ryan <holler+git@hollowhemlock.com>
1 parent 8ea331c commit a64f02c

11 files changed

Lines changed: 128 additions & 14 deletions

File tree

config/profile.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ type ScheduleBaseSection struct {
326326
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"`
327327
ScheduleAfterNetworkOnline maybe.Bool `mapstructure:"schedule-after-network-online" show:"noshow" description:"Don't start this schedule when the network is offline (supported in \"systemd\")"`
328328
ScheduleHideWindow maybe.Bool `mapstructure:"schedule-hide-window" show:"noshow" default:"false" description:"Hide schedule window when running in foreground (Windows only)"`
329+
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)"`
329330
}
330331

331332
func (s *ScheduleBaseSection) setRootPath(_ *Profile, _ string) {

config/schedule.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type ScheduleBaseConfig struct {
4747
AfterNetworkOnline maybe.Bool `mapstructure:"after-network-online" description:"Don't start this schedule when the network is offline (supported in \"systemd\")"`
4848
SystemdDropInFiles []string `mapstructure:"systemd-drop-in-files" default:"" description:"Files containing systemd drop-in (override) files - see https://creativeprojects.github.io/resticprofile/schedules/systemd/"`
4949
HideWindow maybe.Bool `mapstructure:"hide-window" default:"false" description:"Hide schedule window when running in foreground (Windows only)"`
50+
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)"`
5051
}
5152

5253
// scheduleBaseConfigDefaults declares built-in scheduling defaults
@@ -100,6 +101,9 @@ func (s *ScheduleBaseConfig) init(defaults *ScheduleBaseConfig) {
100101
if !s.HideWindow.HasValue() {
101102
s.HideWindow = defaults.HideWindow
102103
}
104+
if !s.StartWhenAvailable.HasValue() {
105+
s.StartWhenAvailable = defaults.StartWhenAvailable
106+
}
103107
}
104108

105109
func (s *ScheduleBaseConfig) applyOverrides(section *ScheduleBaseSection) {
@@ -116,6 +120,7 @@ func (s *ScheduleBaseConfig) applyOverrides(section *ScheduleBaseSection) {
116120
s.IgnoreOnBattery = section.ScheduleIgnoreOnBattery
117121
s.AfterNetworkOnline = section.ScheduleAfterNetworkOnline
118122
s.HideWindow = section.ScheduleHideWindow
123+
s.StartWhenAvailable = section.ScheduleStartWhenAvailable
119124
// re-init with defaults
120125
s.init(&defaults)
121126
}

docs/content/schedules/configuration.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@ Note: It works only on Windows and makes sense only with `user_logged_on` permis
201201

202202
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).
203203

204+
## schedule-start-when-available
205+
206+
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.
207+
208+
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.
209+
210+
Note: This option only works on Windows.
211+
204212
## Example
205213

206214
Here's an example of a scheduling configuration:

docs/content/schedules/task_scheduler/index.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,16 @@ It's easy to spot a terminal window opened with Administrator privileges:
3434

3535
> [!IMPORTANT]
3636
> Running the schedule command might cause Windows to delete _resticprofile.exe_, treating it as a threat.
37+
38+
## Start when available
39+
40+
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.
41+
42+
```yaml
43+
profile:
44+
backup:
45+
schedule: "03:00"
46+
schedule-start-when-available: true
47+
```
48+
49+
This sets the "Start the task as soon as possible after a scheduled start is missed" option in Windows Task Scheduler.

schedule/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type Config struct {
2525
AfterNetworkOnline bool
2626
SystemdDropInFiles []string
2727
HideWindow bool
28+
StartWhenAvailable bool
2829
removeOnly bool
2930
}
3031

schedule/handler_windows.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,14 @@ func (h *HandlerWindows) CreateJob(job *Config, schedules []*calendar.Event, per
7777
}
7878

7979
jobConfig := &schtasks.Config{
80-
ProfileName: job.ProfileName,
81-
CommandName: job.CommandName,
82-
Command: command,
83-
Arguments: arguments.String(),
84-
WorkingDirectory: job.WorkingDirectory,
85-
JobDescription: job.JobDescription,
86-
RunLevel: job.RunLevel,
80+
ProfileName: job.ProfileName,
81+
CommandName: job.CommandName,
82+
Command: command,
83+
Arguments: arguments.String(),
84+
WorkingDirectory: job.WorkingDirectory,
85+
JobDescription: job.JobDescription,
86+
RunLevel: job.RunLevel,
87+
StartWhenAvailable: job.StartWhenAvailable,
8788
}
8889
err := schtasks.Create(jobConfig, schedules, perm)
8990
if err != nil {

schedule/handler_windows_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,31 @@ func TestHideWindowOption(t *testing.T) {
7878
assert.Equal(t, scheduledJobs[0].Command, "conhost.exe")
7979
assert.Equal(t, scheduledJobs[0].Arguments.String(), "--headless echo hello there")
8080
}
81+
82+
func TestStartWhenAvailableOption(t *testing.T) {
83+
job := Config{
84+
ProfileName: "TestStartWhenAvailableOption",
85+
CommandName: "backup",
86+
Command: "echo",
87+
Arguments: NewCommandArguments([]string{"hello", "there"}),
88+
WorkingDirectory: "C:\\",
89+
JobDescription: "TestStartWhenAvailableOption",
90+
StartWhenAvailable: true,
91+
}
92+
93+
handler := NewHandler(SchedulerWindows{}).(*HandlerWindows)
94+
95+
event := calendar.NewEvent()
96+
err := event.Parse("2020-01-02 03:04") // will never get triggered
97+
require.NoError(t, err)
98+
99+
err = handler.CreateJob(&job, []*calendar.Event{event}, PermissionUserLoggedOn)
100+
assert.NoError(t, err)
101+
defer func() {
102+
_ = handler.RemoveJob(&job, PermissionUserLoggedOn)
103+
}()
104+
105+
scheduledJobs, err := handler.Scheduled(job.ProfileName)
106+
assert.NoError(t, err)
107+
assert.Equal(t, 1, len(scheduledJobs))
108+
}

schedule_jobs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,5 +239,6 @@ func scheduleToConfig(sched *config.Schedule) *schedule.Config {
239239
AfterNetworkOnline: sched.AfterNetworkOnline.IsTrue(),
240240
SystemdDropInFiles: sched.SystemdDropInFiles,
241241
HideWindow: sched.HideWindow.IsTrue(),
242+
StartWhenAvailable: sched.StartWhenAvailable.IsTrue(),
242243
}
243244
}

schtasks/config.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
package schtasks
44

55
type Config struct {
6-
ProfileName string
7-
CommandName string
8-
Command string
9-
Arguments string
10-
WorkingDirectory string
11-
JobDescription string
12-
RunLevel string
6+
ProfileName string
7+
CommandName string
8+
Command string
9+
Arguments string
10+
WorkingDirectory string
11+
JobDescription string
12+
RunLevel string
13+
StartWhenAvailable bool
1314
}

schtasks/taskscheduler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ func getTaskPath(profileName, commandName string) string {
184184
func createTaskDefinition(config *Config, schedules []*calendar.Event) Task {
185185
task := NewTask()
186186
task.RegistrationInfo.Description = config.JobDescription
187+
task.Settings.StartWhenAvailable = config.StartWhenAvailable
187188
task.AddExecAction(ExecAction{
188189
Command: config.Command,
189190
Arguments: config.Arguments,

0 commit comments

Comments
 (0)