-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
TouchPortalClient.cs
376 lines (327 loc) · 15.5 KB
/
TouchPortalClient.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
using System;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using TouchPortalSDK.Configuration;
using TouchPortalSDK.Interfaces;
using TouchPortalSDK.Messages.Commands;
using TouchPortalSDK.Messages.Events;
using TouchPortalSDK.Messages.Models;
using TouchPortalSDK.Messages.Models.Enums;
namespace TouchPortalSDK.Clients
{
public class TouchPortalClient : ITouchPortalClient, IMessageHandler
{
/// <inheritdoc cref="ITouchPortalClient" />
public bool IsConnected => !_closing && (_touchPortalSocket?.IsConnected ?? false);
private readonly ILogger<TouchPortalClient> _logger;
private readonly ITouchPortalEventHandler _eventHandler;
private readonly ITouchPortalSocket _touchPortalSocket;
private CancellationTokenSource _cts;
private CancellationToken _cancellationToken;
private readonly ConcurrentQueue<byte[]> _incommingMessages;
private readonly ManualResetEventSlim _messageReadyEvent;
private Task _incomingMessageTask = null;
private bool _closing = false;
private readonly ManualResetEvent _infoWaitHandle;
private InfoEvent _lastInfoEvent;
public TouchPortalClient(ITouchPortalEventHandler eventHandler,
ITouchPortalSocketFactory socketFactory,
ILoggerFactory loggerFactory = null)
{
if (string.IsNullOrWhiteSpace(eventHandler?.PluginId))
throw new InvalidOperationException($"{nameof(ITouchPortalEventHandler)}: PluginId cannot be null or empty.");
_eventHandler = eventHandler;
_touchPortalSocket = socketFactory.Create(this);
_logger = loggerFactory?.CreateLogger<TouchPortalClient>();
_incommingMessages = new ConcurrentQueue<byte[]>();
_messageReadyEvent = new ManualResetEventSlim();
_infoWaitHandle = new ManualResetEvent(false);
}
#region Setup
/// <inheritdoc cref="ITouchPortalClient" />
bool ITouchPortalClient.Connect()
{
//Connect:
_logger?.LogInformation("Connecting to TouchPortal.");
var connected = _touchPortalSocket.Connect();
if (!connected)
return false;
//Listen:
// set up message processing queue and task
_cts = new CancellationTokenSource();
_cancellationToken = _cts.Token;
_incommingMessages.Clear();
_logger?.LogDebug("Starting message processing queue task.");
_incomingMessageTask = Task.Run(MessageHandlerTask);
// start socket reader thread
_logger?.LogDebug("Start Socket listener.");
var listening = _touchPortalSocket.Listen();
if (!listening)
return false;
_logger?.LogDebug("Listener created.");
//Pair:
_logger?.LogInformation("Sending pair message.");
var pairCommand = new PairCommand(_eventHandler.PluginId);
var pairing = ((ICommandHandler)this).SendCommand(pairCommand);
if (!pairing)
return false;
//Waiting for InfoMessage:
if (_infoWaitHandle.WaitOne(10000))
_logger?.LogInformation("Received pair response.");
else
Close("Pair response timed out! Closing connection.");
return _lastInfoEvent != null;
}
/// <inheritdoc cref="ITouchPortalClient" />
void ITouchPortalClient.Close()
=> Close("Closed by plugin.");
private void Close(string message, Exception exception = default)
{
if (_closing)
return;
_closing = true;
_logger?.LogInformation(exception, "Closing TouchPortal Plugin: '{message}'", message);
_eventHandler.OnClosedEvent(message);
_touchPortalSocket?.CloseSocket();
if (_incomingMessageTask == null) // shouldn't happen but JIC
return;
_cts?.Cancel();
if (_incomingMessageTask.Status == TaskStatus.Running && !_incomingMessageTask.Wait(2000))
_logger?.LogWarning("The incoming message processor task is hung!");
try { _incomingMessageTask.Dispose(); }
catch { /* ignore in case it hung */ }
_incomingMessageTask = null;
_cts?.Dispose();
_cts = null;
}
private async void CloseAsync(string message)
{
await Task.Run(delegate { Close(message); });
}
#endregion
#region TouchPortal Command Handlers
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.SettingUpdate(string name, string value)
{
try {
return ((ICommandHandler)this).SendCommand(new SettingUpdateCommand(name, value));
}
catch (ArgumentException e) {
_logger?.LogWarning(e, "SettingUpdateCommand() validation failed.");
return false;
}
}
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.CreateState(string stateId, string desc, string defaultValue, string parentGroup, bool forceUpdate)
{
try {
return ((ICommandHandler)this).SendCommand(new CreateStateCommand(stateId, desc, defaultValue, parentGroup, forceUpdate));
}
catch (ArgumentException e) {
_logger?.LogWarning(e, "CreateStateCommand() validation failed.");
return false;
}
}
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.RemoveState(string stateId)
{
try {
return ((ICommandHandler)this).SendCommand(new RemoveStateCommand(stateId));
}
catch (ArgumentException e) {
_logger?.LogWarning(e, "RemoveStateCommand() validation failed.");
return false;
}
}
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.StateUpdate(string stateId, string value)
{
try {
return ((ICommandHandler)this).SendCommand(new StateUpdateCommand(stateId, value));
}
catch (ArgumentException e) {
_logger?.LogWarning(e, "StateUpdateCommand() validation failed.");
return false;
}
}
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.ChoiceUpdate(string choiceId, string[] values, string instanceId)
{
try {
return ((ICommandHandler)this).SendCommand(new ChoiceUpdateCommand(choiceId, values, instanceId));
}
catch (ArgumentException e) {
_logger?.LogWarning(e, "ChoiceUpdateCommand() validation failed.");
return false;
}
}
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.UpdateActionData(string dataId, double minValue, double maxValue, ActionDataType dataType, string instanceId)
{
try {
return ((ICommandHandler)this).SendCommand(new UpdateActionDataCommand(dataId, minValue, maxValue, dataType, instanceId));
}
catch (ArgumentException e) {
_logger?.LogWarning(e, "UpdateActionDataCommand() validation failed.");
return false;
}
}
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.ShowNotification(string notificationId, string title, string message, NotificationOptions[] notificationOptions)
{
try {
return ((ICommandHandler)this).SendCommand(new ShowNotificationCommand(notificationId, title, message, notificationOptions));
}
catch (ArgumentException e) {
_logger?.LogWarning(e, "ShowNotificationCommand() validation failed.");
return false;
}
}
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.ConnectorUpdate(string connectorId, int value)
{
try {
return ((ICommandHandler)this).SendCommand(new ConnectorUpdateCommand(_eventHandler.PluginId, connectorId, value));
}
catch (ArgumentException e) {
_logger?.LogWarning(e, "ConnectorUpdateCommand() validation failed.");
return false;
}
}
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.ConnectorUpdateShort(string shortId, int value)
{
try {
return ((ICommandHandler)this).SendCommand(new ConnectorUpdateShortCommand(_eventHandler.PluginId, shortId, value));
}
catch (ArgumentException e) {
_logger?.LogWarning(e, "ConnectorUpdateShortCommand() validation failed.");
return false;
}
}
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.TriggerEvent(string eventId, TriggerEventStates states)
{
try {
return ((ICommandHandler)this).SendCommand(new TriggerEventCommand(eventId, states));
}
catch (ArgumentException e) {
_logger?.LogWarning(e, "TriggerEvent() validation failed.");
return false;
}
}
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.StateListUpdate(string stateId, string[] values)
{
try {
return ((ICommandHandler)this).SendCommand(new StateListUpdateCommand(stateId, values));
}
catch (ArgumentException e) {
_logger?.LogWarning(e, "StateListUpdate() validation failed.");
return false;
}
}
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.SendCommand<TCommand>(TCommand command, string callerMemberName)
{
var jsonMessage = JsonSerializer.SerializeToUtf8Bytes(command, Options.JsonSerializerOptions);
var success = _touchPortalSocket.SendMessage(jsonMessage);
_logger?.LogDebug(
"[{CallerMemberName}] sent: {success}; message: \n{Message}",
callerMemberName, success, System.Text.Encoding.UTF8.GetString(jsonMessage)
);
return success;
}
/// <inheritdoc cref="ICommandHandler" />
bool ICommandHandler.SendMessage(string message)
=> _touchPortalSocket.SendMessage(message);
#endregion
#region TouchPortal Event Handler
/// <inheritdoc cref="IMessageHandler" />
public void OnError(string message, Exception exception = default)
// Block here so that we can finish up before the socket listener thread exits. This is not ideal but it works
=> Close("Terminating due to socket error:", exception);
/// <inheritdoc cref="IMessageHandler" />
void IMessageHandler.OnMessage(byte[] message)
{
_incommingMessages.Enqueue(message);
_messageReadyEvent.Set();
}
private void MessageHandlerTask()
{
_logger?.LogDebug("Message processing queue task started.");
while (!_cancellationToken.IsCancellationRequested)
{
try
{
_messageReadyEvent.Wait(_cancellationToken);
_messageReadyEvent.Reset();
while (!_cancellationToken.IsCancellationRequested && _incommingMessages.TryDequeue(out byte[] message)) {
try {
var eventMessage = MessageResolver.ResolveMessage(message);
switch (eventMessage)
{
case InfoEvent infoEvent:
_lastInfoEvent = infoEvent;
_infoWaitHandle.Set();
_eventHandler.OnInfoEvent(infoEvent);
break;
case CloseEvent _:
CloseAsync("TouchPortal sent a Plugin close event.");
break;
case ListChangeEvent listChangeEvent:
_eventHandler.OnListChangedEvent(listChangeEvent);
break;
case BroadcastEvent broadcastEvent:
_eventHandler.OnBroadcastEvent(broadcastEvent);
break;
case SettingsEvent settingsEvent:
_eventHandler.OnSettingsEvent(settingsEvent);
break;
case NotificationOptionClickedEvent notificationEvent:
_eventHandler.OnNotificationOptionClickedEvent(notificationEvent);
break;
case ConnectorChangeEvent connectorChangeEvent:
_eventHandler.OnConnecterChangeEvent(connectorChangeEvent);
break;
case ShortConnectorIdNotificationEvent shortConnectorIdEvent:
_eventHandler.OnShortConnectorIdNotificationEvent(shortConnectorIdEvent);
break;
//All of Action, Up, Down:
case ActionEvent actionEvent:
_eventHandler.OnActionEvent(actionEvent);
break;
default:
_eventHandler.OnUnhandledEvent(System.Text.Encoding.UTF8.GetString(message));
break;
}
}
// Catch any parsing exceptions (unlikely)
catch (JsonException e) {
_logger?.LogWarning(e, "JSON parsing exception, see trace for details. Continuing execution with next message.'");
continue;
}
// Catch any exceptions in the plugin user's callback code itself.
// This does assume the plugin author is looking at their logs/console and not relying on us crashing on their exceptions.
catch (Exception e) {
_logger?.LogWarning(e, "Exception in message event handler. Continuing execution with next message.'");
continue;
}
}
}
catch (OperationCanceledException) { break; } // when _messageReadyEvent.Wait() is canceled by token (or task was started with cancellation token)
catch (ObjectDisposedException) { break; } // shouldn't happen but if it does it means we're shutting down anyway
catch (Exception e) { // ok here we really don't know what happened
_logger.LogError(e, "Exception in processing queue task, cannot continue.");
break;
}
}
_logger?.LogDebug("Message processing queue task exited.");
}
#endregion
}
}