From 87c5a6d6ae0ce43c4c0fa170785aeb491dac6ebf Mon Sep 17 00:00:00 2001 From: zumm Date: Tue, 29 Jul 2025 18:52:52 +0700 Subject: [PATCH 01/11] feat(schedule): introduce `schtasks-hide-window` option --- config/profile.go | 1 + config/schedule.go | 5 +++++ schedule/config.go | 1 + schedule/handler_windows.go | 1 + schedule_jobs.go | 1 + schtasks/config.go | 1 + schtasks/taskscheduler.go | 16 ++++++++++++++++ 7 files changed, 26 insertions(+) diff --git a/config/profile.go b/config/profile.go index 818850010..4b58bb0be 100644 --- a/config/profile.go +++ b/config/profile.go @@ -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\")"` + ScheduleSchtasksHideWindow maybe.Bool `mapstructure:"schedule-schtasks-hide-window" show:"noshow" default:"false" description:"Hide schedule window when running in foreground (\"schtasks\" only)` } func (s *ScheduleBaseSection) setRootPath(_ *Profile, _ string) { diff --git a/config/schedule.go b/config/schedule.go index 9d44a312d..c95ecf245 100644 --- a/config/schedule.go +++ b/config/schedule.go @@ -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/"` + SchtasksHideWindow maybe.Bool `mapstructure:"schtasks-hide-window" default:"false" description:"Hide schedule window when running in foreground (\"schtasks\" only)` } // scheduleBaseConfigDefaults declares built-in scheduling defaults @@ -91,6 +92,9 @@ func (s *ScheduleBaseConfig) init(defaults *ScheduleBaseConfig) { if s.SystemdDropInFiles == nil { s.SystemdDropInFiles = slices.Clone(defaults.SystemdDropInFiles) } + if !s.SchtasksHideWindow.HasValue() { + s.SchtasksHideWindow = defaults.SchtasksHideWindow + } } func (s *ScheduleBaseConfig) applyOverrides(section *ScheduleBaseSection) { @@ -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.SchtasksHideWindow = section.ScheduleSchtasksHideWindow // re-init with defaults s.init(&defaults) } diff --git a/schedule/config.go b/schedule/config.go index 8a303004d..04bfe72d8 100644 --- a/schedule/config.go +++ b/schedule/config.go @@ -23,6 +23,7 @@ type Config struct { Flags map[string]string // flags added to the command line AfterNetworkOnline bool SystemdDropInFiles []string + SchtasksHideWindow bool removeOnly bool } diff --git a/schedule/handler_windows.go b/schedule/handler_windows.go index 8189080b9..3e381bb1a 100644 --- a/schedule/handler_windows.go +++ b/schedule/handler_windows.go @@ -62,6 +62,7 @@ func (h *HandlerWindows) CreateJob(job *Config, schedules []*calendar.Event, per Arguments: job.Arguments.String(), WorkingDirectory: job.WorkingDirectory, JobDescription: job.JobDescription, + HideWindow: job.SchtasksHideWindow, } err := schtasks.Create(jobConfig, schedules, perm) if err != nil { diff --git a/schedule_jobs.go b/schedule_jobs.go index f193913df..2a6f3302c 100644 --- a/schedule_jobs.go +++ b/schedule_jobs.go @@ -237,5 +237,6 @@ func scheduleToConfig(sched *config.Schedule) *schedule.Config { Flags: sched.Flags, AfterNetworkOnline: sched.AfterNetworkOnline.IsTrue(), SystemdDropInFiles: sched.SystemdDropInFiles, + SchtasksHideWindow: sched.SchtasksHideWindow.IsTrue(), } } diff --git a/schtasks/config.go b/schtasks/config.go index c1f27fd80..773571cb3 100644 --- a/schtasks/config.go +++ b/schtasks/config.go @@ -9,4 +9,5 @@ type Config struct { Arguments string WorkingDirectory string JobDescription string + HideWindow bool } diff --git a/schtasks/taskscheduler.go b/schtasks/taskscheduler.go index b6b8e9121..e057cc8bf 100644 --- a/schtasks/taskscheduler.go +++ b/schtasks/taskscheduler.go @@ -51,6 +51,22 @@ func Create(config *Config, schedules []*calendar.Event, permission Permission) return fmt.Errorf("cannot delete existing task to replace it: %w", err) } } + + if config.HideWindow { + if permission != UserLoggedOnAccount { + clog.Warning("hidding window makes sense only with \"user_logged_on\" permission") + } + + arguments := fmt.Sprintf( + "--headless '%s' %s", + config.Command, + config.Arguments, + ) + + config.Command = "conhost.exe" + config.Arguments = arguments + } + task := createTaskDefinition(config, schedules) task.RegistrationInfo.URI = taskPath From c9192bf47726956279f0135da82c515dfcf82422 Mon Sep 17 00:00:00 2001 From: zumm Date: Tue, 29 Jul 2025 23:19:01 +0700 Subject: [PATCH 02/11] chore(schedule): fix typos --- config/profile.go | 2 +- schtasks/taskscheduler.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/profile.go b/config/profile.go index 4b58bb0be..3037540c2 100644 --- a/config/profile.go +++ b/config/profile.go @@ -320,7 +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\")"` - ScheduleSchtasksHideWindow maybe.Bool `mapstructure:"schedule-schtasks-hide-window" show:"noshow" default:"false" description:"Hide schedule window when running in foreground (\"schtasks\" only)` + ScheduleSchtasksHideWindow maybe.Bool `mapstructure:"schedule-schtasks-hide-window" show:"noshow" default:"false" description:"Hide schedule window when running in foreground (\"schtasks\" only)"` } func (s *ScheduleBaseSection) setRootPath(_ *Profile, _ string) { diff --git a/schtasks/taskscheduler.go b/schtasks/taskscheduler.go index e057cc8bf..c0e93cb88 100644 --- a/schtasks/taskscheduler.go +++ b/schtasks/taskscheduler.go @@ -54,7 +54,7 @@ func Create(config *Config, schedules []*calendar.Event, permission Permission) if config.HideWindow { if permission != UserLoggedOnAccount { - clog.Warning("hidding window makes sense only with \"user_logged_on\" permission") + clog.Warning("hiding window makes sense only with \"user_logged_on\" permission") } arguments := fmt.Sprintf( From ecf11b1aa5ca11b2083c5decced23fbac0099be9 Mon Sep 17 00:00:00 2001 From: zumm Date: Tue, 29 Jul 2025 23:51:26 +0700 Subject: [PATCH 03/11] docs(schedule): describe `schtasks-hide-window` option --- docs/content/schedules/configuration.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/content/schedules/configuration.md b/docs/content/schedules/configuration.md index 8723d3013..90b20b57f 100644 --- a/docs/content/schedules/configuration.md +++ b/docs/content/schedules/configuration.md @@ -182,6 +182,13 @@ 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-schtasks-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`. + ## Example Here's an example of a scheduling configuration: From 2c99ca5e79365f47e0e461caa19eda59e46fdfde Mon Sep 17 00:00:00 2001 From: zumm Date: Wed, 30 Jul 2025 00:43:59 +0700 Subject: [PATCH 04/11] chore(schedule): fix typo --- config/schedule.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/schedule.go b/config/schedule.go index c95ecf245..68c667c59 100644 --- a/config/schedule.go +++ b/config/schedule.go @@ -45,7 +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/"` - SchtasksHideWindow maybe.Bool `mapstructure:"schtasks-hide-window" default:"false" description:"Hide schedule window when running in foreground (\"schtasks\" only)` + SchtasksHideWindow maybe.Bool `mapstructure:"schtasks-hide-window" default:"false" description:"Hide schedule window when running in foreground (\"schtasks\" only)"` } // scheduleBaseConfigDefaults declares built-in scheduling defaults From 21aaacb503509acc403c623e968d926705a33ed5 Mon Sep 17 00:00:00 2001 From: zumm Date: Wed, 30 Jul 2025 13:16:58 +0700 Subject: [PATCH 05/11] test(schedule): add test for `schtasks-hide-window` option --- schtasks/taskscheduler.go | 3 +++ schtasks/taskscheduler_test.go | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/schtasks/taskscheduler.go b/schtasks/taskscheduler.go index c0e93cb88..bc6323648 100644 --- a/schtasks/taskscheduler.go +++ b/schtasks/taskscheduler.go @@ -65,6 +65,9 @@ func Create(config *Config, schedules []*calendar.Event, permission Permission) config.Command = "conhost.exe" config.Arguments = arguments + // reset the HideWindow flag to prevent duplicate command wrapping + // if this config object is reused + config.HideWindow = false } task := createTaskDefinition(config, schedules) diff --git a/schtasks/taskscheduler_test.go b/schtasks/taskscheduler_test.go index 4b68520b1..c442c809a 100644 --- a/schtasks/taskscheduler_test.go +++ b/schtasks/taskscheduler_test.go @@ -107,6 +107,46 @@ func TestCanCreateTwice(t *testing.T) { assert.NoError(t, err) } +func TestHideWindowOption(t *testing.T) { + task := Config{ + ProfileName: "TestHideWindowOption", + CommandName: "backup", + Command: "echo", + Arguments: "hello there", + WorkingDirectory: "C:\\", + JobDescription: "TestHideWindowOption", + HideWindow: true, + } + + event := calendar.NewEvent() + err := event.Parse("2020-01-02 03:04") // will never get triggered + require.NoError(t, err) + + err = Create(&task, []*calendar.Event{event}, UserLoggedOnAccount) + assert.NoError(t, err) + + defer func() { + _ = Delete(task.ProfileName, task.CommandName) + }() + + registeredTasks, err := Registered() + assert.NoError(t, err) + + found := false + for _, registeredTask := range registeredTasks { + if registeredTask.ProfileName == "TestHideWindowOption" { + found = true + + assert.Equal(t, registeredTask.Command, "conhost.exe") + assert.Equal(t, registeredTask.Arguments, "--headless 'echo' hello there") + + break + } + } + + assert.Equal(t, found, true) +} + func TestTaskSchedulerIntegration(t *testing.T) { // some tests are using the 1st day of the month as a reference, // but this cause issues when we're running the tests on the first day of the month. From bb80f1ac992df6aeebdee4bed414e14fb08adf02 Mon Sep 17 00:00:00 2001 From: zumm Date: Wed, 30 Jul 2025 13:55:40 +0700 Subject: [PATCH 06/11] chore(schedule): fix indentation --- schtasks/taskscheduler_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/schtasks/taskscheduler_test.go b/schtasks/taskscheduler_test.go index c442c809a..837fe4023 100644 --- a/schtasks/taskscheduler_test.go +++ b/schtasks/taskscheduler_test.go @@ -135,12 +135,12 @@ func TestHideWindowOption(t *testing.T) { found := false for _, registeredTask := range registeredTasks { if registeredTask.ProfileName == "TestHideWindowOption" { - found = true + found = true - assert.Equal(t, registeredTask.Command, "conhost.exe") - assert.Equal(t, registeredTask.Arguments, "--headless 'echo' hello there") + assert.Equal(t, registeredTask.Command, "conhost.exe") + assert.Equal(t, registeredTask.Arguments, "--headless 'echo' hello there") - break + break } } From cf903f6078b389613f18cf21b965809595d8d676 Mon Sep 17 00:00:00 2001 From: zumm Date: Wed, 30 Jul 2025 23:43:58 +0700 Subject: [PATCH 07/11] chore(schedule): rename `schtasks-hide-window` option to `hide-window` --- config/profile.go | 2 +- config/schedule.go | 8 ++++---- docs/content/schedules/configuration.md | 4 +++- schedule_jobs.go | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/config/profile.go b/config/profile.go index 3037540c2..2247329e6 100644 --- a/config/profile.go +++ b/config/profile.go @@ -320,7 +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\")"` - ScheduleSchtasksHideWindow maybe.Bool `mapstructure:"schedule-schtasks-hide-window" show:"noshow" default:"false" description:"Hide schedule window when running in foreground (\"schtasks\" only)"` + ScheduleHideWindow maybe.Bool `mapstructure:"schedule-hide-window" show:"noshow" default:"false" description:"Hide schedule window when running in foreground (\"schtasks\" only)"` } func (s *ScheduleBaseSection) setRootPath(_ *Profile, _ string) { diff --git a/config/schedule.go b/config/schedule.go index 68c667c59..335e0a6ac 100644 --- a/config/schedule.go +++ b/config/schedule.go @@ -45,7 +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/"` - SchtasksHideWindow maybe.Bool `mapstructure:"schtasks-hide-window" default:"false" description:"Hide schedule window when running in foreground (\"schtasks\" only)"` + HideWindow maybe.Bool `mapstructure:"hide-window" default:"false" description:"Hide schedule window when running in foreground (\"schtasks\" only)"` } // scheduleBaseConfigDefaults declares built-in scheduling defaults @@ -92,8 +92,8 @@ func (s *ScheduleBaseConfig) init(defaults *ScheduleBaseConfig) { if s.SystemdDropInFiles == nil { s.SystemdDropInFiles = slices.Clone(defaults.SystemdDropInFiles) } - if !s.SchtasksHideWindow.HasValue() { - s.SchtasksHideWindow = defaults.SchtasksHideWindow + if !s.HideWindow.HasValue() { + s.HideWindow = defaults.HideWindow } } @@ -109,7 +109,7 @@ func (s *ScheduleBaseConfig) applyOverrides(section *ScheduleBaseSection) { s.EnvCapture = slices.Clone(section.ScheduleEnvCapture) s.IgnoreOnBattery = section.ScheduleIgnoreOnBattery s.AfterNetworkOnline = section.ScheduleAfterNetworkOnline - s.SchtasksHideWindow = section.ScheduleSchtasksHideWindow + s.HideWindow = section.ScheduleHideWindow // re-init with defaults s.init(&defaults) } diff --git a/docs/content/schedules/configuration.md b/docs/content/schedules/configuration.md index 90b20b57f..918fc1610 100644 --- a/docs/content/schedules/configuration.md +++ b/docs/content/schedules/configuration.md @@ -182,13 +182,15 @@ 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-schtasks-hide-window +## 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 with Windows Task Scheduler and makes sense only with `user_logged_on` permission. + ## Example Here's an example of a scheduling configuration: diff --git a/schedule_jobs.go b/schedule_jobs.go index 2a6f3302c..7ec8c2957 100644 --- a/schedule_jobs.go +++ b/schedule_jobs.go @@ -237,6 +237,6 @@ func scheduleToConfig(sched *config.Schedule) *schedule.Config { Flags: sched.Flags, AfterNetworkOnline: sched.AfterNetworkOnline.IsTrue(), SystemdDropInFiles: sched.SystemdDropInFiles, - SchtasksHideWindow: sched.SchtasksHideWindow.IsTrue(), + SchtasksHideWindow: sched.HideWindow.IsTrue(), } } From 096df9424f785f1a51c19ce891b5ef973a510917 Mon Sep 17 00:00:00 2001 From: zumm Date: Thu, 31 Jul 2025 02:39:44 +0700 Subject: [PATCH 08/11] feat(schedule): move `hide-window` option to windows scope --- schedule/config.go | 2 +- schedule/handler_windows.go | 25 +++++++++++++++++--- schedule/handler_windows_test.go | 33 ++++++++++++++++++++++++++ schedule_jobs.go | 2 +- schtasks/config.go | 1 - schtasks/taskscheduler.go | 18 -------------- schtasks/taskscheduler_test.go | 40 -------------------------------- 7 files changed, 57 insertions(+), 64 deletions(-) diff --git a/schedule/config.go b/schedule/config.go index 04bfe72d8..e0cec555f 100644 --- a/schedule/config.go +++ b/schedule/config.go @@ -23,7 +23,7 @@ type Config struct { Flags map[string]string // flags added to the command line AfterNetworkOnline bool SystemdDropInFiles []string - SchtasksHideWindow bool + HideWindow bool removeOnly bool } diff --git a/schedule/handler_windows.go b/schedule/handler_windows.go index 3e381bb1a..998349847 100644 --- a/schedule/handler_windows.go +++ b/schedule/handler_windows.go @@ -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" @@ -55,14 +56,32 @@ 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, - HideWindow: job.SchtasksHideWindow, } err := schtasks.Create(jobConfig, schedules, perm) if err != nil { diff --git a/schedule/handler_windows_test.go b/schedule/handler_windows_test.go index 0a084ed54..e50ffc576 100644 --- a/schedule/handler_windows_test.go +++ b/schedule/handler_windows_test.go @@ -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 @@ -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") +} diff --git a/schedule_jobs.go b/schedule_jobs.go index 7ec8c2957..276db520e 100644 --- a/schedule_jobs.go +++ b/schedule_jobs.go @@ -237,6 +237,6 @@ func scheduleToConfig(sched *config.Schedule) *schedule.Config { Flags: sched.Flags, AfterNetworkOnline: sched.AfterNetworkOnline.IsTrue(), SystemdDropInFiles: sched.SystemdDropInFiles, - SchtasksHideWindow: sched.HideWindow.IsTrue(), + HideWindow: sched.HideWindow.IsTrue(), } } diff --git a/schtasks/config.go b/schtasks/config.go index 773571cb3..c1f27fd80 100644 --- a/schtasks/config.go +++ b/schtasks/config.go @@ -9,5 +9,4 @@ type Config struct { Arguments string WorkingDirectory string JobDescription string - HideWindow bool } diff --git a/schtasks/taskscheduler.go b/schtasks/taskscheduler.go index bc6323648..aa83ecfd1 100644 --- a/schtasks/taskscheduler.go +++ b/schtasks/taskscheduler.go @@ -52,24 +52,6 @@ func Create(config *Config, schedules []*calendar.Event, permission Permission) } } - if config.HideWindow { - if permission != UserLoggedOnAccount { - clog.Warning("hiding window makes sense only with \"user_logged_on\" permission") - } - - arguments := fmt.Sprintf( - "--headless '%s' %s", - config.Command, - config.Arguments, - ) - - config.Command = "conhost.exe" - config.Arguments = arguments - // reset the HideWindow flag to prevent duplicate command wrapping - // if this config object is reused - config.HideWindow = false - } - task := createTaskDefinition(config, schedules) task.RegistrationInfo.URI = taskPath diff --git a/schtasks/taskscheduler_test.go b/schtasks/taskscheduler_test.go index 837fe4023..4b68520b1 100644 --- a/schtasks/taskscheduler_test.go +++ b/schtasks/taskscheduler_test.go @@ -107,46 +107,6 @@ func TestCanCreateTwice(t *testing.T) { assert.NoError(t, err) } -func TestHideWindowOption(t *testing.T) { - task := Config{ - ProfileName: "TestHideWindowOption", - CommandName: "backup", - Command: "echo", - Arguments: "hello there", - WorkingDirectory: "C:\\", - JobDescription: "TestHideWindowOption", - HideWindow: true, - } - - event := calendar.NewEvent() - err := event.Parse("2020-01-02 03:04") // will never get triggered - require.NoError(t, err) - - err = Create(&task, []*calendar.Event{event}, UserLoggedOnAccount) - assert.NoError(t, err) - - defer func() { - _ = Delete(task.ProfileName, task.CommandName) - }() - - registeredTasks, err := Registered() - assert.NoError(t, err) - - found := false - for _, registeredTask := range registeredTasks { - if registeredTask.ProfileName == "TestHideWindowOption" { - found = true - - assert.Equal(t, registeredTask.Command, "conhost.exe") - assert.Equal(t, registeredTask.Arguments, "--headless 'echo' hello there") - - break - } - } - - assert.Equal(t, found, true) -} - func TestTaskSchedulerIntegration(t *testing.T) { // some tests are using the 1st day of the month as a reference, // but this cause issues when we're running the tests on the first day of the month. From cf8ab2e004f3bba29dc6afc77bb179058c5aa377 Mon Sep 17 00:00:00 2001 From: zumm Date: Thu, 31 Jul 2025 02:53:20 +0700 Subject: [PATCH 09/11] docs(schedule): update `hide-window` description --- config/profile.go | 2 +- config/schedule.go | 2 +- docs/content/schedules/configuration.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/profile.go b/config/profile.go index 2247329e6..73a4a7d26 100644 --- a/config/profile.go +++ b/config/profile.go @@ -320,7 +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 (\"schtasks\" only)"` + 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) { diff --git a/config/schedule.go b/config/schedule.go index 335e0a6ac..f0011503d 100644 --- a/config/schedule.go +++ b/config/schedule.go @@ -45,7 +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 (\"schtasks\" only)"` + HideWindow maybe.Bool `mapstructure:"hide-window" default:"false" description:"Hide schedule window when running in foreground (Windows only)"` } // scheduleBaseConfigDefaults declares built-in scheduling defaults diff --git a/docs/content/schedules/configuration.md b/docs/content/schedules/configuration.md index 918fc1610..b14f830e2 100644 --- a/docs/content/schedules/configuration.md +++ b/docs/content/schedules/configuration.md @@ -189,7 +189,7 @@ 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 with Windows Task Scheduler and makes sense only with `user_logged_on` permission. +Note: It works only on Windows and makes sense only with `user_logged_on` permission. ## Example From cedaf3d8b0f03c9ac829ed4e4214656a70abd49c Mon Sep 17 00:00:00 2001 From: zumm Date: Tue, 5 Aug 2025 01:15:32 +0700 Subject: [PATCH 10/11] docs(schedule): add note about `conhost` instability --- docs/content/schedules/configuration.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/content/schedules/configuration.md b/docs/content/schedules/configuration.md index b14f830e2..1067397e0 100644 --- a/docs/content/schedules/configuration.md +++ b/docs/content/schedules/configuration.md @@ -191,6 +191,9 @@ To prevent that, set this option to `true` to hide the task window by wrapping t Note: It works only on Windows and makes sense only with `user_logged_on` permission. +Note: Behaviour of `conhost.exe` is not stable across Windows versions, so it may not work on some of them, especially older ones. +According our tests, it works on Windows 11 (version 24H2) and doesn't work on Windows 10 (version 1607). + ## Example Here's an example of a scheduling configuration: From e9a785c9c2c1256039b1cd0f06c2de6527bbf83b Mon Sep 17 00:00:00 2001 From: zumm Date: Tue, 5 Aug 2025 01:20:44 +0700 Subject: [PATCH 11/11] docs(schedule): simplify note about `conhost` --- docs/content/schedules/configuration.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/content/schedules/configuration.md b/docs/content/schedules/configuration.md index 1067397e0..a4248b3e1 100644 --- a/docs/content/schedules/configuration.md +++ b/docs/content/schedules/configuration.md @@ -191,8 +191,7 @@ To prevent that, set this option to `true` to hide the task window by wrapping t Note: It works only on Windows and makes sense only with `user_logged_on` permission. -Note: Behaviour of `conhost.exe` is not stable across Windows versions, so it may not work on some of them, especially older ones. -According our tests, it works on Windows 11 (version 24H2) and doesn't work on Windows 10 (version 1607). +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