Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
======================== GIERT'S FORK NOTES ========================
============================ GIERT'S FORK NOTES ============================

Updates dependencies and implements an interval fix from pull request #22 in the original project.
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.

The updates to go-ole are especially important! Don't wast time on strange errors in production like I did...
Note: a TaskService is not goroutine-safe — create, use, and Disconnect it on
the same goroutine. Releases are tagged; pin a version (or commit).

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

[![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
Expand All @@ -30,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 =====================
32 changes: 0 additions & 32 deletions appveyor.yml

This file was deleted.

126 changes: 126 additions & 0 deletions comhelper.go
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 11 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,19 @@ func getTaskSchedulerError(err error) error {
return parseErr
}

// 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 50:
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 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)
Expand Down
Loading
Loading