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
Binary file added docs/04-avalonia/images/analysis-results.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/images/error-invalid-path.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/images/error-unconnected.png
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/04-avalonia/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 04-avalonia

## 一、 功能实现说明

实现了一个图形化客户端,并通过和后端进行异步通信来实现功能。

1. **查看是否连接**
- 通过 `ConnectCommand` 调用 `DesktopDialogHelper` 弹窗引导用户输入 Agent 地址与服务器连接。
- 使用异步方法 `_client.PingAsync()` 校验连接有效性,实时更新底部状态栏(如 `Connected.`、`Connecting...`、`Not connected.`)。

2. **选定分析的目录**
- **切换目录 (`ChangeDirectoryCommand`)**:发送 `ChangeDirectoryRequest` 并异步等待响应,更新服务端当前工作路径。
- **刷新文件 (`RefreshCommand`)**:请求服务端日志列表并清空填充至 `ObservableCollection<LogFileItem>`,实现 UI 列表自动同步。

3. **多个文件同时分析**
- 在 `MainView.axaml.cs` 中启动 `SelectionChanged` 事件,将选中列表绑定回 ViewModel 的 `SelectedFiles`。
- `AnalyzeSelectedFilesCommand`:读取界面输入的并行度(DoP)并校验合法性,向服务端异步发起并发分析任务。
- `AnalyzeAllCommand`:针对目录内所有日志文件发起分析操作。

4. **右键**
- 右键快捷操作
- `Analyze File`:对单选右键文件发起分析。
- `View Analysis Results`:发起 `GetAnalysisResult` 服务端流式 gRPC 调用,通过 `await foreach` 逐行异步读取日志条目并显示至右侧 `ResultEntries` 列表展现。

5. **鲁棒性**
- 封存 `WithClientNotNull` 辅助函数,防止未连接状态下的非法调用。
- 所有 gRPC 远程调用及输入解析均使用 `try-catch` 捕获异常,并使用统一消息弹窗(`ShowMessageDialogAsync`)告知错误信息,确保 GUI 客户端与服务端后台均不会崩溃。

---

## 二、 功能演示与鲁棒性测试

### 1. 正常功能演示

![结果](images/analysis-results.png)

### 2. 鲁棒性与异常处理测试

- **测试 1:未连接服务端时发起操作**
![未连接拦截](images/error-unconnected.png)

- **测试 2:服务端返回错误路径处理**
![非法路径提示](images/error-invalid-path.png)

---

## 三、 问答题解答

### Q4.1
区别在于GUI需要前后端的协同,为每一个元素提供其内置的反馈思路,并且对于协同的要求更为重要,需要仔细组织代码以避免屎山。额外难点:需要设计ui界面,同时阻塞带来的崩溃问题更为严重。异步理解:最大价值在于非阻塞,可以通过grpc流式接收再接收数据的同时操作界面。困难:异步下需要妥善处理异常。

### Q4.2.b

提示词:报错信息,并且令ai完成部分代码的复用。
92 changes: 87 additions & 5 deletions src/LocalCli/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using LogAnalyzer;
using LogParser.Visitors;
using System;

namespace LocalCli
{
Expand Down Expand Up @@ -112,22 +113,103 @@ 6. Exit.

private static void ShowLogFiles(LogFileAnalyzer analyzer)
{
throw new NotImplementedException("T2.3");
var files = analyzer.GetLogFiles();
if (files.Count == 0)
{
Console.WriteLine("No log files found in the current directory.");
return;
}

Console.WriteLine("Log files in directory:");
foreach (var file in files)
{
Console.WriteLine($"- {file}");
}
}

private static void AnalyzeFiles(LogFileAnalyzer analyzer)
{
throw new NotImplementedException("T2.3");
Console.WriteLine("Please input log file names separated by comma:");
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
Console.WriteLine("Input cannot be empty.");
return;
}

var fileNames = input.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(f => f.Trim())
.Where(f => !string.IsNullOrEmpty(f))
.ToList();

if (fileNames.Count == 0)
{
Console.WriteLine("No valid file names provided.");
return;
}

try
{
Console.WriteLine("Analyzing specified files...");
analyzer.AnalyzeFiles(0, fileNames);
Console.WriteLine("Analysis completed.");
}
catch (Exception ex)
{
Console.WriteLine($"Error analyzing files: {ex.Message}");
}
}

private static void AnalyzeAll(LogFileAnalyzer analyzer)
{
throw new NotImplementedException("T2.3");
try
{
Console.WriteLine("Analyzing all log files...");
analyzer.AnalyzeAll(0);
Console.WriteLine("Analysis completed.");
}
catch (Exception ex)
{
Console.WriteLine($"Error analyzing files: {ex.Message}");
}
}

private static void GetAnalysisResult(LogFileAnalyzer analyzer)
{
throw new NotImplementedException("T2.3");
Console.WriteLine("Please input log file name:");
var fileName = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(fileName))
{
Console.WriteLine("Invalid file name.");
return;
}

if (!analyzer.TryGetAnalysisResult(fileName, out var result) || result is null)
{
Console.WriteLine($"File '{fileName}' was not found.");
return;
}

