Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/01-basic/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 01-basic 问答报告

## Q1.1

日志的 CSV 字段不是通过手动调用字符串的 `Split` 方法进行分割的,而是由 CsvHelper 完成。`LogFileParser.Parse` 中的 `csv.GetRecords<LogRecord>()` 逐条读取 CSV 记录;`LogRecordMap` 通过 `Map(...).Index(...)` 指定字段含义,其中索引 0 到 3 依次对应 `LineNo`、`Timestamp`、`PodName` 和 `Message`。

JSON 格式的 `message` 在 `LineParser.ParseLine` 中被判断种类。代码先调用 `JsonDocument.Parse(logRecord.Message)` 得到 JSON 文档,再通过 `root.TryGetProperty("event", out var eventElement)` 读取 `event`,最后根据 `eventElement.GetString()` 的结果在 `switch` 表达式中选择 `CreateCall`、`CreateRequest` 或 `CreateInternal`。

确定日志种类后,代码调用 System.Text.Json 提供的 `JsonSerializer.Deserialize<T>(logRecord.Message, options)`,将 JSON 反序列化为对应的消息记录。各消息记录的属性使用了 `[property: JsonRequired]`,因此必需字段缺失时,反序列化会抛出 `JsonException`。`options` 的 `PropertyNamingPolicy` 被设置为 `JsonNamingPolicy.KebabCaseLower`,所以 C# 中的 `RequestId`、`StatusCode` 等大驼峰属性能够分别对应 JSON 中的 `request-id`、`status-code` 等烤串命名字段。

## Q1.2

以 Call 事件为例,调用链如下:

1. `Dictionary<string, string> KeyValueVisitor.Dump(LogEntry entry)`
2. `TResult CallLogEntry.Accept<TResult>(ILogEntryVisitor<TResult> visitor)`
3. `Dictionary<string, string> KeyValueVisitor.Visit(CallLogEntry entry)`

`Dump` 持有的是 `LogEntry` 基类引用,它先调用具体日志对象的 `Accept`。`CallLogEntry.Accept` 再执行 `visitor.Visit(this)`;由于此处 `this` 的类型是 `CallLogEntry`,最终会进入接收 `CallLogEntry` 参数的 `Visit` 重载。这使访问者能够针对不同日志类型执行不同的导出逻辑。

## Q1.3.b

本次作业使用了 AI。我使用过的主要提示词包括:

> 请你读dotnet-guidance,教我完成01-basic的作业

> 我还是不会T1.3,请给我修改,并完成问答报告

AI 的主要帮助是快速定位了 T1.2 和 T1.3 对应的文件、未实现的方法和测试要求,并结合已有的 Call 实现解释了 Request、Internal 的实现方式。对我不熟悉的访问者模式,AI 把 `Dump -> Accept -> Visit` 的调用关系具体对应到项目代码,比只搜索访问者模式的通用定义更直接。它还可以统一核对字典键名、时间格式和数值转换,减少因为字段拼写不一致造成的测试失败。

AI 的不足是,第一次说明仍然包含较多概念和步骤,没有立刻解决我对 T1.3 具体写法的疑问。AI 能生成能够通过当前测试的实现,但不能证明我已经理解访问者模式,也可能忽略课程测试之外的边界情况。问答报告中的个人体验也无法由 AI 代替,因此我仍需要阅读代码、运行 Release 测试,并根据自己的理解检查和修改生成的内容。相比完全依靠搜索引擎,AI 整理项目内信息更快;相比自己独立完成,它减少了探索过程,但也更容易让我在没有理解代码的情况下直接接受答案。
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions docs/02-multithreading/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 02-multithreading 实现报告

## T2.3 功能说明

本节实现了一个目录级多线程日志分析器及其控制台交互界面,主要功能包括:

1. 输入或切换日志目录,并扫描目录第一层中的全部 `.log` 文件。
2. 显示当前目录中的日志文件名。
3. 根据指定并行度分析一组逗号分隔的日志文件,或者分析全部日志文件;并行度为 `0` 时使用逻辑处理器数量。
4. 查询文件的分析结果。未分析文件会显示提示;分析成功时使用 `KeyValueVisitor.Dump` 输出每条日志;分析失败时显示异常信息。
5. 检查空目录、不存在目录、非法菜单选项、非整数或负数并行度、空文件名以及不存在的日志文件,发生输入错误后返回交互流程,不会导致程序崩溃。

完整功能的自动化控制台会话如下。该会话覆盖文件列表、未分析状态、指定文件分析、成功结果、失败结果和全部文件分析,程序最终以退出码 `0` 结束。

![T2.3 完整功能](./assets/t2.3-functional.png)

