首页
/ Windows Terminal 连接 Azure Cloud Shell 的完整设计与源码实现

Windows Terminal 连接 Azure Cloud Shell 的完整设计与源码实现

2026-09-04 15:08:28作者:柯茵沙

Windows Terminal 允许用户把 Azure Cloud Shell 当作一个"连接"直接打开,本文以规格文档 [Azure cloud shell connector](https://gitcode.com/GitHub_Trending/term/terminal/blob/20588130d8ef2ba40eb56bdae88e04cce7fc5b5d/doc/specs/?utm_source=gitcode_repo_files#1564 - Settings UI/spec.md) 为骨架,完整讲清这一特性的设计动机、认证流程、连接生命周期,并深入仓库源码,逐条印证 ITerminalConnection 接口是如何被 AzureConnection 实现的。读完你能掌握:终端"连接"的抽象模型、基于设备码(Device Code Flow)的无浏览器认证方案,以及令牌存储、多租户选择、WebSocket 建立等关键实现细节。

![Azure Cloud Shell 在 Windows Terminal 中作为独立配置文件出现的示意图](https://gitcode.com/GitHub_Trending/term/terminal/blob/20588130d8ef2ba40eb56bdae88e04cce7fc5b5d/doc/specs/?utm_source=gitcode_repo_files#1235 - Azure cloud shell connector/images/azProf.png)

一、设计目标:把 Azure 服务带进 Windows Terminal

规格文档(issue #1235)开篇的摘要非常直接:这份规格描述了一个功能——让 Windows Terminal 用户连接到 Azure Cloud Shell,并包含实现与设计考量。其灵感(Inspiration)是:让开发者在 Windows Terminal 应用内就能顺滑地访问自己的 Azure 服务,以方便的方式与 Azure 技术打交道。

从仓库的实际落地看,这个"方便"被落实为一项产品能力:当平台支持时,用户会看到一个名为 "Azure Cloud Shell" 的动态配置文件(见上方配图,实际实现中该配置拥有独立图标),选中它即可发起一次完整的云端连接。这一能力对应源码中的配置文件生成器 AzureCloudShellGenerator

规格文档同时明确了整个功能必须"以隔离方式实现"——它对 Windows Terminal 应用本体几乎没有依赖。这样一旦终端支持插件/扩展,这个连接器就能直接变成一个插件。这一点在源码中得到忠实贯彻:AzureConnection 没有侵入终端主流程,而是作为一个普通的连接类型存在。

二、认证方案:设备码流程与 Azure AD v1.0

规格文档对"认证用户"这一步的设计是全文最关键的部分,原因有二:

  1. 为什么用设备码流程(Device Code Flow):因为 Windows Terminal(当时)不支持拉起浏览器。设备码流程允许用户在一个带屏幕的设备(这里就是终端)上输入一段短码,然后在另一台设备的浏览器里完成登录,终端这边通过轮询拿到令牌。
  2. 为什么用 Azure AD v1.0 而不是 v2.0:因为 v2.0(即 Microsoft Identity Platform)彼时不支持个人账号(personal accounts)走设备码流程。而 Azure Cloud Shell 用户中个人账号很常见,因此选 v1.0。

关于令牌的存放,规格文档的要求是:认证成功后把登录/令牌信息存下来,避免用户每次都走一遍设备码流程;由于这是敏感信息,令牌需加密存储。规格当时指向的是 Windows Storage 加上 Windows Security Data Protection(DPAPI)。

这里需要特别指出实现与规格的差异:从源码看,真正落地采用的是 Windows.Security.Credentials 命名空间下的 PasswordVault / PasswordCredential(而非规格里设想的 Storage + DPAPI 组合)。以 AzureConnection.cpp 为例,_StoreCredential 通过 PasswordVault 把"用户名"字段(JSON 序列化的租户信息)与"密码"字段(JSON 序列化的 accessToken / refreshToken / expiry)打包存入,资源名固定为 Terminal

// src/cascadia/TerminalConnection/AzureConnection.cpp
void AzureConnection::_StoreCredential()
{
    WDJ::JsonObject userName;
    userName.SetNamedValue(L"ver", WDJ::JsonValue::CreateNumberValue(CurrentCredentialVersion));
    _packTenant(userName, *_currentTenant);

    WDJ::JsonObject passWord;
    passWord.SetNamedValue(L"accessToken", WDJ::JsonValue::CreateStringValue(_accessToken));
    passWord.SetNamedValue(L"refreshToken", WDJ::JsonValue::CreateStringValue(_refreshToken));
    passWord.SetNamedValue(L"expiry", WDJ::JsonValue::CreateStringValue(std::to_wstring(_expiry)));

    PasswordVault vault;
    PasswordCredential newCredential{ PasswordVaultResourceName, userName.Stringify(), passWord.Stringify() };
    vault.Add(newCredential);
}

其中 CurrentCredentialVersion 是一个"令牌版本"常量(当前为 2)。源码在 _RunAccessState 里会读取每条已存凭据的 ver 字段,凡是版本不一致的旧凭据都会被直接移除vault.Remove(entry)),只保留最新格式。这是一种平滑的存储格式迁移机制。令牌临近过期时(timeNow + _expireLimit > _expiry,其中 _expireLimit 为 2700 秒)会自动走 _RefreshTokens 刷新并回写。

结论:规格文档给出了"隔离实现 + 设备码认证 + 加密令牌存储"的设计意图,而源码在"存储用什么 API"这一细节上与原始设想不同,但总体方向(避免重复登录、安全存储、自动刷新)完全一致。

三、连接生命周期:ITerminalConnection 与状态机

规格文档中反复强调的核心设计原则是:连接器应遵循现有的 ITerminalConnection 接口,使 Azure 只是"Windows Terminal 能建立的一种连接类型"

这个接口定义在 ITerminalConnection.idl,是终端所有后端连接(本地 ConPTY、远程、Azure 等)的统一抽象:

interface ITerminalConnection
{
    void Initialize(Windows.Foundation.Collections.ValueSet settings);
    void Start();
    void WriteInput(Char[] data);
    void Resize(UInt32 rows, UInt32 columns);
    void Close();

    event TerminalOutputHandler TerminalOutput;
    event Windows.Foundation.TypedEventHandler<ITerminalConnection, Object> StateChanged;

    Guid SessionId { get; };
    ConnectionState State { get; };
};

配套的 ConnectionState 枚举(NotConnected / Connecting / Connected / Closing / Closed / Failed)就是连接状态的通用词汇。规格文档所说的"以隔离方式实现、将来可变成插件",本质就是让 AzureConnection 实现这五个方法加两个事件,从而"即插即用"地挂进终端的连接体系。

AzureConnection 的内部状态机

AzureConnectionAzureConnection.h 里定义了一个更细的业务状态机AzureState),用来描述"从拿到账号到进入云端终端"的完整过程:

状态 含义 对应实现方法
AccessStored 检查是否已有保存的凭据,让用户选择"复用/新登录/删除" _RunAccessState
DeviceFlow 无凭据或用户选择新账号,走设备码认证 _RunDeviceFlowState
TenantChoice 账号有多个租户,需用户选择 _RunTenantChoiceState
StoreTokens 询问是否保存本次凭据供下次使用 _RunStoreState
TermConnecting 已备齐 tenantID / 令牌,发起连接 _RunConnectState
TermConnected 已进入云端终端,循环读取 WebSocket (在 _OutputThread 中内联处理)

驱动这台状态机的是 Start() 里创建的一条输出线程 _OutputThread(见 AzureConnection.cpp)。线程内是一个 switch(_state) 大循环:每处理完一个状态就推进到下一个,直到 TermConnected 后进入 WebSocket 读取循环。这里体现了规格中"认证、申请 cloud shell、申请终端走 HTTP,连接终端走 WebSocket"的分层设计。

几个值得注意的实现事实(均可在源码确认):

  • ConnectionType GUID:每个连接类型都有全局唯一标识,AzureConnection 的是 {0xd9fcfdfa, 0xa479, 0x412c, {0x83, 0xb7, 0xc5, 0x64, 0xe, 0x61, 0xcd, 0x62}}。配置文件正是靠这个 GUID 关联到 AzureConnection
  • IsAzureConnectionAvailable():返回 AzureClientID != L"0"。规格与源码注释都说明,客户端 ID 只在正式发布流水线里注入,本地构建会得到占位值 "0",因此本地构建会主动禁用 Azure 连接,避免"连不上还白报错"。
  • Initialize 解析初始尺寸与会话:从 settings 里读取 initialRows / initialCols / sessionId,若 sessionId 为空则用 Utils::CreateGuid() 生成一个。
  • Resize 在已连接时会向 {cloudShellUri}terminals/{id}/size?cols=&rows=&version=2019-01-01 发请求,从而把本地窗口缩放同步到云端终端。

四、从规格到落地:HTTP + WebSocket 的实现

规格文档提出前三步(认证、请求 cloud shell、请求 terminal)用 HTTP,最后一步(连接终端)用 WebSocket,并点名了 cpprestsdk 作为 HTTP 客户端库(理由:同为微软维护,出问题便于内部协同)。

这里再次出现实现与规格的偏差,值得如实说明:从源码看,HTTP 请求用的是 WinRT 的 winrt::Windows::Web::Http::HttpClient(封装在 _SendRequestReturningJson 中),而 WebSocket 升级用的是 WinHttp 系列 API(WinHttpOpenWinHttpConnectWinHttpOpenRequestWinHttpWebSocketCompleteUpgrade),并没有引入 cpprestsdk。也就是说,规格里的"库选型"在落地时被替换成了 Windows 原生的 WinRT / WinHttp 方案。这一点对理解"规格文档与最终代码的关系"很典型:规格记录的是设计阶段的判断,代码才是最终事实。

关键的连接建立逻辑集中在 _RunConnectState_GetTerminal

  1. 拉取用户云控制台设置_GetCloudShellUserSettings),从 properties.preferredShellType 解析用户偏好的 shell,缺省回退到 pwsh_ParsePreferredShellType)。
  2. 申请一个 cloud shell_GetCloudShell),向 providers/Microsoft.Portal/consoles/defaultPUT,请求体固定 {"properties": {"osType": "linux"}},拿到 properties.uri 作为 _cloudShellUri
  3. 为该 cloud shell 申请一个 terminal_GetTerminal),向 {uri}terminals?cols=&rows=&version=2019-01-01&shell={shellType}POST,拿到终端 id
  4. 推导 WebSocket 端点:源码对两种形态做了区分处理——若 cloud shell URI 不含 servicebus,直接把它 https 换成 wss 再拼上 terminals/{id};若含 servicebus,则按 cloud shell 团队自己的规则把 socket URI 重排为 .../$hc/{ns}/terminals/{id}。注释明确写道"这里的逻辑基于 cloud shell 团队自身的方式"。
  5. 升级到 WebSocket 后进入 TermConnected,在输出线程里用 WinHttpWebSocketReceive 循环读取,把 UTF8/BINARY 消息经 til::u8u16 转码后通过 TerminalOutput 事件抛给终端 UI。

