arcane-os 0.92.0 → 0.94.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/browser-runtime/ai/twin-cloud.mjs +109 -46
- package/browser-runtime/core/client.mjs +20 -0
- package/browser-runtime/core/contracts.mjs +20 -0
- package/browser-runtime/core/native-diagnostics.mjs +217 -0
- package/docs/reference/ai/twin-cloud.md +35 -14
- package/docs/reference/availability-and-normalization.md +1 -1
- package/docs/reference/core/arcane-api.md +32 -0
- package/docs/reference/core/arcane-events.md +25 -0
- package/docs/reference/core-client.md +99 -0
- package/docs/reference/inventory/package-api.json +97 -13
- package/docs/reference/local-ai.md +81 -5
- package/docs/reference/native-diagnostics.md +167 -0
- package/docs/reference/native-speech.md +220 -11
- package/docs/reference/runtime-modules.md +21 -18
- package/docs/reference/sdk-api.md +152 -11
- package/package.json +4 -1
- package/schemas/arcane-app.schema.json +2 -2
- package/schemas/arcane-package.schema.json +2 -2
- package/src/core/app-control.mjs +1 -0
- package/src/core/hosts/windows/ArcaneAppControl.cs +1 -1
- package/src/core/hosts/windows/ArcaneHost.cs +167 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +1 -0
- package/src/core/hosts/windows/ArcaneNativeDiagnostics.cs +137 -0
- package/src/core/hosts/windows/ArcaneNotifications.cs +822 -0
- package/src/core/hosts/windows/ArcaneWindowControl.cs +3 -2
- package/src/core/services/speech.mjs +85 -23
- package/src/core-native-diagnostics-source.mjs +13 -0
- package/src/import-map.mjs +1 -0
- package/src/local-ai/config.mjs +12 -1
- package/src/local-ai/dev.mjs +71 -4
- package/src/local-ai/install.mjs +113 -5
- package/src/local-ai/kokoro/frontend.mjs +219 -0
- package/src/local-ai/kokoro/helper.mjs +168 -0
- package/src/local-ai/kokoro/index.mjs +645 -0
- package/src/local-ai/kokoro/native/CMakeLists.txt +45 -0
- package/src/local-ai/kokoro/native/main.cpp +249 -0
- package/src/native/portable-layout.mjs +67 -9
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Diagnostics;
|
|
4
|
+
using System.IO;
|
|
5
|
+
using System.Runtime.InteropServices;
|
|
6
|
+
using System.Text;
|
|
7
|
+
using System.Threading;
|
|
8
|
+
using System.Threading.Tasks;
|
|
9
|
+
using Microsoft.Win32;
|
|
10
|
+
using Windows.Data.Xml.Dom;
|
|
11
|
+
using Windows.UI.Notifications;
|
|
12
|
+
|
|
13
|
+
namespace Arcane.Core.Hosts.Windows
|
|
14
|
+
{
|
|
15
|
+
/// <summary>
|
|
16
|
+
/// Owns live Windows notifications independently of renderer documents. Native
|
|
17
|
+
/// calls and retained state run on one MTA; caller content is never persisted.
|
|
18
|
+
/// The changed callback must not synchronously wait for this adapter's tasks.
|
|
19
|
+
/// </summary>
|
|
20
|
+
internal sealed class ArcaneNotifications
|
|
21
|
+
{
|
|
22
|
+
private readonly string applicationId;
|
|
23
|
+
private readonly string profileDirectory;
|
|
24
|
+
private readonly string title;
|
|
25
|
+
private readonly string iconPath;
|
|
26
|
+
private readonly Action<Dictionary<string, object>> changed;
|
|
27
|
+
private readonly object gate = new object();
|
|
28
|
+
private readonly Queue<Work> work = new Queue<Work>();
|
|
29
|
+
private readonly Dictionary<string, Notice> notices = new Dictionary<string, Notice>(StringComparer.Ordinal);
|
|
30
|
+
private readonly Dictionary<string, Notice> nativeNotices = new Dictionary<string, Notice>(StringComparer.Ordinal);
|
|
31
|
+
private readonly List<Notice> orderedNotices = new List<Notice>();
|
|
32
|
+
private readonly List<Exception> lifetimeErrors = new List<Exception>();
|
|
33
|
+
private readonly TaskCompletionSource<object> completion = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
34
|
+
private AutoResetEvent wake;
|
|
35
|
+
private Thread worker;
|
|
36
|
+
private bool stopping;
|
|
37
|
+
private bool callbacksOpen = true;
|
|
38
|
+
private bool finishRequested;
|
|
39
|
+
private bool comInitialized;
|
|
40
|
+
private bool initialized;
|
|
41
|
+
private Mutex nativeOwner;
|
|
42
|
+
private bool ownsMutex;
|
|
43
|
+
private uint classCookie;
|
|
44
|
+
private bool classRegistered;
|
|
45
|
+
private ArcaneNotificationCom.Factory factory;
|
|
46
|
+
private ToastNotifier notifier;
|
|
47
|
+
private string nativeApplicationId;
|
|
48
|
+
private Guid nativeClassId;
|
|
49
|
+
private long revision;
|
|
50
|
+
private Exception initializationError;
|
|
51
|
+
private string unavailableReason;
|
|
52
|
+
|
|
53
|
+
internal ArcaneNotifications(string applicationId, string profileDirectory, string title,
|
|
54
|
+
string iconPath, Action<Dictionary<string, object>> changed)
|
|
55
|
+
{
|
|
56
|
+
this.applicationId = applicationId;
|
|
57
|
+
this.profileDirectory = profileDirectory;
|
|
58
|
+
this.title = title;
|
|
59
|
+
this.iconPath = iconPath;
|
|
60
|
+
this.changed = changed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
internal Task<Dictionary<string, object>> InvokeAsync(string method,
|
|
64
|
+
Dictionary<string, object> parameters, CancellationToken cancellation)
|
|
65
|
+
{
|
|
66
|
+
TaskCompletionSource<Dictionary<string, object>> result =
|
|
67
|
+
new TaskCompletionSource<Dictionary<string, object>>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
68
|
+
lock (gate)
|
|
69
|
+
{
|
|
70
|
+
if (stopping)
|
|
71
|
+
{
|
|
72
|
+
result.SetException(new ObjectDisposedException("ArcaneNotifications"));
|
|
73
|
+
return result.Task;
|
|
74
|
+
}
|
|
75
|
+
try
|
|
76
|
+
{
|
|
77
|
+
StartWorker();
|
|
78
|
+
work.Enqueue(new Work(delegate
|
|
79
|
+
{
|
|
80
|
+
try
|
|
81
|
+
{
|
|
82
|
+
cancellation.ThrowIfCancellationRequested();
|
|
83
|
+
Dictionary<string, object> value;
|
|
84
|
+
switch (method)
|
|
85
|
+
{
|
|
86
|
+
case "notifications.status": value = Status(); break;
|
|
87
|
+
case "notifications.state": value = Snapshot(parameters); break;
|
|
88
|
+
case "notifications.show": value = Show(parameters, cancellation); break;
|
|
89
|
+
case "notifications.close": value = CloseNotice(FindNotice(RequiredString(parameters, "id"))); break;
|
|
90
|
+
default: throw Error("NOTIFICATIONS_METHOD_UNKNOWN", "Unknown notification method: " + method);
|
|
91
|
+
}
|
|
92
|
+
result.TrySetResult(value);
|
|
93
|
+
}
|
|
94
|
+
catch (OperationCanceledException)
|
|
95
|
+
{
|
|
96
|
+
if (cancellation.IsCancellationRequested) result.TrySetCanceled();
|
|
97
|
+
else throw;
|
|
98
|
+
}
|
|
99
|
+
catch (Exception error) { result.TrySetException(error); }
|
|
100
|
+
}, delegate(Exception error) { result.TrySetException(error); }));
|
|
101
|
+
wake.Set();
|
|
102
|
+
}
|
|
103
|
+
catch (Exception error) { result.TrySetException(error); }
|
|
104
|
+
}
|
|
105
|
+
return result.Task;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
internal Task CloseAsync()
|
|
109
|
+
{
|
|
110
|
+
lock (gate)
|
|
111
|
+
{
|
|
112
|
+
if (stopping) return completion.Task;
|
|
113
|
+
stopping = true;
|
|
114
|
+
if (worker == null)
|
|
115
|
+
{
|
|
116
|
+
callbacksOpen = false;
|
|
117
|
+
completion.TrySetResult(null);
|
|
118
|
+
return completion.Task;
|
|
119
|
+
}
|
|
120
|
+
work.Enqueue(new Work(Shutdown, RecordLifetimeError));
|
|
121
|
+
wake.Set();
|
|
122
|
+
return completion.Task;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private void StartWorker()
|
|
127
|
+
{
|
|
128
|
+
if (worker != null) return;
|
|
129
|
+
wake = new AutoResetEvent(false);
|
|
130
|
+
worker = new Thread(Run) { IsBackground = true, Name = "Arcane native notifications" };
|
|
131
|
+
try
|
|
132
|
+
{
|
|
133
|
+
worker.SetApartmentState(ApartmentState.MTA);
|
|
134
|
+
worker.Start();
|
|
135
|
+
}
|
|
136
|
+
catch
|
|
137
|
+
{
|
|
138
|
+
worker = null;
|
|
139
|
+
wake.Dispose();
|
|
140
|
+
wake = null;
|
|
141
|
+
throw;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private void Run()
|
|
146
|
+
{
|
|
147
|
+
try
|
|
148
|
+
{
|
|
149
|
+
while (true)
|
|
150
|
+
{
|
|
151
|
+
Work next = null;
|
|
152
|
+
lock (gate)
|
|
153
|
+
{
|
|
154
|
+
if (work.Count != 0) next = work.Dequeue();
|
|
155
|
+
else if (finishRequested) break;
|
|
156
|
+
}
|
|
157
|
+
if (next == null) { wake.WaitOne(); continue; }
|
|
158
|
+
try { next.Run(); }
|
|
159
|
+
catch (Exception error) { next.Reject(error); }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (Exception error)
|
|
163
|
+
{
|
|
164
|
+
RecordLifetimeError(error);
|
|
165
|
+
lock (gate)
|
|
166
|
+
{
|
|
167
|
+
stopping = true;
|
|
168
|
+
callbacksOpen = false;
|
|
169
|
+
while (work.Count != 0) work.Dequeue().Reject(error);
|
|
170
|
+
}
|
|
171
|
+
Shutdown();
|
|
172
|
+
}
|
|
173
|
+
finally
|
|
174
|
+
{
|
|
175
|
+
if (comInitialized) Native.CoUninitialize();
|
|
176
|
+
lock (gate)
|
|
177
|
+
{
|
|
178
|
+
callbacksOpen = false;
|
|
179
|
+
wake.Dispose();
|
|
180
|
+
}
|
|
181
|
+
if (lifetimeErrors.Count == 0) completion.TrySetResult(null);
|
|
182
|
+
else completion.TrySetException(new AggregateException("Native notification shutdown encountered errors.", lifetimeErrors));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private bool QueueCallback(Action action)
|
|
187
|
+
{
|
|
188
|
+
lock (gate)
|
|
189
|
+
{
|
|
190
|
+
if (!callbacksOpen) return false;
|
|
191
|
+
work.Enqueue(new Work(action, RecordLifetimeError));
|
|
192
|
+
wake.Set();
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private Dictionary<string, object> Status()
|
|
198
|
+
{
|
|
199
|
+
TryInitialize();
|
|
200
|
+
Dictionary<string, object> status = new Dictionary<string, object>
|
|
201
|
+
{
|
|
202
|
+
{ "platform", "windows" }, { "applicationId", applicationId },
|
|
203
|
+
{ "supported", initialized ? (object)true : null },
|
|
204
|
+
{ "available", initialized }, { "permissionDisabled", false },
|
|
205
|
+
{ "activation", "live-process" }, { "coldActivation", false },
|
|
206
|
+
{ "revision", revision }, { "errors", LifetimeErrorRecords() }
|
|
207
|
+
};
|
|
208
|
+
if (initialized)
|
|
209
|
+
{
|
|
210
|
+
try
|
|
211
|
+
{
|
|
212
|
+
NotificationSetting setting = notifier.Setting;
|
|
213
|
+
status["setting"] = setting.ToString();
|
|
214
|
+
status["permissionDisabled"] = setting != NotificationSetting.Enabled;
|
|
215
|
+
status["available"] = setting == NotificationSetting.Enabled;
|
|
216
|
+
if (setting != NotificationSetting.Enabled) status["reason"] = "notification-permission-disabled";
|
|
217
|
+
}
|
|
218
|
+
catch (Exception error)
|
|
219
|
+
{
|
|
220
|
+
status["available"] = false;
|
|
221
|
+
status["permissionDisabled"] = null;
|
|
222
|
+
status["reason"] = "notification-settings-unavailable";
|
|
223
|
+
status["error"] = ArcaneHost.ErrorRecord(error, "NOTIFICATIONS_STATUS_FAILED");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else
|
|
227
|
+
{
|
|
228
|
+
status["permissionDisabled"] = null;
|
|
229
|
+
status["reason"] = unavailableReason;
|
|
230
|
+
if (initializationError != null) status["error"] = ArcaneHost.ErrorRecord(initializationError, "NOTIFICATIONS_UNAVAILABLE");
|
|
231
|
+
}
|
|
232
|
+
return status;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private void TryInitialize()
|
|
236
|
+
{
|
|
237
|
+
if (initialized) return;
|
|
238
|
+
initializationError = null;
|
|
239
|
+
unavailableReason = null;
|
|
240
|
+
try
|
|
241
|
+
{
|
|
242
|
+
if (classRegistered || nativeOwner != null)
|
|
243
|
+
{
|
|
244
|
+
List<Exception> cleanup = new List<Exception>();
|
|
245
|
+
ReleaseNativeOwner(cleanup);
|
|
246
|
+
if (cleanup.Count != 0) throw new AggregateException("The previous native notification owner could not be released.", cleanup);
|
|
247
|
+
}
|
|
248
|
+
if (!comInitialized)
|
|
249
|
+
{
|
|
250
|
+
Marshal.ThrowExceptionForHR(Native.CoInitializeEx(IntPtr.Zero, 0));
|
|
251
|
+
comInitialized = true;
|
|
252
|
+
}
|
|
253
|
+
nativeClassId = ReadIdentity();
|
|
254
|
+
nativeApplicationId = "Arcane.Notifications." + nativeClassId.ToString("N");
|
|
255
|
+
nativeOwner = new Mutex(false, "Local\\" + nativeApplicationId);
|
|
256
|
+
try { ownsMutex = nativeOwner.WaitOne(0); }
|
|
257
|
+
catch (AbandonedMutexException) { ownsMutex = true; }
|
|
258
|
+
if (!ownsMutex)
|
|
259
|
+
{
|
|
260
|
+
unavailableReason = "notification-owner-already-running";
|
|
261
|
+
nativeOwner.Dispose();
|
|
262
|
+
nativeOwner = null;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
factory = new ArcaneNotificationCom.Factory(Activate);
|
|
266
|
+
Marshal.ThrowExceptionForHR(Native.CoRegisterClassObject(ref nativeClassId, factory, 4, 1, out classCookie));
|
|
267
|
+
classRegistered = true;
|
|
268
|
+
InstallShortcut();
|
|
269
|
+
RegisterDisplayIdentity();
|
|
270
|
+
notifier = ToastNotificationManager.CreateToastNotifier(nativeApplicationId);
|
|
271
|
+
initialized = true;
|
|
272
|
+
}
|
|
273
|
+
catch (Exception error)
|
|
274
|
+
{
|
|
275
|
+
List<Exception> errors = new List<Exception> { error };
|
|
276
|
+
ReleaseNativeOwner(errors);
|
|
277
|
+
initializationError = errors.Count == 1 ? error : new AggregateException("Native notification initialization and cleanup failed.", errors);
|
|
278
|
+
unavailableReason = "notification-initialization-failed";
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private void RegisterDisplayIdentity()
|
|
283
|
+
{
|
|
284
|
+
// Functional unpackaged-app identity, retained with the profile. This
|
|
285
|
+
// registers no executable launcher and never changes notification settings.
|
|
286
|
+
// https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/send-local-toast-other-apps
|
|
287
|
+
using (RegistryKey key = Registry.CurrentUser.CreateSubKey("Software\\Classes\\AppUserModelId\\" + nativeApplicationId))
|
|
288
|
+
{
|
|
289
|
+
key.SetValue("DisplayName", title ?? applicationId, RegistryValueKind.String);
|
|
290
|
+
key.SetValue("CustomActivator", nativeClassId.ToString("B"), RegistryValueKind.String);
|
|
291
|
+
if (!String.IsNullOrEmpty(iconPath)) key.SetValue("IconUri", iconPath, RegistryValueKind.String);
|
|
292
|
+
else key.DeleteValue("IconUri", false);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private Guid ReadIdentity()
|
|
297
|
+
{
|
|
298
|
+
Directory.CreateDirectory(profileDirectory);
|
|
299
|
+
string identityPath = Path.Combine(profileDirectory, "arcane-notification-identity.json");
|
|
300
|
+
Guid identity = Guid.NewGuid();
|
|
301
|
+
FileStream output;
|
|
302
|
+
try { output = new FileStream(identityPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); }
|
|
303
|
+
catch (IOException)
|
|
304
|
+
{
|
|
305
|
+
if (!File.Exists(identityPath)) throw;
|
|
306
|
+
Dictionary<string, object> saved;
|
|
307
|
+
using (FileStream input = new FileStream(identityPath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
|
308
|
+
using (StreamReader reader = new StreamReader(input))
|
|
309
|
+
saved = ArcaneHost.Serializer().Deserialize<Dictionary<string, object>>(reader.ReadToEnd());
|
|
310
|
+
return Guid.Parse(RequiredString(saved, "identity"));
|
|
311
|
+
}
|
|
312
|
+
using (output)
|
|
313
|
+
using (StreamWriter writer = new StreamWriter(output, new UTF8Encoding(false)))
|
|
314
|
+
writer.Write(ArcaneHost.Serializer().Serialize(new Dictionary<string, object> { { "identity", identity.ToString("D") } }));
|
|
315
|
+
return identity;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private void InstallShortcut()
|
|
319
|
+
{
|
|
320
|
+
string programs = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
|
|
321
|
+
if (String.IsNullOrEmpty(programs)) throw new InvalidOperationException("Windows did not provide the user's Start menu Programs directory.");
|
|
322
|
+
Directory.CreateDirectory(programs);
|
|
323
|
+
string shortcut = Path.Combine(programs, "Arcane Notifications " + nativeClassId.ToString("N") + ".lnk");
|
|
324
|
+
object linkObject = Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid("00021401-0000-0000-C000-000000000046")));
|
|
325
|
+
List<Exception> errors = new List<Exception>();
|
|
326
|
+
try
|
|
327
|
+
{
|
|
328
|
+
IShellLink link = (IShellLink)linkObject;
|
|
329
|
+
using (Process process = Process.GetCurrentProcess()) link.SetPath(process.MainModule.FileName);
|
|
330
|
+
link.SetDescription(title ?? applicationId);
|
|
331
|
+
if (!String.IsNullOrEmpty(iconPath)) link.SetIconLocation(iconPath, 0);
|
|
332
|
+
IPropertyStore properties = (IPropertyStore)linkObject;
|
|
333
|
+
PropertyKey appIdKey = new PropertyKey(5);
|
|
334
|
+
PropertyKey activatorKey = new PropertyKey(26);
|
|
335
|
+
PropertyVariant appId = PropertyVariant.FromString(nativeApplicationId);
|
|
336
|
+
SetProperty(properties, ref appIdKey, ref appId);
|
|
337
|
+
PropertyVariant classId = PropertyVariant.FromGuid(nativeClassId);
|
|
338
|
+
SetProperty(properties, ref activatorKey, ref classId);
|
|
339
|
+
properties.Commit();
|
|
340
|
+
((System.Runtime.InteropServices.ComTypes.IPersistFile)linkObject).Save(shortcut, true);
|
|
341
|
+
}
|
|
342
|
+
catch (Exception error) { errors.Add(error); }
|
|
343
|
+
try { Marshal.ReleaseComObject(linkObject); }
|
|
344
|
+
catch (Exception error) { errors.Add(error); }
|
|
345
|
+
if (errors.Count != 0) throw new AggregateException("Windows notification shortcut registration failed.", errors);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private static void SetProperty(IPropertyStore properties, ref PropertyKey key, ref PropertyVariant value)
|
|
349
|
+
{
|
|
350
|
+
List<Exception> errors = new List<Exception>();
|
|
351
|
+
try { properties.SetValue(ref key, ref value); }
|
|
352
|
+
catch (Exception error) { errors.Add(error); }
|
|
353
|
+
try { Marshal.ThrowExceptionForHR(Native.PropVariantClear(ref value)); }
|
|
354
|
+
catch (Exception error) { errors.Add(error); }
|
|
355
|
+
if (errors.Count != 0) throw new AggregateException("Windows notification shortcut property operation failed.", errors);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private Dictionary<string, object> Show(Dictionary<string, object> parameters, CancellationToken cancellation)
|
|
359
|
+
{
|
|
360
|
+
string id = RequiredString(parameters, "id");
|
|
361
|
+
string notificationTitle = RequiredString(parameters, "title");
|
|
362
|
+
string body = RequiredString(parameters, "body");
|
|
363
|
+
Notice existing;
|
|
364
|
+
if (notices.TryGetValue(id, out existing))
|
|
365
|
+
{
|
|
366
|
+
Exception duplicate = Error("NOTIFICATION_ID_EXISTS", "This notification ID was already accepted during the current host lifetime.");
|
|
367
|
+
duplicate.Data["existing"] = Record(existing);
|
|
368
|
+
throw duplicate;
|
|
369
|
+
}
|
|
370
|
+
object data;
|
|
371
|
+
parameters.TryGetValue("data", out data);
|
|
372
|
+
// A fresh native correlation also keeps clicks on notices from a crashed
|
|
373
|
+
// prior host from being attributed to a new caller with the same ID.
|
|
374
|
+
Notice notice = new Notice(this, id, notificationTitle, body, data, Guid.NewGuid().ToString("N"));
|
|
375
|
+
notices.Add(id, notice);
|
|
376
|
+
nativeNotices.Add(notice.NativeId, notice);
|
|
377
|
+
orderedNotices.Add(notice);
|
|
378
|
+
Publish(notice, "pending", "accepted", null, null);
|
|
379
|
+
try
|
|
380
|
+
{
|
|
381
|
+
TryInitialize();
|
|
382
|
+
if (!initialized)
|
|
383
|
+
{
|
|
384
|
+
Exception unavailable = new InvalidOperationException("Windows notifications are unavailable: " + unavailableReason, initializationError);
|
|
385
|
+
unavailable.Data["code"] = "NOTIFICATIONS_UNAVAILABLE";
|
|
386
|
+
unavailable.Data["reason"] = unavailableReason;
|
|
387
|
+
throw unavailable;
|
|
388
|
+
}
|
|
389
|
+
NotificationSetting setting = notifier.Setting;
|
|
390
|
+
if (setting != NotificationSetting.Enabled)
|
|
391
|
+
{
|
|
392
|
+
Exception disabled = Error("NOTIFICATIONS_PERMISSION_DISABLED", "Windows notifications are disabled: " + setting);
|
|
393
|
+
disabled.Data["setting"] = setting.ToString();
|
|
394
|
+
throw disabled;
|
|
395
|
+
}
|
|
396
|
+
XmlDocument xml = new XmlDocument();
|
|
397
|
+
XmlElement toastElement = xml.CreateElement("toast");
|
|
398
|
+
toastElement.SetAttribute("launch", notice.NativeId);
|
|
399
|
+
xml.AppendChild(toastElement);
|
|
400
|
+
XmlElement visual = xml.CreateElement("visual");
|
|
401
|
+
XmlElement binding = xml.CreateElement("binding");
|
|
402
|
+
binding.SetAttribute("template", "ToastGeneric");
|
|
403
|
+
AddText(xml, binding, notice.Title);
|
|
404
|
+
AddText(xml, binding, notice.Body);
|
|
405
|
+
visual.AppendChild(binding);
|
|
406
|
+
toastElement.AppendChild(visual);
|
|
407
|
+
XmlElement audio = xml.CreateElement("audio");
|
|
408
|
+
audio.SetAttribute("silent", "true");
|
|
409
|
+
toastElement.AppendChild(audio);
|
|
410
|
+
notice.Toast = new ToastNotification(xml) { Tag = notice.NativeId, Group = "arcane" };
|
|
411
|
+
notice.Toast.Activated += notice.Activated;
|
|
412
|
+
notice.ActivatedAttached = true;
|
|
413
|
+
notice.Toast.Dismissed += notice.Dismissed;
|
|
414
|
+
notice.DismissedAttached = true;
|
|
415
|
+
notice.Toast.Failed += notice.Failed;
|
|
416
|
+
notice.FailedAttached = true;
|
|
417
|
+
cancellation.ThrowIfCancellationRequested();
|
|
418
|
+
notice.ShowAttempted = true;
|
|
419
|
+
notifier.Show(notice.Toast);
|
|
420
|
+
// Cancellation after this call cannot retract a possible interaction.
|
|
421
|
+
notice.SubmittedAt = Now();
|
|
422
|
+
Publish(notice, "submitted", "submitted", null, null);
|
|
423
|
+
return Record(notice);
|
|
424
|
+
}
|
|
425
|
+
catch (Exception error)
|
|
426
|
+
{
|
|
427
|
+
List<Exception> errors = new List<Exception> { error };
|
|
428
|
+
// Before Show, Windows cannot own this notice. Once Show has been
|
|
429
|
+
// attempted, retain callbacks until explicit removal or shutdown,
|
|
430
|
+
// including the ambiguous case where that native call throws.
|
|
431
|
+
if (!notice.ShowAttempted) DetachCallbacks(notice, errors);
|
|
432
|
+
Exception failure = errors.Count == 1 ? error : new AggregateException("Notification submission and callback cleanup failed.", errors);
|
|
433
|
+
string state = error is OperationCanceledException ? "cancelled" : "failed";
|
|
434
|
+
Publish(notice, state, state, null, failure);
|
|
435
|
+
failure.Data["notification"] = Record(notice);
|
|
436
|
+
if (!Object.ReferenceEquals(failure, error)) throw failure;
|
|
437
|
+
throw;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private static void AddText(XmlDocument xml, XmlElement binding, string content)
|
|
442
|
+
{
|
|
443
|
+
XmlElement text = xml.CreateElement("text");
|
|
444
|
+
text.AppendChild(xml.CreateTextNode(content));
|
|
445
|
+
binding.AppendChild(text);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private Dictionary<string, object> Snapshot(Dictionary<string, object> parameters)
|
|
449
|
+
{
|
|
450
|
+
object requestedId;
|
|
451
|
+
bool filtered = parameters != null && parameters.TryGetValue("id", out requestedId);
|
|
452
|
+
string id = filtered ? RequiredString(parameters, "id") : null;
|
|
453
|
+
List<object> records = new List<object>();
|
|
454
|
+
foreach (Notice notice in orderedNotices)
|
|
455
|
+
if (!filtered || notice.Id == id) records.Add(Record(notice));
|
|
456
|
+
return new Dictionary<string, object>
|
|
457
|
+
{
|
|
458
|
+
{ "revision", revision }, { "notifications", records }, { "errors", LifetimeErrorRecords() }
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private Notice FindNotice(string id)
|
|
463
|
+
{
|
|
464
|
+
Notice notice;
|
|
465
|
+
if (!notices.TryGetValue(id, out notice)) throw Error("NOTIFICATION_NOT_FOUND", "No notification has this ID in the current host lifetime.");
|
|
466
|
+
return notice;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private Dictionary<string, object> CloseNotice(Notice notice)
|
|
470
|
+
{
|
|
471
|
+
if (notice.Closed) return Record(notice);
|
|
472
|
+
List<Exception> errors = new List<Exception>();
|
|
473
|
+
notice.CloseRequestedAt = Now();
|
|
474
|
+
if (notice.ShowAttempted && notice.Toast != null && notifier != null)
|
|
475
|
+
{
|
|
476
|
+
try { notifier.Hide(notice.Toast); }
|
|
477
|
+
catch (Exception error) { errors.Add(error); }
|
|
478
|
+
try { ToastNotificationManager.History.Remove(notice.NativeId, "arcane", nativeApplicationId); }
|
|
479
|
+
catch (Exception error) { errors.Add(error); }
|
|
480
|
+
}
|
|
481
|
+
// Successful native removal retires per-toast subscriptions. Already
|
|
482
|
+
// queued events still drain, and the host-level COM correlation remains
|
|
483
|
+
// available for a click that raced removal.
|
|
484
|
+
if (errors.Count == 0) DetachCallbacks(notice, errors);
|
|
485
|
+
if (errors.Count != 0)
|
|
486
|
+
{
|
|
487
|
+
AggregateException failure = new AggregateException("Windows could not complete notification removal.", errors);
|
|
488
|
+
Publish(notice, "closeFailed", "closeFailed", null, failure);
|
|
489
|
+
failure.Data["notification"] = Record(notice);
|
|
490
|
+
throw failure;
|
|
491
|
+
}
|
|
492
|
+
notice.Closed = true;
|
|
493
|
+
notice.ClosedAt = Now();
|
|
494
|
+
Publish(notice, "closed", "closed", "application", null);
|
|
495
|
+
return Record(notice);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private void Activated(Notice notice)
|
|
499
|
+
{
|
|
500
|
+
if (notice.ActivationObserved) return;
|
|
501
|
+
notice.ActivationObserved = true;
|
|
502
|
+
Publish(notice, notice.Closed ? notice.State : "activated", "activated", null, null);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private bool Activate(string aumid, string arguments)
|
|
506
|
+
{
|
|
507
|
+
return QueueCallback(delegate
|
|
508
|
+
{
|
|
509
|
+
Notice notice;
|
|
510
|
+
if (aumid == nativeApplicationId && arguments != null && nativeNotices.TryGetValue(arguments, out notice)) Activated(notice);
|
|
511
|
+
else throw new InvalidOperationException("Windows notification activation did not identify a notice owned by this host.");
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private void Publish(Notice notice, string state, string eventName, string reason, Exception error)
|
|
516
|
+
{
|
|
517
|
+
notice.State = state;
|
|
518
|
+
notice.Event = eventName;
|
|
519
|
+
notice.Revision = ++revision;
|
|
520
|
+
notice.UpdatedAt = Now();
|
|
521
|
+
notice.Reason = reason;
|
|
522
|
+
if (error != null) notice.Error = ArcaneHost.ErrorRecord(error, "NOTIFICATION_FAILED");
|
|
523
|
+
Dictionary<string, object> observed = new Dictionary<string, object>
|
|
524
|
+
{
|
|
525
|
+
{ "event", eventName }, { "revision", revision }, { "timestamp", notice.UpdatedAt }
|
|
526
|
+
};
|
|
527
|
+
if (reason != null) observed["reason"] = reason;
|
|
528
|
+
if (error != null) observed["error"] = notice.Error;
|
|
529
|
+
notice.Events.Add(observed);
|
|
530
|
+
if (changed == null) return;
|
|
531
|
+
try { changed(Record(notice)); }
|
|
532
|
+
catch (Exception deliveryError)
|
|
533
|
+
{
|
|
534
|
+
notice.DeliveryErrors.Add(ArcaneHost.ErrorRecord(deliveryError, "NOTIFICATION_EVENT_DELIVERY_FAILED"));
|
|
535
|
+
RecordLifetimeError(deliveryError);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private Dictionary<string, object> Record(Notice notice)
|
|
540
|
+
{
|
|
541
|
+
Dictionary<string, object> record = new Dictionary<string, object>
|
|
542
|
+
{
|
|
543
|
+
{ "id", notice.Id }, { "title", notice.Title }, { "body", notice.Body }, { "data", notice.Data },
|
|
544
|
+
{ "state", notice.State }, { "event", notice.Event }, { "revision", notice.Revision },
|
|
545
|
+
{ "createdAt", notice.CreatedAt }, { "updatedAt", notice.UpdatedAt },
|
|
546
|
+
{ "events", new List<Dictionary<string, object>>(notice.Events) }
|
|
547
|
+
};
|
|
548
|
+
if (notice.SubmittedAt != null) record["submittedAt"] = notice.SubmittedAt;
|
|
549
|
+
if (notice.CloseRequestedAt != null) record["closeRequestedAt"] = notice.CloseRequestedAt;
|
|
550
|
+
if (notice.ClosedAt != null) record["closedAt"] = notice.ClosedAt;
|
|
551
|
+
if (notice.Reason != null) record["reason"] = notice.Reason;
|
|
552
|
+
if (notice.Error != null) record["error"] = notice.Error;
|
|
553
|
+
if (notice.DeliveryErrors.Count != 0) record["deliveryErrors"] = new List<object>(notice.DeliveryErrors);
|
|
554
|
+
return record;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private void Shutdown()
|
|
558
|
+
{
|
|
559
|
+
try
|
|
560
|
+
{
|
|
561
|
+
foreach (Notice notice in orderedNotices)
|
|
562
|
+
{
|
|
563
|
+
try { CloseNotice(notice); }
|
|
564
|
+
catch (Exception error) { RecordLifetimeError(error); }
|
|
565
|
+
DetachCallbacks(notice, lifetimeErrors);
|
|
566
|
+
}
|
|
567
|
+
ReleaseNativeOwner(lifetimeErrors);
|
|
568
|
+
}
|
|
569
|
+
finally
|
|
570
|
+
{
|
|
571
|
+
lock (gate)
|
|
572
|
+
{
|
|
573
|
+
callbacksOpen = false;
|
|
574
|
+
finishRequested = true;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private static void DetachCallbacks(Notice notice, List<Exception> errors)
|
|
580
|
+
{
|
|
581
|
+
if (notice.Toast == null) return;
|
|
582
|
+
try
|
|
583
|
+
{
|
|
584
|
+
if (notice.ActivatedAttached) notice.Toast.Activated -= notice.Activated;
|
|
585
|
+
notice.ActivatedAttached = false;
|
|
586
|
+
}
|
|
587
|
+
catch (Exception error) { errors.Add(error); }
|
|
588
|
+
try
|
|
589
|
+
{
|
|
590
|
+
if (notice.DismissedAttached) notice.Toast.Dismissed -= notice.Dismissed;
|
|
591
|
+
notice.DismissedAttached = false;
|
|
592
|
+
}
|
|
593
|
+
catch (Exception error) { errors.Add(error); }
|
|
594
|
+
try
|
|
595
|
+
{
|
|
596
|
+
if (notice.FailedAttached) notice.Toast.Failed -= notice.Failed;
|
|
597
|
+
notice.FailedAttached = false;
|
|
598
|
+
}
|
|
599
|
+
catch (Exception error) { errors.Add(error); }
|
|
600
|
+
if (!notice.ActivatedAttached && !notice.DismissedAttached && !notice.FailedAttached) notice.Toast = null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private void ReleaseNativeOwner(List<Exception> errors)
|
|
604
|
+
{
|
|
605
|
+
initialized = false;
|
|
606
|
+
notifier = null;
|
|
607
|
+
if (classRegistered)
|
|
608
|
+
{
|
|
609
|
+
try { Marshal.ThrowExceptionForHR(Native.CoRevokeClassObject(classCookie)); classRegistered = false; }
|
|
610
|
+
catch (Exception error) { errors.Add(error); }
|
|
611
|
+
}
|
|
612
|
+
if (!classRegistered) factory = null;
|
|
613
|
+
if (ownsMutex && !classRegistered)
|
|
614
|
+
{
|
|
615
|
+
try { nativeOwner.ReleaseMutex(); ownsMutex = false; }
|
|
616
|
+
catch (Exception error) { errors.Add(error); }
|
|
617
|
+
}
|
|
618
|
+
if (nativeOwner != null && !ownsMutex)
|
|
619
|
+
{
|
|
620
|
+
try { nativeOwner.Dispose(); nativeOwner = null; }
|
|
621
|
+
catch (Exception error) { errors.Add(error); }
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
private void RecordLifetimeError(Exception error) { lifetimeErrors.Add(error); }
|
|
626
|
+
private List<object> LifetimeErrorRecords()
|
|
627
|
+
{
|
|
628
|
+
List<object> errors = new List<object>();
|
|
629
|
+
foreach (Exception error in lifetimeErrors) errors.Add(ArcaneHost.ErrorRecord(error, "NOTIFICATIONS_LIFETIME_FAILED"));
|
|
630
|
+
return errors;
|
|
631
|
+
}
|
|
632
|
+
private static string Now() { return DateTimeOffset.UtcNow.ToString("O", System.Globalization.CultureInfo.InvariantCulture); }
|
|
633
|
+
|
|
634
|
+
private static string RequiredString(Dictionary<string, object> parameters, string name)
|
|
635
|
+
{
|
|
636
|
+
object value;
|
|
637
|
+
if (parameters == null || !parameters.TryGetValue(name, out value) || !(value is string))
|
|
638
|
+
throw Error("NOTIFICATIONS_ARGUMENT_REQUIRED", "Notification field '" + name + "' must be a string.");
|
|
639
|
+
return (string)value;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private static Exception Error(string code, string message)
|
|
643
|
+
{
|
|
644
|
+
InvalidOperationException error = new InvalidOperationException(message);
|
|
645
|
+
error.Data["code"] = code;
|
|
646
|
+
return error;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private sealed class Work
|
|
650
|
+
{
|
|
651
|
+
internal readonly Action Run;
|
|
652
|
+
internal readonly Action<Exception> Reject;
|
|
653
|
+
internal Work(Action run, Action<Exception> reject) { Run = run; Reject = reject; }
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private sealed class Notice
|
|
657
|
+
{
|
|
658
|
+
private readonly ArcaneNotifications owner;
|
|
659
|
+
internal readonly string Id;
|
|
660
|
+
internal readonly string Title;
|
|
661
|
+
internal readonly string Body;
|
|
662
|
+
internal readonly object Data;
|
|
663
|
+
internal readonly string NativeId;
|
|
664
|
+
internal readonly string CreatedAt = Now();
|
|
665
|
+
internal readonly List<Dictionary<string, object>> Events = new List<Dictionary<string, object>>();
|
|
666
|
+
internal readonly List<object> DeliveryErrors = new List<object>();
|
|
667
|
+
internal ToastNotification Toast;
|
|
668
|
+
internal bool ActivatedAttached;
|
|
669
|
+
internal bool DismissedAttached;
|
|
670
|
+
internal bool FailedAttached;
|
|
671
|
+
internal bool ShowAttempted;
|
|
672
|
+
internal bool ActivationObserved;
|
|
673
|
+
internal bool Closed;
|
|
674
|
+
internal string State;
|
|
675
|
+
internal string Event;
|
|
676
|
+
internal long Revision;
|
|
677
|
+
internal string UpdatedAt;
|
|
678
|
+
internal string SubmittedAt;
|
|
679
|
+
internal string CloseRequestedAt;
|
|
680
|
+
internal string ClosedAt;
|
|
681
|
+
internal string Reason;
|
|
682
|
+
internal object Error;
|
|
683
|
+
|
|
684
|
+
internal Notice(ArcaneNotifications owner, string id, string title, string body, object data, string nativeId)
|
|
685
|
+
{
|
|
686
|
+
this.owner = owner;
|
|
687
|
+
Id = id; Title = title; Body = body; Data = data; NativeId = nativeId;
|
|
688
|
+
}
|
|
689
|
+
internal void Activated(ToastNotification sender, object arguments)
|
|
690
|
+
{
|
|
691
|
+
owner.QueueCallback(delegate { owner.Activated(this); });
|
|
692
|
+
}
|
|
693
|
+
internal void Dismissed(ToastNotification sender, ToastDismissedEventArgs arguments)
|
|
694
|
+
{
|
|
695
|
+
owner.QueueCallback(delegate { owner.Publish(this, Closed ? State : "dismissed", "dismissed", arguments.Reason.ToString(), null); });
|
|
696
|
+
}
|
|
697
|
+
internal void Failed(ToastNotification sender, ToastFailedEventArgs arguments)
|
|
698
|
+
{
|
|
699
|
+
owner.QueueCallback(delegate { owner.Publish(this, Closed ? State : "failed", "failed", null, arguments.ErrorCode); });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
704
|
+
private struct PropertyKey
|
|
705
|
+
{
|
|
706
|
+
internal Guid Format;
|
|
707
|
+
internal uint Id;
|
|
708
|
+
internal PropertyKey(uint id) { Format = new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"); Id = id; }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// The union layout follows the native PROPVARIANT ABI, including its blob member.
|
|
712
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
713
|
+
private struct VariantBlob { internal uint Count; internal IntPtr Pointer; }
|
|
714
|
+
[StructLayout(LayoutKind.Explicit)]
|
|
715
|
+
private struct PropertyVariant
|
|
716
|
+
{
|
|
717
|
+
[FieldOffset(0)] internal ushort Type;
|
|
718
|
+
[FieldOffset(8)] internal IntPtr Pointer;
|
|
719
|
+
[FieldOffset(8)] internal VariantBlob Blob;
|
|
720
|
+
internal static PropertyVariant FromString(string value)
|
|
721
|
+
{
|
|
722
|
+
return new PropertyVariant { Type = 31, Pointer = Marshal.StringToCoTaskMemUni(value) };
|
|
723
|
+
}
|
|
724
|
+
internal static PropertyVariant FromGuid(Guid value)
|
|
725
|
+
{
|
|
726
|
+
IntPtr memory = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(Guid)));
|
|
727
|
+
Marshal.StructureToPtr(value, memory, false);
|
|
728
|
+
return new PropertyVariant { Type = 72, Pointer = memory };
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
[ComImport, Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
733
|
+
private interface IPropertyStore
|
|
734
|
+
{
|
|
735
|
+
void GetCount(out uint count);
|
|
736
|
+
void GetAt(uint index, out PropertyKey key);
|
|
737
|
+
void GetValue(ref PropertyKey key, out PropertyVariant value);
|
|
738
|
+
void SetValue(ref PropertyKey key, ref PropertyVariant value);
|
|
739
|
+
void Commit();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
[ComImport, Guid("000214F9-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
743
|
+
private interface IShellLink
|
|
744
|
+
{
|
|
745
|
+
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder path, int capacity, IntPtr data, uint flags);
|
|
746
|
+
void GetIDList(out IntPtr list);
|
|
747
|
+
void SetIDList(IntPtr list);
|
|
748
|
+
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder description, int capacity);
|
|
749
|
+
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string description);
|
|
750
|
+
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder directory, int capacity);
|
|
751
|
+
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string directory);
|
|
752
|
+
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder arguments, int capacity);
|
|
753
|
+
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string arguments);
|
|
754
|
+
void GetHotkey(out short key);
|
|
755
|
+
void SetHotkey(short key);
|
|
756
|
+
void GetShowCmd(out int command);
|
|
757
|
+
void SetShowCmd(int command);
|
|
758
|
+
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder path, int capacity, out int index);
|
|
759
|
+
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string path, int index);
|
|
760
|
+
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string path, uint reserved);
|
|
761
|
+
void Resolve(IntPtr window, uint flags);
|
|
762
|
+
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string path);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private static class Native
|
|
766
|
+
{
|
|
767
|
+
[DllImport("ole32.dll")] internal static extern int CoInitializeEx(IntPtr reserved, uint flags);
|
|
768
|
+
[DllImport("ole32.dll")] internal static extern void CoUninitialize();
|
|
769
|
+
[DllImport("ole32.dll")] internal static extern int CoRegisterClassObject(ref Guid clsid,
|
|
770
|
+
[MarshalAs(UnmanagedType.Interface)] ArcaneNotificationCom.IClassFactory factory, uint context, uint flags, out uint cookie);
|
|
771
|
+
[DllImport("ole32.dll")] internal static extern int CoRevokeClassObject(uint cookie);
|
|
772
|
+
[DllImport("ole32.dll")] internal static extern int PropVariantClear(ref PropertyVariant value);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// COM callable interfaces must be public CLR types. The adapter and its
|
|
777
|
+
// managed construction remain internal; these types implement only the OS ABI.
|
|
778
|
+
public static class ArcaneNotificationCom
|
|
779
|
+
{
|
|
780
|
+
[ComVisible(true), ClassInterface(ClassInterfaceType.None)]
|
|
781
|
+
public sealed class Factory : IClassFactory
|
|
782
|
+
{
|
|
783
|
+
private readonly Callback callback;
|
|
784
|
+
internal Factory(Func<string, string, bool> activate) { callback = new Callback(activate); }
|
|
785
|
+
public int CreateInstance(IntPtr outer, ref Guid iid, out IntPtr instance)
|
|
786
|
+
{
|
|
787
|
+
instance = IntPtr.Zero;
|
|
788
|
+
if (outer != IntPtr.Zero) return unchecked((int)0x80040110);
|
|
789
|
+
IntPtr unknown = Marshal.GetIUnknownForObject(callback);
|
|
790
|
+
try { return Marshal.QueryInterface(unknown, ref iid, out instance); }
|
|
791
|
+
finally { Marshal.Release(unknown); }
|
|
792
|
+
}
|
|
793
|
+
public int LockServer(bool locked) { return 0; }
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
[ComVisible(true), ClassInterface(ClassInterfaceType.None)]
|
|
797
|
+
public sealed class Callback : INotificationActivationCallback
|
|
798
|
+
{
|
|
799
|
+
private readonly Func<string, string, bool> activate;
|
|
800
|
+
internal Callback(Func<string, string, bool> activate) { this.activate = activate; }
|
|
801
|
+
public int Activate(string aumid, string arguments, IntPtr inputs, uint count)
|
|
802
|
+
{
|
|
803
|
+
try { return activate(aumid, arguments) ? 0 : unchecked((int)0x800401FD); }
|
|
804
|
+
catch (Exception error) { return Marshal.GetHRForException(error); }
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
[ComVisible(true), Guid("00000001-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
809
|
+
public interface IClassFactory
|
|
810
|
+
{
|
|
811
|
+
[PreserveSig] int CreateInstance(IntPtr outer, ref Guid iid, out IntPtr instance);
|
|
812
|
+
[PreserveSig] int LockServer([MarshalAs(UnmanagedType.Bool)] bool locked);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
[ComVisible(true), Guid("53E31837-6600-4A81-9395-75CFFE746F94"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
816
|
+
public interface INotificationActivationCallback
|
|
817
|
+
{
|
|
818
|
+
[PreserveSig] int Activate([MarshalAs(UnmanagedType.LPWStr)] string aumid,
|
|
819
|
+
[MarshalAs(UnmanagedType.LPWStr)] string arguments, IntPtr inputs, uint count);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|