Windows Terminal 应用状态管理:state.json 与 ApplicationState 的设计与实现
2026/9/7 23:10:51 网站建设 项目流程

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 要解决什么问题

规范文档开篇列出了三类需要跨会话保存、但不适合放进用户配置文件的状态:

  1. 对话框"不再提示"状态:用户在对话框上勾选[ ] Do not ask again后(对应 issue #6641),应用需要记住这一选择,避免下次启动时反复询问;
  2. 动态 Profile 去重记录:记录哪些动态 Profile(如 SSH 主机、match规则生成的 Profile)已经生成过,以解决用户对 Profile "删了又冒出来" 的不满(对应 issue #3231);
  3. 窗口状态恢复:窗口在屏幕上的位置、激活的会话状态、布局等,为将来的窗口恢复功能做准备(对应 issue #961)。

规范作者的观点很明确:上述设置不适合存进用户的settings.json,理由有三:

  • 这些状态不需要立即传播到其他 Windows Terminal 实例;
  • 它们并不面向用户手工编辑;
  • 把它们存到settings.json之外,可以避免程序去修补用户配置文件(patch user's 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(); IVector<guid> GeneratedProfiles; Boolean ShowCloseOnExitWarning; // ... further settings ... } }

其中规范还特别强调了两个设计点:

  • 将 JSON 反/序列化集中在一处:把这些状态暴露到统一命名空间下的唯一动机,就是让 JSON 的读写只发生在ApplicationState这一个地方;
  • 没有显式的SaveCommit机制:对应用状态的修改会在"稍短的一段时间后"被持久化(committed durably a short duration after they're 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.IMapView<String, WindowLayout> AllPersistedWorkspaces(); String SettingsHash; Windows.Foundation.Collections.IVector<WindowLayout> PersistedWindowLayouts; Windows.Foundation.Collections.IVector<String> RecentCommands; Windows.Foundation.Collections.IVector<InfoBarMessage> DismissedMessages; Windows.Foundation.Collections.IVector<String> AllowedCommandlines; }

可以看到规范的三大意图全部保留:SharedInstance()对应GetForCurrentApplication()(返回反序列化自state.json的单例);Reset()对应Clear();而GeneratedProfiles、"不再提示"对话框等状态则演化为DismissedMessages(配合InfoBarMessage枚举,见 ApplicationState.idl)及若干字段。Flush()则提供了规范中"无显式 Save"原则之外的一个强制落盘手段。

3.2 状态文件放在哪里

ApplicationState.cpp 定义了两个文件名:

static constexpr std::wstring_view stateFileName{ L"state.json" }; static constexpr std::wstring_view elevatedStateFileName{ L"elevated-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 的哈希,避免设置未变时做昂贵的处理(如更新 Jumplist)
generatedProfilesGeneratedProfilesunordered_set<guid>Shared记录已生成的动态 Profile,防止"删了又冒出来"
persistedWindowLayoutsPersistedWindowLayoutsIVector<WindowLayout>Local上一次会话遗留的窗口布局队列,用于启动时恢复
recentCommandsRecentCommandsIVector<hstring>Shared命令面板(Command Palette)的历史命令行
dismissedMessagesDismissedMessagesIVector<InfoBarMessage>Shared已被用户关闭的 InfoBar 提示("Do not ask again" 的落地形态)
allowedCommandlinesAllowedCommandlinesIVector<hstring>Local管理员实例中被用户允许执行的命令行
dismissedBadgesDismissedBadgesunordered_set<hstring>Local设置 UI 中被用户隐藏的徽章(badge)
persistedWorkspacesPersistedWorkspacesIMap<hstring, WindowLayout>Local以窗口名命名的工作区布局(具名工作区保存/恢复)
sshFolderGeneratedSSHFolderGeneratedbool(默认falseShared标记 SSH 动态 Profile 文件夹是否已生成

FileSource枚举(ApplicationState.h)区分了两种字段:

  • Shared:存于state.json,提权(管理员)与非提权实例共享;
  • Local:提权与非提权实例各存各的,提权实例写入独立的elevated-state.json

这个设计解决了 Windows 特有的问题:管理员权限的 Terminal 实例不应该把窗口位置、允许的命令行等"本实例私事"写进普通用户实例共享的文件里(反之亦然)。

3.4 WindowLayout:恢复的粒度

被持久化的窗口布局由 WindowLayout 描述,包含四个字段:

  • TabLayoutIVector<ActionAndArgs>):以"动作 + 参数"序列描述该窗口里每个标签页应打开什么;
  • InitialPosition:初始位置;
  • InitialSize:初始尺寸;
  • LaunchMode:启动模式(最大化等)。

其 JSON 转换由 ApplicationState.cpp 中特化的ConversionTrait<WindowLayout>完成,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)以及AppendPersistedWindowLayoutSaveWorkspaceDismissBadge等方法在修改内存状态后都会调用_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.jsonelevated-state.json两个文件,再把内存状态重置为空(ApplicationState.cpp)。注释解释了原因:如果只清空内存对象而不删文件,下一次写盘时std::nullopt的字段不会从 JSON 中移除旧键,数据就会"复活"。Reset()被设置在"清除应用状态"的路径调用(见 CascadiaSettingsSerialization.cpp 与 CascadiaSettingsSerialization.cpp),即规范 UI/UX 一节设想的"reset"入口在设置 UI 中落地的方式。

六、状态模型支撑的实际功能

以下功能均以ApplicationState::SharedInstance()为唯一入口,印证了规范"序列化集中在一处"的目标:

6.1 动态 Profile 去重:GeneratedProfiles

CascadiaSettingsSerialization.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 again":DismissedMessages

规范中ShowCloseOnExitWarning一类布尔字段的通用化形态是DismissedMessagesIVector<InfoBarMessage>,枚举含CloseOnExitInfoKeyboardServiceWarning等)。TerminalPage.cpp 在展示 InfoBar 前查询该集合,用户关闭提示后 ID 即被写入状态并持久化,跨会话生效。

6.3 窗口布局恢复:PersistedWindowLayouts 与具名工作区

  • 窗口关闭时,TerminalPage.cpp 调用SaveWorkspace/AppendPersistedWindowLayout把当前窗口的WindowLayout记入状态;TabManagement.cpp 也会按需保存工作区;
  • 下一次启动时,TerminalWindow.cpp 读取PersistedWindowLayouts并恢复遗留窗口,窗口改名时经 TerminalWindow.cpp 的RenameWorkspace迁移条目;
  • TerminalPage.cpp 通过AllPersistedWorkspaces()列出可恢复的具名工作区,用户删除时调用RemoveWorkspace
  • TakeWorkspace提供**原子的"取出即删除"**语义,源码注释说明这是启动路径专用 API,保证同一工作区只会被一个调用者领取。

6.4 命令面板历史:RecentCommands

CommandPalette.cpp 从RecentCommands读取历史命令行、去重后回填,属于 Shared 字段,提权与非提权实例共享同一份历史。

6.5 设置哈希:SettingsHash

AppLogic.cpp 的_ProcessLazySettingsChanges()将当前settings.json的哈希与applicationState.SettingsHash()比对,仅在设置真正变化时才执行更新 Jumplist 等昂贵操作,并把新哈希写回状态。这是"应用状态与用户配置解耦"带来的一个额外收益。

6.6 设置 UI 徽章:DismissedBadge

ActionsViewModel.cpp 用DismissBadge/BadgeDismissed记住用户对 Actions 页徽章的关闭操作——这是规范中"不再提示"思想在设置 UI 中的新应用。

七、单元测试对关键语义的验证

ApplicationStateTests.cpp 用指向临时目录(%TEMP%\WT_ApplicationStateTests)的一次性 ApplicationState 实例测试工作区持久化 API,不触碰真实用户状态,覆盖:

  • SaveAndLookupWorkspace:保存后可通过AllPersistedWorkspaces查回;
  • RemoveWorkspaceReturnsFalseWhenMissing:删除不存在的条目返回false,删除后再次删除仍为false
  • RenameWorkspaceMigratesEntry/RenameWorkspaceNoOpForEmptyOrEqualNames/RenameWorkspaceNoOpForMissingEntry:改名迁移、空名/同名 no-op、"重命名为空串即删除旧条目"三种边界;
  • TakeWorkspaceRemovesAndReturns/TakeWorkspaceReturnsNullWhenMissing:验证原子性——同一名称第二次TakeWorkspace必须返回 null,这正是启动恢复路径依赖的保证。

八、小结

从 doc/specs/#8324 - Application State (TSM).md.md) 的草案到src/cascadia/TerminalSettingsModel下的实现,这条主线保持了规范的全部核心主张:独立于settings.jsonstate.json文件、集中一处做 JSON 序列化、无显式 Save 的延迟持久化、面向非手工编辑场景、损坏即弃的容错策略。实现在此基础上做了三处实质性扩展:

  1. 字段宏驱动MTSM_APPLICATION_STATE_FIELDS让"加一个持久化字段"退化为在宏里加一行;
  2. Shared/Local 双文件模型state.jsonelevated-state.json分离提权/非提权实例的私有状态,并以"叠加写回"的方式保证互不破坏;
  3. 状态面扩展:从最初的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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询