Files
justme c9beeee8e3 docs: describe the unified Agent topology and link controls
Align architecture, protocol, API, storage, security and deployment documentation with default mutual measurements and the Agent link board.
2026-10-21 11:55:55 +08:00

9.3 KiB
Raw Permalink Blame History

C# 代码风格

基线是 dotnet/runtime 编码风格,再加上本项目的补充规则。所有能自动检查的规则都写在 .editorconfig 或 BannedSymbols.txt 里,违反即编译失败(EnforceCodeStyleInBuild + TreatWarningsAsErrors);无法自动检查的规则由代码评审把关。

1. 格式与命名(.editorconfig 强制)

  • 使用 Allman 大括号(左大括号独占一行),四空格缩进,不使用 Tab;if/for 等语句必须加大括号。
  • 使用文件作用域 namespace;namespace 与目录一致(MoShiro.Agent.Sessions 对应 Sessions/)。
  • using 放在文件顶部、namespace 之外,System.* 排在最前面,其余按字母排序。
  • 始终显式写出可见性,并且放在修饰符的第一位;static readonly 的顺序固定。
元素 规则 示例
类型、方法、属性、常量、局部函数 PascalCase SessionTable、MaxFrameBytes
私有和内部实例字段 _camelCase _sessions
静态字段 s_camelCase s_idleTimeout
线程静态字段 t_camelCase t_buffer
局部变量、参数、主构造函数参数 camelCase windowSeq
接口 I 前缀 IMeasurement
异步方法 Async 后缀 SendReportAsync
  • var 只在右侧类型显而易见时使用(new、强制转换);类型名使用语言关键字(int,而不是 Int32)。
  • 用 nameof(...) 代替字符串字面量;不写多余的 this.。
  • 字段放在类型的最前面;一个文件只放一个主要类型,文件名与类型名相同。
  • 内部类和私有类默认使用 sealed(或 static),除非确实需要被继承。

2. 命名语义

  • 名称里带单位:timeoutMs、rttUs、payloadBytes。API 内部统一用 TimeSpan,只在边界(线协议、配置、JSON)使用带单位后缀的整数。
  • 不使用 Manager、Helper、Util、Service(ASP.NET 约定位置除外)之类的模糊名称,名称要说明职责:UploadQueue、SessionTable、ResultWriter。
  • 布尔值使用 is/has/can 前缀;返回“成功与否加输出值”的方法使用 TryXxx(out ...)。

3. 类型与数据

  • 契约和领域值使用 sealed record 或 readonly record struct,并且不可变。
  • 热路径上的计数器和缓冲区使用可变 struct 或 sealed class,所有权要写清楚(谁创建、谁归还池)。
  • ID 使用强类型(readonly record struct NodeId(string Value)),不直接传裸 string。
  • 枚举序列化成 snake_case 字符串(JsonStringEnumConverter<T> + JsonStringEnumMemberName);新增枚举值不属于破坏性变更,但所有消费方都必须能处理未知值。
  • 可空性:公共成员必须标注;使用 ! 抑制警告时,同一行要写注释说明理由。

4. 错误处理

  • 预期内的失败不抛异常:超时、拒绝连接、认证失败、校验失败都返回结构化结果,其中包含 ErrorCode。
  • 异常只用于程序缺陷或无法继续运行的状态;只在边界(会话循环、请求管线、进程入口)捕获,并记录日志。
  • 禁止空 catch;捕获 OperationCanceledException 时必须确认它确实来自自己的取消令牌。

5. 异步与并发

  • 所有 I/O 都是异步的,并且接受、传递 CancellationToken(作为最后一个参数)。
  • 禁止使用 .Result、.Wait()、Thread.Sleep、async void(事件处理函数除外),BannedSymbols.txt 强制执行。
  • 库代码中的 await 不需要 ConfigureAwait(false),因为三个程序都没有同步上下文,统一省略即可。
  • 生产者和消费者之间使用有界 Channel<T>,明确指定 BoundedChannelFullMode。
  • 共享可变状态要么只由单个循环拥有(推荐),要么使用 lock 保护;不要混用多种方式。

6. 时间

  • 时间来源一律通过 TimeProvider 获得:墙钟使用 GetUtcNow(),耗时使用 GetTimestamp()/GetElapsedTime()。
  • DateTime.Now、DateTime.UtcNow、DateTimeOffset.Now、Stopwatch.StartNew 列入禁用名单。测试使用 FakeTimeProvider 驱动窗口和超时(这个包只出现在测试项目中)。