switch (result.State)
{
case AnalysisState.NotAnalyzed:
Console.WriteLine($"File '{fileName}' has not been analyzed yet.");
break;

case AnalysisState.Failed:
Console.WriteLine($"Analysis failed for '{fileName}':");
Console.WriteLine(result.ErrorMessage);
break;

case AnalysisState.Succeeded:
Console.WriteLine($"Analysis result for '{fileName}':");
var visitor = new KeyValueVisitor();
foreach (var entry in result.Entries)
{
Console.WriteLine(entry);
}
break;
}
}
}
}
}
95 changes: 59 additions & 36 deletions src/LogAnalyzer/LogFileAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
using LogParser.Models;
using LogParser.Parser;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading;

namespace LogAnalyzer
{
Expand Down Expand Up @@ -138,10 +143,8 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable<string> fileNames)
}
fileList = fileNameList.Select(fileName => _logFiles[fileName]).ToList();

/*
* Set _isAnalyzing
*/
// TODO: T2.2
//设置正在分析状态为true
_isAnalyzing = true;
}

try
Expand All @@ -150,11 +153,11 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable<string> fileNames)
}
finally
{
/*
* Unset _isAnalyzing
* Remember to lock _syncRoot to prevent data race
*/
// TODO: T2.2
//分析结束,设置一个锁保护,在其中重置状态
lock (_syncRoot)
{
_isAnalyzing = false;
}
}
}

Expand All @@ -165,11 +168,15 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList<FileInfo> fileLis
{
foreach (var file in fileList)
{
/*
* Filter unparsed files.
* If there is an unknown file, throw System.InvalidOperationException.
*/
throw new NotImplementedException("TODO: T2.2");
if (!_analysisResults.ContainsKey(file.Name))
{
throw new InvalidOperationException($"File '{file.Name}' is unknown.");
}

if (_analysisResults[file.Name].State != AnalysisState.Succeeded)
{
logFilesToParse.Add(file);
}
}
}

Expand All @@ -180,27 +187,29 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList<FileInfo> fileLis

var queue = new WorkQueue<FileInfo>();

/*
* Enqueue log files
*/
// TODO: T2.2
foreach (var file in logFilesToParse)
{
queue.Enqueue(file);
}
queue.CompleteAdding();

degreeOfParallelism = Math.Max(Math.Min(degreeOfParallelism, logFilesToParse.Count), 1);
var workers = new Thread[degreeOfParallelism];
for (int i = 0; i < degreeOfParallelism; i++)
{
int workerId = i;
string threadName = $"log-analyzer-worker-{workerId}";
/*
* Create and start threads to run `WorkerMain`
*/
// TODO: T2.2
workers[i] = new Thread(() => WorkerMain(workerId, queue))
{
Name = threadName
};
workers[i].Start();
}

/*
* Wait for (join) all threads to end
*/
// TODO: T2.2
foreach (var worker in workers)
{
worker.Join();
}
}

private void WorkerMain(int workerId, WorkQueue<FileInfo> queue)
Expand All @@ -212,21 +221,35 @@ private void WorkerMain(int workerId, WorkQueue<FileInfo> queue)
AnalysisResult result;
try
{
// Parse file
throw new NotImplementedException("TODO: T2.2");
using var reader = new StreamReader(file.FullName);
var entries = parser.Parse(reader).ToList();

result = new AnalysisResult(
FileName: file.Name,
FullName: file.FullName,
State: AnalysisState.Succeeded,
Entries: entries,
ErrorMessage: null,
WorkerId: workerId
);
}
catch (Exception ex)
{
// Save exception message to result
throw new NotImplementedException("TODO: T2.2");
result = new AnalysisResult(
FileName: file.Name,
FullName: file.FullName,
State: AnalysisState.Failed,
Entries: Array.Empty<LogEntry>(),
ErrorMessage: ex.ToString(),
WorkerId: workerId
);
}

/*
* Save parse result.
* [!Important] Remember to lock _syncRoot to prevent data race.
*/
throw new NotImplementedException("TODO: T2.2");
lock (_syncRoot)
{
_analysisResults[file.Name] = result;
}
}
}
}
}
}
39 changes: 35 additions & 4 deletions src/LogAnalyzer/WorkQueue.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;

namespace LogAnalyzer
{
Expand All @@ -20,17 +23,45 @@ public bool IsCompleted

public void Enqueue(T item)
{
throw new NotImplementedException("TODO: T2.1");
lock (_items)
{
if (_isCompleted)
{
throw new InvalidOperationException("Cannot enqueue to a completed work queue.");
}
_items.Enqueue(item);
Monitor.Pulse(_items);
}
}

public bool TryDequeue([NotNullWhen(true)] out T? item)
{
throw new NotImplementedException("TODO: T2.1");
lock (_items)
{
while (_items.Count == 0 && !_isCompleted)
{
Monitor.Wait(_items);
}

if (_items.Count > 0)
{
item = _items.Dequeue()!;
return true;
}

item = default;
return false;
}
}

public void CompleteAdding()
{
throw new NotImplementedException("TODO: T2.1");
lock (_items)
{
if (_isCompleted) return;
_isCompleted = true;
Monitor.PulseAll(_items);
}
}
}
}
Loading
Loading