鲁棒性测试会话如下。输入序列包含空目录、不存在目录、非数字菜单、越界菜单、非数字和负数并行度、空文件列表、未知文件、空查询文件名等情况,程序均给出提示并继续运行。

![T2.3 鲁棒性测试](./assets/t2.3-robustness.png)

## Q2.1

`WorkQueue<T>` 中需要在线程间共享的状态是 `_items` 队列中的内容和 `_isCompleted` 完成标志。它们都使用 `_items` 对象作为同一把管程锁,通过 `lock (_items)` 保护。消费者发现队列为空且尚未完成添加时调用 `Monitor.Wait(_items)`,等待期间会释放锁;生产者入队后调用 `Monitor.Pulse(_items)` 唤醒一个消费者;完成添加后调用 `Monitor.PulseAll(_items)` 唤醒全部消费者,使其在队列排空后退出。

`LogFileAnalyzer` 中的共享状态包括 `_currentDirectory`、`_isAnalyzing`、`_logFiles` 和 `_analysisResults`。目录切换、分析状态检查和修改、文件列表快照、结果查询以及工作线程写回结果等操作主要通过 `lock (_syncRoot)` 互斥。`AnalysisResult` 使用不可变记录表示,构造后不再修改,而是整体替换字典中的结果。需要注意,当前框架的 `CurrentDirectory` 和 `HasDirectory` 属性直接读取 `_currentDirectory`,没有获得 `_syncRoot`;现有 CLI 使用方式不会在切换目录时并发读取它们,但如果要求任意公开接口都支持并发调用,这两个 getter 也应加锁。

如果等待条件只使用 `if`,线程从 `Monitor.Wait` 返回后就会继续执行,不会重新检查队列是否仍为空。发生虚假唤醒,或者另一个消费者先取走了刚加入的元素时,当前消费者可能对空队列执行 `Dequeue` 并抛出异常。对应无限容量生产者消费者问题,就是消费者在仓库仍为空时错误地取走不存在的产品,可能使产品计数变为负数。因此必须使用 `while`,每次醒来并重新取得锁后再次检查“队列为空且添加未完成”这一条件。

## Q2.2

`LogFileAnalyzer.ChangeDirectory` 中以下调用负责扫描给定目录第一层的 `.log` 文件:

```csharp
Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly)
```

如果需要递归扫描全部子目录,可以把 `SearchOption.TopDirectoryOnly` 改为 `SearchOption.AllDirectories`。递归后不同子目录可能存在同名文件,因此不能继续只用 `Path.GetFileName` 作为字典键;可以改用相对于根目录的路径或完整路径来区分文件。

## Q2.3.b

本次作业使用了 AI。主要提示词包括:

> 请阅读我更新的内容,确定第二节要完成的多线程的任务

> 请你帮我完善这个文件,并告诉我每个地方为什么这样做。我已经在 visual studio 2026 里打开了这个文件

> 请继续完善这个文件中的代码

> 现在请你继续阅读 guidance.md 完成 S2.3 和 T2.3,文件已在 visual studio 2026 中打开

我既让AI帮我解释代码,又让AI在我无法完善代码的情况下帮我完善代码,并给我解释为什么要这样做。

AI 首先解释了代码框架中类、对象、方法和共享变量的作用,随后帮助实现了 `LogFileAnalyzer`、`WorkQueue<T>` 和 `LocalCli` 的代码,并运行 `test-02-multithreading` 以及控制台正常/非法输入会话进行验证。使用方式不仅是查询接口,还包括让 AI 编写部分作业代码、解释同步设计,并根据测试结果修正实现。

AI 的第一版 `WorkQueue<T>` 能通过功能测试,但对 `Dequeue` 的赋值触发了 `CS8762` 可空性警告;随后通过与 `[NotNullWhen(true)]` 返回约定一致的空宽容标注消除了警告。`LogFileAnalyzer` 首次运行 T2.2 测试时也因为当时尚未实现 T2.1 的 `WorkQueue.Enqueue` 而全部停止,这不是 T2.2 本身的测试结论,因此完成队列后重新运行了完整测试。最终 `test-02-multithreading` 的 8 项测试全部通过。

我认为本节整体难度偏高。单个 `lock`、`Monitor.Wait` 或 `Thread.Join` 接口并不复杂,主要难点是同时维护队列状态和完成状态、使用 `while` 应对虚假唤醒、保证异常时恢复 `_isAnalyzing`,以及区分某个测试失败究竟来自队列还是日志分析器。
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions docs/03-async-grpc/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 03-async-grpc 实现报告

## T3.1 Agent

本节完成了日志分析系统与 Protobuf 类型之间的转换,以及 gRPC Agent 的请求处理逻辑。

