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();
            }
        }
Esempio n. 2
0
        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);
                }
            }
        }
Esempio n. 9
0
        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());
Esempio n. 10
0
        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;
            }
        }