static void Main(string[] args) { // Connect to DCS CommandConnection connection = new CommandConnection(); if (args.Length == 0) { connection.Connect().Wait(); } else { connection.Connect(args[0]).Wait(); } Console.WriteLine("Connected!"); // Start reading lines from stdin and send them to DCS as simple codes. // When the code has finished, the result is printed to stdout string input = Console.ReadLine(); while (connection.IsConnected && input != null && input != "exit" && input != "quit") { try { string output = connection.PerformSimpleCode(input).Result; Console.Write(output); } catch (AggregateException ae) { Console.WriteLine(ae.InnerException.Message); } input = Console.ReadLine(); } }
private async Task <CommandConnection> BuildConnection() { CommandConnection connection = new CommandConnection(); await connection.Connect(_configuration.GetValue("SocketPath", DuetAPI.Connection.Defaults.FullSocketPath)); return(connection); }
private async Task ValidateProvider() { using (CommandConnection connection = new CommandConnection()) { await connection.Connect(_configuration.GetValue("SocketPath", DuetAPI.Connection.Defaults.SocketPath)); string wwwRoot = await connection.ResolvePath("0:/www"); if (wwwRoot != _wwwRoot) { _provider = new PhysicalFileProvider(wwwRoot); _wwwRoot = wwwRoot; } } }
public static async Task Main(string[] args) { // Parse the command line arguments string lastArg = null, codeToExecute = null, socketPath = Defaults.FullSocketPath; bool quiet = false; foreach (string arg in args) { if (lastArg == "-s" || lastArg == "--socket") { socketPath = arg; } else if (lastArg == "-c" || lastArg == "-c") { codeToExecute = 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("-c, --code <code>: Execute the given code(s), wait for the result and exit"); Console.WriteLine("-q, --quiet: Do not display when a connection has been established (only applicable in interactive mode)"); Console.WriteLine("-h, --help: Display this help text"); return; } lastArg = arg; } // Create a new connection and connect to DuetControlServer using CommandConnection connection = new CommandConnection(); await connection.Connect(socketPath); // Check if this is an interactive session if (codeToExecute == null) { if (!quiet) { // Notify the user that a connection has been established Console.WriteLine("Connected!"); } // Register an (interactive) user session int sessionId = await connection.AddUserSession(DuetAPI.Machine.AccessLevel.ReadWrite, DuetAPI.Machine.SessionType.Local, "console"); // Start reading lines from stdin and send them to DCS as simple codes. // When the code has finished, the result is printed to stdout string input = Console.ReadLine(); while (input != null && input != "exit" && input != "quit") { try { string output = await connection.PerformSimpleCode(input); if (output.EndsWith(Environment.NewLine)) { Console.Write(output); } else { Console.WriteLine(output); } } catch (SocketException) { Console.WriteLine("Server has closed the connection"); break; } catch (Exception e) { if (e is AggregateException ae) { e = ae.InnerException; } Console.WriteLine(e.Message); } input = Console.ReadLine(); } // Unregister this session again if (connection.IsConnected) { await connection.RemoveUserSession(sessionId); } } else { // Execute only the given code(s) and quit string output = await connection.PerformSimpleCode(codeToExecute); if (output.EndsWith('\n')) { Console.Write(output); } else { Console.WriteLine(output); } } }
/// <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"); } } } }
/// <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 } }
/// <summary> /// Entry point /// </summary> /// <param name="args">Command-line arguments</param> /// <returns>Asynchronous task</returns> public static async Task Main(string[] args) { // Parse the command line arguments string lastArg = null, socketPath = Defaults.FullSocketPath, ns = "custom-http-endpoint", path = "demo"; foreach (string arg in args) { if (lastArg == "-s" || lastArg == "--socket") { socketPath = arg; } else if (lastArg == "-m" || lastArg == "--method") { if (!Enum.TryParse(arg, true, out _method)) { Console.WriteLine("Error: Invalid HTTP method"); return; } } else if (lastArg == "-n" || lastArg == "--namespace") { ns = arg; } else if (lastArg == "-p" || lastArg == "--path") { path = arg; } else if (lastArg == "-e" || lastArg == "--exec") { _cmd = arg; } else if (lastArg == "-a" || lastArg == "--args") { _args = arg; } else if (arg == "-q" || lastArg == "--quiet") { _quiet = true; } else if (arg == "-h" || arg == "--help") { Console.WriteLine("Create a custom HTTP endpoint in the format /machine/{namespace}/{path}"); Console.WriteLine("Available command line options:"); Console.WriteLine("-s, --socket <socket>: UNIX socket to connect to"); Console.WriteLine("-m, --method [GET, POST, PUT, PATCH, TRACE, DELETE, OPTIONS, WebSocket]: HTTP method to use (defaults to GET)"); Console.WriteLine("-n, --namespace <namespace>: Namespace to use (defaults to custom-http-endpoint)"); Console.WriteLine("-p, --path <path>: HTTP query path (defaults to demo)"); Console.WriteLine("-e, --exec <executable>: Command to execute when an HTTP query is received, stdout and stderr are returned as the response body"); Console.WriteLine("-a, --args <arguments>: Arguments for the executable command. Query values in % chars are replaced with query options (e.g. %myvalue%). Not applicable for WebSockets"); Console.WriteLine("-q, --quiet: Do not display info text"); Console.WriteLine("-h, --help: Displays this text"); return; } lastArg = arg; } if (_method == HttpEndpointType.WebSocket && (!string.IsNullOrWhiteSpace(_cmd) || !string.IsNullOrWhiteSpace(_args))) { Console.WriteLine("Error: Cannot use --exec parameter if method equals WebSocket"); } // Create a new Command connection CommandConnection connection = new CommandConnection(); await connection.Connect(socketPath); // Create a new HTTP GET endpoint and keep listening for new requests try { using HttpEndpointUnixSocket socket = await connection.AddHttpEndpoint(_method, ns, path); socket.OnEndpointRequestReceived += OnHttpRequestReceived; // Display a message if (!_quiet) { Console.WriteLine("{0} endpoint has been created and is now accessible via /machine/{1}/{2}", _method, ns, path); if (_method == HttpEndpointType.WebSocket) { Console.WriteLine("IO from the first WebSocket connection will be redirected to stdio. Additional connections will be automatically closed."); } else if (string.IsNullOrWhiteSpace(_cmd)) { Console.WriteLine("Press RETURN to close this program again"); } } // Wait forever (or for Ctrl+C) in WebSocket mode or for the user to press RETURN in interactive REST mode. // If the connection is terminated while waiting, continue as well if (_method == HttpEndpointType.WebSocket || string.IsNullOrWhiteSpace(_cmd)) { Task primaryTask = (_method == HttpEndpointType.WebSocket) ? Task.Delay(-1) : Task.Run(() => Console.ReadLine()); await Task.WhenAny(primaryTask, PollConnection(connection)); } } catch (SocketException) { // You may want to try to unregister your endpoint here and try again... Console.WriteLine("Failed to create new HTTP socket. Perhaps another instance of this program is already running?"); } finally { if (connection.IsConnected) { // Remove the endpoint again when the plugin is being unloaded await connection.RemoveHttpEndpoint(_method, ns, path); } } }
public static async Task Main(string[] args) { // Parse the command line arguments string lastArg = null, codeToExecute = null, socketPath = Defaults.FullSocketPath; bool quiet = false; foreach (string arg in args) { if (lastArg == "-s" || lastArg == "--socket") { socketPath = arg; } else if (lastArg == "-c" || lastArg == "-c") { codeToExecute = 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("-c, --code <code>: Execute the given code(s), wait for the result and exit. Alternative codes: startUpdate (Set DSF to updating), endUpdate (End DSF updating state)"); Console.WriteLine("-q, --quiet: Do not output any messages (not applicable for code replies in interactive mode)"); Console.WriteLine("-h, --help: Display this help text"); return; } lastArg = arg; } // Create a new connection and connect to DuetControlServer using CommandConnection connection = new CommandConnection(); await connection.Connect(socketPath); // Check if this is an interactive session if (codeToExecute == null) { if (!quiet) { // Notify the user that a connection has been established Console.WriteLine("Connected!"); } // Register an (interactive) user session int sessionId = await connection.AddUserSession(DuetAPI.ObjectModel.AccessLevel.ReadWrite, DuetAPI.ObjectModel.SessionType.Local, "console"); // Start reading lines from stdin and send them to DCS as simple codes. // When the code has finished, the result is printed to stdout string input = Console.ReadLine(); while (input != null && input != "exit" && input != "quit") { try { if (input.Equals("startUpdate", StringComparison.InvariantCultureIgnoreCase)) { await connection.SetUpdateStatus(true); Console.WriteLine("DSF is now in update mode"); } else if (input.Equals("endUpdate", StringComparison.InvariantCultureIgnoreCase)) { await connection.SetUpdateStatus(false); Console.WriteLine("DSF is no longer in update mode"); } else if (input.StartsWith("eval ", StringComparison.InvariantCultureIgnoreCase)) { JsonElement result = await connection.EvaluateExpression <JsonElement>(input[5..].Trim());
public static async Task Main(string[] args) { // Parse the command line arguments PluginOperation operation = PluginOperation.List; string lastArg = null, plugin = null, socketPath = Defaults.FullSocketPath; foreach (string arg in args) { if (lastArg == "-s" || lastArg == "--socket") { socketPath = arg; } else if (lastArg == "install") { operation = PluginOperation.Install; plugin = arg; } else if (lastArg == "start") { operation = PluginOperation.Start; plugin = arg; } else if (lastArg == "set-data") { operation = PluginOperation.SetData; plugin = arg; } else if (lastArg == "stop") { operation = PluginOperation.Stop; plugin = arg; } else if (lastArg == "uninstall") { operation = PluginOperation.Uninstall; plugin = arg; } else if (arg == "list") { operation = PluginOperation.List; } else if (arg == "list-data") { operation = PluginOperation.ListData; } else if (arg == "-h" || arg == "--help") { Console.WriteLine("Available command line arguments:"); Console.WriteLine("list: List plugin status (default)"); Console.WriteLine("list-data: List plugin data"); Console.WriteLine("install <zipfile>: Install new ZIP bundle"); Console.WriteLine("start <name>: Start a plugin"); Console.WriteLine("set-data <plugin>:<key>=<value>: Set plugin data (JSON or text)"); Console.WriteLine("stop <name>: Stop a plugin"); Console.WriteLine("uninstall <name>: Uninstall a plugin"); Console.WriteLine("-s, --socket <socket>: UNIX socket to connect to"); Console.WriteLine("-h, --help: Display this help text"); return; } lastArg = arg; } // Create a new connection and connect to DuetControlServer using CommandConnection connection = new CommandConnection(); await connection.Connect(socketPath); // Check what to do ObjectModel model; switch (operation) { case PluginOperation.List: model = await connection.GetObjectModel(); if (model.Plugins.Count > 0) { Console.WriteLine("{0,-24} {1,-16} {2,-24} {3,-24} {4,-12}", "Plugin", "Version", "Author", "License", "Status"); foreach (Plugin item in model.Plugins) { string pluginState = "n/a"; if (!string.IsNullOrEmpty(item.SbcExecutable)) { pluginState = (item.Pid > 0) ? "Started" : "Stopped"; } Console.WriteLine("{0,-24} {1,-16} {2,-24} {3,-24} {4,-12}", item.Name, item.Version, item.Author, item.License, pluginState); } } else { Console.WriteLine("No plugins installed"); } break; case PluginOperation.ListData: model = await connection.GetObjectModel(); if (model.Plugins.Count > 0) { foreach (Plugin item in model.Plugins) { Console.WriteLine("Plugin {0}:", item.Name); foreach (var kv in item.SbcData) { Console.WriteLine("{0} = {1}", kv.Key, JsonSerializer.Serialize(kv.Value, DuetAPI.Utility.JsonHelper.DefaultJsonOptions)); } Console.WriteLine(); } } else { Console.WriteLine("No plugins installed"); } break; case PluginOperation.Install: try { await connection.InstallPlugin(plugin); Console.WriteLine("Plugin installed"); } catch (Exception e) { Console.WriteLine("Failed to install plugin: {0}", e.Message); } break; case PluginOperation.Start: try { await connection.StartPlugin(plugin); Console.WriteLine("Plugin started"); } catch (Exception e) { Console.WriteLine("Failed to start plugin: {0}", e.Message); } break; case PluginOperation.SetData: // Parse plugin argument in the format // <plugin>:<key>=<value> string pluginName = string.Empty, key = string.Empty, value = string.Empty; int state = 0; foreach (char c in plugin) { switch (state) { case 0: if (c == ':') { state++; } else { pluginName += c; } break; case 1: if (c == '=') { state++; } else { key += c; } break; case 2: value += c; break; } } // Try to set the data try { try { using JsonDocument json = JsonDocument.Parse(value); await connection.SetPluginData(key, json.RootElement, pluginName); } catch (JsonException) { await connection.SetPluginData(key, value, pluginName); } Console.WriteLine("Plugin data set"); } catch (Exception e) { Console.WriteLine("Failed to set plugin data: {0}", e.Message); } break; case PluginOperation.Stop: try { await connection.StopPlugin(plugin); Console.WriteLine("Plugin stopped"); } catch (Exception e) { Console.WriteLine("Failed to stop plugin: {0}", e.Message); } break; case PluginOperation.Uninstall: try { await connection.UninstallPlugin(plugin); } catch (Exception e) { Console.WriteLine("Failed to uninstall plugin: {0}", e.Message); } break; } }