Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
GatewayService.cs
1using System;
3using System.IO;
4using System.Net;
7using System.ServiceProcess;
8using System.Text;
9using System.Threading;
10using System.Threading.Tasks;
11using Waher.Content;
13using Waher.Events;
18
19
20#pragma warning disable CA1416 // Validate platform compatibility
21
23{
27 public class GatewayService : ServiceBase
28 {
29 private bool autoPaused = false;
30 private bool starting = false;
31
37 public GatewayService(string ServiceName, string InstanceName)
38 : base()
39 {
40 this.ServiceName = ServiceName;
41 if (!string.IsNullOrEmpty(InstanceName))
42 this.ServiceName += " " + InstanceName;
43
44 this.AutoLog = true;
45 this.CanHandlePowerEvent = true;
46 this.CanHandleSessionChangeEvent = true;
47 this.CanPauseAndContinue = true;
48 this.CanShutdown = true;
49 this.CanStop = true;
50 }
51
53 protected override void OnStart(string[] args)
54 {
55 try
56 {
57 bool Started;
58
59 if (this.starting)
60 Started = false;
61 else
62 {
63 using PendingTimer Timer = new(this);
64
65 Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
66
67 Gateway.GetDatabaseProvider += Program.GetDatabase;
68 Gateway.RegistrationSuccessful += Program.RegistrationSuccessful;
69 Gateway.OnTerminate += this.TerminateService;
70
71 this.starting = true;
72 try
73 {
74 Started = Gateway.Start(false, true, Program.InstanceName).Result;
75 Types.SetModuleParameter("SERVICE_NAME", this.ServiceName);
76 }
77 finally
78 {
79 this.starting = false;
80 }
81 }
82
83 if (!Started)
84 {
85 Log.Alert("Gateway being started in another process.");
86 ThreadPool.QueueUserWorkItem(_ => this.Stop());
87 return;
88 }
89 }
90 catch (Exception ex)
91 {
92 this.ExitCode = 1;
93 Log.Alert(ex);
94 }
95 }
96
97 private Task TerminateService(object Sender, EventArgs e)
98 {
99 this.ExitCode = 1;
100 ThreadPool.QueueUserWorkItem(_ => this.Stop());
101 return Task.CompletedTask;
102 }
103
104 private class PendingTimer : IDisposable
105 {
106 private readonly GatewayService service;
107 private Timer timer;
108 private bool disposed = false;
109
110 public PendingTimer(GatewayService Service)
111 {
112 this.service = Service;
113 this.timer = new Timer(this.MoreTime, null, 0, 1000);
114 }
115
116 public void Dispose()
117 {
118 this.disposed = true;
119 this.timer?.Dispose();
120 this.timer = null;
121 }
122
123 private void MoreTime(object State)
124 {
125 if (!this.disposed)
126 {
127 try
128 {
129 this.service.RequestAdditionalTime(2000);
130 }
131 catch (InvalidOperationException)
132 {
133 this.timer?.Dispose();
134 this.timer = null;
135 }
136 catch (Exception)
137 {
138 // Ignore
139 }
140 }
141 }
142 }
143
145 protected override void OnPause()
146 {
147 this.OnStop();
148 }
149
151 protected override void OnContinue()
152 {
153 try
154 {
155 bool Started;
156
157 if (this.starting)
158 Started = false;
159 else
160 {
161 using PendingTimer Timer = new(this);
162
163 Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
164
165 Gateway.GetDatabaseProvider += Program.GetDatabase;
166 Gateway.RegistrationSuccessful += Program.RegistrationSuccessful;
167 Gateway.OnTerminate += this.TerminateService;
168
169 this.starting = true;
170 try
171 {
172 Started = Gateway.Start(false, true, Program.InstanceName).Result;
173 }
174 finally
175 {
176 this.starting = false;
177 }
178 }
179
180 if (!Started)
181 {
182 Log.Alert("Gateway being started in another process.");
183 ThreadPool.QueueUserWorkItem(_ => this.Stop());
184 return;
185 }
186 }
187 catch (Exception ex)
188 {
189 this.ExitCode = 1;
190 Log.Alert(ex);
191 }
192 }
193
195 protected override bool OnPowerEvent(PowerBroadcastStatus powerStatus)
196 {
197 try
198 {
199 switch (powerStatus)
200 {
201 case PowerBroadcastStatus.BatteryLow:
202 case PowerBroadcastStatus.OemEvent:
203 case PowerBroadcastStatus.PowerStatusChange:
204 case PowerBroadcastStatus.QuerySuspend:
205 Flush();
206 break;
207
208 case PowerBroadcastStatus.ResumeAutomatic:
209 case PowerBroadcastStatus.ResumeCritical:
210 case PowerBroadcastStatus.ResumeSuspend:
211 if (this.autoPaused)
212 {
213 this.autoPaused = false;
214
215 if (this.starting)
216 Log.Warning("Gateway is in the process of starting, called from another source.");
217 else
218 {
219 Log.Notice("Resuming service.");
220 this.OnContinue();
221 }
222 }
223 break;
224
225 case PowerBroadcastStatus.Suspend:
226 this.autoPaused = true;
227 Log.Notice("Suspending service.");
228 this.OnStop();
229 break;
230
231 case PowerBroadcastStatus.QuerySuspendFailed:
232 default:
233 break;
234 }
235 }
236 catch (Exception ex)
237 {
238 Log.Exception(ex);
239 }
240
241 return true;
242 }
243
245 protected override void OnSessionChange(SessionChangeDescription ChangeDescription)
246 {
247 DateTime Now = DateTime.UtcNow;
248 int SessionId = ChangeDescription.SessionId;
249 List<KeyValuePair<string, object>> Tags =
250 [
251 new("Date", Now.ToShortDateString()),
252 new("Time", Now.ToLongTimeString() + "Z"),
253 new("SessionId", SessionId),
254 new("Domain", Gateway.Domain?.Value ?? "N/A")
255 ];
256
257 AddWtsUserName(Tags, SessionId);
258 AddWtsName(Tags, "Initial Program", SessionId, WtsInfoClass.WTSInitialProgram, NullTerminatedString);
259 AddWtsName(Tags, "Application Name", SessionId, WtsInfoClass.WTSApplicationName, NullTerminatedString);
260 AddWtsName(Tags, "Working Directory", SessionId, WtsInfoClass.WTSWorkingDirectory, NullTerminatedString);
261 AddWtsName(Tags, "Station Name", SessionId, WtsInfoClass.WTSWinStationName, NullTerminatedString);
262 AddWtsName(Tags, "Connect State", SessionId, WtsInfoClass.WTSConnectState, EnumerationString<WtsConnectClass>);
263 AddWtsName(Tags, "Client Build Number", SessionId, WtsInfoClass.WTSClientBuildNumber, IntegerValue);
264 AddWtsName(Tags, "Client Name", SessionId, WtsInfoClass.WTSClientName, NullTerminatedString);
265 AddWtsName(Tags, "Client Directory", SessionId, WtsInfoClass.WTSClientDirectory, NullTerminatedString);
266 AddWtsName(Tags, "Client Product ID", SessionId, WtsInfoClass.WTSClientProductId, IntegerValue);
267 AddWtsName(Tags, "Client Hardware ID", SessionId, WtsInfoClass.WTSClientHardwareId, IntegerValue);
268 AddWtsName(Tags, "Client Address", SessionId, WtsInfoClass.WTSClientAddress, ClientAddress);
269 AddWtsName(Tags, "Client Display", SessionId, WtsInfoClass.WTSClientDisplay, ClientDisplay);
270 AddWtsName(Tags, "Client Protocol Type", SessionId, WtsInfoClass.WTSClientProtocolType, EnumerationString<WtsClientProtocolType>);
271 AddWtsName(Tags, "Client Info", SessionId, WtsInfoClass.WTSClientInfo, NullTerminatedString);
272
273 string Message;
274
275 switch (ChangeDescription.Reason)
276 {
277 case SessionChangeReason.ConsoleConnect:
278 Message = "User connected to machine via console interface.";
279 break;
280
281 case SessionChangeReason.ConsoleDisconnect:
282 Message = "User disconnected console interface.";
283 break;
284
285 case SessionChangeReason.RemoteConnect:
286 Message = "User connected remotely to machine.";
287 break;
288
289 case SessionChangeReason.RemoteDisconnect:
290 Message = "User disconnected remote interface.";
291 break;
292
293 case SessionChangeReason.SessionLock:
294 Message = "User session locked.";
295 break;
296
297 case SessionChangeReason.SessionLogoff:
298 Message = "User logged off.";
299 break;
300
301 case SessionChangeReason.SessionLogon:
302 Message = "User logged on.";
303 break;
304
305 case SessionChangeReason.SessionRemoteControl:
306 Message = "User remote control status of session has changed.";
307 break;
308
309 case SessionChangeReason.SessionUnlock:
310 Message = "User session unlocked.";
311 break;
312
313 default:
314 Tags.Add(new KeyValuePair<string, object>("Reason", ChangeDescription.Reason.ToString()));
315 Message = "Session changed.";
316 break;
317 }
318
319 if (!Gateway.HasDomain)
320 Log.Notice(Message, [.. Tags]);
321 else
322 {
323 if ((Setup.NotificationConfiguration.Instance.Addresses?.Length ?? 0) == 0)
324 Log.Alert(Message, [.. Tags]);
325 else
326 {
327 Log.Notice(Message, [.. Tags]);
328
329 StringBuilder Markdown = new();
330
331 Markdown.AppendLine(MarkdownDocument.Encode(Message));
332 Markdown.AppendLine();
333 Markdown.AppendLine("| Details ||");
334 Markdown.AppendLine("|:----|:---|");
335
336 foreach (KeyValuePair<string, object> Tag in Tags)
337 {
338 Markdown.Append("| ");
339 Markdown.Append(MarkdownDocument.Encode(Tag.Key));
340 Markdown.Append(" | ");
341 Markdown.Append(MarkdownDocument.Encode(Tag.Value?.ToString() ?? string.Empty));
342 Markdown.AppendLine(" |");
343 }
344
345 Gateway.SendNotification(Markdown.ToString());
346 }
347 }
348 }
349
350 private static void AddWtsUserName(List<KeyValuePair<string, object>> Tags, int SessionId)
351 {
352 string Value = GetUserName(SessionId);
353 if (!string.IsNullOrEmpty(Value))
354 Tags.Add(new KeyValuePair<string, object>("User Name", Value));
355 }
356
357 private static void AddWtsName(List<KeyValuePair<string, object>> Tags, string Key, int SessionId,
358 WtsInfoClass InfoClass, ParseWtsInfo Parser)
359 {
360 object Value = GetWtsValue(SessionId, InfoClass, Parser);
361 if (Value is not null)
362 Tags.Add(new KeyValuePair<string, object>(Key, Value));
363 }
364
365 private static string GetUserName(int SessionId)
366 {
367 try
368 {
369 string UserName = GetWtsValue(SessionId, WtsInfoClass.WTSUserName, NullTerminatedString) as string;
370 if (string.IsNullOrEmpty(UserName))
371 return null;
372
373 string Domain = GetWtsValue(SessionId, WtsInfoClass.WTSDomainName, NullTerminatedString) as string;
374 if (!string.IsNullOrEmpty(Domain))
375 UserName = Domain + "\\" + UserName;
376
377 return UserName;
378 }
379 catch (Exception ex)
380 {
381 Log.Exception(ex);
382 return null;
383 }
384 }
385
386 private delegate object ParseWtsInfo(IntPtr Buffer, int Len);
387
388 private static object NullTerminatedString(IntPtr Buffer, int _)
389 {
390 string s = Marshal.PtrToStringAnsi(Buffer);
391
392 if (!string.IsNullOrEmpty(s))
393 s = CommonTypes.Escape(s, specialCharactersToEscape, specialCharacterEscapes);
394
395 return s;
396 }
397
398 private static long GetInteger(IntPtr Buffer, int Len)
399 {
400 switch (Len)
401 {
402 case 1: return Marshal.ReadByte(Buffer, 0);
403 case 2: return Marshal.ReadInt16(Buffer, 0);
404 case 4: return Marshal.ReadInt32(Buffer, 0);
405 case 8: return Marshal.ReadInt64(Buffer, 0);
406 default:
407 if (Len > 8)
408 Len = 8;
409
410 byte[] Bin = new byte[8];
411 int i;
412
413 for (i = 0; i < Len; i++)
414 Bin[i] = Marshal.ReadByte(Buffer, i);
415
416 return BitConverter.ToInt64(Bin, 0);
417 }
418 }
419
420 private static object IntegerValue(IntPtr Buffer, int Len)
421 {
422 return GetInteger(Buffer, Len);
423 }
424
425 private static object EnumerationString<T>(IntPtr Buffer, int Len)
426 where T : Enum
427 {
428 long i = GetInteger(Buffer, Len);
429 return Enum.ToObject(typeof(T), i);
430 }
431
432 private static object ClientAddress(IntPtr Buffer, int Len)
433 {
434 // https://www.pinvoke.net/default.aspx/wtsapi32/WTS_CLIENT_ADDRESS.html
435
436 if (EnumerationString<AddressFamily>(Buffer, Math.Min(4, Len)) is not AddressFamily AddressFamily)
437 return null;
438
439 switch (AddressFamily)
440 {
441 case AddressFamily.InterNetwork:
442 if (Len < 10)
443 return AddressFamily;
444
445 byte[] Bin;
446
447 try
448 {
449 Bin = new byte[4];
450 Marshal.Copy(Buffer, Bin, 6, 4);
451
452 return new IPAddress(Bin).ToString();
453 }
454 catch (Exception)
455 {
456 return AddressFamily;
457 }
458
459 case AddressFamily.InterNetworkV6:
460 if (Len < 20)
461 return AddressFamily;
462
463 try
464 {
465 Bin = new byte[16];
466 Marshal.Copy(Buffer, Bin, 4, 16);
467
468 return new IPAddress(Bin).ToString();
469 }
470 catch (Exception)
471 {
472 return AddressFamily;
473 }
474
475 default:
476 return AddressFamily;
477 }
478 }
479
480 private static object ClientDisplay(IntPtr Buffer, int Len)
481 {
482 // https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/ns-wtsapi32-wts_client_display
483
484 if (Len < 12)
485 return null;
486
487 byte[] Bin = new byte[12];
488 Marshal.Copy(Buffer, Bin, 0, 12);
489
490 int Width = BitConverter.ToInt32(Bin, 0);
491 int Height = BitConverter.ToInt32(Bin, 4);
492 int ColorDepth = BitConverter.ToInt32(Bin, 8);
493 string s = Width.ToString() + "x" + Height.ToString();
494
495 switch (ColorDepth)
496 {
497 case 1:
498 s += " (4 bits per pixel.)";
499 break;
500
501 case 2:
502 s += " (8 bits per pixel.)";
503 break;
504
505 case 4:
506 s += " (16 bits per pixel.)";
507 break;
508
509 case 8:
510 s += " (3 byte RGB).";
511 break;
512
513 case 16:
514 s += " (15 bits per pixel.)";
515 break;
516
517 case 24:
518 s += " (24 bits per pixel.)";
519 break;
520
521 case 32:
522 s += " (32 bits per pixel.)";
523 break;
524 }
525
526 return s;
527 }
528
529 private static object GetWtsValue(int SessionId, WtsInfoClass InfoClass, ParseWtsInfo Parser)
530 {
531 object Result;
532
533 try
534 {
535 if (Win32.WTSQuerySessionInformation(IntPtr.Zero, SessionId, InfoClass, out IntPtr Buffer, out int Len) &&
536 Buffer != IntPtr.Zero && Len > 1)
537 {
538 try
539 {
540 Result = Parser(Buffer, Len);
541 Win32.WTSFreeMemory(Buffer);
542 Buffer = IntPtr.Zero;
543 }
544 finally
545 {
546 if (Buffer != IntPtr.Zero)
547 Win32.WTSFreeMemory(Buffer);
548 }
549 }
550 else
551 return null;
552 }
553 catch (Exception ex)
554 {
555 Log.Exception(ex);
556 return null;
557 }
558
559 return Result;
560 }
561
562 private static readonly char[] specialCharactersToEscape =
563 [
564 '\x00',
565 '\x01',
566 '\x02',
567 '\x03',
568 '\x04',
569 '\x05',
570 '\x06',
571 '\a', // 7 - 0x07
572 '\b', // 8 - 0x08
573 '\n', // 10 - 0x0a
574 '\v', // 11 - 0x0b
575 '\f', // 12 - 0x0c
576 '\r', // 13 - 0x0d
577 '\x0e',
578 '\x0f',
579 '\x10',
580 '\x11',
581 '\x12',
582 '\x13',
583 '\x14',
584 '\x15',
585 '\x16',
586 '\x17',
587 '\x18',
588 '\x19',
589 '\x1a',
590 '\x1b',
591 '\x1c',
592 '\x1d',
593 '\x1e',
594 '\x1f'
595 ];
596 private static readonly string[] specialCharacterEscapes =
597 [
598 "<NUL>", // '\x00',
599 "<SOH>", // '\x01',
600 "<STX>", // '\x02',
601 "<ETX>", // '\x03',
602 "<EOT>", // '\x04',
603 "<ENQ>", // '\x05',
604 "<ACK>", // '\x06',
605 "<BEL>", // '\a', // 7 - 0x07
606 "<BS>", // '\b', // 8 - 0x08
607 "<LF>", // '\n', // 10 - 0x0a
608 "<VT>", // '\v', // 11 - 0x0b
609 "<FF>", // '\f', // 12 - 0x0c
610 "<CR>", // '\r', // 13 - 0x0d
611 "<SO>", // '\x0e',
612 "<SI>", // '\x0f',
613 "<DLE>", // '\x10',
614 "<DC1>", // '\x11',
615 "<DC2>", // '\x12',
616 "<DC3>", // '\x13',
617 "<DC4>", // '\x14',
618 "<NAK>", // '\x15',
619 "<SYN>", // '\x16',
620 "<ETB>", // '\x17',
621 "<CAN>", // '\x18',
622 "<EM>", // '\x19',
623 "<SUB>", // '\x1a',
624 "<ESC>", // '\x1b',
625 "<FS>", // '\x1c',
626 "<GS>", // '\x1d',
627 "<RS>", // '\x1e',
628 "<US>" // '\x1f'
629 ];
630
631
633 protected override void OnShutdown()
634 {
635 Log.Notice("System is shutting down.");
636 this.Stop();
637 }
638
640 protected override void OnStop()
641 {
642 Log.Notice("Service is being stopped.");
643 try
644 {
645 using PendingTimer Timer = new(this);
646
647 Gateway.GetDatabaseProvider -= Program.GetDatabase;
648 Gateway.RegistrationSuccessful -= Program.RegistrationSuccessful;
649 Gateway.OnTerminate -= this.TerminateService;
650
651 Flush();
652 Gateway.Stop().Wait();
653 Log.TerminateAsync().Wait();
654 }
655 catch (Exception ex)
656 {
657 Log.Alert(ex);
658 }
659 }
660
661 private static void Flush()
662 {
664 Database.Provider.Flush().Wait();
665
667 Ledger.Provider.Flush().Wait();
668 }
669
671 protected override async void OnCustomCommand(int command)
672 {
673 try
674 {
675 await Gateway.ExecuteServiceCommand(command);
676 }
677 catch (Exception ex)
678 {
679 Log.Exception(ex);
680 }
681 }
682 }
683}
684
685#pragma warning restore CA1416 // Validate platform compatibility
Helps with parsing of commong data types.
Definition: CommonTypes.cs:15
static string Escape(string s, char[] CharactersToEscape, string EscapeSequence)
Escapes a set of characters in a string.
Definition: CommonTypes.cs:830
Contains a markdown document. This markdown document class supports original markdown,...
static string Encode(string s)
Encodes all special characters in a string so that it can be included in a markdown document without ...
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:14
static void Exception(Exception Exception, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, params KeyValuePair< string, object >[] Tags)
Logs an exception. Event type will be determined by the severity of the exception.
Definition: Log.cs:1657
static void Warning(string Message, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, string StackTrace, params KeyValuePair< string, object >[] Tags)
Logs a warning event.
Definition: Log.cs:576
static void Notice(string Message, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, string StackTrace, params KeyValuePair< string, object >[] Tags)
Logs a notice event.
Definition: Log.cs:460
static async Task TerminateAsync()
Must be called when the application is terminated. Stops all event sinks that have been registered.
Definition: Log.cs:100
static void Alert(string Message, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, string StackTrace, params KeyValuePair< string, object >[] Tags)
Logs an alert event.
Definition: Log.cs:1237
Static class managing the runtime environment of the IoT Gateway.
Definition: Gateway.cs:147
static CaseInsensitiveString Domain
Domain name.
Definition: Gateway.cs:3087
static Task< bool > Start(bool ConsoleOutput)
Starts the gateway.
Definition: Gateway.cs:254
static async Task Stop()
Stops the gateway.
Definition: Gateway.cs:2816
static Task SendNotification(Graph Graph)
Sends a graph as a notification message to configured notification recipients.
Definition: Gateway.cs:4677
static bool HasDomain
If a domain name is configured.
Definition: Gateway.cs:3093
static async Task< bool > ExecuteServiceCommand(int CommandNr)
Executes a service command.
Definition: Gateway.cs:4153
override void OnStart(string[] args)
override async void OnCustomCommand(int command)
override bool OnPowerEvent(PowerBroadcastStatus powerStatus)
override void OnSessionChange(SessionChangeDescription ChangeDescription)
GatewayService(string ServiceName, string InstanceName)
Gateway Service
IoT Gateway Windows Service Application.
Definition: Program.cs:49
static string InstanceName
Instance name
Definition: Program.cs:55
Handles interaction with Windows Service API.
Definition: Win32.cs:16
string Value
String-representation of the case-insensitive string. (Representation is case sensitive....
Static interface for database persistence. In order to work, a database provider has to be assigned t...
Definition: Database.cs:21
static bool HasProvider
If a database provider is registered.
Definition: Database.cs:81
static IDatabaseProvider Provider
Registered database provider.
Definition: Database.cs:59
Static interface for ledger persistence. In order to work, a ledger provider has to be assigned to it...
Definition: Ledger.cs:14
static bool HasProvider
If a ledger provider is registered.
Definition: Ledger.cs:105
static ILedgerProvider Provider
Registered ledger provider.
Definition: Ledger.cs:83
Static class that dynamically manages types and interfaces available in the runtime environment.
Definition: Types.cs:15
static void SetModuleParameter(string Name, object Value)
Sets a module parameter. This parameter value will be accessible to modules when they are loaded.
Definition: Types.cs:584
Task Flush()
Persists any pending changes.
Task Flush()
Persists any pending changes.
Definition: ImplTypes.g.cs:58
WtsInfoClass
Windows Terminal Services Infomration class.
Definition: WtsInfoClass.cs:7