commit 24043fcd0c1ee0d52cf31599ca74ec2c33d3cf95
parent bc9200482b415b309b45e1da61817f1fa8798c28
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Wed, 15 Mar 2023 18:29:50 +0800
bind: extract registry logic to file
This separates the registry manipulation from the DLL interaction.
Also enhanced the documentation.
Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Diffstat:
3 files changed, 114 insertions(+), 77 deletions(-)
diff --git a/internal/bind/bind.go b/internal/bind/bind.go
@@ -2,9 +2,7 @@
package bind
import (
- "errors"
"fmt"
- "path/filepath"
"sync"
"syscall"
"unicode/utf16"
@@ -12,11 +10,11 @@ import (
"github.com/go-ole/go-ole"
"golang.org/x/sys/windows"
- "golang.org/x/sys/windows/registry"
)
// 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.
//
// It's also possible that we could dynamically generate a new GUID per app run, or derive
@@ -29,74 +27,6 @@ var (
procSetActivationCallback = toast.NewProc("SetActivationCallback")
)
-// ConfigureRegistry installs various keys that tell the Windows Runtime details about our
-// application. This is where we install a path to the CustomActivator, which is a binary
-// that Windows will invoke when the toast notification is activated and our process is not
-// running. This can be used to cold start the application from a notification interaction.
-func ConfigureRegistry(appID, appDisplayName, backgroundColor, exePath string) error {
-
- // This first key establishes the declared CLSID GUID within the class registry.
- // We set the LocalServer32 to the exePath. This is the exe that will be invoked
- // by the Windows Runtime when the app is closed; allowing us to do cold starts.
- clsidKey, _, err := registry.CreateKey(registry.CURRENT_USER, filepath.Join("SOFTWARE", "Classes", "CLSID", iNotificationActivationCallbackGUID, "LocalServer32"), registry.SET_VALUE)
- if err != nil {
- return fmt.Errorf("setting the exe path for LocalServer reponse: %w", err)
- }
- if err := clsidKey.SetStringValue("", exePath); err != nil {
- return fmt.Errorf("seting LocalServer32: %w", err)
- }
- if err := clsidKey.Close(); err != nil {
- return fmt.Errorf("closing CLSID key: %w", err)
- }
-
- // This next key establishes details about our application. This is where we map the prior CLSID
- // to be the CustomActivator for our application among other things.
- //
- // Toast Activation -> CustomActivator (get CLSID) -> LocalServer32 (activate exe for CLSID)
- appIDKey, _, err := registry.CreateKey(registry.CURRENT_USER, filepath.Join("SOFTWARE", "Classes", "AppUserModelId", appID), registry.SET_VALUE)
- if err != nil {
- return fmt.Errorf("opening registry: %w", err)
- }
- defer func() {
- err = errors.Join(err, appIDKey.Close())
- }()
- if err := appIDKey.SetStringValue("DisplayName", appDisplayName); err != nil {
- return fmt.Errorf("setting DisplayName: %w", err)
- }
- if err := appIDKey.SetStringValue("IconBackgroundColor", backgroundColor); err != nil {
- return fmt.Errorf("setting IconBackgroundColor: %w", err)
- }
- if err := appIDKey.SetStringValue("CustomActivator", iNotificationActivationCallbackGUID); err != nil {
- return fmt.Errorf("setting CustomActivator: %w", err)
- }
-
- return nil
-}
-
-var initLock sync.Mutex
-var didInitialize bool
-
-// initialize attempts to initialize the Windows Runtime.
-// Each invocation will retry RoInitialize until a successful initialization
-// is achieved. Once initialized, we avoid invoking RoInitialize since subsequent
-// reinitialization generates errors.
-func initialize() (err error) {
- initLock.Lock()
- defer initLock.Unlock()
-
- if didInitialize {
- return nil
- }
-
- if err := ole.RoInitialize(1); err != nil {
- return fmt.Errorf("RoInitialize: %w", err)
- }
-
- didInitialize = true
-
- return nil
-}
-
// GenerateToast notification via the specified xml content.
//
// No validation is performed on this xml content and the caller assumes responsibility
@@ -130,6 +60,13 @@ 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)
@@ -154,11 +91,28 @@ func SetActivationCallback(cb ActivationCallback) {
procSetActivationCallback.Call(callback)
}
-// UserData contains Key:Value pairs generated within the notification, based
-// on the XML content of the notification.
-type UserData struct {
- Key string
- Value string
+var initLock sync.Mutex
+var didInitialize bool
+
+// initialize attempts to initialize the Windows Runtime.
+// Each invocation will retry RoInitialize until a successful initialization
+// is achieved. Once initialized, we avoid invoking RoInitialize since subsequent
+// reinitialization generates errors.
+func initialize() (err error) {
+ initLock.Lock()
+ defer initLock.Unlock()
+
+ if didInitialize {
+ return nil
+ }
+
+ if err := ole.RoInitialize(1); err != nil {
+ return fmt.Errorf("RoInitialize: %w", err)
+ }
+
+ didInitialize = true
+
+ return nil
}
// sliceUserDataFromUnsafe builds a slice of UserData out of an unsafe pointer.
diff --git a/internal/bind/registry.go b/internal/bind/registry.go
@@ -0,0 +1,77 @@
+package bind
+
+import (
+ "errors"
+ "fmt"
+ "path/filepath"
+
+ "golang.org/x/sys/windows/registry"
+)
+
+// AppData describes the application to the Windows Runtime.
+type AppData struct {
+ ID string
+ DisplayName string // optional, if empty the ID will be used
+ ExePath string // optional
+ IconPath string // optional
+ IconBackgroundColor string // optional
+}
+
+// SetAppData teaches the Windows Runtime about our application and establishes the activation GUID
+// so Windows will know how to invoke us back.
+func SetAppData(data AppData) error {
+ if data.ID == "" {
+ return fmt.Errorf("empty app ID")
+ }
+ if data.DisplayName == "" {
+ data.DisplayName = data.ID
+ }
+ appIDKey, _, err := registry.CreateKey(registry.CURRENT_USER, filepath.Join("SOFTWARE", "Classes", "AppUserModelId", data.ID), registry.SET_VALUE)
+ if err != nil {
+ return fmt.Errorf("opening registry: %w", err)
+ }
+ defer func() {
+ err = errors.Join(err, appIDKey.Close())
+ }()
+ if err := appIDKey.SetStringValue("DisplayName", data.DisplayName); err != nil {
+ return fmt.Errorf("setting DisplayName: %w", err)
+ }
+ // 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 {
+ return fmt.Errorf("setting CustomActivator: %w", err)
+ }
+ if data.IconPath != "" {
+ if err := appIDKey.SetStringValue("IconUri", data.IconPath); err != nil {
+ return fmt.Errorf("setting IconUri: %w", err)
+ }
+ if err := appIDKey.SetStringValue("IconBackgroundColor", data.IconBackgroundColor); err != nil {
+ return fmt.Errorf("setting IconBackgroundColor: %w", err)
+ }
+ }
+ if data.ExePath != "" {
+ if err := setActivationExecutable(data.ExePath); err != nil {
+ return fmt.Errorf("setting activation executable: %w", err)
+ }
+ }
+ return nil
+}
+
+// The Windows registry package uses empty string for the "(Default)" key.
+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)
+ if err != nil {
+ return fmt.Errorf("setting the exe path for LocalServer reponse: %w", err)
+ }
+ defer func() {
+ err = errors.Join(err, clsidKey.Close())
+ }()
+ if err := clsidKey.SetStringValue(registryDefaultKey, exe); err != nil {
+ return fmt.Errorf("seting LocalServer32: %w", err)
+ }
+ return nil
+}
diff --git a/toast.go b/toast.go
@@ -175,7 +175,13 @@ func (n *Notification) Push() error {
if err != nil {
return err
}
- if err := bind.ConfigureRegistry(n.AppID, n.AppID, n.IconBackgroundColor, n.ActivationExe); err != nil {
+ if err := bind.SetAppData(bind.AppData{
+ ID: n.AppID,
+ DisplayName: n.AppID,
+ IconPath: n.Icon,
+ IconBackgroundColor: n.IconBackgroundColor,
+ ExePath: n.ActivationExe,
+ }); err != nil {
return fmt.Errorf("configuring registry: %w", err)
}
return bind.GenerateToast(n.AppID, xml)