Skip to content
Draft
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
37 changes: 24 additions & 13 deletions schtasks/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ type Task struct {
Principals Principals `xml:"Principals"`
Settings Settings `xml:"Settings"`
Actions Actions `xml:"Actions"`
fromNow time.Time `xml:"-"`
}

func NewTask() Task {
func NewTask(options ...TaskOption) Task {
var userID string
if currentUser, err := user.Current(); err == nil {
userID = currentUser.Uid
Expand Down Expand Up @@ -69,6 +70,11 @@ func NewTask() Task {
Actions: Actions{
Context: author,
},
fromNow: time.Now(),
}

for _, option := range options {
option.apply(&task)
}
return task
}
Expand Down Expand Up @@ -105,9 +111,14 @@ func (t *Task) AddSchedules(schedules []*calendar.Event) {
}
}

func (t *Task) setFromNow(fromNow time.Time) {
t.fromNow = fromNow
t.RegistrationInfo.Date = fromNow.Format(dateFormat)
}

func (t *Task) addTimeTrigger(triggerOnce time.Time) {
timeTrigger := TimeTrigger{
StartBoundary: triggerOnce.Format(dateFormat),
StartBoundary: &triggerOnce,
}
if t.Triggers.TimeTrigger == nil {
t.Triggers.TimeTrigger = []TimeTrigger{timeTrigger}
Expand All @@ -125,7 +136,7 @@ func (t *Task) addCalendarTrigger(trigger CalendarTrigger) {
}

func (t *Task) addDailyTrigger(schedule *calendar.Event) {
start := schedule.Next(time.Now())
start := schedule.Next(t.fromNow)
// get all recurrences in the same day
recurrences := schedule.GetAllInBetween(start, start.Add(24*time.Hour))
if len(recurrences) == 0 {
Expand All @@ -135,7 +146,7 @@ func (t *Task) addDailyTrigger(schedule *calendar.Event) {
// Is it only once a day?
if len(recurrences) == 1 {
t.addCalendarTrigger(CalendarTrigger{
StartBoundary: recurrences[0].Format(dateFormat),
StartBoundary: &recurrences[0],
ScheduleByDay: &ScheduleByDay{
DaysInterval: 1,
},
Expand All @@ -149,7 +160,7 @@ func (t *Task) addDailyTrigger(schedule *calendar.Event) {
// case with regular repetition
interval := period.NewOf(compactDifferences[0])
t.addCalendarTrigger(CalendarTrigger{
StartBoundary: start.Format(dateFormat),
StartBoundary: &start,
ScheduleByDay: &ScheduleByDay{
DaysInterval: 1,
},
Expand All @@ -168,7 +179,7 @@ func (t *Task) addDailyTrigger(schedule *calendar.Event) {
// install them all
for _, recurrence := range recurrences {
t.addCalendarTrigger(CalendarTrigger{
StartBoundary: recurrence.Format(dateFormat),
StartBoundary: &recurrence,
ScheduleByDay: &ScheduleByDay{
DaysInterval: 1,
},
Expand All @@ -177,7 +188,7 @@ func (t *Task) addDailyTrigger(schedule *calendar.Event) {
}

func (t *Task) addWeeklyTrigger(schedule *calendar.Event) {
start := schedule.Next(time.Now())
start := schedule.Next(t.fromNow)
// get all recurrences in the same day
recurrences := schedule.GetAllInBetween(start, start.Add(24*time.Hour))
if len(recurrences) == 0 {
Expand All @@ -187,7 +198,7 @@ func (t *Task) addWeeklyTrigger(schedule *calendar.Event) {
// Is it only once per 24h?
if len(recurrences) == 1 {
t.addCalendarTrigger(CalendarTrigger{
StartBoundary: recurrences[0].Format(dateFormat),
StartBoundary: &recurrences[0],
ScheduleByWeek: &ScheduleByWeek{
WeeksInterval: 1,
DaysOfWeek: convertWeekdays(schedule.WeekDay.GetRangeValues()),
Expand All @@ -202,7 +213,7 @@ func (t *Task) addWeeklyTrigger(schedule *calendar.Event) {
// case with regular repetition
interval := period.NewOf(compactDifferences[0])
t.addCalendarTrigger(CalendarTrigger{
StartBoundary: start.Format(dateFormat),
StartBoundary: &start,
ScheduleByWeek: &ScheduleByWeek{
WeeksInterval: 1,
DaysOfWeek: convertWeekdays(schedule.WeekDay.GetRangeValues()),
Expand All @@ -222,7 +233,7 @@ func (t *Task) addWeeklyTrigger(schedule *calendar.Event) {
// install them all
for _, recurrence := range recurrences {
t.addCalendarTrigger(CalendarTrigger{
StartBoundary: recurrence.Format(dateFormat),
StartBoundary: &recurrence,
ScheduleByWeek: &ScheduleByWeek{
WeeksInterval: 1,
DaysOfWeek: convertWeekdays(schedule.WeekDay.GetRangeValues()),
Expand All @@ -232,7 +243,7 @@ func (t *Task) addWeeklyTrigger(schedule *calendar.Event) {
}

func (t *Task) addMonthlyTrigger(schedule *calendar.Event) {
start := schedule.Next(time.Now())
start := schedule.Next(t.fromNow)
// get all recurrences in the same day
recurrences := schedule.GetAllInBetween(start, start.Add(24*time.Hour))
if len(recurrences) == 0 {
Expand All @@ -252,7 +263,7 @@ func (t *Task) addMonthlyTrigger(schedule *calendar.Event) {
}
if schedule.WeekDay.HasValue() {
t.addCalendarTrigger(CalendarTrigger{
StartBoundary: recurrence.Format(dateFormat),
StartBoundary: &recurrence,
ScheduleByMonthDayOfWeek: &ScheduleByMonthDayOfWeek{
DaysOfWeek: convertWeekdays(schedule.WeekDay.GetRangeValues()),
Weeks: AllWeeks,
Expand All @@ -262,7 +273,7 @@ func (t *Task) addMonthlyTrigger(schedule *calendar.Event) {
continue
}
t.addCalendarTrigger(CalendarTrigger{
StartBoundary: recurrence.Format(dateFormat),
StartBoundary: &recurrence,
ScheduleByMonth: &ScheduleByMonth{
DaysOfMonth: convertDaysOfMonth(schedule.Day.GetRangeValues()),
Months: convertMonths(schedule.Month.GetRangeValues()),
Expand Down
21 changes: 21 additions & 0 deletions schtasks/task_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//go:build windows

package schtasks

import "time"

type TaskOption interface {
apply(t *Task)
}

type WithFromNowOption struct {
now time.Time
}

func WithFromNow(now time.Time) WithFromNowOption {
return WithFromNowOption{now: now}
}

func (w WithFromNowOption) apply(t *Task) {
t.setFromNow(w.now)
}
12 changes: 8 additions & 4 deletions schtasks/taskscheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"slices"
"strings"
"text/tabwriter"
"time"

"github.com/creativeprojects/clog"
"github.com/creativeprojects/resticprofile/calendar"
Expand All @@ -53,8 +54,7 @@ func Create(config *Config, schedules []*calendar.Event, permission Permission)
return fmt.Errorf("cannot delete existing task to replace it: %w", err)
}
}

task := createTaskDefinition(config, schedules)
task := createTaskDefinition(config, schedules, time.Time{})
task.RegistrationInfo.URI = taskPath

switch config.RunLevel {
Expand Down Expand Up @@ -181,8 +181,12 @@ func getTaskPath(profileName, commandName string) string {
return fmt.Sprintf("%s%s %s", tasksPathPrefix, profileName, commandName)
}

func createTaskDefinition(config *Config, schedules []*calendar.Event) Task {
task := NewTask()
func createTaskDefinition(config *Config, schedules []*calendar.Event, from time.Time) Task {
options := make([]TaskOption, 0, 1)
if !from.IsZero() {
options = append(options, WithFromNow(from))
}
task := NewTask(options...)
task.RegistrationInfo.Description = config.JobDescription
task.AddExecAction(ExecAction{
Command: config.Command,
Expand Down
60 changes: 58 additions & 2 deletions schtasks/taskscheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,98 +125,131 @@ func TestTaskSchedulerIntegration(t *testing.T) {
fixtures := []struct {
description string
schedules []string
fromNow time.Time
}{
{
"only once",
[]string{"2020-01-02 03:04"},
time.Time{},
},
// daily
{
"once every day",
[]string{"*-*-* 03:04"},
time.Time{},
},
{
"every hour",
[]string{"*-*-* *:04"},
time.Time{},
},
{
"every minute",
[]string{"*-*-* *:*"},
time.Time{},
},
{
"every minute at 12",
"every minute at 12 (before 12)",
[]string{"*-*-* 12:*"},
time.Date(2025, 7, 27, 11, 20, 0, 0, time.UTC),
},
// this creates 60 triggers
// {
// "every minute at 12",
// []string{"*-*-* 12:*"},
// time.Date(2025, 7, 27, 12, 20, 0, 0, time.UTC),
// },
{
"every minute at 12 (after 12)",
[]string{"*-*-* 12:*"},
time.Date(2025, 7, 27, 13, 20, 0, 0, time.UTC),
},
// daily - more than one
{
"three times a day",
[]string{"*-*-* 03..05:04"},
time.Time{},
},
{
"twice every hour",
[]string{"*-*-* *:04..05"},
time.Time{},
},
// weekly
{
"once weekly",
[]string{"mon *-*-* 03:04"},
time.Time{},
},
{
"every hour on mondays",
[]string{strings.ToLower(fixedDay)[:3] + " *-*-* *:04"},
time.Time{},
},
{
"every minute on mondays",
[]string{strings.ToLower(fixedDay)[:3] + " *-*-* *:*"},
time.Time{},
},
{
"every minute at 12 on mondays",
[]string{"mon *-*-* 12:*"},
time.Time{},
},
// more than once weekly
{
"twice weekly",
[]string{"mon *-*-* 03..04:04"},
time.Time{},
},
{
"twice mondays and tuesdays",
[]string{"mon,tue *-*-* 03:04..06"},
time.Time{},
},
{
"twice on fridays",
[]string{"fri *-*-* *:04..05"},
time.Time{},
},
// monthly
{
"once monthly",
[]string{"*-01-* 03:04"},
time.Time{},
},
{
"every hour in january",
[]string{"*-01-* *:04"},
time.Time{},
},
// monthly with weekdays
{
"mondays in January",
[]string{"mon *-01-* 03:04"},
time.Time{},
},
{
"every hour on Mondays in january",
[]string{"mon *-01-* *:04"},
time.Time{},
},
// some days every month
{
"one day per month",
[]string{"*-*-0" + dayOfTheMonth + " 03:04"},
time.Time{},
},
{
"every hour on the 1st of each month",
[]string{"*-*-0" + dayOfTheMonth + " *:04"},
time.Time{},
},
// more than once per month
{
"twice in one day per month",
[]string{"*-*-0" + dayOfTheMonth + " 03..04:04"},
time.Time{},
},
}

Expand Down Expand Up @@ -247,13 +280,15 @@ func TestTaskSchedulerIntegration(t *testing.T) {
defer file.Close()

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

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

t.Logf("task contains %d time triggers and %d calendar triggers", len(sourceTask.Triggers.TimeTrigger), len(sourceTask.Triggers.CalendarTrigger))

result, err := createTask(taskPath, file.Name(), "", "")
t.Log(result)
require.NoError(t, err)
Expand All @@ -271,6 +306,9 @@ func TestTaskSchedulerIntegration(t *testing.T) {
err = decoder.Decode(&readTask)
require.NoError(t, err)

sourceTask.fromNow = time.Time{} // ignore fromNow in the source task
taskInUTC(&sourceTask)
taskInUTC(readTask)
assert.Equal(t, sourceTask, *readTask)

result, err = deleteTask(taskPath)
Expand All @@ -287,3 +325,21 @@ func TestRunLevelOption(t *testing.T) {
// see related: https://github.com/creativeprojects/resticprofile/issues/545
// TODO: implement test when possible
}

func taskInUTC(task *Task) {
// Windows Task Scheduler is using the current timezone when loading dates into the XML definition.
// This is a workaround to ensure that the tests run consistently.
for i := range task.Triggers.TimeTrigger {
if task.Triggers.TimeTrigger[i].StartBoundary != nil {
*task.Triggers.TimeTrigger[i].StartBoundary = task.Triggers.TimeTrigger[i].StartBoundary.UTC()
}
}
for i := range task.Triggers.CalendarTrigger {
if task.Triggers.CalendarTrigger[i].StartBoundary != nil {
*task.Triggers.CalendarTrigger[i].StartBoundary = task.Triggers.CalendarTrigger[i].StartBoundary.UTC()
}
if task.Triggers.CalendarTrigger[i].EndBoundary != nil {
*task.Triggers.CalendarTrigger[i].EndBoundary = task.Triggers.CalendarTrigger[i].EndBoundary.UTC()
}
}
}
Loading
Loading