static async Task Main(string[] args) { // Get an optional filter string string filter; if (args.Length > 0 && args[0] != "-") { filter = args[0]; } else { Console.WriteLine("Please enter a filter expression or press RETURN to receive partial model updates:"); filter = Console.ReadLine().Trim(); } // Connect to DCS using SubscribeConnection connection = new SubscribeConnection(); if (args.Length < 2) { await connection.Connect(SubscriptionMode.Patch, filter); } else { await connection.Connect(SubscriptionMode.Patch, filter, args[1]); } Console.WriteLine("Connected!"); // In Patch mode the whole object model is sent over after connecting. Dump it (or call connection.GetMachineModel() to deserialize it) _ = await connection.GetSerializedMachineModel(); // Then keep listening for (filtered) patches while (connection.IsConnected) { using JsonDocument patch = await connection.GetMachineModelPatch(); if (patch == null) { break; } Console.WriteLine(GetIndentedJson(patch)); } }
private static async Task Main(string[] args) { // Parse the command line arguments string lastArg = null, socketPath = Defaults.FullSocketPath, filter = null; bool quiet = false; foreach (string arg in args) { if (lastArg == "-s" || lastArg == "--socket") { socketPath = arg; } else if (lastArg == "-f" || lastArg == "--filter") { filter = arg; } else if (arg == "-q" || arg == "--quiet") { quiet = true; } else if (arg == "-h" || arg == "--help") { Console.WriteLine("Available command line arguments:"); Console.WriteLine("-s, --socket <socket>: UNIX socket to connect to"); Console.WriteLine("-f, --filter <filter>: UNIX socket to connect to"); Console.WriteLine("-q, --quiet: Do not display when a connection has been established"); Console.WriteLine("-h, --help: Display this help text"); return; } lastArg = arg; } // Get an optional filter string if (string.IsNullOrWhiteSpace(filter)) { Console.WriteLine("Please enter a filter expression or press RETURN to receive partial model updates:"); filter = Console.ReadLine().Trim(); } // Connect to DCS using SubscribeConnection connection = new SubscribeConnection(); #pragma warning disable CS0612 // Type or member is obsolete await connection.Connect(SubscriptionMode.Patch, filter, socketPath); #pragma warning restore CS0612 // Type or member is obsolete if (!quiet) { Console.WriteLine("Connected!"); } // Write incoming fragments indented to the console do { try { using JsonDocument patch = await connection.GetObjectModelPatch(); Console.WriteLine(GetIndentedJson(patch)); } catch (SocketException) { if (!quiet) { Console.WriteLine("Server has closed the connection"); } break; } }while (true); }
/// <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> /// <param name="socketPath">Path to the UNIX socket</param> /// <param name="logger">Logger instance</param> /// <returns>Asynchronous task</returns> public static async Task Process(WebSocket webSocket, string socketPath, ILogger logger) { using (SubscribeConnection connection = new SubscribeConnection()) { // 1. Authentication // TODO // 2. Connect to DCS try { await connection.Connect(SubscriptionMode.Patch, socketPath, Program.CancelSource.Token); } catch (AggregateException ae) when(ae.InnerException is IncompatibleVersionException) { logger.LogError($"[{nameof(WebSocketController)}] Incompatible DCS version"); await CloseConnection(webSocket, WebSocketCloseStatus.InternalServerError, "Incompatible DCS version"); return; } catch (Exception) { logger.LogError($"[{nameof(WebSocketController)}] DCS is unavailable"); await CloseConnection(webSocket, WebSocketCloseStatus.EndpointUnavailable, "DCS is unavailable"); return; } // 3. Keep the client up-to-date try { // 3a. Fetch full model copy and send it over initially string json = await connection.GetSerializedMachineModel(); await webSocket.SendAsync(Encoding.UTF8.GetBytes(json), WebSocketMessageType.Text, true, Program.CancelSource.Token); // 3b. Keep sending updates to the client and wait for "OK" after each update do { // 3c. Wait for response from the client byte[] receivedBytes = new byte[8]; await webSocket.ReceiveAsync(receivedBytes, Program.CancelSource.Token); string receivedData = Encoding.UTF8.GetString(receivedBytes); // 3d. Deal with PING requests if (receivedData.Equals("PING\n", StringComparison.InvariantCultureIgnoreCase)) { await webSocket.SendAsync(PONG, WebSocketMessageType.Text, true, Program.CancelSource.Token); continue; } // 3e. Check if the client has acknowledged the received data if (!receivedData.Equals("OK\n", StringComparison.InvariantCultureIgnoreCase)) { // Terminate the connection if anything else than "OK" is received await CloseConnection(webSocket, WebSocketCloseStatus.InvalidMessageType, "Invalid command"); break; } // 3f. Check for another update and send it to the client json = await connection.GetSerializedMachineModel(); await webSocket.SendAsync(Encoding.UTF8.GetBytes(json), WebSocketMessageType.Text, true, Program.CancelSource.Token); } while (webSocket.State == WebSocketState.Open); } catch (Exception e) { logger.LogError(e, "WebSocket terminated with an exception"); await CloseConnection(webSocket, WebSocketCloseStatus.InternalServerError, e.Message); } } }
/// <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, null, socketPath); } catch (AggregateException ae) when(ae.InnerException is IncompatibleVersionException) { _logger.LogError($"[{nameof(WebSocketController)}] Incompatible DCS version"); await CloseConnection(webSocket, WebSocketCloseStatus.InternalServerError, "Incompatible DCS version"); return; } catch (SocketException) { _logger.LogError($"[{nameof(WebSocketController)}] DCS is unavailable"); await CloseConnection(webSocket, WebSocketCloseStatus.EndpointUnavailable, "DCS is unavailable"); 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 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.GetSerializedMachineModel()) { await webSocket.SendAsync(json.ToArray(), WebSocketMessageType.Text, true, default); } // 4c. Deal with this connection in full-duplex mode using CancellationTokenSource cts = new CancellationTokenSource(); 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); cts.Cancel(); if (terminatedTask.IsFaulted) { throw terminatedTask.Exception; } } catch (Exception e) { _logger.LogError(e, "WebSocket from {0}:{1} terminated with an exception", ipAddress, port); await CloseConnection(webSocket, WebSocketCloseStatus.InternalServerError, e.Message); } finally { _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"); } } } }
private static async Task Main(string[] args) { // Parse the command line arguments string lastArg = null, socketPath = Defaults.FullSocketPath, filter = null; bool quiet = false; foreach (string arg in args) { if (lastArg == "-s" || lastArg == "--socket") { socketPath = arg; } else if (lastArg == "-f" || lastArg == "--filter") { filter = arg; } else if (arg == "-q" || arg == "--quiet") { quiet = true; } else if (arg == "-h" || arg == "--help") { Console.WriteLine("Available command line arguments:"); Console.WriteLine("-s, --socket <socket>: UNIX socket to connect to"); Console.WriteLine("-f, --filter <filter>: UNIX socket to connect to"); Console.WriteLine("-q, --quiet: Do not display when a connection has been established"); Console.WriteLine("-h, --help: Display this help text"); return; } lastArg = arg; } // Get an optional filter string if (string.IsNullOrWhiteSpace(filter)) { Console.WriteLine("Please enter a filter expression or press RETURN to receive partial model updates:"); filter = Console.ReadLine().Trim(); } // Connect to DCS using SubscribeConnection connection = new SubscribeConnection(); await connection.Connect(SubscriptionMode.Patch, filter, socketPath); if (!quiet) { Console.WriteLine("Connected!"); } // In Patch mode the whole object model is sent over after connecting. // Dump it (or call connection.GetMachineModel() to deserialize it) _ = await connection.GetSerializedMachineModel(); // Then keep listening for (filtered) patches do { try { using JsonDocument patch = await connection.GetMachineModelPatch(); Console.WriteLine(GetIndentedJson(patch)); } catch (SocketException) { if (!quiet) { Console.WriteLine("Server has closed the connection"); } break; } }while (true); }
/// <summary> /// Synchronize all registered endpoints and user sessions /// </summary> public async Task Execute() { string unixSocket = _configuration.GetValue("SocketPath", DuetAPI.Connection.Defaults.FullSocketPath); int retryDelay = _configuration.GetValue("ModelRetryDelay", 5000); MachineModel model; try { do { try { // Establish connections to DCS using SubscribeConnection subscribeConnection = new SubscribeConnection(); using CommandConnection commandConnection = new CommandConnection(); await subscribeConnection.Connect(DuetAPI.Connection.SubscriptionMode.Patch, "directories/www|httpEndpoints/**|userSessions/**", unixSocket); await commandConnection.Connect(unixSocket); _logger.LogInformation("Connections to DuetControlServer established"); // Get the machine model and keep it up-to-date model = await subscribeConnection.GetMachineModel(_stopRequest.Token); lock (Endpoints) { foreach (HttpEndpoint ep in model.HttpEndpoints) { string fullPath = $"{ep.EndpointType}/machine/{ep.Namespace}/{ep.Path}"; Endpoints[fullPath] = ep; _logger.LogInformation("Registered HTTP {0} endpoint via /machine/{1}/{2}", ep.EndpointType, ep.Namespace, ep.Path); } } // Keep track of the web directory _commandConnection = commandConnection; model.Directories.PropertyChanged += Directories_PropertyChanged; string wwwDirectory = await commandConnection.ResolvePath(model.Directories.WWW); OnWebDirectoryChanged?.Invoke(wwwDirectory); do { // Wait for more updates using JsonDocument jsonPatch = await subscribeConnection.GetMachineModelPatch(_stopRequest.Token); DuetAPI.Utility.JsonPatch.Patch(model, jsonPatch); // Check if the HTTP sessions have changed and rebuild them on demand if (jsonPatch.RootElement.TryGetProperty("httpEndpoints", out _)) { _logger.LogInformation("New number of custom HTTP endpoints: {0}", model.HttpEndpoints.Count); lock (Endpoints) { Endpoints.Clear(); foreach (HttpEndpoint ep in model.HttpEndpoints) { string fullPath = $"{ep.EndpointType}/machine/{ep.Namespace}/{ep.Path}"; Endpoints[fullPath] = ep; _logger.LogInformation("Registered HTTP {0} endpoint via /machine/{1}/{2}", ep.EndpointType, ep.Namespace, ep.Path); } } } // Rebuild the list of user sessions on demand if (jsonPatch.RootElement.TryGetProperty("userSessions", out _)) { lock (UserSessions) { UserSessions.Clear(); foreach (UserSession session in model.UserSessions) { UserSessions[session.Origin] = session.Id; } } } }while (!!_stopRequest.IsCancellationRequested); } catch (Exception e) when(!(e is OperationCanceledException)) { _logger.LogWarning(e, "Failed to synchronize machine model"); await Task.Delay(retryDelay, _stopRequest.Token); } }while (!_stopRequest.IsCancellationRequested); } catch (Exception e) { if (!(e is OperationCanceledException)) { _logger.LogError(e, "Failed to synchronize object model"); } } }
/// <summary> /// Synchronize all registered endpoints and user sessions /// </summary> public async Task Execute() { string unixSocket = _configuration.GetValue("SocketPath", DuetAPI.Connection.Defaults.FullSocketPath); int retryDelay = _configuration.GetValue("ModelRetryDelay", 5000); ObjectModel model; try { do { try { // Establish connections to DCS using SubscribeConnection subscribeConnection = new SubscribeConnection(); using CommandConnection commandConnection = new CommandConnection(); await subscribeConnection.Connect(DuetAPI.Connection.SubscriptionMode.Patch, new string[] { "directories/www", "httpEndpoints/**", "network/corsSite", "userSessions/**" }, unixSocket); await commandConnection.Connect(unixSocket); _logger.LogInformation("Connections to DuetControlServer established"); // Get the machine model and keep it up-to-date model = await subscribeConnection.GetObjectModel(_stopRequest.Token); if (!string.IsNullOrEmpty(model.Network.CorsSite)) { _logger.LogInformation("Changing CORS policy to accept site '{0}'", model.Network.CorsSite); CorsPolicy.Origins.Add(model.Network.CorsSite); } lock (Endpoints) { Endpoints.Clear(); foreach (HttpEndpoint ep in model.HttpEndpoints) { string fullPath = (ep.Namespace == HttpEndpoint.RepRapFirmwareNamespace) ? $"{ep.EndpointType}/rr_{ep.Path}" : $"{ep.EndpointType}/machine/{ep.Namespace}/{ep.Path}"; Endpoints[fullPath] = ep; _logger.LogInformation("Registered HTTP endpoint {0}", fullPath); } } // Keep track of the web directory _commandConnection = commandConnection; model.Directories.PropertyChanged += Directories_PropertyChanged; string wwwDirectory = await commandConnection.ResolvePath(model.Directories.Web); OnWebDirectoryChanged?.Invoke(wwwDirectory); do { // Wait for more updates using JsonDocument jsonPatch = await subscribeConnection.GetObjectModelPatch(_stopRequest.Token); model.UpdateFromJson(jsonPatch.RootElement); // Check for updated CORS site if (jsonPatch.RootElement.TryGetProperty("network", out _)) { CorsPolicy.Origins.Clear(); if (!string.IsNullOrEmpty(model.Network.CorsSite)) { _logger.LogInformation("Changing CORS policy to accept site '{0}'", model.Network.CorsSite); CorsPolicy.Origins.Add(model.Network.CorsSite); } else { _logger.LogInformation("Reset CORS policy"); } } // Check if the HTTP sessions have changed and rebuild them on demand if (jsonPatch.RootElement.TryGetProperty("httpEndpoints", out _)) { _logger.LogInformation("New number of custom HTTP endpoints: {0}", model.HttpEndpoints.Count); lock (Endpoints) { Endpoints.Clear(); foreach (HttpEndpoint ep in model.HttpEndpoints) { string fullPath = $"{ep.EndpointType}/machine/{ep.Namespace}/{ep.Path}"; Endpoints[fullPath] = ep; _logger.LogInformation("Registered HTTP {0} endpoint via /machine/{1}/{2}", ep.EndpointType, ep.Namespace, ep.Path); } } } // Rebuild the list of user sessions on demand if (jsonPatch.RootElement.TryGetProperty("userSessions", out _)) { lock (UserSessions) { UserSessions.Clear(); foreach (UserSession session in model.UserSessions) { UserSessions[session.Origin] = session.Id; } } } }while (!_stopRequest.IsCancellationRequested); } catch (Exception e) when(!(e is OperationCanceledException)) { _logger.LogWarning(e, "Failed to synchronize machine model"); await Task.Delay(retryDelay, _stopRequest.Token); } }while (!_stopRequest.IsCancellationRequested); } catch (OperationCanceledException) { // unhandled } }