`GrpcLogEntryVisitor` 使用访问者模式把 `CallLogEntry`、`RequestLogEntry` 和 `InternalLogEntry` 转换为对应的 `LogEntryMessage`。`GrpcTypeConverter` 负责分析状态、日志级别、事件类型和三种日志记录的双向转换。

`AgentSession` 实现了目录切换、全部日志分析、指定日志分析和分析结果查询。请求参数或当前状态不合法时,Agent 会返回对应的 `INVALID_ARGUMENT`、`DIRECTORY_NOT_FOUND`、`FILE_NOT_FOUND` 或 `INVALID_OPERATION`,其他异常转换为 `INTERNAL_ERROR`,避免请求异常导致服务退出。分析成功的结果首先返回 header,随后按原顺序返回每条日志记录。

`AgentService` 只负责把 gRPC 请求和取消令牌转交给 `AgentSession`。对于分析结果,它使用服务器流逐条写出 `GetAnalysisResultResponse`。

在 Release 配置运行 `test-03-async-grpc`,T3.1.1 至 T3.1.3 共 3 项测试全部通过。

## T3.2 RemoteCli

`RemoteCli` 实现了以下功能:

1. 切换 Agent 使用的日志目录。
2. 获取当前目录中的日志文件列表。
3. 按指定并行度分析部分或全部日志文件,其中并行度 `0` 使用逻辑处理器数量。
4. 查询未分析、分析成功和分析失败三种状态。
5. 异步读取 gRPC 服务器流,并把每条 Protobuf 日志转换回日志模型后输出。
6. 处理空目录、无效目录、非法菜单、非法并行度、空文件列表和不存在的日志文件。

正常会话覆盖了目录切换、文件列表、未分析状态、指定文件分析、三种日志类型的成功结果、失败结果和全部文件分析。程序最终以退出码 `0` 结束。

![RemoteCli 完整功能](./assets/remote-cli-functional-result.png)

鲁棒性会话连续输入多种非法内容。每次错误后程序都会给出原因并返回菜单或重新提示输入,没有因客户端参数错误退出。

![RemoteCli 鲁棒性测试](./assets/remote-cli-robustness-result.png)

## Q3.1

网络应用与以前本地程序最直接的区别是调用跨越了进程边界。调用方不能直接共享 Agent 中的对象,只能使用 `.proto` 约定的消息,因此需要维护内部模型和 Protobuf 模型之间的转换。网络调用还可能遇到服务未启动、地址错误、连接中断和请求取消;这些情况在普通本地方法调用中通常不需要考虑。

本节另一个明显区别是调试时必须同时运行 Agent 和客户端。客户端创建 channel 并不表示连接已经成功,第一次 RPC 才真正建立连接,所以程序先调用 `PingAsync`。查询分析结果又不能假定响应可以一次装入内存,而是要先处理 header,再异步读取服务器流中的日志记录。

服务端的输入也不能被信任。目录、文件名和并行度都需要检查,分析器当前是否已有目录或正在分析也会影响请求是否合法。Agent 是常驻服务,因此这些错误必须转换为协议状态,而不能让异常终止服务。

## Q3.2.b

本次作业使用了 AI。主要提示词包括:

> 请你阅读 guidance 中的更新内容,帮助我完成 03-async-grpc 中的任务。

> 我在 Visual Studio 2026 中已经打开了这个项目,请你帮助我完成第 3 节全部要求。

> 请告诉我为什么要这样做

AI 用于阅读任务说明、解释代码框架、补全 T3.1/T3.2 的 TODO、运行测试和组织控制台验证。过程中 AI 一开始为了增强鲁棒性改动了 `RemoteCli` 已有的入口和菜单流程,这不符合工程只修改 TODO 区域的规则。指出问题后,AI 阅读了 `00-prepare/guidance.md`,撤回了 TODO 之外的改动,并通过 `git diff` 重新检查修改范围。

通过本次实现,我更清楚地理解了 gRPC 的惰性连接、普通异步 RPC 与服务器流的区别,以及 `oneof` 消息如何承载 header 或日志记录。服务器流客户端方法本身没有 `Async` 后缀,但响应通过 `ReadAllAsync` 异步读取;这与一次返回单个响应的 `ChangeDirectoryAsync` 等调用不同。
Binary file added docs/04-avalonia/assets/analysis-failed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/04-avalonia/assets/functional-client.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/04-avalonia/assets/result-not-analyzed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions docs/04-avalonia/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 04-avalonia 实验报告

## 功能实现

本次作业完成了以下 GUI 客户端功能:

