{"record":{"id":"a2da62d435384f08","repo":"mgth/LittleBigMouse","slug":"the-live-configuration-was-applied-but-crash-recovery","errorCode":null,"errorMessage":"The live configuration was applied, but crash-recovery settings could not be saved.","messagePattern":"The live configuration was applied, but crash-recovery settings could not be saved\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"LittleBigMouse.Ui/LittleBigMouse.Ui.Avalonia/Remote/LittleBigMouseClientService.cs","lineNumber":208,"sourceCode":"    }\n\n    /// <param name=\"persist\">\n    /// False for a send the daemon should run but not remember (live preview): the\n    /// recovery file keeps describing the last applied layout. Serializing a layout is\n    /// the expensive half of a send, so the recovery copy is only built when it is\n    /// actually going to be written.\n    /// </param>\n    async Task SendMessagesAsync(IEnumerable<CommandMessage> messages,\n        CancellationToken token = default, int timeout = 5000, bool persist = true)\n    {\n        var commands = messages.ToList();\n        var wireXml = $\"<Messages>{string.Concat(commands.Select(command => command.Serialize()))}</Messages>\";\n\n        var persistenceFailure = await _recovery.PersistAsync(commands, persist, token);\n\n        await _client.SendMessageAsync(wireXml, TimeSpan.FromMilliseconds(timeout), token);\n        if (persistenceFailure is not null)\n            throw new InvalidOperationException(\n                \"The live configuration was applied, but crash-recovery settings could not be saved.\",\n                persistenceFailure);\n    }\n\n    public void Dispose()\n    {\n        _client.Dispose();\n        _processManager.Dispose();\n        GC.SuppressFinalize(this);\n    }\n}\n","sourceCodeStart":190,"sourceCodeEnd":220,"githubUrl":"https://github.com/mgth/LittleBigMouse/blob/7a42f01d47d99d223b8ee33ba4019af82adf1c48/LittleBigMouse.Ui/LittleBigMouse.Ui.Avalonia/Remote/LittleBigMouseClientService.cs#L190-L220","documentation":"LittleBigMouseClientService.SendMessagesAsync sends live commands over the IPC connection and then asks a recovery store (_recovery.PersistAsync) to persist crash-recovery state. PersistAsync does not throw on failure — it returns the exception as persistenceFailure — so the live configuration is already applied when this InvalidOperationException is thrown, wrapping the underlying persistence exception as InnerException. The message stresses that the client state and the daemon state are now out of sync only with respect to what happens after a crash.","triggerScenarios":"Calling SendMessagesAsync (directly or via StartAsync, SendLiveAsync, SendShortcutAsync, SendMessageAsync) when the recovery store cannot write its persistence file — disk full, read-only config directory, permission denied on the recovery file, or serialization/IO failure inside PersistAsync.","commonSituations":"Running LittleBigMouse with a read-only or full disk; config directory owned by another user; Flatpak/sandbox restricting writes to the app config path; the recovery file locked by another process; a corrupted recovery store causing the write to fail.","solutions":["Inspect InnerException (persistenceFailure) to see the real IO error and fix the root cause — free disk space, repair permissions, or point the recovery store at a writable directory.","Verify the crash-recovery file's directory is writable by the running user (ls -l on the config dir; chown/chmod if needed).","Delete a corrupted recovery/persistence file so the store can recreate it, then resend the configuration.","If the loss of crash-recovery persistence is acceptable, catch InvalidOperationException around Send* calls and continue — the live configuration was applied regardless."],"exampleFix":"// before\nawait client.SendLiveAsync(state); // throws after apply when persistence fails\n// after\ntry { await client.SendLiveAsync(state); }\ncatch (InvalidOperationException ex)\n{\n    _logger.Warn(ex.InnerException,\n        \"Config applied but crash-recovery not saved; check disk/permissions for the recovery file\");\n}","handlingStrategy":"try-catch","validationCode":"// before sending, check the recovery store is writable\nvar recoveryPath = recoveryStore.FilePath;\nif (File.Exists(recoveryPath) && File.GetAttributes(recoveryPath).HasFlag(FileAttributes.ReadOnly))\n    throw new InvalidOperationException(\"Recovery file is read-only; fix before sending live config\");","typeGuard":"bool RecoveryStoreWritable(ILittleBigMouseRecovery recovery) =>\n{\n    try\n    {\n        var probe = Path.Combine(Path.GetDirectoryName(recovery.FilePath)!, \".write-probe\");\n        File.WriteAllText(probe, \"ok\"); File.Delete(probe);\n        return true;\n    }\n    catch (IOException) { return false; }\n    catch (UnauthorizedAccessException) { return false; }\n};","tryCatchPattern":"try { await client.SendMessagesAsync(commands); }\ncatch (InvalidOperationException ex) when (ex.InnerException is not null)\n{ _logger.Warn(ex.InnerException, \"Config applied; crash-recovery save failed — check disk space/permissions\"); }","preventionTips":["Keep the app config directory writable by the running user; avoid running as a different user than the installer.","Monitor free disk space in the config volume before pushing large live configurations.","Verify sandbox/Flatpak permissions include the recovery file path.","Always inspect InnerException — it carries the real IO failure from PersistAsync."],"tags":["ipc","persistence","io","crash-recovery"],"backgroundTag":"file-write-failed","analyzedSha":"7a42f01d47d99d223b8ee33ba4019af82adf1c48","analyzedAt":"2026-09-16T00:35:00.514Z","contentChangedAt":"2026-09-16T00:35:00.514Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}