Skip to content
Merged
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 @@ -320,6 +320,7 @@ type ScheduleBaseSection struct {
ScheduleIgnoreOnBattery maybe.Bool `mapstructure:"schedule-ignore-on-battery" show:"noshow" default:"false" description:"Don't start this schedule when running on battery"`
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)"`
}

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 @@ -45,6 +45,7 @@ type ScheduleBaseConfig struct {
IgnoreOnBatteryLessThan int `mapstructure:"ignore-on-battery-less-than" 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"`
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)"`
}

// scheduleBaseConfigDefaults declares built-in scheduling defaults
Expand Down Expand Up @@ -91,6 +92,9 @@ func (s *ScheduleBaseConfig) init(defaults *ScheduleBaseConfig) {
if s.SystemdDropInFiles == nil {
s.SystemdDropInFiles = slices.Clone(defaults.SystemdDropInFiles)
}
if !s.HideWindow.HasValue() {
s.HideWindow = defaults.HideWindow
}
}

func (s *ScheduleBaseConfig) applyOverrides(section *ScheduleBaseSection) {
Expand All @@ -105,6 +109,7 @@ func (s *ScheduleBaseConfig) applyOverrides(section *ScheduleBaseSection) {
s.EnvCapture = slices.Clone(section.ScheduleEnvCapture)
s.IgnoreOnBattery = section.ScheduleIgnoreOnBattery
s.AfterNetworkOnline = section.ScheduleAfterNetworkOnline
s.HideWindow = section.ScheduleHideWindow
// re-init with defaults
s.init(&defaults)
}
Expand Down
11 changes: 11 additions & 0 deletions docs/content/schedules/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,17 @@ If set to `true`, the schedule won't start if the system is running on battery (

If set to a number, the schedule won't start if the system is running on battery and the charge is less than or equal to the specified number.

## schedule-hide-window

When `schedule-permission` is set to `user_logged_on`, Windows Task Scheduler runs tasks in the foreground.
This behavior may interrupt the user's activity and is often undesirable.

To prevent that, set this option to `true` to hide the task window by wrapping the execution in `conhost.exe --headless`.

Note: It works only on Windows and makes sense only with `user_logged_on` permission.

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

## Example

Here's an example of a scheduling configuration:
Expand Down
1 change: 1 addition & 0 deletions schedule/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Config struct {
Flags map[string]string // flags added to the command line
AfterNetworkOnline bool
SystemdDropInFiles []string
HideWindow bool
removeOnly bool
}

Expand Down
24 changes: 22 additions & 2 deletions schedule/handler_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package schedule
import (
"errors"

"github.com/creativeprojects/clog"
"github.com/creativeprojects/resticprofile/calendar"
"github.com/creativeprojects/resticprofile/constants"
"github.com/creativeprojects/resticprofile/schtasks"
Expand Down Expand Up @@ -55,11 +56,30 @@ func (h *HandlerWindows) CreateJob(job *Config, schedules []*calendar.Event, per
} else if permission == PermissionUserLoggedOn {
perm = schtasks.UserLoggedOnAccount
}

var command string
var arguments CommandArguments

if job.HideWindow {
if permission != PermissionUserLoggedOn {
clog.Warning("hiding window makes sense only with \"user_logged_on\" permission")
}

command = "conhost.exe"
arguments = NewCommandArguments(append(
[]string{"--headless", job.Command},
job.Arguments.RawArgs()...,
))
} else {
command = job.Command
arguments = job.Arguments
}

jobConfig := &schtasks.Config{
ProfileName: job.ProfileName,
CommandName: job.CommandName,
Command: job.Command,
Arguments: job.Arguments.String(),
Command: command,
Arguments: arguments.String(),
WorkingDirectory: job.WorkingDirectory,
JobDescription: job.JobDescription,
}
Expand Down
33 changes: 33 additions & 0 deletions schedule/handler_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package schedule
import (
"testing"

"github.com/creativeprojects/resticprofile/calendar"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// Support for Windows removed as it was broken
Expand Down Expand Up @@ -45,3 +47,34 @@ func TestDetectPermissionTaskScheduler(t *testing.T) {
})
}
}

func TestHideWindowOption(t *testing.T) {
job := Config{
ProfileName: "TestHideWindowOption",
CommandName: "backup",
Command: "echo",
Arguments: NewCommandArguments([]string{"hello", "there"}),
WorkingDirectory: "C:\\",
JobDescription: "TestHideWindowOption",
HideWindow: 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, len(scheduledJobs), 1)

assert.Equal(t, scheduledJobs[0].Command, "conhost.exe")
assert.Equal(t, scheduledJobs[0].Arguments.String(), "--headless echo hello there")
}
1 change: 1 addition & 0 deletions schedule_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,5 +237,6 @@ func scheduleToConfig(sched *config.Schedule) *schedule.Config {
Flags: sched.Flags,
AfterNetworkOnline: sched.AfterNetworkOnline.IsTrue(),
SystemdDropInFiles: sched.SystemdDropInFiles,
HideWindow: sched.HideWindow.IsTrue(),
}
}
1 change: 1 addition & 0 deletions schtasks/taskscheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,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.RegistrationInfo.URI = taskPath

Expand Down
Loading