Windows Terminal 应用状态管理state.json 与 ApplicationState 的设计与实现【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal本文基于 Windows Terminal 仓库中的设计规范 doc/specs/#8324 - Application State (TSM).md.md) 展开讲解 Windows Terminal 如何为跨会话应用状态不再提示对话框、动态 Profile 去重、窗口布局恢复等提供一套独立于用户配置文件settings.json的持久化机制state.json并结合src/cascadia/TerminalSettingsModel下的实际源码说明该规范如何演化为当前ApplicationState类的完整 API、双文件存储模型与延迟写盘策略。读完本文你将掌握该机制解决什么问题、为什么不在settings.json中存这些状态、state.json放在哪里、包含哪些字段、读写与容错流程是怎样的以及窗口布局恢复、命令面板历史、动态 Profile 等特性是如何构建在这套状态模型之上的。一、Application State 要解决什么问题规范文档开篇列出了三类需要跨会话保存、但不适合放进用户配置文件的状态对话框不再提示状态用户在对话框上勾选[ ] Do not ask again后对应 issue #6641应用需要记住这一选择避免下次启动时反复询问动态 Profile 去重记录记录哪些动态 Profile如 SSH 主机、match规则生成的 Profile已经生成过以解决用户对 Profile 删了又冒出来 的不满对应 issue #3231窗口状态恢复窗口在屏幕上的位置、激活的会话状态、布局等为将来的窗口恢复功能做准备对应 issue #961。规范作者的观点很明确上述设置不适合存进用户的settings.json理由有三这些状态不需要立即传播到其他 Windows Terminal 实例它们并不面向用户手工编辑把它们存到settings.json之外可以避免程序去修补用户配置文件patch users settings file所固有的风险。因此规范的解法是在settings.json旁边单独存放一个应用状态档案state.json并通过Microsoft.Terminal.Settings命名空间下的一组 API 来访问。二、规范中的 API 设计与无显式 Save原则规范给出的初始 API 草图WinRT IDL如下namespace Microsoft.Terminal.Settings { [default_interface] runtimeclass ApplicationState { // GetForCurrentApplication will return an object deserialized from state.json. static ApplicationState GetForCurrentApplication(); void Clear(); IVectorguid GeneratedProfiles; Boolean ShowCloseOnExitWarning; // ... further settings ... } }其中规范还特别强调了两个设计点将 JSON 反/序列化集中在一处把这些状态暴露到统一命名空间下的唯一动机就是让 JSON 的读写只发生在ApplicationState这一个地方没有显式的Save或Commit机制对应用状态的修改会在稍短的一段时间后被持久化committed durably a short duration after theyre made。UI/UX 层面规范认为该机制不直接影响界面但可以考虑在设置页加一个重置所有对话框按钮reset all dialogs。同时规范明确state.json不预期被手工编辑因此无需为了人类可读性做缩进序列化。可靠性与潜在问题部分还给出两条重要原则复用现有的 JSON 解析器不引入新的安全攻击面一旦状态文件损坏应抛弃整个状态载荷而不是尝试抢救——宁可丢失状态也要做正确的事。这两点在源码中都有直接对应下文逐一展开。三、实际实现ApplicationState 与双文件模型3.1 从规范到落地API 的演化规范草稿中的GetForCurrentApplication/Clear/ShowCloseOnExitWarning在落地时演化成了 ApplicationState.idl 中定义的 API命名空间也扩展为Microsoft.Terminal.Settings.Model[default_interface] runtimeclass ApplicationState { static ApplicationState SharedInstance(); void Flush(); void Reset(); void AppendPersistedWindowLayout(WindowLayout layout); Boolean DismissBadge(String badgeId); Boolean BadgeDismissed(String badgeId); void SaveWorkspace(String name, WindowLayout layout); Boolean RemoveWorkspace(String name); Boolean RenameWorkspace(String oldName, String newName); WindowLayout TakeWorkspace(String name); Windows.Foundation.Collections.IMapViewString, WindowLayout AllPersistedWorkspaces(); String SettingsHash; Windows.Foundation.Collections.IVectorWindowLayout PersistedWindowLayouts; Windows.Foundation.Collections.IVectorString RecentCommands; Windows.Foundation.Collections.IVectorInfoBarMessage DismissedMessages; Windows.Foundation.Collections.IVectorString AllowedCommandlines; }可以看到规范的三大意图全部保留SharedInstance()对应GetForCurrentApplication()返回反序列化自state.json的单例Reset()对应Clear()而GeneratedProfiles、不再提示对话框等状态则演化为DismissedMessages配合InfoBarMessage枚举见 ApplicationState.idl及若干字段。Flush()则提供了规范中无显式 Save原则之外的一个强制落盘手段。3.2 状态文件放在哪里ApplicationState.cpp 定义了两个文件名static constexpr std::wstring_view stateFileName{ Lstate.json }; static constexpr std::wstring_view elevatedStateFileName{ Lelevated-state.json };目录由 FileUtils.cpp 中的GetBaseSettingsPath()决定与settings.json同目录打包安装的包Microsoft Store 版%LOCALAPPDATA%\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\非打包版本GitHub 直装版%LOCALAPPDATA%\Microsoft\Windows Terminal\便携模式可执行文件旁存在.portable标记文件可执行文件所在目录下的settings\子目录。构造函数ApplicationState(stateRoot)会基于该目录初始化两条路径_sharedPathstate.json与_elevatedPathelevated-state.json并立即_read()。SharedInstance()使用 C 函数内静态对象保证全局单例ApplicationState.cpp。3.3 状态字段清单Shared 与 Local 两类ApplicationState.h 中用一个 X-macroMTSM_APPLICATION_STATE_FIELDS(X)集中声明了所有持久化字段这也是头文件注释所说的加新字段只需要改 IDL 和这个宏JSON 键C 属性类型来源用途settingsHashSettingsHashhstringShared缓存 settings.json 的哈希避免设置未变时做昂贵的处理如更新 JumplistgeneratedProfilesGeneratedProfilesunordered_setguidShared记录已生成的动态 Profile防止删了又冒出来persistedWindowLayoutsPersistedWindowLayoutsIVectorWindowLayoutLocal上一次会话遗留的窗口布局队列用于启动时恢复recentCommandsRecentCommandsIVectorhstringShared命令面板Command Palette的历史命令行dismissedMessagesDismissedMessagesIVectorInfoBarMessageShared已被用户关闭的 InfoBar 提示Do not ask again 的落地形态allowedCommandlinesAllowedCommandlinesIVectorhstringLocal管理员实例中被用户允许执行的命令行dismissedBadgesDismissedBadgesunordered_sethstringLocal设置 UI 中被用户隐藏的徽章badgepersistedWorkspacesPersistedWorkspacesIMaphstring, WindowLayoutLocal以窗口名命名的工作区布局具名工作区保存/恢复sshFolderGeneratedSSHFolderGeneratedbool默认falseShared标记 SSH 动态 Profile 文件夹是否已生成FileSource枚举ApplicationState.h区分了两种字段Shared存于state.json提权管理员与非提权实例共享Local提权与非提权实例各存各的提权实例写入独立的elevated-state.json。这个设计解决了 Windows 特有的问题管理员权限的 Terminal 实例不应该把窗口位置、允许的命令行等本实例私事写进普通用户实例共享的文件里反之亦然。3.4 WindowLayout恢复的粒度被持久化的窗口布局由 WindowLayout 描述包含四个字段TabLayoutIVectorActionAndArgs以动作 参数序列描述该窗口里每个标签页应打开什么InitialPosition初始位置InitialSize初始尺寸LaunchMode启动模式最大化等。其 JSON 转换由 ApplicationState.cpp 中特化的ConversionTraitWindowLayout完成ToJson/FromJson静态方法支持把单个布局序列化成字符串便于外部存储场景复用。四、写盘机制延迟提交、防抖与强制刷新规范中修改会在稍后持久化、没有显式 Save的原则在实现中由一个til::throttled_func精确落地ApplicationState.cpp_throttler{ til::throttled_func_options{ .delay std::chrono::seconds{ 1 }, .debounce true, .trailing true, }, [this]() { _write(); } }延迟 1 秒、防抖debounce、尾部触发trailing连续快速修改只会在静默 1 秒后真正写盘一次所有 setter宏MTSM_APPLICATION_STATE_GEN生成的存取器见 ApplicationState.cpp以及AppendPersistedWindowLayout、SaveWorkspace、DismissBadge等方法在修改内存状态后都会调用_throttler()排程一次写盘Flush()会取消等待中的计时器并立即同步执行写盘析构函数中调用它保证进程退出前最后一次修改不丢ApplicationState.cpp写state.json使用til::io::write_utf8_string_to_file_atomic原子写先写临时文件再重命名普通实例的 Local/Shared 状态全部原子写入同一个state.json。提权实例的写盘更讲究。_write()ApplicationState.cpp在提权时不直接覆盖state.json而是先把现有state.json读成一个 JSON blob再把本实例可见的 Shared 属性叠写到该 blob 之上后写回——这样普通用户实例的 Local 属性如窗口布局能原样留在state.json中不被清空本提权实例自己的 Local 属性则单独写入elevated-state.json。读取侧_read对称地处理提权时只从state.json读 Shared 字段再叠加elevated-state.json中的 Local 字段非提权时从state.json读全部字段。此外_readLocalContents()在提权读取elevated-state.json时会校验文件权限权限不对就删除该文件避免读到恶意篡改的数据ApplicationState.cpp。写elevated-state.json时特意不用原子写防止未提权用户通过替换文件重命名的方式覆写提权文件。五、容错策略损坏即弃重置即删规范Potential Issues一节说状态文件是用户可能误编辑的又一个文件一旦损坏应丢弃整个载荷而不是抢救。源码注释直接印证了这一点ApplicationState.cpp// * ANY errors during app state will result in the creation of a new empty state. // * ANY errors during runtime will result in changes being partially ignored._read()中任何 JSON 解析失败都会以WEB_E_INVALID_JSON_STRING抛出并进入CATCH_LOG()最终得到一份空状态——即重新开始与规范意图完全一致。规范中的Clear()演化为Reset()实现上比清空对象更彻底直接删除state.json与elevated-state.json两个文件再把内存状态重置为空ApplicationState.cpp。注释解释了原因如果只清空内存对象而不删文件下一次写盘时std::nullopt的字段不会从 JSON 中移除旧键数据就会复活。Reset()被设置在清除应用状态的路径调用见 CascadiaSettingsSerialization.cpp 与 CascadiaSettingsSerialization.cpp即规范 UI/UX 一节设想的reset入口在设置 UI 中落地的方式。六、状态模型支撑的实际功能以下功能均以ApplicationState::SharedInstance()为唯一入口印证了规范序列化集中在一处的目标6.1 动态 Profile 去重GeneratedProfilesCascadiaSettingsSerialization.cpp 中的SettingsLoader::DisableDeletedProfiles()正是规范哪些动态 Profile 已生成的落地遍历所有非用户来源generated的 Profile若其 GUID 不在GeneratedProfiles集合中则加入集合新面孔允许显示若已在集合中即上次会话已生成过则把该 Profile 标记为Deleted(true)与Hidden(true)。效果用户从settings.json或设置 UI 删掉一个动态 Profile 后下次加载它会被自动隐藏不会再冒出来——这正是规范开头提到的用户不满issue #3231的解法。SSH 文件夹的生成则用SSHFolderGenerated布尔位做一次性标记。6.2 Do not ask againDismissedMessages规范中ShowCloseOnExitWarning一类布尔字段的通用化形态是DismissedMessagesIVectorInfoBarMessage枚举含CloseOnExitInfo、KeyboardServiceWarning等。TerminalPage.cpp 在展示 InfoBar 前查询该集合用户关闭提示后 ID 即被写入状态并持久化跨会话生效。6.3 窗口布局恢复PersistedWindowLayouts 与具名工作区窗口关闭时TerminalPage.cpp 调用SaveWorkspace/AppendPersistedWindowLayout把当前窗口的WindowLayout记入状态TabManagement.cpp 也会按需保存工作区下一次启动时TerminalWindow.cpp 读取PersistedWindowLayouts并恢复遗留窗口窗口改名时经 TerminalWindow.cpp 的RenameWorkspace迁移条目TerminalPage.cpp 通过AllPersistedWorkspaces()列出可恢复的具名工作区用户删除时调用RemoveWorkspaceTakeWorkspace提供**原子的取出即删除**语义源码注释说明这是启动路径专用 API保证同一工作区只会被一个调用者领取。6.4 命令面板历史RecentCommandsCommandPalette.cpp 从RecentCommands读取历史命令行、去重后回填属于 Shared 字段提权与非提权实例共享同一份历史。6.5 设置哈希SettingsHashAppLogic.cpp 的_ProcessLazySettingsChanges()将当前settings.json的哈希与applicationState.SettingsHash()比对仅在设置真正变化时才执行更新 Jumplist 等昂贵操作并把新哈希写回状态。这是应用状态与用户配置解耦带来的一个额外收益。6.6 设置 UI 徽章DismissedBadgeActionsViewModel.cpp 用DismissBadge/BadgeDismissed记住用户对 Actions 页徽章的关闭操作——这是规范中不再提示思想在设置 UI 中的新应用。七、单元测试对关键语义的验证ApplicationStateTests.cpp 用指向临时目录%TEMP%\WT_ApplicationStateTests的一次性 ApplicationState 实例测试工作区持久化 API不触碰真实用户状态覆盖SaveAndLookupWorkspace保存后可通过AllPersistedWorkspaces查回RemoveWorkspaceReturnsFalseWhenMissing删除不存在的条目返回false删除后再次删除仍为falseRenameWorkspaceMigratesEntry/RenameWorkspaceNoOpForEmptyOrEqualNames/RenameWorkspaceNoOpForMissingEntry改名迁移、空名/同名 no-op、重命名为空串即删除旧条目三种边界TakeWorkspaceRemovesAndReturns/TakeWorkspaceReturnsNullWhenMissing验证原子性——同一名称第二次TakeWorkspace必须返回 null这正是启动恢复路径依赖的保证。八、小结从 doc/specs/#8324 - Application State (TSM).md.md) 的草案到src/cascadia/TerminalSettingsModel下的实现这条主线保持了规范的全部核心主张独立于settings.json的state.json文件、集中一处做 JSON 序列化、无显式 Save 的延迟持久化、面向非手工编辑场景、损坏即弃的容错策略。实现在此基础上做了三处实质性扩展字段宏驱动MTSM_APPLICATION_STATE_FIELDS让加一个持久化字段退化为在宏里加一行Shared/Local 双文件模型state.json与elevated-state.json分离提权/非提权实例的私有状态并以叠加写回的方式保证互不破坏状态面扩展从最初的GeneratedProfiles扩展到窗口布局队列、具名工作区、命令历史、InfoBar 免打扰、设置哈希等成为窗口恢复、命令面板、动态 Profile 去重等多个特性的公共底座。读者若要进一步深入可依次查看 ApplicationState.idl对外 API、ApplicationState.h字段与实现骨架、ApplicationState.cpp读写与提权逻辑、FileUtils.cpp状态文件目录解析与 ApplicationStateTests.cpp行为验证。【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考