/// <summary> /// Keep writing object model updates to the client /// </summary> /// <param name="webSocket">WebSocket to write to</param> /// <param name="subscribeConnection">IPC connection to supply model updates</param> /// <param name="dataAcknowledged">Event that is triggered when the client has acknowledged data</param> /// <param name="cancellationToken">Cancellation token</param> /// <returns>Asynchronous task</returns> private async Task WriteToClient(WebSocket webSocket, SubscribeConnection subscribeConnection, AsyncAutoResetEvent dataAcknowledged, CancellationToken cancellationToken) { do { // Wait for the client to acknowledge the receipt of the last JSON object await dataAcknowledged.WaitAsync(cancellationToken); if (cancellationToken.IsCancellationRequested) { break; } // Wait for another object model update and send it to the client using MemoryStream objectModelPatch = await subscribeConnection.GetSerializedObjectModel(cancellationToken); await webSocket.SendAsync(objectModelPatch.ToArray(), WebSocketMessageType.Text, true, cancellationToken); }while (webSocket.State == WebSocketState.Open); }
/// <summary> /// Deal with a newly opened WebSocket. /// A client may receive one of the WS codes: (1001) Endpoint unavailable (1003) Invalid command (1011) Internal error /// </summary> /// <param name="webSocket">WebSocket connection</param> /// <returns>Asynchronous task</returns> public async Task Process(WebSocket webSocket) { string socketPath = _configuration.GetValue("SocketPath", Defaults.FullSocketPath); // 1. Authentification. This will require an extra API command using CommandConnection commandConnection = new CommandConnection(); // TODO // 2. Connect to DCS using SubscribeConnection subscribeConnection = new SubscribeConnection(); try { // Subscribe to object model updates await subscribeConnection.Connect(SubscriptionMode.Patch, Array.Empty <string>(), socketPath); } catch (Exception e) { if (e is AggregateException ae) { e = ae.InnerException; } if (e is IncompatibleVersionException) { _logger.LogError($"[{nameof(WebSocketController)}] Incompatible DCS version"); await CloseConnection(webSocket, WebSocketCloseStatus.InternalServerError, "Incompatible DCS version"); return; } if (e is SocketException) { _logger.LogError($"[{nameof(WebSocketController)}] DCS is not started"); await CloseConnection(webSocket, WebSocketCloseStatus.EndpointUnavailable, "Failed to connect to Duet, please check your connection (DCS is not started)"); return; } _logger.LogError(e, $"[{nameof(WebSocketController)}] Failed to connect to DCS"); await CloseConnection(webSocket, WebSocketCloseStatus.EndpointUnavailable, e.Message); return; } // 3. Log this event string ipAddress = HttpContext.Connection.RemoteIpAddress.ToString(); int port = HttpContext.Connection.RemotePort; _logger.LogInformation("WebSocket connected from {0}:{1}", ipAddress, port); // 4. Register this client and keep it up-to-date using CancellationTokenSource cts = new CancellationTokenSource(); int sessionId = -1; try { // 4a. Register this user session. Once authentification has been implemented, the access level may vary await commandConnection.Connect(socketPath); sessionId = await commandConnection.AddUserSession(AccessLevel.ReadWrite, SessionType.HTTP, ipAddress, port); // 4b. Fetch full model copy and send it over initially using (MemoryStream json = await subscribeConnection.GetSerializedObjectModel()) { await webSocket.SendAsync(json.ToArray(), WebSocketMessageType.Text, true, default); } // 4c. Deal with this connection in full-duplex mode AsyncAutoResetEvent dataAcknowledged = new AsyncAutoResetEvent(); Task rxTask = ReadFromClient(webSocket, dataAcknowledged, cts.Token); Task txTask = WriteToClient(webSocket, subscribeConnection, dataAcknowledged, cts.Token); // 4d. Deal with the tasks' lifecycles Task terminatedTask = await Task.WhenAny(rxTask, txTask); if (terminatedTask.IsFaulted) { throw terminatedTask.Exception; } } catch (Exception e) { if (e is AggregateException ae) { e = ae.InnerException; } if (e is SocketException) { _logger.LogError($"[{nameof(WebSocketController)}] DCS has been stopped"); await CloseConnection(webSocket, WebSocketCloseStatus.EndpointUnavailable, "DCS has been stopped"); } else if (e is OperationCanceledException) { await CloseConnection(webSocket, WebSocketCloseStatus.EndpointUnavailable, "DWS is shutting down"); } else { _logger.LogError(e, $"[{nameof(WebSocketController)}] Connection from {ipAddress}:{port} terminated with an exception"); await CloseConnection(webSocket, WebSocketCloseStatus.InternalServerError, e.Message); } } finally { cts.Cancel(); _logger.LogInformation("WebSocket disconnected from {0}:{1}", ipAddress, port); try { // Try to remove this user session again await commandConnection.RemoveUserSession(sessionId); } catch (Exception e) { if (!(e is SocketException)) { _logger.LogError(e, "Failed to unregister user session"); } } } }