Move remote deployment to a vendored source bundle built on the target host via Docker so redeploys no longer require local cross-compilation or host Go installation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
111 lines
3.1 KiB
Go
111 lines
3.1 KiB
Go
//go:build windows
|
|
|
|
// This file contains registry manipulation code.
|
|
// This logic is orthogonal to, but works in tandem with the COM code; since the
|
|
// Windows Runtime uses the registry as it's primary source of state.
|
|
package wintoast
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"golang.org/x/sys/windows/registry"
|
|
)
|
|
|
|
var (
|
|
// allows diffing the new call from the previous so that we can early-out,
|
|
// and avoid touching the registry more than necessary.
|
|
// It also allows empty app data to be supplied to the Notifcation type,
|
|
// without erasing the data that has been set via the global function.
|
|
appData AppData
|
|
appDataMu sync.Mutex
|
|
)
|
|
|
|
// Overridden in testing.
|
|
var (
|
|
writeStringValue = writeStringValueImpl
|
|
setAppDataFunc = setAppDataImpl
|
|
)
|
|
|
|
var (
|
|
// appKeyRoot is the root path for app metadata.
|
|
appKeyRoot = filepath.Join("SOFTWARE", "Classes", "AppUserModelId")
|
|
// activationKey is the root path to the activation executable.
|
|
activationKey = filepath.Join("SOFTWARE", "Classes", "CLSID", GUID_ImplNotificationActivationCallback.String(), "LocalServer32")
|
|
)
|
|
|
|
// The Windows registry package uses empty string for the "(Default)" key.
|
|
const registryDefaultKey string = ""
|
|
|
|
func setAppDataImpl(data AppData) error {
|
|
if data.AppID == "" {
|
|
return fmt.Errorf("empty app ID")
|
|
}
|
|
|
|
appKey := filepath.Join(appKeyRoot, data.AppID)
|
|
|
|
if err := writeStringValue(appKey, "DisplayName", data.AppID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// CustomActivator teaches Window what COM class to use as the callback when
|
|
// a toast notification is activated.
|
|
if err := writeStringValue(appKey, "CustomActivator", GUID_ImplNotificationActivationCallback.String()); err != nil {
|
|
return err
|
|
}
|
|
|
|
if data.IconPath != "" {
|
|
if err := writeStringValue(appKey, "IconUri", data.IconPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if data.IconBackgroundColor != "" {
|
|
if err := writeStringValue(appKey, "IconBackgroundColor", data.IconBackgroundColor); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if data.ActivationExe != "" {
|
|
if err := writeStringValue(activationKey, registryDefaultKey, data.ActivationExe); err != nil {
|
|
return fmt.Errorf("setting activation executable: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeStringValue writes a string value to the path, where name is the subkey and
|
|
// value is the literal value.
|
|
func writeStringValueImpl(path, name, value string) error {
|
|
if keyExists(path, name) {
|
|
return nil
|
|
}
|
|
key, _, err := registry.CreateKey(registry.CURRENT_USER, path, registry.SET_VALUE)
|
|
if err != nil {
|
|
return fmt.Errorf("opening registry key: %s: %w", path, err)
|
|
}
|
|
if err := key.SetStringValue(name, value); err != nil {
|
|
return fmt.Errorf("setting string value: (%s) %s=%s: %w", path, name, value, err)
|
|
}
|
|
if err := key.Close(); err != nil {
|
|
return fmt.Errorf("closing key: %s: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// keyExists returns true if the key exists.
|
|
func keyExists(path, name string) bool {
|
|
key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer key.Close()
|
|
v, _, err := key.GetStringValue(name)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return v != ""
|
|
}
|