- 使用 `Refresh` RPC 异步刷新远程日志目录中的文件列表,并清理已经失效的选择状态。
- 支持多选日志文件后调用 `AnalyzeFiles`,并对空选择进行本地校验。
- 添加 `All` 按钮,通过 `AnalyzeAll` 分析当前目录内的全部日志文件。
- 支持通过右键菜单分析单个日志文件,以及通过服务端流式 RPC 获取分析结果。
- 将日志条目转换为键值字段并按 `序号 | 字段: 值` 的形式显示。
- 分别显示分析成功、分析失败和尚未分析三种结果状态。
- 对空地址、非法目录、非法 DoP、未连接、空文件选择和 RPC 异常使用消息框提示,避免非法输入导致程序退出。

所有 gRPC 调用均使用异步接口并进行 `await`。连接新 Agent 时,只有候选客户端成功完成 `PingAsync` 后才会替换当前客户端;如果切换失败,已有可用连接仍会保留。

## 功能截图

下图展示了连接 Agent、切换日志目录、刷新文件列表、使用 `All` 分析以及查看成功分析结果后的完整界面。

![完整功能界面](./assets/functional-client.png)

尚未分析的文件会显示明确的状态信息:

![尚未分析](./assets/result-not-analyzed.png)

日志解析失败时会显示解析器返回的错误原因:

![分析失败](./assets/analysis-failed.png)

## 鲁棒性测试

连接地址为空时,客户端在创建 gRPC Client 前进行拦截:

![空连接地址](./assets/robustness-empty-address.png)

远程目录不存在时,客户端显示 Agent 返回的 `DirectoryNotFound` 状态,文件列表和程序进程保持可用:

![非法目录](./assets/robustness-invalid-directory.png)

DoP 不是非负整数时,客户端进行本地校验,不发送分析请求:

![非法 DoP](./assets/robustness-invalid-dop.png)

## 构建与测试

- `LogAnalyzerClient.Desktop` 在 Release 配置下构建成功,0 个错误;完整构建会产生 5 个来自 gRPC 自动生成文件中 `pb/pbc/pbr/grpc` 类型名称的既有 `CS8981` 警告,本次修改的客户端代码没有编译警告。
- `test-00-prepare`:1/1 通过。
- `test-01-basic`:16/16 通过。
- `test-02-multithreading`:8/8 通过。
- `test-03-async-grpc`:3/3 通过。
- 使用本地 Agent `http://127.0.0.1:5000` 和 `src/dataset` 完成了连接、目录切换、刷新、Analyze All、单文件分析及流式结果显示的实际操作验证。

## Q4.1

控制台程序通常按照“读取输入、执行、输出结果”的顺序运行,程序在某一步等待时是容易理解的。GUI 程序则由事件驱动,连接、按钮点击、列表选择和右键菜单可能以不同顺序发生,因此需要维护连接状态、当前目录、选择项和结果列表之间的一致性。界面还增加了数据绑定、命令生成、控件选择状态和消息框生命周期等问题,同一个错误不能只写到标准输出,而要转化为用户能看到并能恢复操作的界面状态。

这次作业让我更直观地理解了 `async`/`await` 的作用。gRPC 请求如果同步等待,会占用唯一的 UI 线程,窗口不能重绘也不能响应输入;`await` 会在等待网络结果时把 UI 线程让出来,并在请求完成后继续更新绑定属性。异步也带来了额外复杂度,例如请求失败后的状态恢复、多个命令可能交叉执行,以及异常需要在 `await` 的调用链上统一处理。本次实现通过异步命令、统一的客户端非空检查和消息框异常处理来控制这些问题。

## Q4.2.b

本次作业使用了 AI。主要提示词是:“仔细阅读 `04-avalonia` 和 `00-prepare` 的指导页面,了解写作业和提交作业的要求后,完成 `04-avalonia` 任务并直接完善项目。”

AI 参与了指导文档梳理、代码框架分析、ViewModel 和结果模型实现、Release 构建、测试运行以及 GUI 自动化验证。它不只是查询接口,也编写了本次作业的一部分代码。过程中出现过可验证的错误:最初在属性访问器中使用了变量名 `field`,它在 C# 14 中成为上下文关键字,Release 构建失败后改名并重新构建通过;自动化截图脚本也出现过 PowerShell 变量名和窗口焦点问题,但这些脚本不属于提交代码,修正后才生成报告截图。

通过这次过程,我进一步理解了 gRPC 服务端流需要使用 `await foreach` 异步读取,以及 UI 状态只能在请求成功并完成校验后更新。AI 给出的实现仍然需要依靠编译、现有测试和真实界面操作来判断是否正确,不能只根据生成的代码表面判断。
Loading
Loading