首页
/ Windows Terminal 应用状态管理:state.json 与 ApplicationState 的设计与实现

Windows Terminal 应用状态管理:state.json 与 ApplicationState 的设计与实现

2026-09-06 12:25:19作者:伍希望

本文基于 Windows Terminal 仓库中的设计规范 [doc/specs/#8324 - Application State (TSM).md](https://gitcode.com/GitHub_Trending/term/terminal/blob/20588130d8ef2ba40eb56bdae88e04cce7fc5b5d/doc/specs/?utm_source=gitcode_repo_files#8324 - Application State (TSM).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-macro MTSM_APPLICATION_STATE_FIELDS(X) 集中声明了所有持久化字段,这也是头文件注释所说的"加新字段只需要改 IDL 和这个宏":

JSON 键 C++ 属性 类型 来源 用途
settingsHash SettingsHash hstring Shared 缓存 settings.json 的哈希,避免设置未变时做昂贵的处理(如更新 Jumplist)
generatedProfiles GeneratedProfiles unordered_set<guid> Shared 记录已生成的动态 Profile,防止"删了又冒出来"
persistedWindowLayouts PersistedWindowLayouts IVector<WindowLayout> Local 上一次会话遗留的窗口布局队列,用于启动时恢复
recentCommands RecentCommands IVector<hstring> Shared 命令面板(Command Palette)的历史命令行
dismissedMessages DismissedMessages IVector<InfoBarMessage> Shared 已被用户关闭的 InfoBar 提示("Do not ask again" 的落地形态)
allowedCommandlines AllowedCommandlines IVector<hstring> Local 管理员实例中被用户允许执行的命令行
dismissedBadges DismissedBadges unordered_set<hstring> Local 设置 UI 中被用户隐藏的徽章(badge)
persistedWorkspaces PersistedWorkspaces IMap<hstring, WindowLayout> Local 以窗口名命名的工作区布局(具名工作区保存/恢复)
sshFolderGenerated SSHFolderGenerated bool(默认 false Shared 标记 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.cppCascadiaSettingsSerialization.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.cppRenameWorkspace 迁移条目;
  • TerminalPage.cpp 通过 AllPersistedWorkspaces() 列出可恢复的具名工作区,用户删除时调用 RemoveWorkspace
  • TakeWorkspace 提供**原子的"取出即删除"**语义,源码注释说明这是启动路径专用 API,保证同一工作区只会被一个调用者领取。

6.4 命令面板历史:RecentCommands

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

6.5 设置哈希:SettingsHash

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

6.6 设置 UI 徽章:DismissedBadge

ActionsViewModel.cppDismissBadge/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](https://gitcode.com/GitHub_Trending/term/terminal/blob/20588130d8ef2ba40eb56bdae88e04cce7fc5b5d/doc/specs/?utm_source=gitcode_repo_files#8324 - Application State (TSM).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(行为验证)。

登录后查看全文
热门项目推荐
相关项目推荐