7. NativeAOT 与轻量化

  • 序列化只使用 System.Text.Json 源生成上下文(或手写的二进制编解码);禁用无上下文的 JsonSerializer.Serialize<T>(T) 重载。
  • 不做程序集扫描、Activator.CreateInstance、Type.GetType(string)、动态代理或 Reflection.Emit。
  • Agent:不使用 DI 容器,在 Program.cs 中手工组装对象,依赖关系一眼可见;不使用 LINQ;不使用 HttpClient(Agent 的 WebSocket 使用 ClientWebSocket)。
  • Backend:使用 CreateSlimBuilder + Minimal API;只能通过 Microsoft.Data.Sqlite 执行手写的参数化 SQL,禁止字符串拼接 SQL。
  • 热路径:使用 Span<T>、stackalloc(≤ 256 B)、ArrayPool<byte>.Shared;每个包的处理过程中不分配内存、不写日志。
  • 新增任何第三方包之前,需要先更新 project-layout.md 第 3 节的依赖表,并在 PR 中说明理由。

8. 日志

  • 统一使用 logfmt 字段名:ts、level、msg、agent、dialer、session、task、result、err。
  • Backend 使用 Microsoft.Extensions.Logging 源生成的 [LoggerMessage];Agent 使用 NodeLog,两者输出的字段名相同。
  • 令牌、session_key 和 MAC 不能出现在日志里;IP 地址可以记录。
  • 每个会话或连接的生命周期事件记录为 Info;逐包事件不记录,只累加计数。

9. 节点能力边界(Agent 与 Fleet 节点侧)

  • 由 BannedSymbols.Node.txt 在编译期强制:禁止启动进程、动态加载程序集或本机库、使用 HttpClient、在配置目录以外写文件。唯一的豁免点是 Fleet.Hosting.ConfigStore,豁免必须写 #pragma 并附上理由注释。注册在 fleet-enroll 中完成,更新由节点宿主机上的更新器完成,常驻进程当前不需要这些能力。
  • 节点不能新增任何“由远端触发的非采集动作”对应的 RPC 方法。凡是新增 RPC 方法的 PR,都要在描述中说明它不会扩大权限边界。
  • Backend 下发的任何参数都要先经过 ParamSpec 校验,再经过本地硬上限(BudgetGuard)检查,两道校验都不能省略。
  • 日志中不能出现私钥、签名、session_key、令牌或 MAC。

9.1 Fleet 的通用性

  • backend/fleet/** 不能引用任何 MoShiro.*,也不能出现产品相关的概念、命名或默认值(例如端口 7443、moshiro 字样),由 ArchitectureTests 和代码评审把关。
  • 新增的公开 API 如果只有 MoShiro 使用,应先放在 MoShiro 中,等出现第二个使用者后再下沉到 Fleet。
  • Fleet 不提供 exec、shell、文件读写或远程下发代码的方法;确需此类能力时走 extension 方案,见 security.md 第 9 节。
  • Fleet 公开 API 的变更,PR 描述需说明是否属于破坏性变更(不使用 PublicApiAnalyzers)。

10. 声明式扩展的写法

  • Kind:只放在 Protocol/Catalog/Kinds,一个 Kind 一个静态字段;参数一律使用 P.Int、P.Enum 等 ParamSpec 写出默认值和上下限,禁止写没有上下限的参数。
  • 积木:实现拨号方 / 监听方接口,在 BlockCatalog 中登记 Kind 到积木的映射;积木内部不能读取全局配置,所有输入都来自参数。
  • 指标:只放在 Analysis/Metrics,一行一个 MetricDef,并加入 MetricRegistry.All;指标 id 使用 领域.名称(如 tcp.stall),导出值为 id.统计量(如 rtt.p95)。已发布的 id 不能改名,只能新增。
  • 算子:纯函数,输入列视图,输出累加器更新;每个算子都要有边界用例测试(空输入、单个样本、乱序、时钟回绕)。
  • 模型:只读取指标,不读取原始列;评分函数需要有单调性测试(指标变差时,分数不能升高)。

11. 测试

  • 使用 xUnit v3。测试项目与被测项目一一对应,放在同名目录下的 tests/ 中。
  • 测试名采用 方法_场景_期望 格式,例如 Merge_TwoHistograms_PreservesP95。
  • 协议编解码必须有“黄金字节”测试:固定的输入对应固定的字节序列。布局发生变化时测试会失败,提醒升级协议版本。
  • 采集端必须覆盖:逐包记录的完整性(发送数 = 记录数)、丢失标记、重复、乱序、非法长度、重放、会话过期、版本不兼容。
  • 分析模型必须有“黄金原始块 → 期望输出”的测试;模型升版时,同时更新期望输出并说明原因。
  • 端到端测试应针对 AOT 发布产物运行,而不是 JIT 构建。

12. 提交与评审

  • 提交信息使用 <范围>: <摘要> 格式,范围取 dialer、agent、backend、shared、web、docs、scripts;是否触发 CI 的标记见 AGENTS.md。
  • 修改 backend/shared/MoShiro.Protocol 的 PR 必须说明兼容性影响(新增字段、行为变化、需要升级版本)。
  • 修改 backend/openapi/v1.json 的 PR 必须按照 api.md 第 7 节的「版本与兼容性」判断是否属于破坏性变更。