From 21058e93c48fa629283d81e9ebc1419ab7b2b6e0 Mon Sep 17 00:00:00 2001 From: fredrik Date: Thu, 4 Jun 2026 15:38:53 +0200 Subject: [PATCH 01/21] additional hardening --- README.md | 6 +++--- appveyor.yml | 32 -------------------------------- manage.go | 26 +++++++++++++++++--------- tasks.go | 1 + utils.go | 1 + validate.go | 28 ++++++++-------------------- 6 files changed, 30 insertions(+), 64 deletions(-) delete mode 100644 appveyor.yml diff --git a/README.md b/README.md index fb5887a..096dccb 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ The library has a tendency to panic over even parsing errors. My long-term goal ======================= /GIERT'S FORK NOTES> ======================= -[![Build status](https://ci.appveyor.com/api/projects/status/b3gllq093c8ex5ew?svg=true)](https://ci.appveyor.com/project/capnspacehook/taskmaster) -[![Go Report Card](https://goreportcard.com/badge/github.com/capnspacehook/taskmaster)](https://goreportcard.com/report/github.com/capnspacehook/taskmaster) -[![PkgGoDev](https://pkg.go.dev/badge/github.com/capnspacehook/taskmaster)](https://pkg.go.dev/github.com/capnspacehook/taskmaster) +[![CI](https://github.com/giert/taskmaster/actions/workflows/ci.yml/badge.svg)](https://github.com/giert/taskmaster/actions/workflows/ci.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/giert/taskmaster)](https://goreportcard.com/report/github.com/giert/taskmaster) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/giert/taskmaster)](https://pkg.go.dev/github.com/giert/taskmaster) # taskmaster Windows Task Scheduler Library for Go diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 50bc7e9..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: "1.0.{build}" - -stack: go 1.15 - -clone_folder: c:\gopath\src\github.com\capnspacehook\taskmaster - -environment: - GOPATH: c:\gopath - matrix: - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - GOARCH: 386 - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - GOARCH: amd64 - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - GOARCH: 386 - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - GOARCH: amd64 - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 - GOARCH: 386 - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 - GOARCH: amd64 - -platform: - - x64 - -build_script: - - cd c:\gopath\src\github.com\capnspacehook\taskmaster - - go build - -test_script: - - cd c:\gopath\src\github.com\capnspacehook\taskmaster - - go test -cover diff --git a/manage.go b/manage.go index f398192..44b82b4 100644 --- a/manage.go +++ b/manage.go @@ -24,7 +24,11 @@ func (t *TaskService) initialize() error { err = ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED) if err != nil { - code := err.(*ole.OleError).Code() + oleErr, ok := err.(*ole.OleError) + if !ok { + return err + } + code := oleErr.Code() if code != ole.S_OK && code != S_FALSE { return err } @@ -95,7 +99,12 @@ func ConnectWithOptions(serverName, domain, username, password string) (TaskServ if err != nil { return TaskService{}, err } - username = strings.Split(currentUser.Username, `\`)[1] + // currentUser.Username is usually "DOMAIN\user" on Windows, but fall back to the raw value when no domain prefix is present + if idx := strings.LastIndex(currentUser.Username, `\`); idx != -1 { + username = currentUser.Username[idx+1:] + } else { + username = currentUser.Username + } } taskService.connectedDomain = domain taskService.connectedComputerName = serverName @@ -172,7 +181,6 @@ func (t *TaskService) GetRegisteredTasks() (RegisteredTaskCollection, error) { defer rootTaskCollection.Release() err = oleutil.ForEach(rootTaskCollection, func(v *ole.VARIANT) error { task := v.ToIDispatch() - defer task.Release() registeredTask, path, err := parseRegisteredTask(task) if err != nil { @@ -248,7 +256,7 @@ func (t *TaskService) GetRegisteredTasks() (RegisteredTaskCollection, error) { // pointer to it if it exists. If it doesn't exist, nil will be returned in place of // the registered task. func (t *TaskService) GetRegisteredTask(path string) (RegisteredTask, error) { - if path[0] != '\\' { + if len(path) == 0 || path[0] != '\\' { return RegisteredTask{}, ErrInvalidPath } @@ -275,7 +283,7 @@ func (t TaskService) GetTaskFolders() (TaskFolder, error) { // registered tasks under the folder specified, if it exists. If it doesn't exist, nil will be // returned in place of the task folder. func (t TaskService) GetTaskFolder(path string) (TaskFolder, error) { - if path[0] != '\\' { + if len(path) == 0 || path[0] != '\\' { return TaskFolder{}, ErrInvalidPath } @@ -433,7 +441,7 @@ func (t *TaskService) CreateTask(path string, newTaskDef Definition, overwrite b func (t *TaskService) CreateTaskEx(path string, newTaskDef Definition, username, password string, logonType TaskLogonType, overwrite bool) (RegisteredTask, bool, error) { var err error - if path[0] != '\\' { + if len(path) == 0 || path[0] != '\\' { return RegisteredTask{}, false, ErrInvalidPath } else if err = validateDefinition(newTaskDef); err != nil { return RegisteredTask{}, false, err @@ -486,7 +494,7 @@ func (t *TaskService) UpdateTask(path string, newTaskDef Definition) (Registered func (t *TaskService) UpdateTaskEx(path string, newTaskDef Definition, username, password string, logonType TaskLogonType) (RegisteredTask, error) { var err error - if path[0] != '\\' { + if len(path) == 0 || path[0] != '\\' { return RegisteredTask{}, ErrInvalidPath } else if err = validateDefinition(newTaskDef); err != nil { return RegisteredTask{}, err @@ -538,7 +546,7 @@ func (t *TaskService) modifyTask(path string, newTaskDef Definition, username, p func (t *TaskService) DeleteFolder(path string, deleteRecursively bool) (bool, error) { var err error - if path[0] != '\\' { + if len(path) == 0 || path[0] != '\\' { return false, ErrInvalidPath } @@ -644,7 +652,7 @@ func (t *TaskService) DeleteFolder(path string, deleteRecursively bool) (bool, e func (t *TaskService) DeleteTask(path string) error { var err error - if path[0] != '\\' { + if len(path) == 0 || path[0] != '\\' { return ErrInvalidPath } diff --git a/tasks.go b/tasks.go index 91889ea..a49e599 100644 --- a/tasks.go +++ b/tasks.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package taskmaster diff --git a/utils.go b/utils.go index 33bf659..b7d5929 100644 --- a/utils.go +++ b/utils.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package taskmaster diff --git a/validate.go b/validate.go index de96324..57e8fe8 100644 --- a/validate.go +++ b/validate.go @@ -33,10 +33,8 @@ func validateDefinition(def Definition) error { func validateActions(actions []Action) error { for _, action := range actions { switch action.GetType() { - case TASK_ACTION_EXEC: - return nil - case TASK_ACTION_COM_HANDLER: - return nil + case TASK_ACTION_EXEC, TASK_ACTION_COM_HANDLER: + // valid; keep validating the remaining actions default: return errors.New("invalid task action type") } @@ -49,25 +47,21 @@ func validateTriggers(triggers []Trigger) error { for _, trigger := range triggers { switch t := trigger.(type) { case BootTrigger: - return nil + // no required fields case DailyTrigger: if t.GetStartBoundary() == defaultTime { return errors.New("invalid DailyTrigger: StartBoundary is required") } else if t.DayInterval > EveryOtherDay { return errors.New("invalid DailyTrigger: invalid DayInterval") } - - return nil case EventTrigger: if t.Subscription == "" { return errors.New("invalid EventTrigger: Subscription is required") } - - return nil case IdleTrigger: - return nil + // no required fields case LogonTrigger: - return nil + // no required fields case MonthlyDOWTrigger: if t.GetStartBoundary() == defaultTime { return errors.New("invalid MonthlyDOWTrigger: StartBoundary is required") @@ -84,8 +78,6 @@ func validateTriggers(triggers []Trigger) error { } else if t.WeeksOfMonth > AllWeeks { return errors.New("invalid MonthlyDOWTrigger: invalid WeeksOfMonth") } - - return nil case MonthlyTrigger: if t.GetStartBoundary() == defaultTime { return errors.New("invalid MonthlyTrigger: StartBoundary is required") @@ -98,14 +90,12 @@ func validateTriggers(triggers []Trigger) error { } else if t.MonthsOfYear > AllMonths { return errors.New("invalid MonthlyTrigger: invalid MonthsOfYear") } - - return nil case RegistrationTrigger: - return nil + // no required fields case SessionStateChangeTrigger: - return nil + // no required fields case TimeTrigger: - return nil + // no required fields case WeeklyTrigger: if t.GetStartBoundary() == defaultTime { return errors.New("invalid WeeklyTrigger: StartBoundary is required") @@ -118,8 +108,6 @@ func validateTriggers(triggers []Trigger) error { } else if t.WeekInterval > EveryOtherWeek { return errors.New("invalid WeeklyTrigger: invalid WeekInterval") } - - return nil default: return errors.New("invalid task trigger type") } From 2fa1525f66be0f975b1323788d268e80f8fc8e43 Mon Sep 17 00:00:00 2001 From: fredrik Date: Thu, 4 Jun 2026 16:02:37 +0200 Subject: [PATCH 02/21] check if time is zero the right way --- utils.go | 3 +-- validate.go | 15 +++++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/utils.go b/utils.go index b7d5929..8d30d08 100644 --- a/utils.go +++ b/utils.go @@ -25,8 +25,7 @@ func IntToDayOfMonth(dayOfMonth int) (DayOfMonth, error) { } func TimeToTaskDate(t time.Time) string { - defaultTime := time.Time{} - if t == defaultTime { + if t.IsZero() { return "" } diff --git a/validate.go b/validate.go index 57e8fe8..b06c599 100644 --- a/validate.go +++ b/validate.go @@ -3,12 +3,7 @@ package taskmaster -import ( - "errors" - "time" -) - -var defaultTime = time.Time{} +import "errors" func validateDefinition(def Definition) error { var err error @@ -49,7 +44,7 @@ func validateTriggers(triggers []Trigger) error { case BootTrigger: // no required fields case DailyTrigger: - if t.GetStartBoundary() == defaultTime { + if t.GetStartBoundary().IsZero() { return errors.New("invalid DailyTrigger: StartBoundary is required") } else if t.DayInterval > EveryOtherDay { return errors.New("invalid DailyTrigger: invalid DayInterval") @@ -63,7 +58,7 @@ func validateTriggers(triggers []Trigger) error { case LogonTrigger: // no required fields case MonthlyDOWTrigger: - if t.GetStartBoundary() == defaultTime { + if t.GetStartBoundary().IsZero() { return errors.New("invalid MonthlyDOWTrigger: StartBoundary is required") } else if t.DaysOfWeek == 0 { return errors.New("invalid MonthlyDOWTrigger: DaysOfWeek is required") @@ -79,7 +74,7 @@ func validateTriggers(triggers []Trigger) error { return errors.New("invalid MonthlyDOWTrigger: invalid WeeksOfMonth") } case MonthlyTrigger: - if t.GetStartBoundary() == defaultTime { + if t.GetStartBoundary().IsZero() { return errors.New("invalid MonthlyTrigger: StartBoundary is required") } else if t.DaysOfMonth == 0 { return errors.New("invalid MonthlyTrigger: DaysOfMonth is required") @@ -97,7 +92,7 @@ func validateTriggers(triggers []Trigger) error { case TimeTrigger: // no required fields case WeeklyTrigger: - if t.GetStartBoundary() == defaultTime { + if t.GetStartBoundary().IsZero() { return errors.New("invalid WeeklyTrigger: StartBoundary is required") } else if t.DaysOfWeek == 0 { return errors.New("invalid WeeklyTrigger: DaysOfWeek is required") From 322a7fb5b3c877a3e1e0abfb6fa5ab7f2dc4f50a Mon Sep 17 00:00:00 2001 From: fredrik Date: Thu, 4 Jun 2026 18:42:08 +0200 Subject: [PATCH 03/21] oleHelper replaces must with err returns --- comhelper.go | 126 ++++++++++++++ fill.go | 341 +++++++++++++++++++++++-------------- manage.go | 43 ++++- parse.go | 461 ++++++++++++++++++++++++++++++--------------------- 4 files changed, 647 insertions(+), 324 deletions(-) create mode 100644 comhelper.go diff --git a/comhelper.go b/comhelper.go new file mode 100644 index 0000000..fee5195 --- /dev/null +++ b/comhelper.go @@ -0,0 +1,126 @@ +//go:build windows +// +build windows + +package taskmaster + +import ( + "fmt" + + ole "github.com/go-ole/go-ole" + "github.com/go-ole/go-ole/oleutil" +) + +// oleHelper performs a batch of COM property reads/writes against Task Scheduler +// objects, recording the first error encountered. Once an error is recorded, +// every subsequent operation becomes a no-op returning the zero value, so a long +// sequence of calls can be issued and the accumulated error checked once at a +// convenient point via h.err. +// +// This replaces the panic-on-error oleutil.Must* helpers with ordinary error propagation +// +// IMPORTANT: getObject and query return nil once an error has been recorded. +// Callers MUST check h.err immediately after such a call, before dereferencing or +// deferring Release on the result, e.g.: +// +// obj := h.getObject(parent, "Child") +// if h.err != nil { +// return h.err +// } +// defer obj.Release() +type oleHelper struct { + err error +} + +func (h *oleHelper) getString(obj *ole.IDispatch, name string) string { + if h.err != nil { + return "" + } + v, err := oleutil.GetProperty(obj, name) + if err != nil { + h.err = fmt.Errorf("error getting property %q: %w", name, err) + return "" + } + defer v.Clear() + return v.ToString() +} + +func (h *oleHelper) getInt(obj *ole.IDispatch, name string) int64 { + if h.err != nil { + return 0 + } + v, err := oleutil.GetProperty(obj, name) + if err != nil { + h.err = fmt.Errorf("error getting property %q: %w", name, err) + return 0 + } + defer v.Clear() + return v.Val +} + +func (h *oleHelper) getBool(obj *ole.IDispatch, name string) bool { + if h.err != nil { + return false + } + v, err := oleutil.GetProperty(obj, name) + if err != nil { + h.err = fmt.Errorf("error getting property %q: %w", name, err) + return false + } + defer v.Clear() + b, ok := v.Value().(bool) + if !ok { + h.err = fmt.Errorf("property %q is not a boolean", name) + return false + } + return b +} + +func (h *oleHelper) getVariant(obj *ole.IDispatch, name string) *ole.VARIANT { + if h.err != nil { + return nil + } + v, err := oleutil.GetProperty(obj, name) + if err != nil { + h.err = fmt.Errorf("error getting property %q: %w", name, err) + return nil + } + return v +} + +func (h *oleHelper) getObject(obj *ole.IDispatch, name string) *ole.IDispatch { + if h.err != nil { + return nil + } + v, err := oleutil.GetProperty(obj, name) + if err != nil { + h.err = fmt.Errorf("error getting property %q: %w", name, err) + return nil + } + d := v.ToIDispatch() + if d == nil { + h.err = fmt.Errorf("property %q is not an object", name) + return nil + } + return d +} + +func (h *oleHelper) put(obj *ole.IDispatch, name string, args ...any) { + if h.err != nil { + return + } + if _, err := oleutil.PutProperty(obj, name, args...); err != nil { + h.err = fmt.Errorf("error setting property %q: %w", name, err) + } +} + +func (h *oleHelper) query(obj *ole.IDispatch, iid *ole.GUID) *ole.IDispatch { + if h.err != nil { + return nil + } + d, err := obj.QueryInterface(iid) + if err != nil { + h.err = fmt.Errorf("error querying COM interface: %w", err) + return nil + } + return d +} diff --git a/fill.go b/fill.go index 0228bea..c600051 100644 --- a/fill.go +++ b/fill.go @@ -11,228 +11,325 @@ import ( ) func fillDefinitionObj(definition Definition, definitionObj *ole.IDispatch) error { - var err error + h := &oleHelper{} - actionsObj := oleutil.MustGetProperty(definitionObj, "Actions").ToIDispatch() + actionsObj := h.getObject(definitionObj, "Actions") + if h.err != nil { + return h.err + } defer actionsObj.Release() - oleutil.MustPutProperty(actionsObj, "Context", definition.Context) - err = fillActionsObj(definition.Actions, actionsObj) - if err != nil { - return fmt.Errorf("error filling IAction objects: %v", err) + h.put(actionsObj, "Context", definition.Context) + if h.err != nil { + return h.err + } + if err := fillActionsObj(definition.Actions, actionsObj); err != nil { + return fmt.Errorf("error filling IAction objects: %w", err) } - oleutil.MustPutProperty(definitionObj, "Data", definition.Data) + h.put(definitionObj, "Data", definition.Data) + if h.err != nil { + return h.err + } - principalObj := oleutil.MustGetProperty(definitionObj, "Principal").ToIDispatch() + principalObj := h.getObject(definitionObj, "Principal") + if h.err != nil { + return h.err + } defer principalObj.Release() - fillPrincipalObj(definition.Principal, principalObj) + if err := fillPrincipalObj(definition.Principal, principalObj); err != nil { + return fmt.Errorf("error filling IPrincipal object: %w", err) + } - regInfoObj := oleutil.MustGetProperty(definitionObj, "RegistrationInfo").ToIDispatch() + regInfoObj := h.getObject(definitionObj, "RegistrationInfo") + if h.err != nil { + return h.err + } defer regInfoObj.Release() - fillRegistrationInfoObj(definition.RegistrationInfo, regInfoObj) + if err := fillRegistrationInfoObj(definition.RegistrationInfo, regInfoObj); err != nil { + return fmt.Errorf("error filling IRegistrationInfo object: %w", err) + } - settingsObj := oleutil.MustGetProperty(definitionObj, "Settings").ToIDispatch() + settingsObj := h.getObject(definitionObj, "Settings") + if h.err != nil { + return h.err + } defer settingsObj.Release() - fillTaskSettingsObj(definition.Settings, settingsObj) + if err := fillTaskSettingsObj(definition.Settings, settingsObj); err != nil { + return fmt.Errorf("error filling ITaskSettings object: %w", err) + } - triggersObj := oleutil.MustGetProperty(definitionObj, "Triggers").ToIDispatch() + triggersObj := h.getObject(definitionObj, "Triggers") + if h.err != nil { + return h.err + } defer triggersObj.Release() - err = fillTaskTriggersObj(definition.Triggers, triggersObj) - if err != nil { - return fmt.Errorf("error filling ITrigger objects: %v", err) + if err := fillTaskTriggersObj(definition.Triggers, triggersObj); err != nil { + return fmt.Errorf("error filling ITrigger objects: %w", err) } return nil } + func fillActionsObj(actions []Action, actionsObj *ole.IDispatch) error { for _, action := range actions { actionType := action.GetType() res, err := oleutil.CallMethod(actionsObj, "Create", uint(actionType)) if err != nil { - return fmt.Errorf("error creating IAction object: %v", getTaskSchedulerError(err)) + return fmt.Errorf("error creating IAction object: %w", getTaskSchedulerError(err)) } actionObj := res.ToIDispatch() defer actionObj.Release() - oleutil.MustPutProperty(actionObj, "Id", action.GetID()) + + h := &oleHelper{} + h.put(actionObj, "Id", action.GetID()) switch actionType { case TASK_ACTION_EXEC: execAction := action.(ExecAction) - exeActionObj := actionObj.MustQueryInterface(ole.NewGUID("{4c3d624d-fd6b-49a3-b9b7-09cb3cd3f047}")) + exeActionObj := h.query(actionObj, ole.NewGUID("{4c3d624d-fd6b-49a3-b9b7-09cb3cd3f047}")) + if h.err != nil { + return h.err + } defer exeActionObj.Release() - oleutil.MustPutProperty(exeActionObj, "Arguments", execAction.Args) - oleutil.MustPutProperty(exeActionObj, "Path", execAction.Path) - oleutil.MustPutProperty(exeActionObj, "WorkingDirectory", execAction.WorkingDir) + h.put(exeActionObj, "Arguments", execAction.Args) + h.put(exeActionObj, "Path", execAction.Path) + h.put(exeActionObj, "WorkingDirectory", execAction.WorkingDir) case TASK_ACTION_COM_HANDLER: comHandlerAction := action.(ComHandlerAction) - comHandlerActionObj := actionObj.MustQueryInterface(ole.NewGUID("{6d2fd252-75c5-4f66-90ba-2a7d8cc3039f}")) + comHandlerActionObj := h.query(actionObj, ole.NewGUID("{6d2fd252-75c5-4f66-90ba-2a7d8cc3039f}")) + if h.err != nil { + return h.err + } defer comHandlerActionObj.Release() - oleutil.MustPutProperty(comHandlerActionObj, "ClassId", comHandlerAction.ClassID) - oleutil.MustPutProperty(comHandlerActionObj, "Data", comHandlerAction.Data) + h.put(comHandlerActionObj, "ClassId", comHandlerAction.ClassID) + h.put(comHandlerActionObj, "Data", comHandlerAction.Data) + } + + if h.err != nil { + return h.err } } return nil } -func fillPrincipalObj(principal Principal, principalObj *ole.IDispatch) { - oleutil.MustPutProperty(principalObj, "DisplayName", principal.Name) - oleutil.MustPutProperty(principalObj, "GroupId", principal.GroupID) - oleutil.MustPutProperty(principalObj, "Id", principal.ID) - oleutil.MustPutProperty(principalObj, "LogonType", uint(principal.LogonType)) - oleutil.MustPutProperty(principalObj, "RunLevel", uint(principal.RunLevel)) - oleutil.MustPutProperty(principalObj, "UserId", principal.UserID) +func fillPrincipalObj(principal Principal, principalObj *ole.IDispatch) error { + h := &oleHelper{} + h.put(principalObj, "DisplayName", principal.Name) + h.put(principalObj, "GroupId", principal.GroupID) + h.put(principalObj, "Id", principal.ID) + h.put(principalObj, "LogonType", uint(principal.LogonType)) + h.put(principalObj, "RunLevel", uint(principal.RunLevel)) + h.put(principalObj, "UserId", principal.UserID) + return h.err } -func fillRegistrationInfoObj(regInfo RegistrationInfo, regInfoObj *ole.IDispatch) { - oleutil.MustPutProperty(regInfoObj, "Author", regInfo.Author) - oleutil.MustPutProperty(regInfoObj, "Date", TimeToTaskDate(regInfo.Date)) - oleutil.MustPutProperty(regInfoObj, "Description", regInfo.Description) - oleutil.MustPutProperty(regInfoObj, "Documentation", regInfo.Documentation) - oleutil.MustPutProperty(regInfoObj, "SecurityDescriptor", regInfo.SecurityDescriptor) - oleutil.MustPutProperty(regInfoObj, "Source", regInfo.Source) - oleutil.MustPutProperty(regInfoObj, "URI", regInfo.URI) - oleutil.MustPutProperty(regInfoObj, "Version", regInfo.Version) +func fillRegistrationInfoObj(regInfo RegistrationInfo, regInfoObj *ole.IDispatch) error { + h := &oleHelper{} + h.put(regInfoObj, "Author", regInfo.Author) + h.put(regInfoObj, "Date", TimeToTaskDate(regInfo.Date)) + h.put(regInfoObj, "Description", regInfo.Description) + h.put(regInfoObj, "Documentation", regInfo.Documentation) + h.put(regInfoObj, "SecurityDescriptor", regInfo.SecurityDescriptor) + h.put(regInfoObj, "Source", regInfo.Source) + h.put(regInfoObj, "URI", regInfo.URI) + h.put(regInfoObj, "Version", regInfo.Version) + return h.err } -func fillTaskSettingsObj(settings TaskSettings, settingsObj *ole.IDispatch) { - oleutil.MustPutProperty(settingsObj, "AllowDemandStart", settings.AllowDemandStart) - oleutil.MustPutProperty(settingsObj, "AllowHardTerminate", settings.AllowHardTerminate) - oleutil.MustPutProperty(settingsObj, "Compatibility", uint(settings.Compatibility)) - oleutil.MustPutProperty(settingsObj, "DeleteExpiredTaskAfter", settings.DeleteExpiredTaskAfter) - oleutil.MustPutProperty(settingsObj, "DisallowStartIfOnBatteries", settings.DontStartOnBatteries) - oleutil.MustPutProperty(settingsObj, "Enabled", settings.Enabled) - oleutil.MustPutProperty(settingsObj, "ExecutionTimeLimit", settings.TimeLimit.String()) - oleutil.MustPutProperty(settingsObj, "Hidden", settings.Hidden) - - idlesettingsObj := oleutil.MustGetProperty(settingsObj, "IdleSettings").ToIDispatch() +func fillTaskSettingsObj(settings TaskSettings, settingsObj *ole.IDispatch) error { + h := &oleHelper{} + h.put(settingsObj, "AllowDemandStart", settings.AllowDemandStart) + h.put(settingsObj, "AllowHardTerminate", settings.AllowHardTerminate) + h.put(settingsObj, "Compatibility", uint(settings.Compatibility)) + h.put(settingsObj, "DeleteExpiredTaskAfter", settings.DeleteExpiredTaskAfter) + h.put(settingsObj, "DisallowStartIfOnBatteries", settings.DontStartOnBatteries) + h.put(settingsObj, "Enabled", settings.Enabled) + h.put(settingsObj, "ExecutionTimeLimit", settings.TimeLimit.String()) + h.put(settingsObj, "Hidden", settings.Hidden) + + idlesettingsObj := h.getObject(settingsObj, "IdleSettings") + if h.err != nil { + return h.err + } defer idlesettingsObj.Release() - oleutil.MustPutProperty(idlesettingsObj, "IdleDuration", settings.IdleSettings.IdleDuration.String()) - oleutil.MustPutProperty(idlesettingsObj, "RestartOnIdle", settings.IdleSettings.RestartOnIdle) - oleutil.MustPutProperty(idlesettingsObj, "StopOnIdleEnd", settings.IdleSettings.StopOnIdleEnd) - oleutil.MustPutProperty(idlesettingsObj, "WaitTimeout", settings.IdleSettings.WaitTimeout.String()) + h.put(idlesettingsObj, "IdleDuration", settings.IdleSettings.IdleDuration.String()) + h.put(idlesettingsObj, "RestartOnIdle", settings.IdleSettings.RestartOnIdle) + h.put(idlesettingsObj, "StopOnIdleEnd", settings.IdleSettings.StopOnIdleEnd) + h.put(idlesettingsObj, "WaitTimeout", settings.IdleSettings.WaitTimeout.String()) - oleutil.MustPutProperty(settingsObj, "MultipleInstances", uint(settings.MultipleInstances)) + h.put(settingsObj, "MultipleInstances", uint(settings.MultipleInstances)) - networksettingsObj := oleutil.MustGetProperty(settingsObj, "NetworkSettings").ToIDispatch() + networksettingsObj := h.getObject(settingsObj, "NetworkSettings") + if h.err != nil { + return h.err + } defer networksettingsObj.Release() - oleutil.MustPutProperty(networksettingsObj, "Id", settings.NetworkSettings.ID) - oleutil.MustPutProperty(networksettingsObj, "Name", settings.NetworkSettings.Name) - - oleutil.MustPutProperty(settingsObj, "Priority", settings.Priority) - oleutil.MustPutProperty(settingsObj, "RestartCount", settings.RestartCount) - oleutil.MustPutProperty(settingsObj, "RestartInterval", PeriodToString(settings.RestartInterval)) - oleutil.MustPutProperty(settingsObj, "RunOnlyIfIdle", settings.RunOnlyIfIdle) - oleutil.MustPutProperty(settingsObj, "RunOnlyIfNetworkAvailable", settings.RunOnlyIfNetworkAvailable) - oleutil.MustPutProperty(settingsObj, "StartWhenAvailable", settings.StartWhenAvailable) - oleutil.MustPutProperty(settingsObj, "StopIfGoingOnBatteries", settings.StopIfGoingOnBatteries) - oleutil.MustPutProperty(settingsObj, "WakeToRun", settings.WakeToRun) + h.put(networksettingsObj, "Id", settings.NetworkSettings.ID) + h.put(networksettingsObj, "Name", settings.NetworkSettings.Name) + + h.put(settingsObj, "Priority", settings.Priority) + h.put(settingsObj, "RestartCount", settings.RestartCount) + h.put(settingsObj, "RestartInterval", PeriodToString(settings.RestartInterval)) + h.put(settingsObj, "RunOnlyIfIdle", settings.RunOnlyIfIdle) + h.put(settingsObj, "RunOnlyIfNetworkAvailable", settings.RunOnlyIfNetworkAvailable) + h.put(settingsObj, "StartWhenAvailable", settings.StartWhenAvailable) + h.put(settingsObj, "StopIfGoingOnBatteries", settings.StopIfGoingOnBatteries) + h.put(settingsObj, "WakeToRun", settings.WakeToRun) + + return h.err } func fillTaskTriggersObj(triggers []Trigger, triggersObj *ole.IDispatch) error { for _, trigger := range triggers { res, err := oleutil.CallMethod(triggersObj, "Create", uint(trigger.GetType())) if err != nil { - return fmt.Errorf("error creating ITrigger object: %v", getTaskSchedulerError(err)) + return fmt.Errorf("error creating ITrigger object: %w", getTaskSchedulerError(err)) } triggerObj := res.ToIDispatch() defer triggerObj.Release() - oleutil.MustPutProperty(triggerObj, "Enabled", trigger.GetEnabled()) - oleutil.MustPutProperty(triggerObj, "EndBoundary", TimeToTaskDate(trigger.GetEndBoundary())) - oleutil.MustPutProperty(triggerObj, "ExecutionTimeLimit", trigger.GetExecutionTimeLimit().String()) - oleutil.MustPutProperty(triggerObj, "Id", trigger.GetID()) + h := &oleHelper{} + h.put(triggerObj, "Enabled", trigger.GetEnabled()) + h.put(triggerObj, "EndBoundary", TimeToTaskDate(trigger.GetEndBoundary())) + h.put(triggerObj, "ExecutionTimeLimit", trigger.GetExecutionTimeLimit().String()) + h.put(triggerObj, "Id", trigger.GetID()) - repetitionObj := oleutil.MustGetProperty(triggerObj, "Repetition").ToIDispatch() + repetitionObj := h.getObject(triggerObj, "Repetition") + if h.err != nil { + return h.err + } defer repetitionObj.Release() - oleutil.MustPutProperty(repetitionObj, "Duration", trigger.GetRepetitionDuration().String()) - oleutil.MustPutProperty(repetitionObj, "Interval", PeriodToString(trigger.GetRepetitionInterval())) - oleutil.MustPutProperty(repetitionObj, "StopAtDurationEnd", trigger.GetStopAtDurationEnd()) + h.put(repetitionObj, "Duration", trigger.GetRepetitionDuration().String()) + h.put(repetitionObj, "Interval", PeriodToString(trigger.GetRepetitionInterval())) + h.put(repetitionObj, "StopAtDurationEnd", trigger.GetStopAtDurationEnd()) - oleutil.MustPutProperty(triggerObj, "StartBoundary", TimeToTaskDate(trigger.GetStartBoundary())) + h.put(triggerObj, "StartBoundary", TimeToTaskDate(trigger.GetStartBoundary())) + if h.err != nil { + return h.err + } switch t := trigger.(type) { case BootTrigger: - bootTriggerObj := triggerObj.MustQueryInterface(ole.NewGUID("{2a9c35da-d357-41f4-bbc1-207ac1b1f3cb}")) + bootTriggerObj := h.query(triggerObj, ole.NewGUID("{2a9c35da-d357-41f4-bbc1-207ac1b1f3cb}")) + if h.err != nil { + return h.err + } defer bootTriggerObj.Release() - oleutil.MustPutProperty(bootTriggerObj, "Delay", t.Delay.String()) + h.put(bootTriggerObj, "Delay", t.Delay.String()) case DailyTrigger: - dailyTriggerObj := triggerObj.MustQueryInterface(ole.NewGUID("{126c5cd8-b288-41d5-8dbf-e491446adc5c}")) + dailyTriggerObj := h.query(triggerObj, ole.NewGUID("{126c5cd8-b288-41d5-8dbf-e491446adc5c}")) + if h.err != nil { + return h.err + } defer dailyTriggerObj.Release() - oleutil.MustPutProperty(dailyTriggerObj, "DaysInterval", uint(t.DayInterval)) - oleutil.MustPutProperty(dailyTriggerObj, "RandomDelay", t.RandomDelay.String()) + h.put(dailyTriggerObj, "DaysInterval", uint(t.DayInterval)) + h.put(dailyTriggerObj, "RandomDelay", t.RandomDelay.String()) case EventTrigger: - eventTriggerObj := triggerObj.MustQueryInterface(ole.NewGUID("{d45b0167-9653-4eef-b94f-0732ca7af251}")) + eventTriggerObj := h.query(triggerObj, ole.NewGUID("{d45b0167-9653-4eef-b94f-0732ca7af251}")) + if h.err != nil { + return h.err + } defer eventTriggerObj.Release() - oleutil.MustPutProperty(eventTriggerObj, "Delay", t.Delay.String()) - oleutil.MustPutProperty(eventTriggerObj, "Subscription", t.Subscription) - valueQueriesObj := oleutil.MustGetProperty(eventTriggerObj, "ValueQueries").ToIDispatch() + h.put(eventTriggerObj, "Delay", t.Delay.String()) + h.put(eventTriggerObj, "Subscription", t.Subscription) + valueQueriesObj := h.getObject(eventTriggerObj, "ValueQueries") + if h.err != nil { + return h.err + } defer valueQueriesObj.Release() for name, value := range t.ValueQueries { - _, err = oleutil.CallMethod(valueQueriesObj, "Create", name, value) - if err != nil { - return fmt.Errorf("error creating value %s: %v", name, getTaskSchedulerError(err)) + if _, err := oleutil.CallMethod(valueQueriesObj, "Create", name, value); err != nil { + return fmt.Errorf("error creating value %s: %w", name, getTaskSchedulerError(err)) } } case IdleTrigger: - idleTriggerObj := triggerObj.MustQueryInterface(ole.NewGUID("{d537d2b0-9fb3-4d34-9739-1ff5ce7b1ef3}")) + idleTriggerObj := h.query(triggerObj, ole.NewGUID("{d537d2b0-9fb3-4d34-9739-1ff5ce7b1ef3}")) + if h.err != nil { + return h.err + } defer idleTriggerObj.Release() case LogonTrigger: - logonTriggerObj := triggerObj.MustQueryInterface(ole.NewGUID("{72dade38-fae4-4b3e-baf4-5d009af02b1c}")) + logonTriggerObj := h.query(triggerObj, ole.NewGUID("{72dade38-fae4-4b3e-baf4-5d009af02b1c}")) + if h.err != nil { + return h.err + } defer logonTriggerObj.Release() - oleutil.MustPutProperty(logonTriggerObj, "Delay", t.Delay.String()) - oleutil.MustPutProperty(logonTriggerObj, "UserId", t.UserID) + h.put(logonTriggerObj, "Delay", t.Delay.String()) + h.put(logonTriggerObj, "UserId", t.UserID) case MonthlyDOWTrigger: - monthlyDOWTriggerObj := triggerObj.MustQueryInterface(ole.NewGUID("{77d025a3-90fa-43aa-b52e-cda5499b946a}")) + monthlyDOWTriggerObj := h.query(triggerObj, ole.NewGUID("{77d025a3-90fa-43aa-b52e-cda5499b946a}")) + if h.err != nil { + return h.err + } defer monthlyDOWTriggerObj.Release() - oleutil.MustPutProperty(monthlyDOWTriggerObj, "DaysOfWeek", uint(t.DaysOfWeek)) - oleutil.MustPutProperty(monthlyDOWTriggerObj, "MonthsOfYear", uint(t.MonthsOfYear)) - oleutil.MustPutProperty(monthlyDOWTriggerObj, "RandomDelay", t.RandomDelay.String()) - oleutil.MustPutProperty(monthlyDOWTriggerObj, "RunOnLastWeekOfMonth", t.RunOnLastWeekOfMonth) - oleutil.MustPutProperty(monthlyDOWTriggerObj, "WeeksOfMonth", uint(t.WeeksOfMonth)) + h.put(monthlyDOWTriggerObj, "DaysOfWeek", uint(t.DaysOfWeek)) + h.put(monthlyDOWTriggerObj, "MonthsOfYear", uint(t.MonthsOfYear)) + h.put(monthlyDOWTriggerObj, "RandomDelay", t.RandomDelay.String()) + h.put(monthlyDOWTriggerObj, "RunOnLastWeekOfMonth", t.RunOnLastWeekOfMonth) + h.put(monthlyDOWTriggerObj, "WeeksOfMonth", uint(t.WeeksOfMonth)) case MonthlyTrigger: - monthlyTriggerObj := triggerObj.MustQueryInterface(ole.NewGUID("{97c45ef1-6b02-4a1a-9c0e-1ebfba1500ac}")) + monthlyTriggerObj := h.query(triggerObj, ole.NewGUID("{97c45ef1-6b02-4a1a-9c0e-1ebfba1500ac}")) + if h.err != nil { + return h.err + } defer monthlyTriggerObj.Release() - oleutil.MustPutProperty(monthlyTriggerObj, "DaysOfMonth", uint(t.DaysOfMonth)) - oleutil.MustPutProperty(monthlyTriggerObj, "MonthsOfYear", uint(t.MonthsOfYear)) - oleutil.MustPutProperty(monthlyTriggerObj, "RandomDelay", t.RandomDelay.String()) - oleutil.MustPutProperty(monthlyTriggerObj, "RunOnLastDayOfMonth", t.RunOnLastDayOfMonth) + h.put(monthlyTriggerObj, "DaysOfMonth", uint(t.DaysOfMonth)) + h.put(monthlyTriggerObj, "MonthsOfYear", uint(t.MonthsOfYear)) + h.put(monthlyTriggerObj, "RandomDelay", t.RandomDelay.String()) + h.put(monthlyTriggerObj, "RunOnLastDayOfMonth", t.RunOnLastDayOfMonth) case RegistrationTrigger: - registrationTriggerObj := triggerObj.MustQueryInterface(ole.NewGUID("{4c8fec3a-c218-4e0c-b23d-629024db91a2}")) + registrationTriggerObj := h.query(triggerObj, ole.NewGUID("{4c8fec3a-c218-4e0c-b23d-629024db91a2}")) + if h.err != nil { + return h.err + } defer registrationTriggerObj.Release() - oleutil.MustPutProperty(registrationTriggerObj, "Delay", t.Delay.String()) + h.put(registrationTriggerObj, "Delay", t.Delay.String()) case TimeTrigger: - timeTriggerObj := triggerObj.MustQueryInterface(ole.NewGUID("{b45747e0-eba7-4276-9f29-85c5bb300006}")) + timeTriggerObj := h.query(triggerObj, ole.NewGUID("{b45747e0-eba7-4276-9f29-85c5bb300006}")) + if h.err != nil { + return h.err + } defer timeTriggerObj.Release() - oleutil.MustPutProperty(timeTriggerObj, "RandomDelay", t.RandomDelay.String()) + h.put(timeTriggerObj, "RandomDelay", t.RandomDelay.String()) case WeeklyTrigger: - weeklyTriggerObj := triggerObj.MustQueryInterface(ole.NewGUID("{5038fc98-82ff-436d-8728-a512a57c9dc1}")) + weeklyTriggerObj := h.query(triggerObj, ole.NewGUID("{5038fc98-82ff-436d-8728-a512a57c9dc1}")) + if h.err != nil { + return h.err + } defer weeklyTriggerObj.Release() - oleutil.MustPutProperty(weeklyTriggerObj, "DaysOfWeek", uint(t.DaysOfWeek)) - oleutil.MustPutProperty(weeklyTriggerObj, "RandomDelay", t.RandomDelay.String()) - oleutil.MustPutProperty(weeklyTriggerObj, "WeeksInterval", uint(t.WeekInterval)) + h.put(weeklyTriggerObj, "DaysOfWeek", uint(t.DaysOfWeek)) + h.put(weeklyTriggerObj, "RandomDelay", t.RandomDelay.String()) + h.put(weeklyTriggerObj, "WeeksInterval", uint(t.WeekInterval)) case SessionStateChangeTrigger: - sessionStateChangeTriggerObj := triggerObj.MustQueryInterface(ole.NewGUID("{754da71b-4385-4475-9dd9-598294fa3641}")) + sessionStateChangeTriggerObj := h.query(triggerObj, ole.NewGUID("{754da71b-4385-4475-9dd9-598294fa3641}")) + if h.err != nil { + return h.err + } defer sessionStateChangeTriggerObj.Release() - oleutil.MustPutProperty(sessionStateChangeTriggerObj, "Delay", t.Delay.String()) - oleutil.MustPutProperty(sessionStateChangeTriggerObj, "StateChange", uint(t.StateChange)) - oleutil.MustPutProperty(sessionStateChangeTriggerObj, "UserId", t.UserId) - // need to find GUID - /*case TASK_TRIGGER_CUSTOM_TRIGGER_01: - return nil*/ + h.put(sessionStateChangeTriggerObj, "Delay", t.Delay.String()) + h.put(sessionStateChangeTriggerObj, "StateChange", uint(t.StateChange)) + h.put(sessionStateChangeTriggerObj, "UserId", t.UserId) + // CustomTrigger (TASK_TRIGGER_CUSTOM_TRIGGER_01) has no public CLSID + // and cannot be created programmatically, so it is intentionally not + // handled here. + } + + if h.err != nil { + return h.err } } diff --git a/manage.go b/manage.go index 44b82b4..29afb97 100644 --- a/manage.go +++ b/manage.go @@ -50,7 +50,11 @@ func (t *TaskService) initialize() error { } defer taskSchedulerObj.Release() - tskSchdlr := taskSchedulerObj.MustQueryInterface(ole.IID_IDispatch) + tskSchdlr, err := taskSchedulerObj.QueryInterface(ole.IID_IDispatch) + if err != nil { + ole.CoUninitialize() + return getTaskSchedulerError(err) + } t.taskServiceObj = tskSchdlr t.isInitialized = true @@ -337,8 +341,12 @@ func (t TaskService) GetTaskFolder(path string) (TaskFolder, error) { taskFolder := v.ToIDispatch() defer taskFolder.Release() - name := oleutil.MustGetProperty(taskFolder, "Name").ToString() - path := oleutil.MustGetProperty(taskFolder, "Path").ToString() + h := &oleHelper{} + name := h.getString(taskFolder, "Name") + path := h.getString(taskFolder, "Path") + if h.err != nil { + return h.err + } res, err := oleutil.CallMethod(taskFolder, "GetTasks", int(TASK_ENUM_HIDDEN)) if err != nil { return fmt.Errorf("error getting tasks of folder %s: %w", path, getTaskSchedulerError(err)) @@ -563,7 +571,12 @@ func (t *TaskService) DeleteFolder(path string, deleteRecursively bool) (bool, e } taskCollection := res.ToIDispatch() defer taskCollection.Release() - if !deleteRecursively && oleutil.MustGetProperty(taskCollection, "Count").Val > 0 { + h := &oleHelper{} + taskCount := h.getInt(taskCollection, "Count") + if h.err != nil { + return false, fmt.Errorf("error getting task count of folder %s: %w", path, h.err) + } + if !deleteRecursively && taskCount > 0 { return false, nil } @@ -573,7 +586,11 @@ func (t *TaskService) DeleteFolder(path string, deleteRecursively bool) (bool, e } folderCollection := res.ToIDispatch() defer folderCollection.Release() - if !deleteRecursively && oleutil.MustGetProperty(folderCollection, "Count").Val > 0 { + folderCount := h.getInt(folderCollection, "Count") + if h.err != nil { + return false, fmt.Errorf("error getting subfolder count of folder %s: %w", path, h.err) + } + if !deleteRecursively && folderCount > 0 { return false, nil } @@ -583,9 +600,13 @@ func (t *TaskService) DeleteFolder(path string, deleteRecursively bool) (bool, e taskObj := v.ToIDispatch() defer taskObj.Release() - name := oleutil.MustGetProperty(taskObj, "Path").ToString() + h := &oleHelper{} + taskPath := h.getString(taskObj, "Path") + if h.err != nil { + return h.err + } - return t.DeleteTask(name) + return t.DeleteTask(taskPath) } err = oleutil.ForEach(taskCollection, deleteAllTasks) if err != nil { @@ -623,10 +644,14 @@ func (t *TaskService) DeleteFolder(path string, deleteRecursively bool) (bool, e return err } - currentFolderPath := oleutil.MustGetProperty(folderObj, "Path").ToString() + h := &oleHelper{} + currentFolderPath := h.getString(folderObj, "Path") + if h.err != nil { + return h.err + } _, err = oleutil.CallMethod(t.rootFolderObj, "DeleteFolder", currentFolderPath, 0) if err != nil { - return fmt.Errorf("error deleting task folder %s: %w", path, getTaskSchedulerError(err)) + return fmt.Errorf("error deleting task folder %s: %w", currentFolderPath, getTaskSchedulerError(err)) } return nil diff --git a/parse.go b/parse.go index 4bbec6b..b16f9b3 100644 --- a/parse.go +++ b/parse.go @@ -55,77 +55,39 @@ func parseRunningTask(task *ole.IDispatch) (RunningTask, error) { } func parseRegisteredTask(task *ole.IDispatch) (RegisteredTask, string, error) { - var err error - - nameVar, err := oleutil.GetProperty(task, "Name") - if err != nil { - return RegisteredTask{}, "", err - } - name := nameVar.ToString() - - pathVar, err := oleutil.GetProperty(task, "Path") - if err != nil { - return RegisteredTask{}, "", err - } - path := pathVar.ToString() - - enabledVar, err := oleutil.GetProperty(task, "Enabled") - if err != nil { - return RegisteredTask{}, "", err - } - enabled := enabledVar.Value().(bool) + h := &oleHelper{} - stateVar, err := oleutil.GetProperty(task, "State") - if err != nil { - return RegisteredTask{}, "", err - } - state := TaskState(stateVar.Val) - - missedRunsVar, err := oleutil.GetProperty(task, "NumberOfMissedRuns") - if err != nil { - return RegisteredTask{}, "", err + name := h.getString(task, "Name") + path := h.getString(task, "Path") + enabled := h.getBool(task, "Enabled") + state := TaskState(h.getInt(task, "State")) + missedRuns := uint(h.getInt(task, "NumberOfMissedRuns")) + nextRunTime := variantTimeOrZero(h.getVariant(task, "NextRunTime")) + lastRunTime := variantTimeOrZero(h.getVariant(task, "LastRunTime")) + lastTaskResult := TaskResult(h.getInt(task, "LastTaskResult")) + if h.err != nil { + return RegisteredTask{}, path, h.err } - missedRuns := uint(missedRunsVar.Val) - nextRunTimeVar, err := oleutil.GetProperty(task, "NextRunTime") - if err != nil { - return RegisteredTask{}, "", err - } - nextRunTime := variantTimeOrZero(nextRunTimeVar) - - lastRunTimeVar, err := oleutil.GetProperty(task, "LastRunTime") - if err != nil { - return RegisteredTask{}, "", err + definition := h.getObject(task, "Definition") + if h.err != nil { + return RegisteredTask{}, path, h.err } - lastRunTime := variantTimeOrZero(lastRunTimeVar) - - lastTaskResultVar, err := oleutil.GetProperty(task, "LastTaskResult") - if err != nil { - return RegisteredTask{}, "", err - } - lastTaskResult := TaskResult(lastTaskResultVar.Val) - - definitionVar, err := oleutil.GetProperty(task, "Definition") - if err != nil { - return RegisteredTask{}, "", err - } - definition := definitionVar.ToIDispatch() defer definition.Release() - actionsVar, err := oleutil.GetProperty(definition, "Actions") - if err != nil { - return RegisteredTask{}, "", err + + actions := h.getObject(definition, "Actions") + if h.err != nil { + return RegisteredTask{}, path, h.err } - actions := actionsVar.ToIDispatch() defer actions.Release() - contextVar, err := oleutil.GetProperty(actions, "Context") - if err != nil { - return RegisteredTask{}, "", err + context := h.getString(actions, "Context") + if h.err != nil { + return RegisteredTask{}, path, h.err } - context := contextVar.ToString() var taskActions []Action - err = oleutil.ForEach(actions, func(v *ole.VARIANT) error { + err := oleutil.ForEach(actions, func(v *ole.VARIANT) error { action := v.ToIDispatch() defer action.Release() @@ -139,51 +101,48 @@ func parseRegisteredTask(task *ole.IDispatch) (RegisteredTask, string, error) { return nil }) if err != nil { - return RegisteredTask{}, path, fmt.Errorf("error parsing IAction object: %v", err) + return RegisteredTask{}, path, fmt.Errorf("error parsing IAction object: %w", err) } - principalVar, err := oleutil.GetProperty(definition, "Principal") - if err != nil { - return RegisteredTask{}, "", err + principalObj := h.getObject(definition, "Principal") + if h.err != nil { + return RegisteredTask{}, path, h.err } - principal := principalVar.ToIDispatch() - - xmlTextVar, err := oleutil.GetProperty(definition, "XmlText") + defer principalObj.Release() + taskPrincipal, err := parsePrincipal(principalObj) if err != nil { - return RegisteredTask{}, "", err + return RegisteredTask{}, path, fmt.Errorf("error parsing IPrincipal object: %w", err) } - xmlText := xmlTextVar.ToString() - defer principal.Release() - taskPrincipal := parsePrincipal(principal) + xmlText := h.getString(definition, "XmlText") + if h.err != nil { + return RegisteredTask{}, path, h.err + } - regInfoVar, err := oleutil.GetProperty(definition, "RegistrationInfo") - if err != nil { - return RegisteredTask{}, "", err + regInfo := h.getObject(definition, "RegistrationInfo") + if h.err != nil { + return RegisteredTask{}, path, h.err } - regInfo := regInfoVar.ToIDispatch() defer regInfo.Release() registrationInfo, err := parseRegistrationInfo(regInfo) if err != nil { - return RegisteredTask{}, path, fmt.Errorf("error parsing IRegistrationInfo object: %v", err) + return RegisteredTask{}, path, fmt.Errorf("error parsing IRegistrationInfo object: %w", err) } - settingsVar, err := oleutil.GetProperty(definition, "Settings") - if err != nil { - return RegisteredTask{}, "", err + settings := h.getObject(definition, "Settings") + if h.err != nil { + return RegisteredTask{}, path, h.err } - settings := settingsVar.ToIDispatch() defer settings.Release() taskSettings, err := parseTaskSettings(settings) if err != nil { - return RegisteredTask{}, path, fmt.Errorf("error parsing ITaskSettings object: %v", err) + return RegisteredTask{}, path, fmt.Errorf("error parsing ITaskSettings object: %w", err) } - triggersVar, err := oleutil.GetProperty(definition, "Triggers") - if err != nil { - return RegisteredTask{}, "", err + triggers := h.getObject(definition, "Triggers") + if h.err != nil { + return RegisteredTask{}, path, h.err } - triggers := triggersVar.ToIDispatch() defer triggers.Release() var taskTriggers []Trigger @@ -200,7 +159,7 @@ func parseRegisteredTask(task *ole.IDispatch) (RegisteredTask, string, error) { return nil }) if err != nil { - return RegisteredTask{}, path, fmt.Errorf("error parsing ITrigger object: %v", err) + return RegisteredTask{}, path, fmt.Errorf("error parsing ITrigger object: %w", err) } taskDef := Definition{ @@ -230,14 +189,22 @@ func parseRegisteredTask(task *ole.IDispatch) (RegisteredTask, string, error) { } func parseTaskAction(action *ole.IDispatch) (Action, error) { - id := oleutil.MustGetProperty(action, "Id").ToString() - actionType := TaskActionType(oleutil.MustGetProperty(action, "Type").Val) + h := &oleHelper{} + + id := h.getString(action, "Id") + actionType := TaskActionType(h.getInt(action, "Type")) + if h.err != nil { + return nil, h.err + } switch actionType { case TASK_ACTION_EXEC: - args := oleutil.MustGetProperty(action, "Arguments").ToString() - path := oleutil.MustGetProperty(action, "Path").ToString() - workingDir := oleutil.MustGetProperty(action, "WorkingDirectory").ToString() + args := h.getString(action, "Arguments") + path := h.getString(action, "Path") + workingDir := h.getString(action, "WorkingDirectory") + if h.err != nil { + return nil, h.err + } execAction := ExecAction{ ID: id, @@ -248,8 +215,11 @@ func parseTaskAction(action *ole.IDispatch) (Action, error) { return execAction, nil case TASK_ACTION_COM_HANDLER: - classID := oleutil.MustGetProperty(action, "ClassId").ToString() - data := oleutil.MustGetProperty(action, "Data").ToString() + classID := h.getString(action, "ClassId") + data := h.getString(action, "Data") + if h.err != nil { + return nil, h.err + } comHandlerAction := ComHandlerAction{ ID: id, @@ -263,13 +233,18 @@ func parseTaskAction(action *ole.IDispatch) (Action, error) { } } -func parsePrincipal(principleObj *ole.IDispatch) Principal { - name := oleutil.MustGetProperty(principleObj, "DisplayName").ToString() - groupID := oleutil.MustGetProperty(principleObj, "GroupId").ToString() - id := oleutil.MustGetProperty(principleObj, "Id").ToString() - logonType := TaskLogonType(oleutil.MustGetProperty(principleObj, "LogonType").Val) - runLevel := TaskRunLevel(oleutil.MustGetProperty(principleObj, "RunLevel").Val) - userID := oleutil.MustGetProperty(principleObj, "UserId").ToString() +func parsePrincipal(principleObj *ole.IDispatch) (Principal, error) { + h := &oleHelper{} + + name := h.getString(principleObj, "DisplayName") + groupID := h.getString(principleObj, "GroupId") + id := h.getString(principleObj, "Id") + logonType := TaskLogonType(h.getInt(principleObj, "LogonType")) + runLevel := TaskRunLevel(h.getInt(principleObj, "RunLevel")) + userID := h.getString(principleObj, "UserId") + if h.err != nil { + return Principal{}, h.err + } principle := Principal{ Name: name, @@ -280,21 +255,28 @@ func parsePrincipal(principleObj *ole.IDispatch) Principal { UserID: userID, } - return principle + return principle, nil } func parseRegistrationInfo(regInfo *ole.IDispatch) (*RegistrationInfo, error) { - author := oleutil.MustGetProperty(regInfo, "Author").ToString() - date, err := TaskDateToTime(oleutil.MustGetProperty(regInfo, "Date").ToString()) + h := &oleHelper{} + + author := h.getString(regInfo, "Author") + dateStr := h.getString(regInfo, "Date") + description := h.getString(regInfo, "Description") + documentation := h.getString(regInfo, "Documentation") + securityDescriptor := h.getString(regInfo, "SecurityDescriptor") + source := h.getString(regInfo, "Source") + uri := h.getString(regInfo, "URI") + version := h.getString(regInfo, "Version") + if h.err != nil { + return nil, h.err + } + + date, err := TaskDateToTime(dateStr) if err != nil { - return nil, fmt.Errorf("error parsing Date field: %v", err) + return nil, fmt.Errorf("error parsing Date field: %w", err) } - description := oleutil.MustGetProperty(regInfo, "Description").ToString() - documentation := oleutil.MustGetProperty(regInfo, "Documentation").ToString() - securityDescriptor := oleutil.MustGetProperty(regInfo, "SecurityDescriptor").ToString() - source := oleutil.MustGetProperty(regInfo, "Source").ToString() - uri := oleutil.MustGetProperty(regInfo, "URI").ToString() - version := oleutil.MustGetProperty(regInfo, "Version").ToString() registrationInfo := &RegistrationInfo{ Author: author, @@ -311,49 +293,70 @@ func parseRegistrationInfo(regInfo *ole.IDispatch) (*RegistrationInfo, error) { } func parseTaskSettings(settings *ole.IDispatch) (*TaskSettings, error) { - allowDemandStart := oleutil.MustGetProperty(settings, "AllowDemandStart").Value().(bool) - allowHardTerminate := oleutil.MustGetProperty(settings, "AllowHardTerminate").Value().(bool) - compatibility := TaskCompatibility(oleutil.MustGetProperty(settings, "Compatibility").Val) - deleteExpiredTaskAfter := oleutil.MustGetProperty(settings, "DeleteExpiredTaskAfter").ToString() - dontStartOnBatteries := oleutil.MustGetProperty(settings, "DisallowStartIfOnBatteries").Value().(bool) - enabled := oleutil.MustGetProperty(settings, "Enabled").Value().(bool) - timeLimit, err := StringToPeriod(oleutil.MustGetProperty(settings, "ExecutionTimeLimit").ToString()) + h := &oleHelper{} + + allowDemandStart := h.getBool(settings, "AllowDemandStart") + allowHardTerminate := h.getBool(settings, "AllowHardTerminate") + compatibility := TaskCompatibility(h.getInt(settings, "Compatibility")) + deleteExpiredTaskAfter := h.getString(settings, "DeleteExpiredTaskAfter") + dontStartOnBatteries := h.getBool(settings, "DisallowStartIfOnBatteries") + enabled := h.getBool(settings, "Enabled") + timeLimitStr := h.getString(settings, "ExecutionTimeLimit") + hidden := h.getBool(settings, "Hidden") + if h.err != nil { + return nil, h.err + } + timeLimit, err := StringToPeriod(timeLimitStr) if err != nil { - return nil, fmt.Errorf("error parsing ExecutionTimeLimit field: %v", err) + return nil, fmt.Errorf("error parsing ExecutionTimeLimit field: %w", err) } - hidden := oleutil.MustGetProperty(settings, "Hidden").Value().(bool) - idleSettings := oleutil.MustGetProperty(settings, "IdleSettings").ToIDispatch() + idleSettings := h.getObject(settings, "IdleSettings") + if h.err != nil { + return nil, h.err + } defer idleSettings.Release() - idleDuration, err := StringToPeriod(oleutil.MustGetProperty(idleSettings, "IdleDuration").ToString()) + idleDurationStr := h.getString(idleSettings, "IdleDuration") + restartOnIdle := h.getBool(idleSettings, "RestartOnIdle") + stopOnIdleEnd := h.getBool(idleSettings, "StopOnIdleEnd") + waitTimeoutStr := h.getString(idleSettings, "WaitTimeout") + if h.err != nil { + return nil, h.err + } + idleDuration, err := StringToPeriod(idleDurationStr) if err != nil { - return nil, fmt.Errorf("error parsing IdleDuration field: %v", err) + return nil, fmt.Errorf("error parsing IdleDuration field: %w", err) } - restartOnIdle := oleutil.MustGetProperty(idleSettings, "RestartOnIdle").Value().(bool) - stopOnIdleEnd := oleutil.MustGetProperty(idleSettings, "StopOnIdleEnd").Value().(bool) - waitTimeOut, err := StringToPeriod(oleutil.MustGetProperty(idleSettings, "WaitTimeout").ToString()) + waitTimeOut, err := StringToPeriod(waitTimeoutStr) if err != nil { - return nil, fmt.Errorf("error parsing WaitTimeout field: %v", err) + return nil, fmt.Errorf("error parsing WaitTimeout field: %w", err) } - multipleInstances := TaskInstancesPolicy(oleutil.MustGetProperty(settings, "MultipleInstances").Val) + multipleInstances := TaskInstancesPolicy(h.getInt(settings, "MultipleInstances")) - networkSettings := oleutil.MustGetProperty(settings, "NetworkSettings").ToIDispatch() + networkSettings := h.getObject(settings, "NetworkSettings") + if h.err != nil { + return nil, h.err + } defer networkSettings.Release() - id := oleutil.MustGetProperty(networkSettings, "Id").ToString() - name := oleutil.MustGetProperty(networkSettings, "Name").ToString() + id := h.getString(networkSettings, "Id") + networkName := h.getString(networkSettings, "Name") - priority := uint(oleutil.MustGetProperty(settings, "Priority").Val) - restartCount := uint(oleutil.MustGetProperty(settings, "RestartCount").Val) - restartInterval, err := StringToPeriod(oleutil.MustGetProperty(settings, "RestartInterval").ToString()) + priority := uint(h.getInt(settings, "Priority")) + restartCount := uint(h.getInt(settings, "RestartCount")) + restartIntervalStr := h.getString(settings, "RestartInterval") + runOnlyIfIdle := h.getBool(settings, "RunOnlyIfIdle") + runOnlyIfNetworkAvailable := h.getBool(settings, "RunOnlyIfNetworkAvailable") + startWhenAvailable := h.getBool(settings, "StartWhenAvailable") + stopIfGoingOnBatteries := h.getBool(settings, "StopIfGoingOnBatteries") + wakeToRun := h.getBool(settings, "WakeToRun") + if h.err != nil { + return nil, h.err + } + restartInterval, err := StringToPeriod(restartIntervalStr) if err != nil { - return nil, fmt.Errorf("error parsing RestartInterval field: %v", err) + return nil, fmt.Errorf("error parsing RestartInterval field: %w", err) } - runOnlyIfIdle := oleutil.MustGetProperty(settings, "RunOnlyIfIdle").Value().(bool) - runOnlyIfNetworkAvailable := oleutil.MustGetProperty(settings, "RunOnlyIfNetworkAvailable").Value().(bool) - startWhenAvailable := oleutil.MustGetProperty(settings, "StartWhenAvailable").Value().(bool) - stopIfGoingOnBatteries := oleutil.MustGetProperty(settings, "StopIfGoingOnBatteries").Value().(bool) - wakeToRun := oleutil.MustGetProperty(settings, "WakeToRun").Value().(bool) idleTaskSettings := IdleSettings{ IdleDuration: idleDuration, @@ -364,7 +367,7 @@ func parseTaskSettings(settings *ole.IDispatch) (*TaskSettings, error) { networkTaskSettings := NetworkSettings{ ID: id, - Name: name, + Name: networkName, } taskSettings := &TaskSettings{ @@ -393,34 +396,53 @@ func parseTaskSettings(settings *ole.IDispatch) (*TaskSettings, error) { } func parseTaskTrigger(trigger *ole.IDispatch) (Trigger, error) { - enabled := oleutil.MustGetProperty(trigger, "Enabled").Value().(bool) - endBoundary, err := TaskDateToTime(oleutil.MustGetProperty(trigger, "EndBoundary").ToString()) + h := &oleHelper{} + + enabled := h.getBool(trigger, "Enabled") + endBoundaryStr := h.getString(trigger, "EndBoundary") + executionTimeLimitStr := h.getString(trigger, "ExecutionTimeLimit") + id := h.getString(trigger, "Id") + if h.err != nil { + return nil, h.err + } + endBoundary, err := TaskDateToTime(endBoundaryStr) if err != nil { - return nil, fmt.Errorf("error parsing EndBoundary field: %v", err) + return nil, fmt.Errorf("error parsing EndBoundary field: %w", err) } - executionTimeLimit, err := StringToPeriod(oleutil.MustGetProperty(trigger, "ExecutionTimeLimit").ToString()) + executionTimeLimit, err := StringToPeriod(executionTimeLimitStr) if err != nil { - return nil, fmt.Errorf("error parsing ExecutionTimeLimit field: %v", err) + return nil, fmt.Errorf("error parsing ExecutionTimeLimit field: %w", err) } - id := oleutil.MustGetProperty(trigger, "Id").ToString() - repetition := oleutil.MustGetProperty(trigger, "Repetition").ToIDispatch() + repetition := h.getObject(trigger, "Repetition") + if h.err != nil { + return nil, h.err + } defer repetition.Release() - duration, err := StringToPeriod(oleutil.MustGetProperty(repetition, "Duration").ToString()) + durationStr := h.getString(repetition, "Duration") + intervalStr := h.getString(repetition, "Interval") + stopAtDurationEnd := h.getBool(repetition, "StopAtDurationEnd") + if h.err != nil { + return nil, h.err + } + duration, err := StringToPeriod(durationStr) if err != nil { - return nil, fmt.Errorf("error parsing Duration field: %v", err) + return nil, fmt.Errorf("error parsing Duration field: %w", err) } - interval, err := StringToPeriod(oleutil.MustGetProperty(repetition, "Interval").ToString()) + interval, err := StringToPeriod(intervalStr) if err != nil { - return nil, fmt.Errorf("error parsing Interval field: %v", err) + return nil, fmt.Errorf("error parsing Interval field: %w", err) } - stopAtDurationEnd := oleutil.MustGetProperty(repetition, "StopAtDurationEnd").Value().(bool) - startBoundary, err := TaskDateToTime(oleutil.MustGetProperty(trigger, "StartBoundary").ToString()) + startBoundaryStr := h.getString(trigger, "StartBoundary") + triggerType := TaskTriggerType(h.getInt(trigger, "Type")) + if h.err != nil { + return nil, h.err + } + startBoundary, err := TaskDateToTime(startBoundaryStr) if err != nil { - return nil, fmt.Errorf("error parsing StartBoundary field: %v", err) + return nil, fmt.Errorf("error parsing StartBoundary field: %w", err) } - triggerType := TaskTriggerType(oleutil.MustGetProperty(trigger, "Type").Val) taskTriggerObj := TaskTrigger{ Enabled: enabled, @@ -437,9 +459,13 @@ func parseTaskTrigger(trigger *ole.IDispatch) (Trigger, error) { switch triggerType { case TASK_TRIGGER_BOOT: - delay, err := StringToPeriod(oleutil.MustGetProperty(trigger, "Delay").ToString()) + delayStr := h.getString(trigger, "Delay") + if h.err != nil { + return nil, h.err + } + delay, err := StringToPeriod(delayStr) if err != nil { - return nil, fmt.Errorf("error parsing IBootTrigger object: error parsing Delay field: %v", err) + return nil, fmt.Errorf("error parsing IBootTrigger object: error parsing Delay field: %w", err) } bootTrigger := BootTrigger{ @@ -449,10 +475,14 @@ func parseTaskTrigger(trigger *ole.IDispatch) (Trigger, error) { return bootTrigger, nil case TASK_TRIGGER_DAILY: - daysInterval := DayInterval(oleutil.MustGetProperty(trigger, "DaysInterval").Val) - randomDelay, err := StringToPeriod(oleutil.MustGetProperty(trigger, "RandomDelay").ToString()) + daysInterval := DayInterval(h.getInt(trigger, "DaysInterval")) + randomDelayStr := h.getString(trigger, "RandomDelay") + if h.err != nil { + return nil, h.err + } + randomDelay, err := StringToPeriod(randomDelayStr) if err != nil { - return nil, fmt.Errorf("error parsing IDailyTrigger object: error parsing RandomDelay field: %v", err) + return nil, fmt.Errorf("error parsing IDailyTrigger object: error parsing RandomDelay field: %w", err) } dailyTrigger := DailyTrigger{ @@ -463,26 +493,41 @@ func parseTaskTrigger(trigger *ole.IDispatch) (Trigger, error) { return dailyTrigger, nil case TASK_TRIGGER_EVENT: - delay, err := StringToPeriod(oleutil.MustGetProperty(trigger, "Delay").ToString()) + delayStr := h.getString(trigger, "Delay") + subscription := h.getString(trigger, "Subscription") + if h.err != nil { + return nil, h.err + } + delay, err := StringToPeriod(delayStr) if err != nil { - return nil, fmt.Errorf("error parsing IEventTrigger object: error parsing Delay field: %v", err) + return nil, fmt.Errorf("error parsing IEventTrigger object: error parsing Delay field: %w", err) + } + + valueQueriesObj := h.getObject(trigger, "ValueQueries") + if h.err != nil { + return nil, h.err } - subscription := oleutil.MustGetProperty(trigger, "Subscription").ToString() - valueQueriesObj := oleutil.MustGetProperty(trigger, "ValueQueries").ToIDispatch() defer valueQueriesObj.Release() valQueryMap := make(map[string]string) - oleutil.ForEach(valueQueriesObj, func(v *ole.VARIANT) error { + err = oleutil.ForEach(valueQueriesObj, func(v *ole.VARIANT) error { valueQuery := v.ToIDispatch() defer valueQuery.Release() - name := oleutil.MustGetProperty(valueQuery, "Name").ToString() - value := oleutil.MustGetProperty(valueQuery, "Value").ToString() + vh := &oleHelper{} + name := vh.getString(valueQuery, "Name") + value := vh.getString(valueQuery, "Value") + if vh.err != nil { + return vh.err + } valQueryMap[name] = value return nil }) + if err != nil { + return nil, fmt.Errorf("error parsing IEventTrigger ValueQueries: %w", err) + } eventTrigger := EventTrigger{ TaskTrigger: taskTriggerObj, @@ -499,11 +544,15 @@ func parseTaskTrigger(trigger *ole.IDispatch) (Trigger, error) { return idleTrigger, nil case TASK_TRIGGER_LOGON: - delay, err := StringToPeriod(oleutil.MustGetProperty(trigger, "Delay").ToString()) + delayStr := h.getString(trigger, "Delay") + userID := h.getString(trigger, "UserId") + if h.err != nil { + return nil, h.err + } + delay, err := StringToPeriod(delayStr) if err != nil { - return nil, fmt.Errorf("error parsing ILogonTrigger object: error parsing Delay field: %v", err) + return nil, fmt.Errorf("error parsing ILogonTrigger object: error parsing Delay field: %w", err) } - userID := oleutil.MustGetProperty(trigger, "UserId").ToString() logonTrigger := LogonTrigger{ TaskTrigger: taskTriggerObj, @@ -513,14 +562,18 @@ func parseTaskTrigger(trigger *ole.IDispatch) (Trigger, error) { return logonTrigger, nil case TASK_TRIGGER_MONTHLYDOW: - daysOfWeek := DayOfWeek(oleutil.MustGetProperty(trigger, "DaysOfWeek").Val) - monthsOfYear := Month(oleutil.MustGetProperty(trigger, "MonthsOfYear").Val) - randomDelay, err := StringToPeriod(oleutil.MustGetProperty(trigger, "RandomDelay").ToString()) + daysOfWeek := DayOfWeek(h.getInt(trigger, "DaysOfWeek")) + monthsOfYear := Month(h.getInt(trigger, "MonthsOfYear")) + randomDelayStr := h.getString(trigger, "RandomDelay") + runOnLastWeekOfMonth := h.getBool(trigger, "RunOnLastWeekOfMonth") + weeksOfMonth := Week(h.getInt(trigger, "WeeksOfMonth")) + if h.err != nil { + return nil, h.err + } + randomDelay, err := StringToPeriod(randomDelayStr) if err != nil { - return nil, fmt.Errorf("error parsing IMonthlyDOWTrigger object: error parsing RandomDelay field: %v", err) + return nil, fmt.Errorf("error parsing IMonthlyDOWTrigger object: error parsing RandomDelay field: %w", err) } - runOnLastWeekOfMonth := oleutil.MustGetProperty(trigger, "RunOnLastWeekOfMonth").Value().(bool) - weeksOfMonth := Week(oleutil.MustGetProperty(trigger, "WeeksOfMonth").Val) monthlyDOWTrigger := MonthlyDOWTrigger{ TaskTrigger: taskTriggerObj, @@ -533,13 +586,17 @@ func parseTaskTrigger(trigger *ole.IDispatch) (Trigger, error) { return monthlyDOWTrigger, nil case TASK_TRIGGER_MONTHLY: - daysOfMonth := DayOfMonth(oleutil.MustGetProperty(trigger, "DaysOfMonth").Val) - monthsOfYear := Month(oleutil.MustGetProperty(trigger, "MonthsOfYear").Val) - randomDelay, err := StringToPeriod(oleutil.MustGetProperty(trigger, "RandomDelay").ToString()) + daysOfMonth := DayOfMonth(h.getInt(trigger, "DaysOfMonth")) + monthsOfYear := Month(h.getInt(trigger, "MonthsOfYear")) + randomDelayStr := h.getString(trigger, "RandomDelay") + runOnLastDayOfMonth := h.getBool(trigger, "RunOnLastDayOfMonth") + if h.err != nil { + return nil, h.err + } + randomDelay, err := StringToPeriod(randomDelayStr) if err != nil { - return nil, fmt.Errorf("error parsing IMonthlyTrigger object: error parsing RandomDelay field: %v", err) + return nil, fmt.Errorf("error parsing IMonthlyTrigger object: error parsing RandomDelay field: %w", err) } - runOnLastDayOfMonth := oleutil.MustGetProperty(trigger, "RunOnLastDayOfMonth").Value().(bool) monthlyTrigger := MonthlyTrigger{ TaskTrigger: taskTriggerObj, @@ -551,10 +608,15 @@ func parseTaskTrigger(trigger *ole.IDispatch) (Trigger, error) { return monthlyTrigger, nil case TASK_TRIGGER_REGISTRATION: - delay, err := StringToPeriod(oleutil.MustGetProperty(trigger, "Delay").ToString()) + delayStr := h.getString(trigger, "Delay") + if h.err != nil { + return nil, h.err + } + delay, err := StringToPeriod(delayStr) if err != nil { - return nil, fmt.Errorf("error parsing IRegistrationTrigger object: error parsing Delay field: %v", err) + return nil, fmt.Errorf("error parsing IRegistrationTrigger object: error parsing Delay field: %w", err) } + registrationTrigger := RegistrationTrigger{ TaskTrigger: taskTriggerObj, Delay: delay, @@ -562,10 +624,15 @@ func parseTaskTrigger(trigger *ole.IDispatch) (Trigger, error) { return registrationTrigger, nil case TASK_TRIGGER_TIME: - randomDelay, err := StringToPeriod(oleutil.MustGetProperty(trigger, "RandomDelay").ToString()) + randomDelayStr := h.getString(trigger, "RandomDelay") + if h.err != nil { + return nil, h.err + } + randomDelay, err := StringToPeriod(randomDelayStr) if err != nil { - return nil, fmt.Errorf("error parsing ITimeTrigger object: error parsing RandomDelay field: %v", err) + return nil, fmt.Errorf("error parsing ITimeTrigger object: error parsing RandomDelay field: %w", err) } + timetrigger := TimeTrigger{ TaskTrigger: taskTriggerObj, RandomDelay: randomDelay, @@ -573,12 +640,16 @@ func parseTaskTrigger(trigger *ole.IDispatch) (Trigger, error) { return timetrigger, nil case TASK_TRIGGER_WEEKLY: - daysOfWeek := DayOfWeek(oleutil.MustGetProperty(trigger, "DaysOfWeek").Val) - randomDelay, err := StringToPeriod(oleutil.MustGetProperty(trigger, "RandomDelay").ToString()) + daysOfWeek := DayOfWeek(h.getInt(trigger, "DaysOfWeek")) + randomDelayStr := h.getString(trigger, "RandomDelay") + weeksInterval := WeekInterval(h.getInt(trigger, "WeeksInterval")) + if h.err != nil { + return nil, h.err + } + randomDelay, err := StringToPeriod(randomDelayStr) if err != nil { - return nil, fmt.Errorf("error parsing IWeeklyTrigger object: error parsing RandomDelay field: %v", err) + return nil, fmt.Errorf("error parsing IWeeklyTrigger object: error parsing RandomDelay field: %w", err) } - weeksInterval := WeekInterval(oleutil.MustGetProperty(trigger, "WeeksInterval").Val) weeklyTrigger := WeeklyTrigger{ TaskTrigger: taskTriggerObj, @@ -589,12 +660,16 @@ func parseTaskTrigger(trigger *ole.IDispatch) (Trigger, error) { return weeklyTrigger, nil case TASK_TRIGGER_SESSION_STATE_CHANGE: - delay, err := StringToPeriod(oleutil.MustGetProperty(trigger, "Delay").ToString()) + delayStr := h.getString(trigger, "Delay") + stateChange := TaskSessionStateChangeType(h.getInt(trigger, "StateChange")) + userID := h.getString(trigger, "UserId") + if h.err != nil { + return nil, h.err + } + delay, err := StringToPeriod(delayStr) if err != nil { - return nil, fmt.Errorf("error parsing ISessionStateChangeTrigger object: error parsing RandomDelay field: %v", err) + return nil, fmt.Errorf("error parsing ISessionStateChangeTrigger object: error parsing Delay field: %w", err) } - stateChange := TaskSessionStateChangeType(oleutil.MustGetProperty(trigger, "StateChange").Val) - userID := oleutil.MustGetProperty(trigger, "UserId").ToString() sessionStateChangeTrigger := SessionStateChangeTrigger{ TaskTrigger: taskTriggerObj, From e694fa1d7dc48be3130a478f48ce06a77430bef5 Mon Sep 17 00:00:00 2001 From: fredrik Date: Thu, 4 Jun 2026 19:54:37 +0200 Subject: [PATCH 04/21] CoUninitialize must run on the same thread that called CoInitializeEx --- manage.go | 58 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/manage.go b/manage.go index 29afb97..fde2834 100644 --- a/manage.go +++ b/manage.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "os/user" + "runtime" "strings" "time" @@ -20,39 +21,55 @@ import ( const S_FALSE = 0x00000001 func (t *TaskService) initialize() error { - var err error + // COM apartment membership is per-OS-thread, and CoUninitialize must run on + // the same thread that called CoInitializeEx. Pin this goroutine to its OS + // thread for the lifetime of the connection so initialization, every COM + // call, and the CoUninitialize in Disconnect all happen on one thread; + // Disconnect performs the matching UnlockOSThread. This is why a TaskService + // is not safe for concurrent use and must be created, used, and disconnected + // on the same goroutine. + runtime.LockOSThread() - err = ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED) + err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED) if err != nil { oleErr, ok := err.(*ole.OleError) if !ok { + runtime.UnlockOSThread() return err } code := oleErr.Code() if code != ole.S_OK && code != S_FALSE { + runtime.UnlockOSThread() return err } } + // On any failure after COM has been initialized, undo CoInitializeEx and the + // thread lock so the goroutine is not left pinned. + cleanup := func() { + ole.CoUninitialize() + runtime.UnlockOSThread() + } + schedClassID, err := ole.ClassIDFrom("Schedule.Service.1") if err != nil { - ole.CoUninitialize() + cleanup() return getTaskSchedulerError(err) } taskSchedulerObj, err := ole.CreateInstance(schedClassID, nil) if err != nil { - ole.CoUninitialize() + cleanup() return getTaskSchedulerError(err) } if taskSchedulerObj == nil { - ole.CoUninitialize() - return errors.New("Could not create ITaskService object") + cleanup() + return errors.New("could not create ITaskService object") } defer taskSchedulerObj.Release() tskSchdlr, err := taskSchedulerObj.QueryInterface(ole.IID_IDispatch) if err != nil { - ole.CoUninitialize() + cleanup() return getTaskSchedulerError(err) } t.taskServiceObj = tskSchdlr @@ -73,6 +90,11 @@ func Connect() (TaskService, error) { // serverName parameter is empty, a connection to the local Task Scheduler service // will be attempted. If the user and password parameters are empty, the current // token will be used for authentication. +// +// Connecting pins the calling goroutine to its OS thread (COM requires the +// CoInitializeEx/CoUninitialize pair to run on the same thread; see initialize). +// The returned TaskService is therefore NOT safe for concurrent use: create it, +// use it, and call Disconnect all from the same goroutine. func ConnectWithOptions(serverName, domain, username, password string) (TaskService, error) { var err error var taskService TaskService @@ -86,12 +108,14 @@ func ConnectWithOptions(serverName, domain, username, password string) (TaskServ _, err = oleutil.CallMethod(taskService.taskServiceObj, "Connect", serverName, username, domain, password) if err != nil { + taskService.Disconnect() return TaskService{}, fmt.Errorf("error connecting to Task Scheduler service: %w", getTaskSchedulerError(err)) } if serverName == "" { serverName, err = os.Hostname() if err != nil { + taskService.Disconnect() return TaskService{}, err } } @@ -101,6 +125,7 @@ func ConnectWithOptions(serverName, domain, username, password string) (TaskServ if username == "" { currentUser, err := user.Current() if err != nil { + taskService.Disconnect() return TaskService{}, err } // currentUser.Username is usually "DOMAIN\user" on Windows, but fall back to the raw value when no domain prefix is present @@ -116,6 +141,7 @@ func ConnectWithOptions(serverName, domain, username, password string) (TaskServ res, err := oleutil.CallMethod(taskService.taskServiceObj, "GetFolder", `\`) if err != nil { + taskService.Disconnect() return TaskService{}, fmt.Errorf("error getting the root folder: %w", getTaskSchedulerError(err)) } taskService.rootFolderObj = res.ToIDispatch() @@ -124,19 +150,25 @@ func ConnectWithOptions(serverName, domain, username, password string) (TaskServ return taskService, nil } -// Disconnect frees all the Task Scheduler COM objects that have been created. -// If this function is not called before the parent program terminates, -// memory leaks will occur. +// Disconnect frees all the Task Scheduler COM objects that have been created and +// releases the OS-thread lock taken when connecting. It must be called, on the +// same goroutine that connected, before that goroutine exits or the program +// terminates; otherwise COM objects and the thread lock leak. Disconnect is safe +// to call more than once. func (t *TaskService) Disconnect() { - if t.isConnected { + if t.taskServiceObj != nil { t.taskServiceObj.Release() + t.taskServiceObj = nil + } + if t.rootFolderObj != nil { t.rootFolderObj.Release() + t.rootFolderObj = nil } if t.isInitialized { ole.CoUninitialize() + runtime.UnlockOSThread() + t.isInitialized = false } - - t.isInitialized = false t.isConnected = false } From ad40e33d4150df2d63767a0ad6a27387b6791fa9 Mon Sep 17 00:00:00 2001 From: fredrik Date: Thu, 4 Jun 2026 23:54:10 +0200 Subject: [PATCH 05/21] add DefaultDefinition --- manage.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/manage.go b/manage.go index fde2834..78e7900 100644 --- a/manage.go +++ b/manage.go @@ -434,15 +434,17 @@ func (t TaskService) GetTaskFolder(path string) (TaskFolder, error) { return topFolder, nil } -// NewTaskDefinition returns a new task definition that can be used to register a new task. -// Task settings and properties are set to Task Scheduler default values. -func (t TaskService) NewTaskDefinition() Definition { +// DefaultDefinition returns a task definition pre-populated with the Task +// Scheduler default settings. Unlike TaskService.NewTaskDefinition it does not +// require a connection and does not set RegistrationInfo.Author (which is derived +// from the connected user), so callers can build definitions without a connected +// TaskService or when supplying their own RegistrationInfo. +func DefaultDefinition() Definition { var newDef Definition newDef.Principal.LogonType = TASK_LOGON_INTERACTIVE_TOKEN newDef.Principal.RunLevel = TASK_RUNLEVEL_LUA - newDef.RegistrationInfo.Author = t.connectedDomain + `\` + t.connectedUser newDef.RegistrationInfo.Date = time.Now() newDef.Settings.AllowDemandStart = true @@ -468,6 +470,15 @@ func (t TaskService) NewTaskDefinition() Definition { return newDef } +// NewTaskDefinition returns a new task definition that can be used to register a +// new task. Task settings and properties are set to Task Scheduler default values +// (see DefaultDefinition) and the Author is set to the connected user. +func (t TaskService) NewTaskDefinition() Definition { + newDef := DefaultDefinition() + newDef.RegistrationInfo.Author = t.connectedDomain + `\` + t.connectedUser + return newDef +} + // CreateTask creates a registered task on the connected computer. CreateTask returns // true if the task was successfully registered, and false if the overwrite parameter // is false and a task at the specified path already exists. From 58d9a0d946a2bcb25ec6832d1aed2a9dbf5b633e Mon Sep 17 00:00:00 2001 From: fredrik Date: Thu, 4 Jun 2026 23:56:01 +0200 Subject: [PATCH 06/21] internal logic tests that do not require a real task scheduler connection --- utils_test.go | 140 +++++++++++++++++++++++++++++++++++++++++++++++ validate_test.go | 108 ++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 utils_test.go create mode 100644 validate_test.go diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..f4dce82 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,140 @@ +//go:build windows +// +build windows + +package taskmaster + +import ( + "testing" + "time" + + "github.com/rickb777/date/period" +) + +func TestTaskDateToTime(t *testing.T) { + tests := []struct { + name string + input string + wantZero bool + wantEqual time.Time + wantErr bool + }{ + {name: "empty returns zero", input: "", wantZero: true}, + {name: "no timezone parsed as UTC", input: "2024-01-02T15:04:05", wantEqual: time.Date(2024, 1, 2, 15, 4, 5, 0, time.UTC)}, + {name: "explicit UTC Z", input: "2024-01-02T15:04:05Z", wantEqual: time.Date(2024, 1, 2, 15, 4, 5, 0, time.UTC)}, + {name: "positive offset", input: "2024-01-02T15:04:05+02:00", wantEqual: time.Date(2024, 1, 2, 13, 4, 5, 0, time.UTC)}, + {name: "negative offset", input: "2024-01-02T15:04:05-05:00", wantEqual: time.Date(2024, 1, 2, 20, 4, 5, 0, time.UTC)}, + {name: "invalid", input: "not-a-date", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := TaskDateToTime(tt.input) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error for %q, got none", tt.input) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tt.wantZero { + if !got.IsZero() { + t.Fatalf("expected zero time, got %v", got) + } + return + } + if !got.Equal(tt.wantEqual) { + t.Fatalf("expected %v, got %v", tt.wantEqual, got) + } + }) + } +} + +func TestTimeToTaskDate(t *testing.T) { + if got := TimeToTaskDate(time.Time{}); got != "" { + t.Errorf("zero time: want empty string, got %q", got) + } + + tm := time.Date(2024, 3, 4, 5, 6, 7, 0, time.UTC) + if got, want := TimeToTaskDate(tm), "2024-03-04T05:06:07"; got != want { + t.Errorf("want %q, got %q", want, got) + } +} + +func TestStringToPeriod(t *testing.T) { + tests := []struct { + name string + input string + want string // expected Period.String() + wantErr bool + }{ + {name: "empty is zero period", input: "", want: "P0D"}, + {name: "minutes", input: "PT10M", want: "PT10M"}, + {name: "hours", input: "PT1H", want: "PT1H"}, + {name: "invalid", input: "nope", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := StringToPeriod(tt.input) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error for %q, got none", tt.input) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.String() != tt.want { + t.Fatalf("want %q, got %q", tt.want, got.String()) + } + }) + } +} + +func TestPeriodToString(t *testing.T) { + if got := PeriodToString(period.Period{}); got != "" { + t.Errorf("zero period: want empty string, got %q", got) + } + + if got, want := PeriodToString(period.NewHMS(0, 30, 0)), "PT30M"; got != want { + t.Errorf("want %q, got %q", want, got) + } +} + +func TestIntToDayOfMonth(t *testing.T) { + tests := []struct { + name string + input int + want DayOfMonth + wantErr bool + }{ + {name: "zero invalid", input: 0, wantErr: true}, + {name: "negative invalid", input: -3, wantErr: true}, + {name: "first", input: 1, want: One}, + {name: "second", input: 2, want: Two}, + {name: "thirty-first", input: 31, want: ThirtyOne}, + {name: "thirty-two maps to last day", input: 32, want: LastDayOfMonth}, + {name: "thirty-three invalid", input: 33, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := IntToDayOfMonth(tt.input) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error for %d, got none", tt.input) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Fatalf("want %d, got %d", tt.want, got) + } + }) + } +} diff --git a/validate_test.go b/validate_test.go new file mode 100644 index 0000000..417f6bf --- /dev/null +++ b/validate_test.go @@ -0,0 +1,108 @@ +//go:build windows +// +build windows + +package taskmaster + +import ( + "errors" + "testing" + "time" +) + +// fakeAction is an Action whose type is not supported, used to exercise the +// validation error path without a live Task Scheduler. +type fakeAction struct{} + +func (fakeAction) GetID() string { return "" } +func (fakeAction) GetType() TaskActionType { return TaskActionType(999) } + +// fakeTrigger is a Trigger whose type is not supported. It embeds TaskTrigger to +// satisfy the rest of the Trigger interface. +type fakeTrigger struct { + TaskTrigger +} + +func (fakeTrigger) GetType() TaskTriggerType { return TaskTriggerType(999) } + +func TestValidateActions(t *testing.T) { + tests := []struct { + name string + actions []Action + wantErr bool + }{ + {name: "exec", actions: []Action{ExecAction{Path: "cmd.exe"}}}, + {name: "com handler", actions: []Action{ComHandlerAction{ClassID: "{F0001111-0000-0000-0000-0000FEEDACDC}"}}}, + {name: "multiple valid", actions: []Action{ExecAction{}, ComHandlerAction{}}}, + {name: "unsupported type", actions: []Action{fakeAction{}}, wantErr: true}, + // regression: a valid first action must not mask an invalid later one + {name: "valid then invalid", actions: []Action{ExecAction{}, fakeAction{}}, wantErr: true}, + {name: "empty", actions: nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateActions(tt.actions) + if (err != nil) != tt.wantErr { + t.Fatalf("wantErr=%v, got err=%v", tt.wantErr, err) + } + }) + } +} + +func TestValidateTriggers(t *testing.T) { + withStart := TaskTrigger{StartBoundary: time.Now()} + + tests := []struct { + name string + triggers []Trigger + wantErr bool + }{ + {name: "boot", triggers: []Trigger{BootTrigger{}}}, + {name: "daily ok", triggers: []Trigger{DailyTrigger{DayInterval: EveryDay, TaskTrigger: withStart}}}, + {name: "daily missing start boundary", triggers: []Trigger{DailyTrigger{DayInterval: EveryDay}}, wantErr: true}, + {name: "event ok", triggers: []Trigger{EventTrigger{Subscription: ""}}}, + {name: "event missing subscription", triggers: []Trigger{EventTrigger{}}, wantErr: true}, + {name: "weekly ok", triggers: []Trigger{WeeklyTrigger{DaysOfWeek: Monday, WeekInterval: EveryWeek, TaskTrigger: withStart}}}, + {name: "weekly missing days", triggers: []Trigger{WeeklyTrigger{WeekInterval: EveryWeek, TaskTrigger: withStart}}, wantErr: true}, + {name: "monthly ok", triggers: []Trigger{MonthlyTrigger{DaysOfMonth: One, MonthsOfYear: January, TaskTrigger: withStart}}}, + {name: "monthly dow ok", triggers: []Trigger{MonthlyDOWTrigger{DaysOfWeek: Monday, WeeksOfMonth: First, MonthsOfYear: January, TaskTrigger: withStart}}}, + {name: "unsupported type", triggers: []Trigger{fakeTrigger{}}, wantErr: true}, + // regression: a valid first trigger must not mask an invalid later one + {name: "valid then invalid", triggers: []Trigger{BootTrigger{}, DailyTrigger{DayInterval: EveryDay}}, wantErr: true}, + {name: "empty", triggers: nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateTriggers(tt.triggers) + if (err != nil) != tt.wantErr { + t.Fatalf("wantErr=%v, got err=%v", tt.wantErr, err) + } + }) + } +} + +func TestValidateDefinition(t *testing.T) { + t.Run("no actions returns ErrNoActions", func(t *testing.T) { + if err := validateDefinition(Definition{}); !errors.Is(err, ErrNoActions) { + t.Fatalf("want ErrNoActions, got %v", err) + } + }) + + t.Run("UserID and GroupID are mutually exclusive", func(t *testing.T) { + def := Definition{ + Actions: []Action{ExecAction{Path: "cmd.exe"}}, + Principal: Principal{UserID: "user", GroupID: "group"}, + } + if err := validateDefinition(def); !errors.Is(err, ErrInvalidPrincipal) { + t.Fatalf("want ErrInvalidPrincipal, got %v", err) + } + }) + + t.Run("valid definition", func(t *testing.T) { + def := Definition{Actions: []Action{ExecAction{Path: "cmd.exe"}}} + if err := validateDefinition(def); err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) +} From 20d03cf642d1dc663d0f5038c8481401ca1fef1f Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 08:08:38 +0200 Subject: [PATCH 07/21] error codes --- errors.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/errors.go b/errors.go index 8d7a684..8af0de9 100644 --- a/errors.go +++ b/errors.go @@ -25,10 +25,15 @@ func getTaskSchedulerError(err error) error { return parseErr } + // Task Scheduler connection failures surface either as a bare Win32 error + // code or as the equivalent HRESULT (HRESULT_FROM_WIN32 -> 0x8007xxxx), so + // both forms are handled here. switch errCode { - case 50: + case 50: // ERROR_NOT_SUPPORTED: target is an unsupported OS (e.g. XP / Server 2003) return ErrTargetUnsupported - case 0x80070032, 53: + case 53, // ERROR_BAD_NETPATH (raw) + 0x80070035, // HRESULT_FROM_WIN32(ERROR_BAD_NETPATH) + 0x80070032: // observed when the remote Task Scheduler cannot be reached return ErrConnectionFailure default: return syscall.Errno(errCode) From 14deea88712d6a529e14fff9b54f493ff073b766 Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 08:09:03 +0200 Subject: [PATCH 08/21] ole date is local --- parse.go | 3 ++- parse_test.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/parse.go b/parse.go index b16f9b3..efa1eeb 100644 --- a/parse.go +++ b/parse.go @@ -711,5 +711,6 @@ func oleDateToTime(value float64) time.Time { dayDuration := time.Duration(int64(days)) * day fracDuration := time.Duration(frac * float64(day)) - return oleAutomationEpoch.Add(dayDuration + fracDuration) + wall := oleAutomationEpoch.Add(dayDuration + fracDuration) + return time.Date(wall.Year(), wall.Month(), wall.Day(), wall.Hour(), wall.Minute(), wall.Second(), wall.Nanosecond(), time.Local) } diff --git a/parse_test.go b/parse_test.go index a52b78c..4182561 100644 --- a/parse_test.go +++ b/parse_test.go @@ -22,7 +22,7 @@ func TestVariantTimeOrZero(t *testing.T) { vtDate := &ole.VARIANT{VT: ole.VT_DATE, Val: int64(math.Float64bits(2.5))} got := variantTimeOrZero(vtDate) - expected := time.Date(1900, time.January, 1, 12, 0, 0, 0, time.UTC) + expected := time.Date(1900, time.January, 1, 12, 0, 0, 0, time.Local) if !got.Equal(expected) { t.Fatalf("expected %v, got %v", expected, got) } From 034a37cedc20a2d2e25d19059d87b597b928b05b Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 08:09:21 +0200 Subject: [PATCH 09/21] renovate config:base to config:recommended --- renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index f45d8f1..b42c4fb 100644 --- a/renovate.json +++ b/renovate.json @@ -1,5 +1,5 @@ { "extends": [ - "config:base" + "config:recommended" ] } From 4ca51783e8e76c60db72f09428c46ac04c0bed47 Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 08:09:53 +0200 Subject: [PATCH 10/21] recursively release subfolders to avoid memory leaks --- tasks.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tasks.go b/tasks.go index a49e599..feb0b76 100644 --- a/tasks.go +++ b/tasks.go @@ -171,12 +171,14 @@ func (r RegisteredTaskCollection) Release() { // all subfolders. Must be called before program termination to avoid // memory leaks. func (f *TaskFolder) Release() { - if !f.isReleased { - f.RegisteredTasks.Release() - for _, subFolder := range f.SubFolders { - subFolder.RegisteredTasks.Release() - } + if f.isReleased { + return + } - f.isReleased = true + f.RegisteredTasks.Release() + for _, subFolder := range f.SubFolders { + subFolder.Release() } + + f.isReleased = true } From 430d355e58fc7f7506a9458d9b71a617735fe98a Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 08:34:31 +0200 Subject: [PATCH 11/21] %v to %w --- tasks.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tasks.go b/tasks.go index feb0b76..3599217 100644 --- a/tasks.go +++ b/tasks.go @@ -24,7 +24,7 @@ func (d *Definition) AddTrigger(trigger Trigger) { func (r RunningTask) Refresh() error { _, err := oleutil.CallMethod(r.taskObj, "Refresh") if err != nil { - return fmt.Errorf("error refreshing running task %s: %v", r.Path, getTaskSchedulerError(err)) + return fmt.Errorf("error refreshing running task %s: %w", r.Path, getTaskSchedulerError(err)) } return nil @@ -35,7 +35,7 @@ func (r RunningTask) Refresh() error { func (r *RunningTask) Stop() error { _, err := oleutil.CallMethod(r.taskObj, "Stop") if err != nil { - return fmt.Errorf("error stopping running task %s: %v", r.Path, getTaskSchedulerError(err)) + return fmt.Errorf("error stopping running task %s: %w", r.Path, getTaskSchedulerError(err)) } r.Release() @@ -69,7 +69,7 @@ func (r *RegisteredTask) RunEx(args []string, flags TaskRunFlags, sessionID int, runningTaskObj, err := oleutil.CallMethod(r.taskObj, "RunEx", args, int(flags), sessionID, user) if err != nil { - return RunningTask{}, fmt.Errorf("error running registered task %s: %v", r.Path, getTaskSchedulerError(err)) + return RunningTask{}, fmt.Errorf("error running registered task %s: %w", r.Path, getTaskSchedulerError(err)) } return parseRunningTask(runningTaskObj.ToIDispatch()) @@ -80,7 +80,7 @@ func (r *RegisteredTask) RunEx(args []string, flags TaskRunFlags, sessionID int, func (r *RegisteredTask) GetInstances() (RunningTaskCollection, error) { runningTasks, err := oleutil.CallMethod(r.taskObj, "GetInstances", 0) if err != nil { - return nil, fmt.Errorf("error getting instances of registered task %s: %v", r.Path, getTaskSchedulerError(err)) + return nil, fmt.Errorf("error getting instances of registered task %s: %w", r.Path, getTaskSchedulerError(err)) } runningTasksObj := runningTasks.ToIDispatch() @@ -95,7 +95,7 @@ func (r *RegisteredTask) GetInstances() (RunningTaskCollection, error) { if errors.Is(err, ErrRunningTaskCompleted) { return nil } - return fmt.Errorf("error parsing running task: %v", err) + return fmt.Errorf("error parsing running task: %w", err) } parsedRunningTasks = append(parsedRunningTasks, parsedRunningTask) @@ -116,7 +116,7 @@ func (r *RegisteredTask) GetInstances() (RunningTaskCollection, error) { func (r *RegisteredTask) Stop() error { _, err := oleutil.CallMethod(r.taskObj, "Stop", 0) if err != nil { - return fmt.Errorf("error stopping registered task %s: %v", r.Path, getTaskSchedulerError(err)) + return fmt.Errorf("error stopping registered task %s: %w", r.Path, getTaskSchedulerError(err)) } return nil From 6d239b7a1ab25d7191b284f49b8d2069bba04fca Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 08:38:44 +0200 Subject: [PATCH 12/21] release COM-objects earlier to avoid accumulating until the whole collection has been filled --- fill.go | 332 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 170 insertions(+), 162 deletions(-) diff --git a/fill.go b/fill.go index c600051..396cf5e 100644 --- a/fill.go +++ b/fill.go @@ -72,47 +72,51 @@ func fillDefinitionObj(definition Definition, definitionObj *ole.IDispatch) erro func fillActionsObj(actions []Action, actionsObj *ole.IDispatch) error { for _, action := range actions { - actionType := action.GetType() - res, err := oleutil.CallMethod(actionsObj, "Create", uint(actionType)) - if err != nil { - return fmt.Errorf("error creating IAction object: %w", getTaskSchedulerError(err)) + if err := fillAction(actionsObj, action); err != nil { + return err } - actionObj := res.ToIDispatch() - defer actionObj.Release() - - h := &oleHelper{} - h.put(actionObj, "Id", action.GetID()) - - switch actionType { - case TASK_ACTION_EXEC: - execAction := action.(ExecAction) - exeActionObj := h.query(actionObj, ole.NewGUID("{4c3d624d-fd6b-49a3-b9b7-09cb3cd3f047}")) - if h.err != nil { - return h.err - } - defer exeActionObj.Release() - - h.put(exeActionObj, "Arguments", execAction.Args) - h.put(exeActionObj, "Path", execAction.Path) - h.put(exeActionObj, "WorkingDirectory", execAction.WorkingDir) - case TASK_ACTION_COM_HANDLER: - comHandlerAction := action.(ComHandlerAction) - comHandlerActionObj := h.query(actionObj, ole.NewGUID("{6d2fd252-75c5-4f66-90ba-2a7d8cc3039f}")) - if h.err != nil { - return h.err - } - defer comHandlerActionObj.Release() + } - h.put(comHandlerActionObj, "ClassId", comHandlerAction.ClassID) - h.put(comHandlerActionObj, "Data", comHandlerAction.Data) - } + return nil +} + +func fillAction(actionsObj *ole.IDispatch, action Action) error { + actionType := action.GetType() + res, err := oleutil.CallMethod(actionsObj, "Create", uint(actionType)) + if err != nil { + return fmt.Errorf("error creating IAction object: %w", getTaskSchedulerError(err)) + } + actionObj := res.ToIDispatch() + defer actionObj.Release() + + h := &oleHelper{} + h.put(actionObj, "Id", action.GetID()) + switch actionType { + case TASK_ACTION_EXEC: + execAction := action.(ExecAction) + exeActionObj := h.query(actionObj, ole.NewGUID("{4c3d624d-fd6b-49a3-b9b7-09cb3cd3f047}")) if h.err != nil { return h.err } + defer exeActionObj.Release() + + h.put(exeActionObj, "Arguments", execAction.Args) + h.put(exeActionObj, "Path", execAction.Path) + h.put(exeActionObj, "WorkingDirectory", execAction.WorkingDir) + case TASK_ACTION_COM_HANDLER: + comHandlerAction := action.(ComHandlerAction) + comHandlerActionObj := h.query(actionObj, ole.NewGUID("{6d2fd252-75c5-4f66-90ba-2a7d8cc3039f}")) + if h.err != nil { + return h.err + } + defer comHandlerActionObj.Release() + + h.put(comHandlerActionObj, "ClassId", comHandlerAction.ClassID) + h.put(comHandlerActionObj, "Data", comHandlerAction.Data) } - return nil + return h.err } func fillPrincipalObj(principal Principal, principalObj *ole.IDispatch) error { @@ -147,7 +151,7 @@ func fillTaskSettingsObj(settings TaskSettings, settingsObj *ole.IDispatch) erro h.put(settingsObj, "DeleteExpiredTaskAfter", settings.DeleteExpiredTaskAfter) h.put(settingsObj, "DisallowStartIfOnBatteries", settings.DontStartOnBatteries) h.put(settingsObj, "Enabled", settings.Enabled) - h.put(settingsObj, "ExecutionTimeLimit", settings.TimeLimit.String()) + h.put(settingsObj, "ExecutionTimeLimit", PeriodToString(settings.TimeLimit)) h.put(settingsObj, "Hidden", settings.Hidden) idlesettingsObj := h.getObject(settingsObj, "IdleSettings") @@ -155,10 +159,10 @@ func fillTaskSettingsObj(settings TaskSettings, settingsObj *ole.IDispatch) erro return h.err } defer idlesettingsObj.Release() - h.put(idlesettingsObj, "IdleDuration", settings.IdleSettings.IdleDuration.String()) + h.put(idlesettingsObj, "IdleDuration", PeriodToString(settings.IdleSettings.IdleDuration)) h.put(idlesettingsObj, "RestartOnIdle", settings.IdleSettings.RestartOnIdle) h.put(idlesettingsObj, "StopOnIdleEnd", settings.IdleSettings.StopOnIdleEnd) - h.put(idlesettingsObj, "WaitTimeout", settings.IdleSettings.WaitTimeout.String()) + h.put(idlesettingsObj, "WaitTimeout", PeriodToString(settings.IdleSettings.WaitTimeout)) h.put(settingsObj, "MultipleInstances", uint(settings.MultipleInstances)) @@ -184,154 +188,158 @@ func fillTaskSettingsObj(settings TaskSettings, settingsObj *ole.IDispatch) erro func fillTaskTriggersObj(triggers []Trigger, triggersObj *ole.IDispatch) error { for _, trigger := range triggers { - res, err := oleutil.CallMethod(triggersObj, "Create", uint(trigger.GetType())) - if err != nil { - return fmt.Errorf("error creating ITrigger object: %w", getTaskSchedulerError(err)) + if err := fillTrigger(triggersObj, trigger); err != nil { + return err } - triggerObj := res.ToIDispatch() - defer triggerObj.Release() + } + + return nil +} + +func fillTrigger(triggersObj *ole.IDispatch, trigger Trigger) error { + res, err := oleutil.CallMethod(triggersObj, "Create", uint(trigger.GetType())) + if err != nil { + return fmt.Errorf("error creating ITrigger object: %w", getTaskSchedulerError(err)) + } + triggerObj := res.ToIDispatch() + defer triggerObj.Release() + + h := &oleHelper{} + h.put(triggerObj, "Enabled", trigger.GetEnabled()) + h.put(triggerObj, "EndBoundary", TimeToTaskDate(trigger.GetEndBoundary())) + h.put(triggerObj, "ExecutionTimeLimit", PeriodToString(trigger.GetExecutionTimeLimit())) + h.put(triggerObj, "Id", trigger.GetID()) + + repetitionObj := h.getObject(triggerObj, "Repetition") + if h.err != nil { + return h.err + } + defer repetitionObj.Release() + h.put(repetitionObj, "Duration", PeriodToString(trigger.GetRepetitionDuration())) + h.put(repetitionObj, "Interval", PeriodToString(trigger.GetRepetitionInterval())) + h.put(repetitionObj, "StopAtDurationEnd", trigger.GetStopAtDurationEnd()) - h := &oleHelper{} - h.put(triggerObj, "Enabled", trigger.GetEnabled()) - h.put(triggerObj, "EndBoundary", TimeToTaskDate(trigger.GetEndBoundary())) - h.put(triggerObj, "ExecutionTimeLimit", trigger.GetExecutionTimeLimit().String()) - h.put(triggerObj, "Id", trigger.GetID()) + h.put(triggerObj, "StartBoundary", TimeToTaskDate(trigger.GetStartBoundary())) + if h.err != nil { + return h.err + } - repetitionObj := h.getObject(triggerObj, "Repetition") + switch t := trigger.(type) { + case BootTrigger: + bootTriggerObj := h.query(triggerObj, ole.NewGUID("{2a9c35da-d357-41f4-bbc1-207ac1b1f3cb}")) if h.err != nil { return h.err } - defer repetitionObj.Release() - h.put(repetitionObj, "Duration", trigger.GetRepetitionDuration().String()) - h.put(repetitionObj, "Interval", PeriodToString(trigger.GetRepetitionInterval())) - h.put(repetitionObj, "StopAtDurationEnd", trigger.GetStopAtDurationEnd()) + defer bootTriggerObj.Release() - h.put(triggerObj, "StartBoundary", TimeToTaskDate(trigger.GetStartBoundary())) + h.put(bootTriggerObj, "Delay", PeriodToString(t.Delay)) + case DailyTrigger: + dailyTriggerObj := h.query(triggerObj, ole.NewGUID("{126c5cd8-b288-41d5-8dbf-e491446adc5c}")) if h.err != nil { return h.err } + defer dailyTriggerObj.Release() - switch t := trigger.(type) { - case BootTrigger: - bootTriggerObj := h.query(triggerObj, ole.NewGUID("{2a9c35da-d357-41f4-bbc1-207ac1b1f3cb}")) - if h.err != nil { - return h.err - } - defer bootTriggerObj.Release() + h.put(dailyTriggerObj, "DaysInterval", uint(t.DayInterval)) + h.put(dailyTriggerObj, "RandomDelay", PeriodToString(t.RandomDelay)) + case EventTrigger: + eventTriggerObj := h.query(triggerObj, ole.NewGUID("{d45b0167-9653-4eef-b94f-0732ca7af251}")) + if h.err != nil { + return h.err + } + defer eventTriggerObj.Release() - h.put(bootTriggerObj, "Delay", t.Delay.String()) - case DailyTrigger: - dailyTriggerObj := h.query(triggerObj, ole.NewGUID("{126c5cd8-b288-41d5-8dbf-e491446adc5c}")) - if h.err != nil { - return h.err - } - defer dailyTriggerObj.Release() - - h.put(dailyTriggerObj, "DaysInterval", uint(t.DayInterval)) - h.put(dailyTriggerObj, "RandomDelay", t.RandomDelay.String()) - case EventTrigger: - eventTriggerObj := h.query(triggerObj, ole.NewGUID("{d45b0167-9653-4eef-b94f-0732ca7af251}")) - if h.err != nil { - return h.err - } - defer eventTriggerObj.Release() + h.put(eventTriggerObj, "Delay", PeriodToString(t.Delay)) + h.put(eventTriggerObj, "Subscription", t.Subscription) + valueQueriesObj := h.getObject(eventTriggerObj, "ValueQueries") + if h.err != nil { + return h.err + } + defer valueQueriesObj.Release() - h.put(eventTriggerObj, "Delay", t.Delay.String()) - h.put(eventTriggerObj, "Subscription", t.Subscription) - valueQueriesObj := h.getObject(eventTriggerObj, "ValueQueries") - if h.err != nil { - return h.err + for name, value := range t.ValueQueries { + if _, err := oleutil.CallMethod(valueQueriesObj, "Create", name, value); err != nil { + return fmt.Errorf("error creating value %s: %w", name, getTaskSchedulerError(err)) } - defer valueQueriesObj.Release() + } + case IdleTrigger: + idleTriggerObj := h.query(triggerObj, ole.NewGUID("{d537d2b0-9fb3-4d34-9739-1ff5ce7b1ef3}")) + if h.err != nil { + return h.err + } + defer idleTriggerObj.Release() + case LogonTrigger: + logonTriggerObj := h.query(triggerObj, ole.NewGUID("{72dade38-fae4-4b3e-baf4-5d009af02b1c}")) + if h.err != nil { + return h.err + } + defer logonTriggerObj.Release() - for name, value := range t.ValueQueries { - if _, err := oleutil.CallMethod(valueQueriesObj, "Create", name, value); err != nil { - return fmt.Errorf("error creating value %s: %w", name, getTaskSchedulerError(err)) - } - } - case IdleTrigger: - idleTriggerObj := h.query(triggerObj, ole.NewGUID("{d537d2b0-9fb3-4d34-9739-1ff5ce7b1ef3}")) - if h.err != nil { - return h.err - } - defer idleTriggerObj.Release() - case LogonTrigger: - logonTriggerObj := h.query(triggerObj, ole.NewGUID("{72dade38-fae4-4b3e-baf4-5d009af02b1c}")) - if h.err != nil { - return h.err - } - defer logonTriggerObj.Release() - - h.put(logonTriggerObj, "Delay", t.Delay.String()) - h.put(logonTriggerObj, "UserId", t.UserID) - case MonthlyDOWTrigger: - monthlyDOWTriggerObj := h.query(triggerObj, ole.NewGUID("{77d025a3-90fa-43aa-b52e-cda5499b946a}")) - if h.err != nil { - return h.err - } - defer monthlyDOWTriggerObj.Release() - - h.put(monthlyDOWTriggerObj, "DaysOfWeek", uint(t.DaysOfWeek)) - h.put(monthlyDOWTriggerObj, "MonthsOfYear", uint(t.MonthsOfYear)) - h.put(monthlyDOWTriggerObj, "RandomDelay", t.RandomDelay.String()) - h.put(monthlyDOWTriggerObj, "RunOnLastWeekOfMonth", t.RunOnLastWeekOfMonth) - h.put(monthlyDOWTriggerObj, "WeeksOfMonth", uint(t.WeeksOfMonth)) - case MonthlyTrigger: - monthlyTriggerObj := h.query(triggerObj, ole.NewGUID("{97c45ef1-6b02-4a1a-9c0e-1ebfba1500ac}")) - if h.err != nil { - return h.err - } - defer monthlyTriggerObj.Release() - - h.put(monthlyTriggerObj, "DaysOfMonth", uint(t.DaysOfMonth)) - h.put(monthlyTriggerObj, "MonthsOfYear", uint(t.MonthsOfYear)) - h.put(monthlyTriggerObj, "RandomDelay", t.RandomDelay.String()) - h.put(monthlyTriggerObj, "RunOnLastDayOfMonth", t.RunOnLastDayOfMonth) - case RegistrationTrigger: - registrationTriggerObj := h.query(triggerObj, ole.NewGUID("{4c8fec3a-c218-4e0c-b23d-629024db91a2}")) - if h.err != nil { - return h.err - } - defer registrationTriggerObj.Release() + h.put(logonTriggerObj, "Delay", PeriodToString(t.Delay)) + h.put(logonTriggerObj, "UserId", t.UserID) + case MonthlyDOWTrigger: + monthlyDOWTriggerObj := h.query(triggerObj, ole.NewGUID("{77d025a3-90fa-43aa-b52e-cda5499b946a}")) + if h.err != nil { + return h.err + } + defer monthlyDOWTriggerObj.Release() + + h.put(monthlyDOWTriggerObj, "DaysOfWeek", uint(t.DaysOfWeek)) + h.put(monthlyDOWTriggerObj, "MonthsOfYear", uint(t.MonthsOfYear)) + h.put(monthlyDOWTriggerObj, "RandomDelay", PeriodToString(t.RandomDelay)) + h.put(monthlyDOWTriggerObj, "RunOnLastWeekOfMonth", t.RunOnLastWeekOfMonth) + h.put(monthlyDOWTriggerObj, "WeeksOfMonth", uint(t.WeeksOfMonth)) + case MonthlyTrigger: + monthlyTriggerObj := h.query(triggerObj, ole.NewGUID("{97c45ef1-6b02-4a1a-9c0e-1ebfba1500ac}")) + if h.err != nil { + return h.err + } + defer monthlyTriggerObj.Release() + + h.put(monthlyTriggerObj, "DaysOfMonth", uint(t.DaysOfMonth)) + h.put(monthlyTriggerObj, "MonthsOfYear", uint(t.MonthsOfYear)) + h.put(monthlyTriggerObj, "RandomDelay", PeriodToString(t.RandomDelay)) + h.put(monthlyTriggerObj, "RunOnLastDayOfMonth", t.RunOnLastDayOfMonth) + case RegistrationTrigger: + registrationTriggerObj := h.query(triggerObj, ole.NewGUID("{4c8fec3a-c218-4e0c-b23d-629024db91a2}")) + if h.err != nil { + return h.err + } + defer registrationTriggerObj.Release() - h.put(registrationTriggerObj, "Delay", t.Delay.String()) - case TimeTrigger: - timeTriggerObj := h.query(triggerObj, ole.NewGUID("{b45747e0-eba7-4276-9f29-85c5bb300006}")) - if h.err != nil { - return h.err - } - defer timeTriggerObj.Release() + h.put(registrationTriggerObj, "Delay", PeriodToString(t.Delay)) + case TimeTrigger: + timeTriggerObj := h.query(triggerObj, ole.NewGUID("{b45747e0-eba7-4276-9f29-85c5bb300006}")) + if h.err != nil { + return h.err + } + defer timeTriggerObj.Release() - h.put(timeTriggerObj, "RandomDelay", t.RandomDelay.String()) - case WeeklyTrigger: - weeklyTriggerObj := h.query(triggerObj, ole.NewGUID("{5038fc98-82ff-436d-8728-a512a57c9dc1}")) - if h.err != nil { - return h.err - } - defer weeklyTriggerObj.Release() - - h.put(weeklyTriggerObj, "DaysOfWeek", uint(t.DaysOfWeek)) - h.put(weeklyTriggerObj, "RandomDelay", t.RandomDelay.String()) - h.put(weeklyTriggerObj, "WeeksInterval", uint(t.WeekInterval)) - case SessionStateChangeTrigger: - sessionStateChangeTriggerObj := h.query(triggerObj, ole.NewGUID("{754da71b-4385-4475-9dd9-598294fa3641}")) - if h.err != nil { - return h.err - } - defer sessionStateChangeTriggerObj.Release() - - h.put(sessionStateChangeTriggerObj, "Delay", t.Delay.String()) - h.put(sessionStateChangeTriggerObj, "StateChange", uint(t.StateChange)) - h.put(sessionStateChangeTriggerObj, "UserId", t.UserId) - // CustomTrigger (TASK_TRIGGER_CUSTOM_TRIGGER_01) has no public CLSID - // and cannot be created programmatically, so it is intentionally not - // handled here. + h.put(timeTriggerObj, "RandomDelay", PeriodToString(t.RandomDelay)) + case WeeklyTrigger: + weeklyTriggerObj := h.query(triggerObj, ole.NewGUID("{5038fc98-82ff-436d-8728-a512a57c9dc1}")) + if h.err != nil { + return h.err } + defer weeklyTriggerObj.Release() + h.put(weeklyTriggerObj, "DaysOfWeek", uint(t.DaysOfWeek)) + h.put(weeklyTriggerObj, "RandomDelay", PeriodToString(t.RandomDelay)) + h.put(weeklyTriggerObj, "WeeksInterval", uint(t.WeekInterval)) + case SessionStateChangeTrigger: + sessionStateChangeTriggerObj := h.query(triggerObj, ole.NewGUID("{754da71b-4385-4475-9dd9-598294fa3641}")) if h.err != nil { return h.err } + defer sessionStateChangeTriggerObj.Release() + + h.put(sessionStateChangeTriggerObj, "Delay", PeriodToString(t.Delay)) + h.put(sessionStateChangeTriggerObj, "StateChange", uint(t.StateChange)) + h.put(sessionStateChangeTriggerObj, "UserId", t.UserId) + // CustomTrigger (TASK_TRIGGER_CUSTOM_TRIGGER_01) has no public CLSID + // and cannot be created programmatically, so it is intentionally not + // handled here. } - return nil + return h.err } From 52013251670b93343410a559d780c14e08e29515 Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 08:39:09 +0200 Subject: [PATCH 13/21] IntToDayOfMonth with bit shift --- utils.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/utils.go b/utils.go index 8d30d08..1b9a2dd 100644 --- a/utils.go +++ b/utils.go @@ -5,7 +5,6 @@ package taskmaster import ( "errors" - "math" "strings" "time" @@ -21,7 +20,7 @@ func IntToDayOfMonth(dayOfMonth int) (DayOfMonth, error) { return 0, errors.New("invalid day of month") } - return DayOfMonth(math.Exp2(float64(dayOfMonth - 1))), nil + return DayOfMonth(1) << (dayOfMonth - 1), nil } func TimeToTaskDate(t time.Time) string { From c16ae0815f0f7bac1e39fdbaea6c2744eae147e8 Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 08:40:42 +0200 Subject: [PATCH 14/21] added GetTasksInFolder --- manage.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/manage.go b/manage.go index 78e7900..9286bcf 100644 --- a/manage.go +++ b/manage.go @@ -309,6 +309,51 @@ func (t *TaskService) GetRegisteredTask(path string) (RegisteredTask, error) { return task, nil } +// GetTasksInFolder returns the registered tasks located directly in the folder at +// the given path, without recursing into subfolders. Unlike GetTaskFolder it does +// not build the whole folder tree, so it is cheaper when only one folder's tasks +// are needed. The caller must Release the returned collection. +func (t TaskService) GetTasksInFolder(path string) (RegisteredTaskCollection, error) { + if len(path) == 0 || path[0] != '\\' { + return nil, ErrInvalidPath + } + + folderObj := t.rootFolderObj + if path != `\` { + folder, err := oleutil.CallMethod(t.taskServiceObj, "GetFolder", path) + if err != nil { + return nil, fmt.Errorf("error getting folder %s: %w", path, getTaskSchedulerError(err)) + } + folderObj = folder.ToIDispatch() + defer folderObj.Release() + } + + res, err := oleutil.CallMethod(folderObj, "GetTasks", int(TASK_ENUM_HIDDEN)) + if err != nil { + return nil, fmt.Errorf("error getting tasks of folder %s: %w", path, getTaskSchedulerError(err)) + } + taskCollection := res.ToIDispatch() + defer taskCollection.Release() + + var registeredTasks RegisteredTaskCollection + err = oleutil.ForEach(taskCollection, func(v *ole.VARIANT) error { + task := v.ToIDispatch() + + registeredTask, taskPath, err := parseRegisteredTask(task) + if err != nil { + return fmt.Errorf("error parsing registered task %s: %w", taskPath, err) + } + registeredTasks = append(registeredTasks, registeredTask) + + return nil + }) + if err != nil { + return nil, err + } + + return registeredTasks, nil +} + // GetTaskFolders enumerates the Task Schedule database for all task folders and currently // registered tasks. func (t TaskService) GetTaskFolders() (TaskFolder, error) { From bdbbbc3d369dd6b012e9300ef5296b18ac9c4084 Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 10:25:34 +0200 Subject: [PATCH 15/21] replaced deprecated rickb777/date with rickb777/period --- go.mod | 3 ++- go.sum | 18 ++++++++++-------- manage.go | 2 +- manage_test.go | 2 +- types.go | 2 +- utils.go | 2 +- utils_test.go | 2 +- 7 files changed, 17 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 93281fa..3102214 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,11 @@ go 1.22.12 require ( github.com/go-ole/go-ole v1.3.0 - github.com/rickb777/date v1.21.1 + github.com/rickb777/period v1.0.8 ) require ( + github.com/govalues/decimal v0.1.32 // indirect github.com/rickb777/plural v1.4.2 // indirect golang.org/x/sys v0.30.0 // indirect ) diff --git a/go.sum b/go.sum index f0315ab..7bfabf6 100644 --- a/go.sum +++ b/go.sum @@ -2,18 +2,20 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= -github.com/rickb777/date v1.21.1 h1:tUcQS8riIRoYK5kUAv5aevllFEYUEk2x8OYDyoldOn4= -github.com/rickb777/date v1.21.1/go.mod h1:gnDexsbXViZr2fCKMrY3m6IfAF5U2vSkEaiGJcNFaLQ= +github.com/govalues/decimal v0.1.32 h1:jsZHwjLKteAlG5nGjlqvhtkGBq7/4SKkk6yGTluwPk0= +github.com/govalues/decimal v0.1.32/go.mod h1:Ee7eI3Llf7hfqDZtpj8Q6NCIgJy1iY3kH1pSwDrNqlM= +github.com/onsi/gomega v1.35.0 h1:xuM1M/UvMp9BCdS4hojhS9/4jEuVqS9Er3bqupeaoPM= +github.com/onsi/gomega v1.35.0/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/rickb777/period v1.0.8 h1:lEo9kb7kpA6TNYG9u8ddFfwrVK+ftHQokt7UKNY+jFc= +github.com/rickb777/period v1.0.8/go.mod h1:M13FB5SGZf4zJmF/zfLDqwfQ0XafHxgOsw6DAL0EFw0= github.com/rickb777/plural v1.4.2 h1:Kl/syFGLFZ5EbuV8c9SVud8s5HI2HpCCtOMw2U1kS+A= github.com/rickb777/plural v1.4.2/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/manage.go b/manage.go index 9286bcf..0fe92e7 100644 --- a/manage.go +++ b/manage.go @@ -14,7 +14,7 @@ import ( ole "github.com/go-ole/go-ole" "github.com/go-ole/go-ole/oleutil" - "github.com/rickb777/date/period" + "github.com/rickb777/period" ) // S_FALSE is returned by CoInitialize if it was already called on this thread. diff --git a/manage_test.go b/manage_test.go index c8ec949..ceebbb1 100644 --- a/manage_test.go +++ b/manage_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/rickb777/date/period" + "github.com/rickb777/period" ) func TestLocalConnect(t *testing.T) { diff --git a/types.go b/types.go index b3b2d0c..81ab99d 100644 --- a/types.go +++ b/types.go @@ -11,7 +11,7 @@ import ( "time" "github.com/go-ole/go-ole" - "github.com/rickb777/date/period" + "github.com/rickb777/period" ) // DayOfWeek is a day of the week. diff --git a/utils.go b/utils.go index 1b9a2dd..4402233 100644 --- a/utils.go +++ b/utils.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/rickb777/date/period" + "github.com/rickb777/period" ) var taskDateFormat = "2006-01-02T15:04:05" diff --git a/utils_test.go b/utils_test.go index f4dce82..8a18051 100644 --- a/utils_test.go +++ b/utils_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/rickb777/date/period" + "github.com/rickb777/period" ) func TestTaskDateToTime(t *testing.T) { From 0eaf7f288347e54060deb57dc6144bbfaf32e962 Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 12:21:27 +0200 Subject: [PATCH 16/21] readme --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 096dccb..8f3c064 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ ======================== GIERT'S FORK NOTES ======================== -Updates dependencies and implements an interval fix from pull request #22 in the original project. - -The updates to go-ole are especially important! Don't wast time on strange errors in production like I did... - -The library has a tendency to panic over even parsing errors. My long-term goal is to gradually transition to returning normal errors in more cases. +Maintained, hardened fork. Compared with upstream: + - Requires Go 1.22+; dependencies updated (go-ole, and the deprecated + rickb777/date replaced with the maintained rickb777/period). + - Parsing/COM errors are returned instead of panicking (no recover() needed). + - Assorted bug fixes and a couple of API additions. + +Note: a TaskService is not goroutine-safe — create, use, and Disconnect it on +the same goroutine. Releases are tagged; pin a version (or commit). ======================= /GIERT'S FORK NOTES> ======================= From dc002f86b71e6d27bedf745f99f2d09795d4febe Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 12:24:15 +0200 Subject: [PATCH 17/21] error mappings --- errors.go | 10 +++++++--- manage.go | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/errors.go b/errors.go index 8af0de9..ac08eaa 100644 --- a/errors.go +++ b/errors.go @@ -25,10 +25,14 @@ func getTaskSchedulerError(err error) error { return parseErr } - // Task Scheduler connection failures surface either as a bare Win32 error - // code or as the equivalent HRESULT (HRESULT_FROM_WIN32 -> 0x8007xxxx), so - // both forms are handled here. + // Task Scheduler errors surface either as a bare Win32 error code or as the + // equivalent HRESULT (HRESULT_FROM_WIN32 -> 0x8007xxxx), so both forms are + // handled here. switch errCode { + case 2, 0x80070002: // ERROR_FILE_NOT_FOUND: the task does not exist + return syscall.ERROR_FILE_NOT_FOUND // matches errors.Is(err, os.ErrNotExist) + case 3, 0x80070003: // ERROR_PATH_NOT_FOUND: the task folder does not exist + return syscall.ERROR_PATH_NOT_FOUND // matches errors.Is(err, os.ErrNotExist) case 50: // ERROR_NOT_SUPPORTED: target is an unsupported OS (e.g. XP / Server 2003) return ErrTargetUnsupported case 53, // ERROR_BAD_NETPATH (raw) diff --git a/manage.go b/manage.go index 0fe92e7..4b99400 100644 --- a/manage.go +++ b/manage.go @@ -288,9 +288,9 @@ func (t *TaskService) GetRegisteredTasks() (RegisteredTaskCollection, error) { return registeredTasks, nil } -// GetRegisteredTask attempts to find the specified registered task and returns a -// pointer to it if it exists. If it doesn't exist, nil will be returned in place of -// the registered task. +// GetRegisteredTask attempts to find the specified registered task. If the task +// does not exist, it returns a zero RegisteredTask and an error for which +// errors.Is(err, os.ErrNotExist) reports true. func (t *TaskService) GetRegisteredTask(path string) (RegisteredTask, error) { if len(path) == 0 || path[0] != '\\' { return RegisteredTask{}, ErrInvalidPath From 57d4d9a0a480b972c0cb3cbb17038e762533cef3 Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 12:25:47 +0200 Subject: [PATCH 18/21] fix TaskResult off-by-one bug --- types.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/types.go b/types.go index 81ab99d..d6833e8 100644 --- a/types.go +++ b/types.go @@ -555,19 +555,19 @@ func (t TaskTriggerType) String() string { type TaskResult uint32 const ( - SCHED_S_SUCCESS TaskResult = 0x0 - SCHED_S_TASK_READY TaskResult = iota + 0x00041300 - SCHED_S_TASK_RUNNING - SCHED_S_TASK_DISABLED - SCHED_S_TASK_HAS_NOT_RUN - SCHED_S_TASK_NO_MORE_RUNS - SCHED_S_TASK_NOT_SCHEDULED - SCHED_S_TASK_TERMINATED - SCHED_S_TASK_NO_VALID_TRIGGERS - SCHED_S_EVENT_TRIGGER - SCHED_S_SOME_TRIGGERS_FAILED TaskResult = 0x0004131B - SCHED_S_BATCH_LOGON_PROBLEM TaskResult = 0x0004131C - SCHED_S_TASK_QUEUED TaskResult = 0x00041325 + SCHED_S_SUCCESS TaskResult = 0x0 + SCHED_S_TASK_READY TaskResult = 0x00041300 + SCHED_S_TASK_RUNNING TaskResult = 0x00041301 + SCHED_S_TASK_DISABLED TaskResult = 0x00041302 + SCHED_S_TASK_HAS_NOT_RUN TaskResult = 0x00041303 + SCHED_S_TASK_NO_MORE_RUNS TaskResult = 0x00041304 + SCHED_S_TASK_NOT_SCHEDULED TaskResult = 0x00041305 + SCHED_S_TASK_TERMINATED TaskResult = 0x00041306 + SCHED_S_TASK_NO_VALID_TRIGGERS TaskResult = 0x00041307 + SCHED_S_EVENT_TRIGGER TaskResult = 0x00041308 + SCHED_S_SOME_TRIGGERS_FAILED TaskResult = 0x0004131B + SCHED_S_BATCH_LOGON_PROBLEM TaskResult = 0x0004131C + SCHED_S_TASK_QUEUED TaskResult = 0x00041325 ) func (r TaskResult) String() string { From ac8662c0b7598280329374bfe8036b163b94b030 Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 12:32:33 +0200 Subject: [PATCH 19/21] better validation errors --- validate.go | 14 ++++++++++++-- validate_test.go | 6 ++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/validate.go b/validate.go index b06c599..d8818c6 100644 --- a/validate.go +++ b/validate.go @@ -3,7 +3,10 @@ package taskmaster -import "errors" +import ( + "errors" + "time" +) func validateDefinition(def Definition) error { var err error @@ -40,6 +43,11 @@ func validateActions(actions []Action) error { func validateTriggers(triggers []Trigger) error { for _, trigger := range triggers { + // RepetitionInterval, when set, must be at least one minute; Task Scheduler rejects a smaller value with an opaque "out of range" error. + if interval := trigger.GetRepetitionInterval(); !interval.IsZero() && interval.DurationApprox() < time.Minute { + return errors.New("invalid trigger: RepetitionInterval must be at least 1 minute") + } + switch t := trigger.(type) { case BootTrigger: // no required fields @@ -90,7 +98,9 @@ func validateTriggers(triggers []Trigger) error { case SessionStateChangeTrigger: // no required fields case TimeTrigger: - // no required fields + if t.GetStartBoundary().IsZero() { + return errors.New("invalid TimeTrigger: StartBoundary is required") + } case WeeklyTrigger: if t.GetStartBoundary().IsZero() { return errors.New("invalid WeeklyTrigger: StartBoundary is required") diff --git a/validate_test.go b/validate_test.go index 417f6bf..acd22ef 100644 --- a/validate_test.go +++ b/validate_test.go @@ -7,6 +7,8 @@ import ( "errors" "testing" "time" + + "github.com/rickb777/period" ) // fakeAction is an Action whose type is not supported, used to exercise the @@ -66,6 +68,10 @@ func TestValidateTriggers(t *testing.T) { {name: "weekly missing days", triggers: []Trigger{WeeklyTrigger{WeekInterval: EveryWeek, TaskTrigger: withStart}}, wantErr: true}, {name: "monthly ok", triggers: []Trigger{MonthlyTrigger{DaysOfMonth: One, MonthsOfYear: January, TaskTrigger: withStart}}}, {name: "monthly dow ok", triggers: []Trigger{MonthlyDOWTrigger{DaysOfWeek: Monday, WeeksOfMonth: First, MonthsOfYear: January, TaskTrigger: withStart}}}, + {name: "time ok", triggers: []Trigger{TimeTrigger{TaskTrigger: withStart}}}, + {name: "time missing start boundary", triggers: []Trigger{TimeTrigger{}}, wantErr: true}, + {name: "sub-minute repetition interval", triggers: []Trigger{BootTrigger{TaskTrigger: TaskTrigger{RepetitionPattern: RepetitionPattern{RepetitionInterval: period.NewHMS(0, 0, 30)}}}}, wantErr: true}, + {name: "one-minute repetition interval ok", triggers: []Trigger{BootTrigger{TaskTrigger: TaskTrigger{RepetitionPattern: RepetitionPattern{RepetitionInterval: period.NewHMS(0, 1, 0)}}}}}, {name: "unsupported type", triggers: []Trigger{fakeTrigger{}}, wantErr: true}, // regression: a valid first trigger must not mask an invalid later one {name: "valid then invalid", triggers: []Trigger{BootTrigger{}, DailyTrigger{DayInterval: EveryDay}}, wantErr: true}, From 638078cb99079d4f65250b7b53280e85a6d1664a Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 13:07:37 +0200 Subject: [PATCH 20/21] added some gotchas to the readme --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 8f3c064..babc892 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Maintained, hardened fork. Compared with upstream: Note: a TaskService is not goroutine-safe — create, use, and Disconnect it on the same goroutine. Releases are tagged; pin a version (or commit). +I also added a list of gotchas that have bitten me over the years at the bottom of the readme. + ======================= /GIERT'S FORK NOTES> ======================= [![CI](https://github.com/giert/taskmaster/actions/workflows/ci.yml/badge.svg)](https://github.com/giert/taskmaster/actions/workflows/ci.yml) @@ -33,3 +35,17 @@ Because taskmaster interfaces directly with Task Scheduler COM objects, it allow As I was researching the Task Scheduler COM interface more and more, I quickly realized just how complex and confusing all the different parts of Task Scheduler are. So I set out to concisely copy the documentation from MSDN into taskmaster, but also consolidate it and add information that is buried in the depths of MSDN. This should make using both taskmaster and the existing Task Scheduler tools easier, having a ton of information and links to Task Scheduler internals available via GoDocs. If you find info that I missed, feel free to submit an issue or better yet open a PR :) There are a lot of hidden gotchas and quirks within Task Scheduler, so I would *highly* recommend perusing the official docs before attempting really anything with this library on [MSDN](https://docs.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-start-page). + +====================== GIERT'S TASK SCHEDULER GOTCHAS ====================== + +This library faithfully exposes most of Windows Task Scheduler's rough edges rather than hiding them. A few to be extra aware of, maybe you can avoid my mistakes and wasted time: + +- **Tasks don't run on battery by default.** `Settings.DontStartOnBatteries` and `StopIfGoingOnBatteries` default to `true` (the Task Scheduler default), so a task can silently never start on a laptop (or suddenly start when the charger is plugged in) — set both to `false` to allow it. +- **A manual `Run`/`RunEx` still honours conditions** ("don't start on battery", "only if idle", …), so it may silently do nothing; pass `TASK_RUN_IGNORE_CONSTRAINTS` to force a run. +- **`RunLevel: TASK_RUNLEVEL_HIGHEST` requires the creator to be elevated**, or registration fails with "Access is denied". +- **Local accounts usually need the machine name** in `Principal.UserID` (`COMPUTERNAME\user`, not a bare username), or registration fails with "No mapping between account names and security IDs was done". +- **Repetition:** `RepetitionInterval` must be at least one minute, and a non-zero `RepetitionDuration` must be longer than the interval. To repeat indefinitely, leave `RepetitionDuration` zero. +- **`DeleteExpiredTaskAfter` only takes effect if a trigger has an `EndBoundary`.** +- **`Settings.Compatibility`** must match the features used — multiple triggers or actions need `TASK_COMPATIBILITY_V2` or newer (the default here). + +====================== /GIERT'S TASK SCHEDULER GOTCHAS ===================== \ No newline at end of file From 30f2400007ddd9f79fc858414b08a1dbec59c200 Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 5 Jun 2026 13:24:02 +0200 Subject: [PATCH 21/21] formatting --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index babc892..b38332e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -======================== GIERT'S FORK NOTES ======================== +============================ GIERT'S FORK NOTES ============================ Maintained, hardened fork. Compared with upstream: - Requires Go 1.22+; dependencies updated (go-ole, and the deprecated @@ -11,7 +11,7 @@ the same goroutine. Releases are tagged; pin a version (or commit). I also added a list of gotchas that have bitten me over the years at the bottom of the readme. -======================= /GIERT'S FORK NOTES> ======================= +=========================== /GIERT'S FORK NOTES =========================== [![CI](https://github.com/giert/taskmaster/actions/workflows/ci.yml/badge.svg)](https://github.com/giert/taskmaster/actions/workflows/ci.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/giert/taskmaster)](https://goreportcard.com/report/github.com/giert/taskmaster) @@ -48,4 +48,4 @@ This library faithfully exposes most of Windows Task Scheduler's rough edges rat - **`DeleteExpiredTaskAfter` only takes effect if a trigger has an `EndBoundary`.** - **`Settings.Compatibility`** must match the features used — multiple triggers or actions need `TASK_COMPATIBILITY_V2` or newer (the default here). -====================== /GIERT'S TASK SCHEDULER GOTCHAS ===================== \ No newline at end of file +===================== /GIERT'S TASK SCHEDULER GOTCHAS ===================== \ No newline at end of file