/// <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.GetSerializedMachineModel(cancellationToken);

                await webSocket.SendAsync(objectModelPatch.ToArray(), WebSocketMessageType.Text, true, cancellationToken);
            }while (webSocket.State == WebSocketState.Open);
        }
        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
            }
        }