bind_windows.go (5282B)
1 //go:build windows 2 3 package wintoast 4 5 import ( 6 "errors" 7 "fmt" 8 "io" 9 "os" 10 "os/exec" 11 "sync/atomic" 12 "syscall" 13 "unsafe" 14 15 "git.sr.ht/~jackmordaunt/go-toast/v2/internal/winrt/data/xml/dom" 16 "git.sr.ht/~jackmordaunt/go-toast/v2/internal/winrt/ui/notifications" 17 "git.sr.ht/~jackmordaunt/go-toast/v2/tmpl" 18 "github.com/go-ole/go-ole" 19 "golang.org/x/sys/windows" 20 ) 21 22 func pushPowershell(xml string) error { 23 f, err := os.CreateTemp("", "*.ps1") 24 if err != nil { 25 return fmt.Errorf("creating temporary script file: %w", err) 26 } 27 28 defer func() { err = errors.Join(err, os.Remove(f.Name())) }() 29 30 // This BOM ensures we can support non-ascii characters in the toast content. 31 bomUtf8 := []byte{0xef, 0xbb, 0xbf} 32 if _, err := f.Write(bomUtf8); err != nil { 33 return fmt.Errorf("writing utf8 byte marker: %w", err) 34 } 35 36 if err := buildPowershell(xml, f); err != nil { 37 return fmt.Errorf("generating powershell script: %w", err) 38 } 39 40 if err := f.Close(); err != nil { 41 return fmt.Errorf("closing script file: %w", err) 42 } 43 44 cmd := exec.Command("PowerShell", "-ExecutionPolicy", "Bypass", "-File", f.Name()) 45 cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 46 if out, err := cmd.CombinedOutput(); err != nil { 47 return fmt.Errorf("executing powershell: %q: %w", string(out), err) 48 } 49 50 return nil 51 } 52 53 func buildPowershell(xml string, w io.Writer) error { 54 type scriptData struct { 55 AppID string 56 XML string 57 } 58 return tmpl.ScriptTemplate.Execute(w, scriptData{AppID: appData.AppID, XML: xml}) 59 } 60 61 // HRESULT E_NOINTERFACE 62 const errNoInterface = 0x80004002 63 64 var comDisabled atomic.Bool 65 66 func pushCOM(appID, xml string) (err error) { 67 if comDisabled.Load() { 68 return nil 69 } 70 71 defer func() { 72 // On Windows 7 WinRT interfaces can be stubbed out, and fail to produce 73 // error values. This leads to a panic when trying to use the interface. 74 // This recover transforms such panics back into an error value for the 75 // caller. 76 // 77 // If the error is "interface not supported" we will permanently disable 78 // this API henceforth. 79 if v := recover(); v != nil { 80 if verr, ok := v.(error); ok { 81 err = verr 82 } 83 if oleErr, ok := v.(*ole.OleError); ok { 84 if oleErr.Code() == errNoInterface { 85 comDisabled.Store(true) 86 } 87 } 88 } 89 }() 90 91 if err := initialize(); err != nil { 92 return err 93 } 94 95 if err := registerClassFactory(ClassFactory); err != nil { 96 return fmt.Errorf("registering class factory: %w", err) 97 } 98 99 doc, err := dom.NewXmlDocument() 100 if err != nil { 101 return fmt.Errorf("dom.NewXmlDocument(): %w", err) 102 } 103 104 defer doc.Release() 105 106 if err := doc.LoadXml(xml); err != nil { 107 return fmt.Errorf("doc.LoadXml(tmpl): %w", err) 108 } 109 110 manager, err := notifications.GetDefault() 111 if err != nil { 112 return fmt.Errorf("notifications.GetDefault(): %w", err) 113 } 114 115 defer manager.Release() 116 117 notifier, err := manager.CreateToastNotifierWithId(appID) 118 if err != nil { 119 return fmt.Errorf("manager.CreateToastNotifier(%q): %w", appID, err) 120 } 121 122 defer notifier.Release() 123 124 toast, err := notifications.CreateToastNotification(doc) 125 if err != nil { 126 return fmt.Errorf("notifications.CreateToastNotification(doc): %w", err) 127 } 128 129 defer toast.Release() 130 131 if err := notifier.Show(toast); err != nil { 132 return fmt.Errorf("notifier.Show(): %w", err) 133 } 134 135 return nil 136 } 137 138 func setAppData(data AppData) (err error) { 139 appDataMu.Lock() 140 defer appDataMu.Unlock() 141 142 // Early out if we have already set this data. 143 // 144 // In the case the data is empty, we don't want to overrite 145 // all of the registry entries to empty. 146 // 147 // This allows the caller to either globally set the app data 148 // or provide it per notification. 149 if appData == data || data.AppID == "" { 150 return nil 151 } 152 153 if data.GUID != "" { 154 GUID_ImplNotificationActivationCallback = ole.NewGUID(data.GUID) 155 } 156 157 // Keep a copy of the saved data for later. 158 defer func() { 159 if err == nil { 160 appData = data 161 } 162 }() 163 164 if err := setAppDataFunc(data); err != nil { 165 return err 166 } 167 168 return nil 169 } 170 171 var initialized atomic.Bool 172 173 // initialize attempts to initialize the Windows Runtime. 174 // Each invocation will retry RoInitialize until a successful initialization 175 // is achieved. Once initialized, we avoid invoking RoInitialize since subsequent 176 // reinitialization generates errors. 177 func initialize() (err error) { 178 if initialized.CompareAndSwap(false, true) { 179 if err := ole.RoInitialize(1); err != nil { 180 return fmt.Errorf("RoInitialize: %w", err) 181 } 182 } 183 return nil 184 } 185 186 // sliceUserDataFromUnsafe builds a slice of UserData out of an unsafe pointer. 187 func sliceUserDataFromUnsafe(ptr unsafe.Pointer, count int) []UserData { 188 // Layout mirrors the memory layout of the C struct that contains this data. 189 // I'm not sure if there's special alignment or packing - though I don't notice 190 // anything in the definition to indicate as such. 191 type layout struct { 192 Key unsafe.Pointer 193 Value unsafe.Pointer 194 } 195 196 // Create a new slice with the appropriate length 197 out := make([]UserData, count) 198 199 // Create a slice with the unsafe data layout. 200 tmp := unsafe.Slice((*layout)(ptr), count) 201 202 // Convert the unsafe layout to safe strings. 203 for ii, it := range tmp { 204 out[ii] = UserData{ 205 Key: windows.UTF16PtrToString((*uint16)(it.Key)), 206 Value: windows.UTF16PtrToString((*uint16)(it.Value)), 207 } 208 } 209 210 return out 211 }