commit 1601fbeb01b6618a331c9acd40ef9e5541e3bd8d
parent 82e616a4b672e5265c685aec155f1fe590aa2fdf
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Thu, 16 Mar 2023 17:02:50 +0800
bind: pure-Go implementation
This commit overhauls the entire internals to use pure-Go
code to orchestrate the Windows Runtime and implement the COM
objects necessary for notification callbacks.
This code does "exactly" what the C code did, but the procedures
are fully implemented in Go, via the help of syscall.NewCallback.
This means we no longer need a C compiler, the Windows C compiler
or nor do we need to distribute a Dll blob to the user.
I've tried to thoroughly document and explain everything so that
no one else has to do the amount of research and tinkering that
I had to.
Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Diffstat:
12 files changed, 582 insertions(+), 662 deletions(-)
diff --git a/internal/bind/bind.go b/internal/bind/bind.go
@@ -1,28 +1,36 @@
-// Package bind wraps the raw DLL functions in safe Go.
+// Package bind provides a pure-Go implementation of toast notifications on Windows.
package bind
import (
"fmt"
"sync"
- "syscall"
"unicode/utf16"
"unsafe"
"github.com/go-ole/go-ole"
)
-// This GUID matches the one defined in the C code. This coincidentally matches that
-// value because I don't want to import C when we don't need to.
-//
-// We could keep them more in sync by exporting a DLL function that returns a copy of the string.
+// UserData contains Key:Value pairs generated within the notification, based
+// on the XML content of the notification. Specifically, all inputs within
+// the XML will generate a corresponding UserData struct.
+type UserData struct {
+ Key string
+ Value string
+}
+
+// ActivationCallback is a function that gets invoked when the toast is activated.
+type ActivationCallback func(appUserModelId string, invokedArgs string, userData []UserData)
+
+// callback is the global callback reference that is invoked by Activate.
//
-// It's also possible that we could dynamically generate a new GUID per app run, or derive
-// it from the app ID.
-const iNotificationActivationCallbackGUID = "{0F82E845-CB89-4039-BDBF-67CA33254C76}"
+// NOTE(jfm): synchronize access to this?
+var callback ActivationCallback = func(model, args string, data []UserData) {}
-// IsDLLAvailable returns true if the DLL can be loaded.
-func IsDLLAvailable() bool {
- return toast.Load() == nil
+// SetActivationCallback establishes the callback `cb` to be invoked when
+// the toast notification is activated. This callback instance should handle
+// being activated from any available toast notification.
+func SetActivationCallback(cb ActivationCallback) {
+ callback = cb
}
// GenerateToast notification via the specified xml content.
@@ -34,9 +42,9 @@ func GenerateToast(appID, xml string) error {
return err
}
- // 1. Allocate ClassFactory implementation.
- // 2. register callback implementation.
- // 3. load noti statics
+ // 1. allocate ClassFactory implementation.
+ // 2. register ClassFactory implementation (provides our ActivationCallback to the runtime)
+ // 3. load noti manager (statics impl)
// 4. load noti factory
// 5. create xml
// 6. create xmlIO
@@ -47,55 +55,19 @@ func GenerateToast(appID, xml string) error {
if classFactory == nil {
return fmt.Errorf("could not allocate class factory")
}
- defer free(classFactory)
if err := registerClassFactory(classFactory); err != nil {
return fmt.Errorf("registering class factory: %w", err)
}
- inspectable, err := ole.RoGetActivationFactory(CLSID_ToastNotificationManager, IID_ToastNotificationManagerStatics)
- if err != nil {
- return fmt.Errorf("getting activation factory: %w", err)
- }
-
- itns := (*IToastNotificationManager)(unsafe.Pointer(inspectable))
-
- notifier, err := itns.CreateToastNotifierWithID(appID)
+ noti, err := newNotiFromXml(xml)
if err != nil {
- return fmt.Errorf("creating toast notifier: %w", err)
+ return fmt.Errorf("building notification: %w", err)
}
- inspectable, err = ole.RoGetActivationFactory(CLSID_ToastNotification, IID_ToastNotificationFactory)
+ notifier, err := newNotifier(appID)
if err != nil {
- return fmt.Errorf("getting activation factory: %w", err)
- }
-
- factory := (*IToastNotificationFactory)(unsafe.Pointer(inspectable))
-
- inspectable, err = ole.RoActivateInstance(CLSID_XMLDocument)
- if err != nil {
- return err
- }
-
- xmlDispatch, err := inspectable.QueryInterface(IID_IXmlDocument)
- if err != nil {
- return err
- }
-
- xmlIODispatch, err := xmlDispatch.QueryInterface(IID_IXmlDocumentIO)
- if err != nil {
- return err
- }
-
- xmlIO := (*IXMLDocumentIO)(unsafe.Pointer(xmlIODispatch))
-
- if err := xmlIO.LoadXml(xml); err != nil {
- return err
- }
-
- noti, err := factory.CreateToastNotification(xmlDispatch)
- if err != nil {
- return fmt.Errorf("creating toast notification: %w", err)
+ return fmt.Errorf("building notifier: %w", err)
}
if err := notifier.Show(noti); err != nil {
@@ -105,37 +77,6 @@ func GenerateToast(appID, xml string) error {
return nil
}
-// UserData contains Key:Value pairs generated within the notification, based
-// on the XML content of the notification.
-type UserData struct {
- Key string
- Value string
-}
-
-// ActivationCallback is a function that gets invoked when the toast is activated.
-type ActivationCallback func(appUserModelId string, invokedArgs string, userData []UserData)
-
-// SetActivationCallback establishes the callback `cb` to be invoked when
-// the toast notification is activated. This callback instance should handle
-// being activated from any available toast notification.
-func SetActivationCallback(cb ActivationCallback) {
- callback := syscall.NewCallback(func(
- this,
- appUserModelId,
- invokedArgs,
- data unsafe.Pointer,
- count uint32,
- ) (ret uintptr) {
- cb(
- utf16PtrToString((*uint16)(appUserModelId)),
- utf16PtrToString((*uint16)(invokedArgs)),
- sliceUserDataFromUnsafe(data, int(count)),
- )
- return
- })
- procSetActivationCallback.Call(callback)
-}
-
var initLock sync.Mutex
var didInitialize bool
@@ -160,6 +101,75 @@ func initialize() (err error) {
return nil
}
+// newNotifier builds an IToastNotifier instance using the given appID.
+func newNotifier(appID string) (*IToastNotifier, error) {
+ managerObject, err := ole.RoGetActivationFactory(CLSID_ToastNotificationManager, IID_ToastNotificationManager)
+ if err != nil {
+ return nil, fmt.Errorf("getting activation factory: %w", err)
+ }
+
+ // Get access to the manager vtable.
+ manager := (*IToastNotificationManager)(unsafe.Pointer(managerObject))
+
+ notifier, err := manager.CreateToastNotifierWithID(appID)
+ if err != nil {
+ return nil, fmt.Errorf("creating toast notifier: %w", err)
+ }
+
+ return notifier, nil
+}
+
+// newNotiFromXml builds an IToastNotification instance from the given xml content.
+func newNotiFromXml(xml string) (*IToastNotification, error) {
+ factoryObject, err := ole.RoGetActivationFactory(CLSID_ToastNotification, IID_ToastNotificationFactory)
+ if err != nil {
+ return nil, fmt.Errorf("getting activation factory: %w", err)
+ }
+
+ // Get access to the factory vtable.
+ factory := (*IToastNotificationFactory)(unsafe.Pointer(factoryObject))
+
+ xmlDoc, err := loadXML(xml)
+ if err != nil {
+ return nil, fmt.Errorf("loading xml: %w", err)
+ }
+
+ noti, err := factory.CreateToastNotification(xmlDoc)
+ if err != nil {
+ return nil, fmt.Errorf("creating toast notification: %w", err)
+ }
+
+ return noti, nil
+}
+
+// loadXML allocates an XML document object (returned as IDispatch because we don't care
+// about representing it's vtable).
+func loadXML(xml string) (*ole.IDispatch, error) {
+ xmlDocObject, err := ole.RoActivateInstance(CLSID_XMLDocument)
+ if err != nil {
+ return nil, fmt.Errorf("RoActivateInstance: %w", err)
+ }
+
+ xmlDoc, err := xmlDocObject.QueryInterface(IID_IXmlDocument)
+ if err != nil {
+ return nil, fmt.Errorf("querying IID_IXmlDocument: %w", err)
+ }
+
+ xmlDocIO, err := xmlDoc.QueryInterface(IID_IXmlDocumentIO)
+ if err != nil {
+ return nil, fmt.Errorf("querying interface IID_IXmlDocumentIO: %w", err)
+ }
+
+ // Get access to the IO vtable.
+ xmlIO := (*IXMLDocumentIO)(unsafe.Pointer(xmlDocIO))
+
+ if err := xmlIO.LoadXml(xml); err != nil {
+ return nil, fmt.Errorf("IXmlDocumentIO.LoadXml: %w", err)
+ }
+
+ return xmlDoc, nil
+}
+
// sliceUserDataFromUnsafe builds a slice of UserData out of an unsafe pointer.
func sliceUserDataFromUnsafe(ptr unsafe.Pointer, count int) []UserData {
diff --git a/internal/bind/impl.go b/internal/bind/impl.go
@@ -0,0 +1,174 @@
+// This file contains our pure-Go implementations of two COM objects that we need
+// to render toast notifications: IClassFactory and INotificationActivationCallback.
+//
+// More specifically we allocate the C callable functions that can be used to populate
+// the vtable at runtime.
+//
+// Unfortunately these functions have to be declared as var not const because the callbacks
+// are built at runtime. They are declared globally because `syscall.NewCallback` never
+// releases the memory it allocates for the functions thus causing an unsolvable memory
+// leak if we were to allocate these per-notification.
+package bind
+
+import (
+ "sync"
+ "syscall"
+ "unsafe"
+
+ "github.com/go-ole/go-ole"
+)
+
+// Static implementations for the IClassFactory.
+// syscall.NewCallback never releases its memory, so we pay that price once.
+var (
+ // factoryLock protects the IClassFactory reference counting.
+ factoryLock sync.Mutex
+
+ IClassFactory_AddRef = syscall.NewCallback(func(this *IClassFactory) (re uintptr) {
+ factoryLock.Lock()
+ defer factoryLock.Unlock()
+ this.RefCount += 1
+ return uintptr(this.RefCount)
+ })
+
+ IClassFactory_Release = syscall.NewCallback(func(this *IClassFactory) (re uintptr) {
+ factoryLock.Lock()
+ defer factoryLock.Unlock()
+ this.RefCount -= 1
+ if this.RefCount == 0 {
+ free(unsafe.Pointer(this.lpVtbl))
+ free(unsafe.Pointer(this))
+ }
+ return uintptr(this.RefCount)
+ })
+
+ IClassFactory_QueryInterface = syscall.NewCallback(func(this *IClassFactory, riid *ole.GUID, out unsafe.Pointer) (re uintptr) {
+ if !ole.IsEqualGUID(riid, IID_IClassFactory) &&
+ !ole.IsEqualGUID(riid, ole.IID_IUnknown) {
+ return ole.E_NOINTERFACE
+ }
+ *(**IClassFactory)(out) = this
+ this.AddRef()
+ return uintptr(ole.S_OK)
+ })
+
+ IClassFactory_LockServer = syscall.NewCallback(func(this *IClassFactory, flock uintptr) (ret uintptr) {
+ return ole.S_OK
+ })
+
+ IClassFactory_CreateInstance = syscall.NewCallback(func(this *IClassFactory, punkOuter *ole.IUnknown, riid *ole.GUID, out unsafe.Pointer) (re uintptr) {
+ if punkOuter != nil {
+ // Should be CLASS_E_NOAGGREGATION but ole doesn't define this.
+ return ole.E_NOINTERFACE
+ }
+ object := newNotificationActivationCallback()
+ if object == nil {
+ return ole.E_OUTOFMEMORY
+ }
+ object.RefCount = 1
+ hr := object.QueryInterface(riid, out)
+ object.Release()
+ return uintptr(hr)
+ })
+)
+
+// Static implementations for the INotificationActivationCallback.
+// syscall.NewCallback never releases its memory, so we pay that price once.
+var (
+ // callbackLock protects the ActivationCallback reference counting.
+ callbackLock sync.Mutex
+
+ INotificationActivationCallback_AddRef = syscall.NewCallback(func(this *INotificationActivationCallback) (re uintptr) {
+ callbackLock.Lock()
+ defer callbackLock.Unlock()
+ this.RefCount += 1
+ return uintptr(this.RefCount)
+ })
+
+ INotificationActivationCallback_Release = syscall.NewCallback(func(this *INotificationActivationCallback) (re uintptr) {
+ callbackLock.Lock()
+ defer callbackLock.Unlock()
+ this.RefCount -= 1
+ if this.RefCount == 0 {
+ free(unsafe.Pointer(this.lpVtbl))
+ free(unsafe.Pointer(this))
+ }
+ return uintptr(this.RefCount)
+ })
+
+ INotificationActivationCallback_QueryInterface = syscall.NewCallback(func(this *INotificationActivationCallback, riid *ole.GUID, out unsafe.Pointer) (re uintptr) {
+ if !ole.IsEqualGUID(riid, IID_INotificationActivationCallback) &&
+ !ole.IsEqualGUID(riid, ole.IID_IUnknown) {
+ return ole.E_NOINTERFACE
+ }
+ *(**INotificationActivationCallback)(out) = this
+ this.AddRef()
+ return uintptr(ole.S_OK)
+ })
+
+ INotificationActivationCallback_Activate = syscall.NewCallback(func(
+ this,
+ appUserModelId,
+ invokedArgs,
+ data unsafe.Pointer,
+ count uint32,
+ ) (ret uintptr) {
+ callback(
+ utf16PtrToString((*uint16)(appUserModelId)),
+ utf16PtrToString((*uint16)(invokedArgs)),
+ sliceUserDataFromUnsafe(data, int(count)),
+ )
+ return
+ })
+)
+
+// newClassFactory allocates our ClassFactory that can build our NotificationActivationCallback object.
+func newClassFactory() *IClassFactory {
+
+ // Allocate the object and its vtable.
+
+ v := (*IClassFactory)(malloc(unsafe.Sizeof(IClassFactory{})))
+ if v == nil {
+ return nil
+ }
+
+ v.lpVtbl = (*IClassFactoryVtbl)(malloc(unsafe.Sizeof(IClassFactoryVtbl{})))
+ if v.lpVtbl == nil {
+ return nil
+ }
+
+ // Provide function implementations in the Vtable.
+
+ v.lpVtbl.AddRef = IClassFactory_AddRef
+ v.lpVtbl.Release = IClassFactory_Release
+ v.lpVtbl.QueryInterface = IClassFactory_QueryInterface
+ v.lpVtbl.LockServer = IClassFactory_LockServer
+ v.lpVtbl.CreateInstance = IClassFactory_CreateInstance
+
+ return v
+}
+
+// newNotificationActivationCallback allocates our implementation of the INotificationActivationCallback.
+func newNotificationActivationCallback() *INotificationActivationCallback {
+
+ // Allocate the object and its vtable.
+
+ v := (*INotificationActivationCallback)(malloc(unsafe.Sizeof(INotificationActivationCallback{})))
+ if v == nil {
+ return nil
+ }
+
+ v.lpVtbl = (*INotificationActivationCallbackVtbl)(malloc(unsafe.Sizeof(INotificationActivationCallbackVtbl{})))
+ if v.lpVtbl == nil {
+ return nil
+ }
+
+ // Provide function implementations in the vtable.
+
+ v.lpVtbl.AddRef = INotificationActivationCallback_AddRef
+ v.lpVtbl.Release = INotificationActivationCallback_Release
+ v.lpVtbl.QueryInterface = INotificationActivationCallback_QueryInterface
+ v.lpVtbl.Activate = INotificationActivationCallback_Activate
+
+ return v
+}
diff --git a/internal/bind/interfaces.go b/internal/bind/interfaces.go
@@ -0,0 +1,243 @@
+// This file contains the various COM interfaces we need to call.
+// Only the methods we need to call have wrappers. All methods
+// that don't have a corresponding Go wrapper method are marked
+// as such.
+//
+// The definitions are derived from:
+// - <combase.h>
+// - <windows.ui.notifications.h
+// - <NotificationActivationCallback.h>
+package bind
+
+import (
+ "fmt"
+ "syscall"
+ "unsafe"
+
+ "github.com/go-ole/go-ole"
+)
+
+// Runtime class names. These correspond to normal COM GUIDs, however the Windows Runtime likes
+// to use string identifiers and derives the GUIDs under the hood.
+const (
+ CLSID_ToastNotificationManager = "Windows.UI.Notifications.ToastNotificationManager"
+ CLSID_ToastNotification = "Windows.UI.Notifications.ToastNotification"
+ CLSID_XMLDocument = "Windows.Data.Xml.Dom.XmlDocument"
+)
+
+// Interface GUIDS. These GUIDS are predefined by the Windows Runtime, identifying the various
+// interfaces we want to make use of.
+var (
+ IID_IClassFactory = ole.NewGUID("{00000001-0000-0000-C000-000000000046}")
+ IID_INotificationActivationCallback = ole.NewGUID("{53E31837-6600-4A81-9395-75CFFE746F94}")
+ IID_ToastNotificationManager = ole.NewGUID("{50AC103F-D235-4598-BBEF-98FE4D1A3AD4}")
+ IID_ToastNotificationFactory = ole.NewGUID("{04124B20-82C6-4229-B109-FD9ED4662B53}")
+ IID_IXmlDocument = ole.NewGUID("{F7F3A506-1E87-42D6-BCFB-B8C809FA5494}")
+ IID_IXmlDocumentIO = ole.NewGUID("{6CD0E74E-EE65-4489-9EBF-CA43E87BA637}")
+)
+
+// This GUID is for our implementation.
+// This was generated and should not collide with any other GUID.
+var GUID_ImplNotificationActivationCallback = ole.NewGUID("{0F82E845-CB89-4039-BDBF-67CA33254C76}")
+
+// HRESULT represents a COM return value.
+type HRESULT uintptr
+
+// IToastNotification represents a single toast notification instance.
+type IToastNotification struct {
+ lpVtbl *IToastNotificationVtbl
+}
+
+// IToastNotificationVtbl is empty because we don't care, as of now, about calling its methods.
+type IToastNotificationVtbl struct {
+ ole.IInspectableVtbl
+}
+
+// IToastNotificationManager can create toast notifier objects.
+type IToastNotificationManager struct {
+ lpVtbl *IToastNotificationManagerVtbl
+
+ GetContent uintptr // not wrapped
+ PutExpirationTime uintptr // not wrapped
+ GetExpirationTime uintptr // not wrapped
+ AddDismissed uintptr // not wrapped
+ RemoveDismissed uintptr // not wrapped
+ AddActivated uintptr // not wrapped
+ RemoveActivated uintptr // not wrapped
+ AddFailed uintptr // not wrapped
+ RemoveFailed uintptr // not wrapped
+}
+
+type IToastNotificationManagerVtbl struct {
+ ole.IInspectableVtbl
+
+ CreateToastNotifier uintptr // not wrapped
+ CreateToastNotifierWithID uintptr
+ GetTemplateContent uintptr // not wrapped
+}
+
+func (v *IToastNotificationManager) CreateToastNotifierWithID(appID string) (ret *IToastNotifier, err error) {
+ hsAppID, err := ole.NewHString(appID)
+ if err != nil {
+ return nil, fmt.Errorf("allocating string: %w", err)
+ }
+ defer ole.DeleteHString(hsAppID)
+ hr, _, _ := syscall.SyscallN(
+ v.lpVtbl.CreateToastNotifierWithID,
+ uintptr(unsafe.Pointer(v)),
+ uintptr(hsAppID),
+ uintptr(unsafe.Pointer(&ret)),
+ )
+ if hr != ole.S_OK {
+ return nil, ole.NewError(hr)
+ }
+ return ret, nil
+}
+
+// IToastNotifier can push notification objects to the runtime.
+type IToastNotifier struct {
+ lpVtbl *IToastNotifierVtbl
+}
+
+type IToastNotifierVtbl struct {
+ ole.IInspectableVtbl
+
+ Show uintptr
+ Hide uintptr // not wrapped
+ GetSetting uintptr // not wrapped
+ AddToSchedule uintptr // not wrapped
+ RemoveFromSchedule uintptr // not wrapped
+ GetScheduledToastNotifications uintptr // not wrapped
+}
+
+func (v *IToastNotifier) Show(noti *IToastNotification) (err error) {
+ hr, _, _ := syscall.SyscallN(
+ v.lpVtbl.Show,
+ uintptr(unsafe.Pointer(v)),
+ uintptr(unsafe.Pointer(noti)),
+ )
+ if hr != ole.S_OK {
+ return ole.NewError(hr)
+ }
+ return nil
+}
+
+// IToastNotificationFactory can create toast notification objects.
+type IToastNotificationFactory struct {
+ lpVtbl *IToastNotificationFactoryVtbl
+}
+
+type IToastNotificationFactoryVtbl struct {
+ ole.IInspectableVtbl
+
+ CreateToastNotification uintptr
+}
+
+func (v *IToastNotificationFactory) CreateToastNotification(xmlDispatch *ole.IDispatch) (ret *IToastNotification, err error) {
+ hr, _, _ := syscall.SyscallN(
+ v.lpVtbl.CreateToastNotification,
+ uintptr(unsafe.Pointer(v)),
+ uintptr(unsafe.Pointer(xmlDispatch)),
+ uintptr(unsafe.Pointer(&ret)),
+ )
+ if hr != ole.S_OK {
+ return nil, ole.NewError(hr)
+ }
+ return ret, nil
+}
+
+// IXMLDocumentIO implements IO for XML documents.
+type IXMLDocumentIO struct {
+ lpVtbl *IXMLDocumentIOVtbl
+}
+
+type IXMLDocumentIOVtbl struct {
+ ole.IInspectableVtbl
+
+ LoadXml uintptr
+ LoadXmlWithSettings uintptr // not wrapped
+ SaveToFileAsync uintptr // not wrapped
+}
+
+func (v *IXMLDocumentIO) LoadXml(xml string) (err error) {
+ hsXML, err := ole.NewHString(xml)
+ if err != nil {
+ return err
+ }
+ defer ole.DeleteHString(hsXML)
+ hr, _, _ := syscall.SyscallN(
+ v.lpVtbl.LoadXml,
+ uintptr(unsafe.Pointer(v)),
+ uintptr(hsXML),
+ )
+ if hr != ole.S_OK {
+ return ole.NewError(hr)
+ }
+ return nil
+}
+
+// IClassFactory is used to build other classes. We will use this to build our implementation
+// of the INotificationActivationCallback interface.
+type IClassFactory struct {
+ lpVtbl *IClassFactoryVtbl
+ RefCount int64
+}
+
+type IClassFactoryVtbl struct {
+ ole.IUnknownVtbl
+ CreateInstance uintptr // not wrapped
+ LockServer uintptr // not wrapped
+}
+
+func (v *IClassFactory) AddRef() int32 {
+ count, _, _ := syscall.SyscallN(
+ v.lpVtbl.AddRef,
+ uintptr(unsafe.Pointer(v)),
+ )
+ return int32(count)
+}
+
+func (v *IClassFactory) Release() int32 {
+ count, _, _ := syscall.SyscallN(
+ v.lpVtbl.Release,
+ uintptr(unsafe.Pointer(v)),
+ )
+ return int32(count)
+}
+
+// INotificationActivationCallback receives activations from toast notifications.
+type INotificationActivationCallback struct {
+ lpVtbl *INotificationActivationCallbackVtbl
+ RefCount int64
+}
+
+type INotificationActivationCallbackVtbl struct {
+ ole.IUnknownVtbl
+ Activate uintptr
+}
+
+func (v *INotificationActivationCallback) QueryInterface(riid *ole.GUID, out unsafe.Pointer) HRESULT {
+ ret, _, _ := syscall.SyscallN(
+ v.lpVtbl.QueryInterface,
+ uintptr(unsafe.Pointer(v)),
+ uintptr(unsafe.Pointer(riid)),
+ uintptr(out),
+ )
+ return HRESULT(ret)
+}
+
+func (v *INotificationActivationCallback) AddRef() int32 {
+ count, _, _ := syscall.SyscallN(
+ v.lpVtbl.AddRef,
+ uintptr(unsafe.Pointer(v)),
+ )
+ return int32(count)
+}
+
+func (v *INotificationActivationCallback) Release() int32 {
+ count, _, _ := syscall.SyscallN(
+ v.lpVtbl.Release,
+ uintptr(unsafe.Pointer(v)),
+ )
+ return int32(count)
+}
diff --git a/internal/bind/objects.go b/internal/bind/objects.go
@@ -1,147 +0,0 @@
-// This file contains the implementations of the various COM objects we need to call.
-// THESE TYPES ARE NOT COMPLETE. Only the behaviour that is needed is implemented.
-// The definitions are derived from `<windows.ui.notifications.h>`.
-package bind
-
-import (
- "fmt"
- "syscall"
- "unsafe"
-
- "github.com/go-ole/go-ole"
-)
-
-// Runtime class names. These correspond to normal COM GUIDs, however the Windows Runtime likes
-// to use string identifiers and derives the GUIDs under the hood.
-var (
- CLSID_ToastNotificationManager = "Windows.UI.Notifications.ToastNotificationManager"
- CLSID_ToastNotification = "Windows.UI.Notifications.ToastNotification"
- CLSID_XMLDocument = "Windows.Data.Xml.Dom.XmlDocument"
-)
-
-// Interface GUIDS. These GUIDS are predefined by the Windows Runtime, identifying the various
-// interfaces we want to make use of.
-var (
- IID_ToastNotificationManagerStatics = ole.NewGUID("{50AC103F-D235-4598-BBEF-98FE4D1A3AD4}")
- IID_ToastNotificationFactory = ole.NewGUID("{04124B20-82C6-4229-B109-FD9ED4662B53}")
- IID_IXmlDocument = ole.NewGUID("{F7F3A506-1E87-42D6-BCFB-B8C809FA5494}")
- IID_IXmlDocumentIO = ole.NewGUID("{6CD0E74E-EE65-4489-9EBF-CA43E87BA637}")
-)
-
-type IToastNotification struct {
- lpVtbl *IToastNotificationVtbl
-}
-
-// IToastNotificationVtbl is empty because we don't care, as of now, about calling its methods.
-type IToastNotificationVtbl struct {
- ole.IInspectableVtbl
-}
-
-type IToastNotificationManager struct {
- lpVtbl *IToastNotificationManagerVtbl
-}
-
-type IToastNotificationManagerVtbl struct {
- ole.IInspectableVtbl
-
- CreateToastNotifier uintptr
- CreateToastNotifierWithID uintptr
- GetTemplateContent uintptr
-}
-
-func (v *IToastNotificationManager) CreateToastNotifierWithID(appID string) (ret *IToastNotifier, err error) {
- hsAppID, err := ole.NewHString(appID)
- if err != nil {
- return nil, fmt.Errorf("allocating string: %w", err)
- }
- defer ole.DeleteHString(hsAppID)
- hr, _, _ := syscall.SyscallN(
- v.lpVtbl.CreateToastNotifierWithID,
- uintptr(unsafe.Pointer(v)),
- uintptr(hsAppID),
- uintptr(unsafe.Pointer(&ret)),
- )
- if hr != ole.S_OK {
- return nil, ole.NewError(hr)
- }
- return ret, nil
-}
-
-type IToastNotifier struct {
- lpVtbl *IToastNotifierVtbl
-}
-
-type IToastNotifierVtbl struct {
- ole.IInspectableVtbl
-
- Show uintptr
- Hide uintptr
- GetSetting uintptr
- AddToSchedule uintptr
- RemoveFromSchedule uintptr
- GetScheduledToastNotifications uintptr
-}
-
-func (v *IToastNotifier) Show(noti *IToastNotification) (err error) {
- hr, _, _ := syscall.SyscallN(
- v.lpVtbl.Show,
- uintptr(unsafe.Pointer(v)),
- uintptr(unsafe.Pointer(noti)),
- )
- if hr != ole.S_OK {
- return ole.NewError(hr)
- }
- return nil
-}
-
-type IToastNotificationFactory struct {
- lpVtbl *IToastNotificationFactoryVtbl
-}
-
-type IToastNotificationFactoryVtbl struct {
- ole.IInspectableVtbl
-
- CreateToastNotification uintptr
-}
-
-func (v *IToastNotificationFactory) CreateToastNotification(xmlDispatch *ole.IDispatch) (ret *IToastNotification, err error) {
- hr, _, _ := syscall.SyscallN(
- v.lpVtbl.CreateToastNotification,
- uintptr(unsafe.Pointer(v)),
- uintptr(unsafe.Pointer(xmlDispatch)),
- uintptr(unsafe.Pointer(&ret)),
- )
- if hr != ole.S_OK {
- return nil, ole.NewError(hr)
- }
- return ret, nil
-}
-
-type IXMLDocumentIO struct {
- lpVtbl *IXMLDocumentIOVtbl
-}
-
-type IXMLDocumentIOVtbl struct {
- ole.IInspectableVtbl
-
- LoadXml uintptr
- LoadXmlWithSettings uintptr
- SaveToFileAsync uintptr
-}
-
-func (v *IXMLDocumentIO) LoadXml(xml string) (err error) {
- hsXML, err := ole.NewHString(xml)
- if err != nil {
- return err
- }
- defer ole.DeleteHString(hsXML)
- hr, _, _ := syscall.SyscallN(
- v.lpVtbl.LoadXml,
- uintptr(unsafe.Pointer(v)),
- uintptr(hsXML),
- )
- if hr != ole.S_OK {
- return ole.NewError(hr)
- }
- return nil
-}
diff --git a/internal/bind/proc.go b/internal/bind/proc.go
@@ -1,60 +0,0 @@
-package bind
-
-import (
- "unsafe"
-
- "github.com/go-ole/go-ole"
- "golang.org/x/sys/windows"
-)
-
-var (
- kernel32 = windows.NewLazySystemDLL("kernel32.dll")
- procMalloc = kernel32.NewProc("GlobalAlloc")
- procFree = kernel32.NewProc("GlobalFree")
-
- modcombase = windows.NewLazySystemDLL("combase.dll")
- procRegisterClassObject = modcombase.NewProc("RoRegisterClassObject")
-
- toast = windows.NewLazyDLL("toast.dll")
- procSetActivationCallback = toast.NewProc("SetActivationCallback")
- procNewClassFactory = toast.NewProc("NewClassFactory")
- procRegisterClassFactory = toast.NewProc("RegisterClassFactory")
-)
-
-// malloc allocates raw memory.
-func malloc(size int) unsafe.Pointer {
- hr, _, _ := procMalloc.Call(uintptr(size))
- if hr == 0 {
- return nil
- }
- return unsafe.Pointer(hr)
-}
-
-// free deallocates raw memory allocated by malloc.
-func free(object unsafe.Pointer) {
- procFree.Call(uintptr(object))
-}
-
-// registerClassObject associates an in memory object with a GUID at runtime.
-func registerClassObject(clsid *ole.GUID, factory unsafe.Pointer) error {
- hr, _, _ := procRegisterClassObject.Call(uintptr(unsafe.Pointer(clsid)), uintptr(factory))
- if hr != ole.S_OK {
- return ole.NewError(hr)
- }
- return nil
-}
-
-// newClassFactory allocates our ClassFactory that can build our NotificationActivationCallback object.
-// Deallocate with free.
-func newClassFactory() unsafe.Pointer {
- object, _, _ := procNewClassFactory.Call()
- return unsafe.Pointer(object)
-}
-
-func registerClassFactory(factory unsafe.Pointer) error {
- hr, _, _ := procRegisterClassFactory.Call(uintptr(factory))
- if hr != ole.S_OK {
- return ole.NewError(hr)
- }
- return nil
-}
diff --git a/internal/bind/procs.go b/internal/bind/procs.go
@@ -0,0 +1,62 @@
+package bind
+
+import (
+ "unsafe"
+
+ "github.com/go-ole/go-ole"
+ "golang.org/x/sys/windows"
+)
+
+var (
+ // Define memory allocation procs. This is how we get a hold of unmanaged memory.
+ kernel32 = windows.NewLazySystemDLL("kernel32.dll")
+ procMalloc = kernel32.NewProc("GlobalAlloc")
+ procFree = kernel32.NewProc("GlobalFree")
+
+ // Define procs that go-ole doesn't provide. This is how we register our Go-implemented
+ // COM objects.
+ modcombase = windows.NewLazySystemDLL("combase.dll")
+ procRegisterClassObject = modcombase.NewProc("CoRegisterClassObject")
+)
+
+// Allocation flags we need.
+// There are more, these are just the ones we use.
+// See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalalloc
+const (
+ GMEM_FIXED = 0x0000
+ GMEM_ZEROINIT = 0x0040
+)
+
+// malloc allocates raw memory using the Windows kernel.
+// In case of out of memory, the returned pointer will be nil.
+// The memory is zeroed out to make sure we don't get garbage that looks like
+// valid Go data types.
+func malloc(size uintptr) unsafe.Pointer {
+ hr, _, _ := procMalloc.Call(uintptr(GMEM_FIXED|GMEM_ZEROINIT), uintptr(size))
+ if hr == 0 {
+ return nil
+ }
+ return unsafe.Pointer(hr)
+}
+
+// free deallocates raw memory allocated by malloc.
+func free(object unsafe.Pointer) {
+ procFree.Call(uintptr(object))
+}
+
+// registerClassFactory teaches the Windows Runtime about our factory that can allocate
+// instances of our ActivationCallback.
+func registerClassFactory(factory *IClassFactory) error {
+ var cookie int64
+ hr, _, _ := procRegisterClassObject.Call(
+ uintptr(unsafe.Pointer(GUID_ImplNotificationActivationCallback)),
+ uintptr(unsafe.Pointer(factory)),
+ uintptr(ole.CLSCTX_LOCAL_SERVER),
+ uintptr(1), /* REGCLS_MULTIPLEUSE */
+ uintptr(unsafe.Pointer(&cookie)),
+ )
+ if hr != ole.S_OK {
+ return ole.NewError(hr)
+ }
+ return nil
+}
diff --git a/internal/bind/registry.go b/internal/bind/registry.go
@@ -41,7 +41,7 @@ func SetAppData(data AppData) error {
}
// CustomActivator teaches Window what COM class to use as the callback when
// a toast notification is activated.
- if err := appIDKey.SetStringValue("CustomActivator", iNotificationActivationCallbackGUID); err != nil {
+ if err := appIDKey.SetStringValue("CustomActivator", GUID_ImplNotificationActivationCallback.String()); err != nil {
return fmt.Errorf("setting CustomActivator: %w", err)
}
if data.IconPath != "" {
@@ -66,7 +66,7 @@ const registryDefaultKey string = ""
// setActivationExecutable registers the given executable path with the CLSID.
// Windows will invoke this executable for cold starts, eg when the application is not running.
func setActivationExecutable(exe string) error {
- clsidKey, _, err := registry.CreateKey(registry.CURRENT_USER, filepath.Join("SOFTWARE", "Classes", "CLSID", iNotificationActivationCallbackGUID, "LocalServer32"), registry.SET_VALUE)
+ clsidKey, _, err := registry.CreateKey(registry.CURRENT_USER, filepath.Join("SOFTWARE", "Classes", "CLSID", GUID_ImplNotificationActivationCallback.String(), "LocalServer32"), registry.SET_VALUE)
if err != nil {
return fmt.Errorf("setting the exe path for LocalServer reponse: %w", err)
}
diff --git a/internal/c-lib/Makefile b/internal/c-lib/Makefile
@@ -1,14 +0,0 @@
-.PHONY: clean
-.PHONY: gosh
-
-toast.dll: toast.c
- gosh build.sh
-
-gosh:
- # Grab the gosh shell interpreter. Requires a Go toolchain.
- go install github.com/mvdan/sh/cmd/gosh@latest
-
-clean:
- rm *.exe ; rm *.dll ; rm *.exp ; rm *.lib ; rm *.obj
-
-
diff --git a/internal/c-lib/build.sh b/internal/c-lib/build.sh
@@ -1,79 +0,0 @@
-# This build script captures the method used to build the MSVC compiled DLL.
-#
-# Make sure you have installed the Widows C++ build tools using the Visual
-# Studio Installer (https://visualstudio.microsoft.com/downloads). Activate
-# the "Desktop development with C++" component and check "MSVC C++ build tools".
-#
-# The latest version at the time of this script (10/03/2023) is MSVC v143.
-#
-# Finally, make sure you install the Windows SDK. This script is specialized
-# to version 10.0.22000.0, however that is probably an unnecessary constraint.
-#
-# After installation make sure you add the MSVC binaries to your system path.
-# In this case, that path is:
-#
-# "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx64\x64"
-#
-# Keep in mind the exact path will be determined by which version of the build
-# you have installed.
-#
-# On Windows, this script can be invoked via https://github.com/mvdan/sh.
-#
-# go install github.com/mvdan/sh/cmd/gosh@latest && gosh build.sh
-#
-# I'm not an expert in Windows build systems by any means, so there might be
-# more correct ways to do things in this script.
-#
-# This script could probably be parameterized by SDK version, OS version,
-# build tools version and CPU architecture.
-#
-# It may also be possible to extract the .lib files and bundle them directly
-# to avoid differences between versions of the various components.
-
-# Host Details
-DRIVE="C:"
-WINDOWS_VERSION="10"
-SDK_VERSION="10.0.22000.0"
-BUILD_TOOLS_VERSION="14.35.32215"
-ARCH="x64"
-
-# Windows SDK
-SDK_ROOT="$DRIVE\\Program Files (x86)\\Windows Kits\\$WINDOWS_VERSION"
-SDK_LIB="$SDK_ROOT\\Lib\\$SDK_VERSION"
-SDK_INCLUDE="$SDK_ROOT\\Include\\$SDK_VERSION"
-
-# MSVC Build Tools
-BUILD_TOOLS_ROOT="$DRIVE\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\$BUILD_TOOLS_VERSION"
-BUILD_TOOLS_LIB="$BUILD_TOOLS_ROOT\\lib"
-BUILD_TOOLS_INCLUDE="$BUILD_TOOLS_ROOT\\include"
-
-# Make this script invocable from parent directories by resolving
-# the source against the script path.
-SOURCE="$0/../toast.c"
-
-# `cl` must be available on system path.
-cl.exe \
- /I "$SDK_INCLUDE\\um" \
- /I "$SDK_INCLUDE\\ucrt" \
- /I "$SDK_INCLUDE\\winrt" \
- /I "$SDK_INCLUDE\\shared" \
- /I "$BUILD_TOOLS_INCLUDE" \
- /LD $SOURCE \
- /link \
- /LIBPATH "$SDK_LIB\\um\\$ARCH\\Uuid.Lib" \
- /LIBPATH "$SDK_LIB\\um\\$ARCH\\Ole32.lib" \
- /LIBPATH "$SDK_LIB\\um\\$ARCH\\oleaut32.lib" \
- /LIBPATH "$SDK_LIB\\um\\$ARCH\\Advapi32.lib" \
- /LIBPATH "$SDK_LIB\\um\\$ARCH\\User32.lib" \
- /LIBPATH "$SDK_LIB\\um\\$ARCH\\kernel32.lib" \
- /LIBPATH "$SDK_LIB\\um\\$ARCH\\runtimeobject.lib" \
- /LIBPATH "$SDK_LIB\\ucrt\\$ARCH\\libucrt.lib" \
- /LIBPATH "$BUILD_TOOLS_LIB\\$ARCH\\libcmt.lib" \
- /LIBPATH "$BUILD_TOOLS_LIB\\$ARCH\\oldnames.lib" \
- /LIBPATH "$BUILD_TOOLS_LIB\\$ARCH\\libvcruntime.lib"
-
-# These artefacts are noisy and not needed for our purposes; so we
-# can clean them up.
-rm toast.exp
-rm toast.lib
-rm toast.obj
-\ No newline at end of file
diff --git a/internal/c-lib/toast.c b/internal/c-lib/toast.c
@@ -1,269 +0,0 @@
-#include <Windows.h>
-#include <Windows.ui.notifications.h>
-#include <notificationactivationcallback.h>
-#include <initguid.h>
-#include <roapi.h>
-#include <tchar.h>
-#include <stdio.h>
-#include <string.h>
-
-#pragma comment(lib, "runtimeobject.lib")
-#pragma comment(lib, "ole32.lib")
-#pragma comment(lib, "oleaut32.lib")
-#pragma comment(lib, "Advapi32.lib")
-#pragma comment(lib, "User32.lib")
-
-DWORD dwMainThreadId = 0;
-
-/*
- * The GUID that we associate with our factory that produces our INotificationActivationCallback interface.
- */
-#define GUID_Impl_INotificationActivationCallback_Textual "0F82E845-CB89-4039-BDBF-67CA33254C76"
-DEFINE_GUID(GUID_Impl_INotificationActivationCallback,
- 0xf82e845, 0xcb89, 0x4039, 0xbd, 0xbf, 0x67, 0xca, 0x33, 0x25, 0x4c, 0x76);
-
-/*
- * All the objects we allocate in this example (our class factory and our INotificationActivationCallback
- * implementation) have this memory layout, and all inherit from IUnknown.
- */
-typedef struct Impl_IGeneric
-{
- IUnknownVtbl* lpVtbl;
- LONG64 dwRefCount;
-} Impl_IGeneric;
-
-static
-ULONG
-STDMETHODCALLTYPE
-Impl_IGeneric_AddRef(Impl_IGeneric* _this)
-{
- return InterlockedIncrement64(&(_this->dwRefCount));
-}
-
-static
-ULONG
-STDMETHODCALLTYPE
-Impl_IGeneric_Release(Impl_IGeneric* _this)
-{
- LONG64 dwNewRefCount = InterlockedDecrement64(&(_this->dwRefCount));
- if (!dwNewRefCount) GlobalFree(_this);
- return dwNewRefCount;
-}
-
-/*
- * Our INotificationActivationCallback implementation.
- */
-static
-HRESULT
-STDMETHODCALLTYPE
-Impl_INotificationActivationCallback_QueryInterface(
- Impl_IGeneric* _this,
- REFIID riid,
- void** ppvObject
-) {
- if (!IsEqualIID(riid, &IID_INotificationActivationCallback) && !IsEqualIID(riid, &IID_IUnknown))
- {
- *ppvObject = NULL;
- return E_NOINTERFACE;
- }
- *ppvObject = _this;
- _this->lpVtbl->AddRef(_this);
- return S_OK;
-}
-
-// ActivationCallback defines the function that will receive the activation data
-// when the toast notification is activated (clicked).
-typedef void (__stdcall *ActivationCallback)(
- INotificationActivationCallback* _this,
- LPCWSTR appUserModelId,
- LPCWSTR invokedArgs,
- const NOTIFICATION_USER_INPUT_DATA* data,
- ULONG count
-);
-
-ActivationCallback _activation_callback;
-
-/*
- * This is where the magic happens when someone interacts with our notification: this method will be called
- * (on another thread !!!).
- */
-static
-HRESULT
-STDMETHODCALLTYPE
-Impl_INotificationActivationCallback_Activate(
- INotificationActivationCallback* _this,
- LPCWSTR appUserModelId,
- LPCWSTR invokedArgs,
- const NOTIFICATION_USER_INPUT_DATA* data,
- ULONG count
-) {
- if (_activation_callback) {
- _activation_callback(_this, appUserModelId, invokedArgs, data, count);
- }
- return S_OK;
-}
-
-static INotificationActivationCallbackVtbl Impl_INotificationActivationCallback_Vtbl = {
- .QueryInterface = Impl_INotificationActivationCallback_QueryInterface,
- .AddRef = Impl_IGeneric_AddRef,
- .Release = Impl_IGeneric_Release,
- .Activate = Impl_INotificationActivationCallback_Activate
-};
-
-/*
- * Our IClassFactory implementation.
- */
-static
-HRESULT
-STDMETHODCALLTYPE
-Impl_IClassFactory_QueryInterface(
- Impl_IGeneric* _this,
- REFIID riid,
- void** ppvObject
-) {
- if (!IsEqualIID(riid, &IID_IClassFactory) && !IsEqualIID(riid, &IID_IUnknown))
- {
- *ppvObject = NULL;
- return E_NOINTERFACE;
- }
- *ppvObject = _this;
- _this->lpVtbl->AddRef(_this);
- return S_OK;
-}
-
-static HRESULT STDMETHODCALLTYPE Impl_IClassFactory_LockServer(IClassFactory* _this, BOOL flock)
-{
- return S_OK;
-}
-
-static
-HRESULT
-STDMETHODCALLTYPE
-Impl_IClassFactory_CreateInstance(
- IClassFactory* _this,
- IUnknown* punkOuter,
- REFIID vTableGuid,
- void** ppv
-) {
- HRESULT hr = E_NOINTERFACE;
- Impl_IGeneric* thisobj = NULL;
- *ppv = 0;
-
- if (punkOuter) hr = CLASS_E_NOAGGREGATION;
- else
- {
- BOOL bOk = FALSE;
- if (!(thisobj = GlobalAlloc(GMEM_FIXED|GMEM_ZEROINIT, sizeof(Impl_IGeneric)))) hr = E_OUTOFMEMORY;
- else
- {
- thisobj->lpVtbl = &Impl_INotificationActivationCallback_Vtbl;
- bOk = TRUE;
- }
- if (bOk)
- {
- thisobj->dwRefCount = 1;
- hr = thisobj->lpVtbl->QueryInterface(thisobj, vTableGuid, ppv);
- thisobj->lpVtbl->Release(thisobj);
- }
- else
- {
- return hr;
- }
- }
-
- return hr;
-}
-
-static IClassFactoryVtbl Impl_IClassFactory_Vtbl = {
- .QueryInterface = Impl_IClassFactory_QueryInterface,
- .AddRef = Impl_IGeneric_AddRef,
- .Release = Impl_IGeneric_Release,
- .LockServer = Impl_IClassFactory_LockServer,
- .CreateInstance = Impl_IClassFactory_CreateInstance
-};
-
-// Must checks the HRESULT value and jumps to cleanup not S_OK.
-//
-// This macro assumes the variable `HRESULT hr;` is in scope so
-// it can set the failed value before returning.
-#define Must(hresult) \
- if (FAILED(hresult)) { \
- hr = hresult; \
- goto cleanup; \
- }
-
-// Release invokes Release on the vtable of the supplied COM object.
-#define Release(ident) \
- if (ident != NULL) { \
- ident->lpVtbl->Release(ident); \
- }
-
-// DeleteString invokes DeleteString on the provided HSTRING.
-#define DeleteString(ident) \
- if (ident) { \
- WindowsDeleteString(ident); \
- }
-
-// Init exports RoInitialize. For go-ole we can use the ole provided function instead.
-// This allows the consumer to handle initialization without relying on a third-party.
-__declspec(dllexport)
-HRESULT Init(RO_INIT_TYPE roInit) {
- return RoInitialize(roInit);
-}
-
-// Deinit exports RoUninitialize. For go-ole we can use the ole provided function instead.
-// This allows the consumer to handle uninitialization without relying on a third-party.
-__declspec(dllexport)
-void Deinit() {
- RoUninitialize();
-}
-
-// Malloc exports memory allocation. This allows the caller to get C memory without importing C.
-__declspec(dllexport)
-void*
-Malloc(size_t size) {
- return GlobalAlloc(GMEM_FIXED|GMEM_ZEROINIT, size);
-}
-
-// Free exports memory deallocation. This allows the caller to free C memory without importing C.
-__declspec(dllexport)
-void
-Free(void* object) {
- GlobalFree(object);
-}
-
-// NewClassFactory allocates a ClassFactory.
-// If the pointer is NULL we are out of memory.
-__declspec(dllexport)
-Impl_IGeneric*
-NewClassFactory() {
- Impl_IGeneric* pClassFactory;
- pClassFactory = GlobalAlloc(GMEM_FIXED|GMEM_ZEROINIT, sizeof(Impl_IGeneric));
- if (pClassFactory == NULL) {
- return NULL;
- }
- pClassFactory->lpVtbl = &Impl_IClassFactory_Vtbl;
- pClassFactory->dwRefCount = 1;
- return pClassFactory;
-}
-
-// RegisterClassFactory associates the object that can build our INotificationActivationCallback
-// with that interface so the runtime can build our objects.
-__declspec(dllexport)
-HRESULT
-RegisterClassFactory(Impl_IGeneric* pClassFactory) {
- DWORD dwCookie = 0;
- return CoRegisterClassObject(
- &GUID_Impl_INotificationActivationCallback,
- pClassFactory,
- CLSCTX_LOCAL_SERVER,
- REGCLS_MULTIPLEUSE,
- &dwCookie
- );
-}
-
-// SetActivationCallback to use when the notification
-__declspec(dllexport)
-void SetActivationCallback(ActivationCallback cb) {
- _activation_callback = cb;
-}
-\ No newline at end of file
diff --git a/toast.dll b/toast.dll
Binary files differ.
diff --git a/toast.go b/toast.go
@@ -165,10 +165,12 @@ func (n *Notification) Push() error {
}); err != nil {
return fmt.Errorf("configuring registry: %w", err)
}
- if bind.IsDLLAvailable() {
- return n.pushCOM(xml)
+ if err := n.pushCOM(xml); err != nil {
+ // If COM api fails attempt the powershell fallback and
+ // report the error.
+ return errors.Join(err, n.pushPowershell(xml))
}
- return n.pushPowershell(xml)
+ return nil
}
func (n *Notification) applyDefaults() {