用户输入侧,WriteInput 在"已连接且已进入 TermConnected"时直接把数据以 WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE 发到 WebSocket;否则(认证阶段)会做本地回显、处理退格、按 InputMode::Line 收集整行输入,供认证流程 _ReadUserInput 读取。

五、配置与使用:动态配置文件如何出现

规格文档在 UI/UX 一节只说"会多出一个新配置文件选项(实现时会配独立图标)"。落到仓库里,这个"多出来"的配置文件是由 AzureCloudShellGenerator 作为动态配置文件生成器注入的,而非写死在用户 JSON 里:

// src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.cpp
void AzureCloudShellGenerator::GenerateProfiles(
    std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
{
    if (AzureConnection::IsAzureConnectionAvailable())
    {
        auto azureCloudShellProfile{ CreateDynamicProfile(L"Azure Cloud Shell") };
        azureCloudShellProfile->StartingDirectory(winrt::hstring{ DEFAULT_STARTING_DIRECTORY });
        azureCloudShellProfile->DefaultAppearance().DarkColorSchemeName(L"Vintage");
        azureCloudShellProfile->DefaultAppearance().LightColorSchemeName(L"Vintage");
        azureCloudShellProfile->ConnectionType(AzureConnection::ConnectionType());
        profiles.emplace_back(std::move(azureCloudShellProfile));
    }
}

可以推断出几个使用层面的事实:

  • 可用性与构建相关IsAzureConnectionAvailable() 为假(如本地构建)时,这个配置文件根本不会生成——这也解释了为什么有些 Windows Terminal 安装里看不到 "Azure Cloud Shell"。
  • 外观走 Vintage 配色:无论深色还是浅色外观,默认都用 Vintage 配色方案,并设置了独立的生成器图标(ProfileGeneratorIcons/AzureCloudShell.png)。
  • 连接类型由 GUID 关联ConnectionType(AzureConnection::ConnectionType()) 把配置文件绑定到前面那个固定 GUID,打开时终端据此实例化 AzureConnection

六、能力边界:可访问性、安全、可靠性与性能

规格文档专设了 Capabilities 一节评估各维度影响,这里完整继承其结论:

  • 可访问性:该功能不影响 Windows Terminal 的无障碍能力。
  • 安全:任何联网功能都引入安全风险;文档认为,通过正确使用 Azure AD v1.0 并谨慎保管服务端下发的令牌,可把风险降到可控范围。从源码看,令牌只经 PasswordVault 这类系统级凭据存储,不落明文。
  • 可靠性 / 性能 / 功耗 / 效率:文档判断这些维度均不受影响。
  • 兼容性:正因为实现与终端本体基本解耦,文档认为不会有既有代码或行为被破坏。

七、潜在风险与未来方向

规格文档如实列出了三条潜在问题,值得读者保留:

  1. 依赖 cpprestsdk 这一开源项目——其代码问题会波及本功能(注:落地实现已改用 WinRT/WinHttp,此条在实现中实际已被规避)。
  2. Azure AD v1.0 可能被弃用——目前仍受支持,但未来有风险;文档给出的兜底是"最坏情况可切换到 Microsoft Identity Platform,只需少量修改 HTTP 请求"。
  3. Azure Cloud Shell 的 API 并非公开——正式落地需要 Azure Cloud Shell 团队授予应用权限,构成一项额外依赖。

文档结尾还提出一个展望:一旦 Windows Terminal 允许插件/扩展,这个 Azure 连接器"有可能是终端的第一个插件"。这与全文反复出现的"隔离实现、可插拔"主线呼应,也解释了为何源码始终让 AzureConnection 只通过 ITerminalConnection 与终端对话。


小结:本文以 [规格文档](https://gitcode.com/GitHub_Trending/term/terminal/blob/20588130d8ef2ba40eb56bdae88e04cce7fc5b5d/doc/specs/?utm_source=gitcode_repo_files#1564 - Settings UI/spec.md) 为主线,还原了"认证 → 请求 cloud shell → 请求 terminal → WebSocket 连接"的完整链路,并逐条对照仓库源码 AzureConnection.cpp / AzureConnection.h / AzureCloudShellGenerator.cpp / ITerminalConnection.idl 印证了实际实现。需要记住的关键差异是:令牌存储从规格的 "Storage + DPAPI" 变成了 PasswordVault,HTTP/WebSocket 从 "cpprestsdk" 变成了 WinRT/WinHttp 原生 API——规格记录设计意图,代码才是最终事实,两者结合阅读才能完整理解这一特性的全貌。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384