diff --git a/docs/01-basic/report.md b/docs/01-basic/report.md new file mode 100644 index 0000000..b52273c --- /dev/null +++ b/docs/01-basic/report.md @@ -0,0 +1,74 @@ +## Q 1.1 +### 1.1.1 +- 以下代码负责按逗号分割: +```c sharp +var config = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false + }; + using var csv = new CsvReader(logFile, config); + csv.Context.RegisterClassMap(); +``` +- 经过CSV分割后,每一行的文本通过以下代码对应到LineNo、Timestamp、PodName、Message四个字段 +```c sharp +public LogRecordMap() + { + Map(m => m.LineNo).Index(0); + Map(m => m.Timestamp).Index(1); + Map(m => m.PodName).Index(2); + Map(m => m.Message).Index(3); + } +``` +,存为一个LogRecord对象,之后单独`JsonDocument.Parse(logRecord.Message)`提取`event`类型,交由`LineParser.CreateCall(logRecord)`等方法,按照`LogEntries.cs`中的定义映射各自含义 +### 1.1.2 +- 在`LineParser::ParseLine(LogRecord logRecord)`中: +```c sharp +if (root.TryGetProperty("event", out var eventElement)) + { + return eventElement.GetString() switch + { + "call" => LineParser.CreateCall(logRecord), + "request" => LineParser.CreateRequest(logRecord), + "internal" => LineParser.CreateInternal(logRecord), + _ => throw new FormatException($"Unknown event type: {eventElement.GetString()} in log message: {logRecord.Message}") + }; + } + else + { + throw new FormatException($"Log message does not contain 'event' property: {logRecord.Message}"); + } +``` +### 1.1.3 +- 使用json库: +```c sharp +using System.Text.Json.Serialization; +... +JsonSerializer.Deserialize(logRecord.Message, options); +... +``` + +- 使用`[property: JsonRequired]`强制指定,不存在时报错 +- 通过options传入源文本的编码方式Kebab +``` c sharp +private static JsonSerializerOptions options = new JsonSerializerOptions +{ + PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, +}; +... +var callMessage = JsonSerializer.Deserialize(logRecord.Message, options) +... + +``` +驼峰命名法在LogEntry的子类中分别指定。 + +## Q1.2 +- `Dictionary KeyValueVisitor.Dump(LogEntry entry)` +- `TResult LogEntry.Accept(ILogEntryVisitor visitor)` +- `TResult CallLogEntry.Accept(ILogEntryVisitor visitor)` +- `Dictionary Visit(CallLogEntry entry)` + +## Q1.3 +- 有使用 +### Q1.3.b +- 主要使用了Copilot的自动补全,相比我自己写更省时间,减少了排错成本,经测试核查无误 +- 也让ai解释了一些语句的含义、函数的用法 \ No newline at end of file diff --git a/docs/02-multithreading/report.md b/docs/02-multithreading/report.md new file mode 100644 index 0000000..6e05a5b --- /dev/null +++ b/docs/02-multithreading/report.md @@ -0,0 +1,25 @@ +## Q2.1 +### 2.1.1 +- WorkQueue共享变量有:`_items`、`_isCompleted` + - `_items`通过`lock(_items){...}`防止竞争; + - `_isCompleted`通过每次读取时`lock (_items)`保证读到的值正确,写入时`lock (_items)`保证单一线程访问 +- LogFileAnalyzer的共享变量有:`_analysisResults`、`_isAnalyzing`、`_logFiles`、`_currentDirectory` + - `_analysisResults`、`_logFiles`通过`lock(_syncRoot){...}`保护 + - `_isAnalyzing`通过每次读取时`lock (_syncRoot)`保证读到的值正确 + - `_currentDirectory`写入在 `lock (_syncRoot)` 中,读取未加锁,不过其一般也不会被多个线程写入 +- 不用while的话,wait被唤醒后就继续向下执行了,就像顾客没有等到生产者通知生产完成就尝试去仓库抢东西,可能产生不符合预期的bug +## Q2.2 +- 扫描全部 .log 后缀的日志文件: +```csharp +var logFiles = Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly) + .Select(filePath => Path.GetFileName(filePath)) + .OrderBy(fileName => fileName); + foreach (var fileName in logFiles){...} +``` +- `SearchOption.AllDirectories` +## Q2.3 +- 有使用 +### 2.3.b +- 主要使用Copilot的自动补全,以及让ai解释一些语句的含义、函数的用法,提示词就是“解释xxx的参数含义与用法” +- 由于自动补全是顺着我的思路写的,可能我起头起偏了,ai写的也会出问题,比如`worker.Join()`的时候没有注意到这个函数会自动阻塞,就写了个while,然后copilot就顺着开编了,这时就需要另外让ai解释一下函数用法,检查一下逻辑是否合理,多给几个角度再自行核对 +- 本节难度适中 \ No newline at end of file diff --git a/docs/03-async-grpc/image-1.png b/docs/03-async-grpc/image-1.png new file mode 100644 index 0000000..afeaaec Binary files /dev/null and b/docs/03-async-grpc/image-1.png differ diff --git a/docs/03-async-grpc/image.png b/docs/03-async-grpc/image.png new file mode 100644 index 0000000..1c8281a Binary files /dev/null and b/docs/03-async-grpc/image.png differ diff --git a/docs/03-async-grpc/report.md b/docs/03-async-grpc/report.md new file mode 100644 index 0000000..6695d2e --- /dev/null +++ b/docs/03-async-grpc/report.md @@ -0,0 +1,14 @@ +- 普通测试 +![alt text](image.png) +- 稳健性测试 +![alt text](image-1.png) +## Q3.1 +- 最大的区别是需要考虑网络通信的延迟、丢包、超时等各种未知情况,这些情况都需要服务端和客户端处理。这也使得异步、握手、状态码等机制在网络通信中得到广泛应用 +- 难点与复杂之处主要在于,需要处理网络通信的不稳定性,除此之外还要考虑前后端通信接口的一致性、可维护性,以及服务器在大量访问请求下的稳定性等 +## Q3.2 +有使用 +### 3.2.b +- AI主要负责自动补全,解释一些语句、接口的用法,以及帮忙找bug +- 提示词:解释一下xxx的用法与参数含义 +- 至少在自动补全方面,AI经常捏造出各种各样实际不存在的字段或参数,比如往`var request = new ChangeDirectoryRequest()`的构造函数加了个不知哪来的`Force = true`,这时就只能翻定义 +- 确有了解更多知识,比如CancellationToken,在gRPC中可以让服务端按需取消某些任务,比如客户端断连时触发,防止服务端做无用功 \ No newline at end of file diff --git a/docs/04-avalonia/image-1.png b/docs/04-avalonia/image-1.png new file mode 100644 index 0000000..c1f9a55 Binary files /dev/null and b/docs/04-avalonia/image-1.png differ diff --git a/docs/04-avalonia/image-2.png b/docs/04-avalonia/image-2.png new file mode 100644 index 0000000..e8fdf88 Binary files /dev/null and b/docs/04-avalonia/image-2.png differ diff --git a/docs/04-avalonia/image-3.png b/docs/04-avalonia/image-3.png new file mode 100644 index 0000000..63c9529 Binary files /dev/null and b/docs/04-avalonia/image-3.png differ diff --git a/docs/04-avalonia/image.png b/docs/04-avalonia/image.png new file mode 100644 index 0000000..8d9b3b9 Binary files /dev/null and b/docs/04-avalonia/image.png differ diff --git a/docs/04-avalonia/report.md b/docs/04-avalonia/report.md new file mode 100644 index 0000000..dd405a1 --- /dev/null +++ b/docs/04-avalonia/report.md @@ -0,0 +1,15 @@ +![alt text](image.png) +![alt text](image-1.png) +![alt text](image-2.png) +![alt text](image-3.png) + +## Q4.1 +- 感觉GUI界面最大的特点就是用户与程序的交互变得多元化了,比如光一个按钮就可能有左键、右键、点击、长按、拖动等多种交互方式。并且UI设计的层次性、美观性也有要求。如何让UI交互直观、顺手、不卡顿闪退,都是难点。 +- 编程过程中更加体会到了异步编程的重要性,UI的流畅性无比重要,不能每点一下就卡一会儿 +- 异步编程并非顺序运行,因此经常感到程序的时序混乱,比如此处要不要等?能容忍多久的延迟?返回之后如何处理?都会带来一些困惑 +## Q4.2 +有使用 +- AI主要负责自动补全,解释一些语句、接口的用法,以及帮忙找bug +- 提示词:解释一下xxx的用法与参数含义/根据04-avalonia中的指南,看看我刚写的ui实现是否正确稳健,之后检查一下xxx的报错原因 +- 至少在自动补全方面,AI经常捏造出各种各样实际不存在的字段或参数,比如往`var request = new ChangeDirectoryRequest()`的构造函数加了个不知哪来的`Force = true`,这时就只能翻定义 +- 确有了解更多知识,比如CancellationToken,在gRPC中可以让服务端按需取消某些任务,比如客户端断连时触发,防止服务端做无用功 \ No newline at end of file diff --git a/src/LocalCli/Program.cs b/src/LocalCli/Program.cs index 17b30db..68b171e 100644 --- a/src/LocalCli/Program.cs +++ b/src/LocalCli/Program.cs @@ -88,8 +88,14 @@ 6. Exit. switch (choice) { case 1: + actions[choice](analyzer); + break; case 2: + actions[choice](analyzer); + break; case 3: + actions[choice](analyzer); + break; case 4: actions[choice](analyzer); break; @@ -112,22 +118,78 @@ 6. Exit. private static void ShowLogFiles(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + analyzer.GetLogFiles().ToList().ForEach(fileName => Console.WriteLine(fileName)); } private static void AnalyzeFiles(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + Console.WriteLine("Please input log file names, separated by space:"); + var input = Console.ReadLine(); + if (input is null) + { + return; + } + var fileNames = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + Console.WriteLine("Please input degree of parallelism:"); + var degreeOfParallelismStr = Console.ReadLine(); + if (degreeOfParallelismStr is null) + { + return; + } + if (!int.TryParse(degreeOfParallelismStr, out var degreeOfParallelism)) + { + Console.WriteLine("Invalid input for degree of parallelism."); + return; + } + analyzer.AnalyzeFiles(degreeOfParallelism, fileNames); + Console.WriteLine("Done."); } private static void AnalyzeAll(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + Console.WriteLine("Please input degree of parallelism:"); + var degreeOfParallelismStr = Console.ReadLine(); + if (degreeOfParallelismStr is null) + { + return; + } + if (!int.TryParse(degreeOfParallelismStr, out var degreeOfParallelism)) + { + Console.WriteLine("Invalid input for degree of parallelism."); + return; + } + analyzer.AnalyzeAll(degreeOfParallelism); + Console.WriteLine("Done."); } private static void GetAnalysisResult(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + analyzer.GetLogFiles().ToList().ForEach(fileName => + { + if (analyzer.TryGetAnalysisResult(fileName, out var result)) + { + if (result == null) + { + Console.WriteLine($"No analysis result for file: {fileName}"); + } + Console.WriteLine($"File: {fileName}"); + Console.WriteLine($"State: {result.State}"); + if (result.State == AnalysisState.Failed) + { + Console.WriteLine($"Error Message: {result.ErrorMessage}"); + } + else + { + Console.WriteLine($"Log Entries Count: {result.Entries.Count}"); + } + Console.WriteLine(); + } + else + { + Console.WriteLine($"No analysis result for file: {fileName}"); + } + }); } } } diff --git a/src/LogAnalyzer/LogFileAnalyzer.cs b/src/LogAnalyzer/LogFileAnalyzer.cs index c3e7691..0631646 100644 --- a/src/LogAnalyzer/LogFileAnalyzer.cs +++ b/src/LogAnalyzer/LogFileAnalyzer.cs @@ -141,7 +141,7 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) /* * Set _isAnalyzing */ - // TODO: T2.2 + _isAnalyzing = true; } try @@ -154,7 +154,10 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) * Unset _isAnalyzing * Remember to lock _syncRoot to prevent data race */ - // TODO: T2.2 + lock (_syncRoot) + { + _isAnalyzing = false; + } } } @@ -169,7 +172,15 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis * 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($"Unknown file: {file.Name}"); + } + if (_analysisResults[file.Name].State == AnalysisState.NotAnalyzed) + { + logFilesToParse.Add(file); + } + } } @@ -183,7 +194,11 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis /* * 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]; @@ -194,13 +209,21 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis /* * Create and start threads to run `WorkerMain` */ - // TODO: T2.2 + workers[i] = new Thread(() => WorkerMain(workerId, queue)) + { + Name = threadName, + IsBackground = true + }; + 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 queue) @@ -213,19 +236,36 @@ private void WorkerMain(int workerId, WorkQueue queue) try { // Parse file - throw new NotImplementedException("TODO: T2.2"); + result = new AnalysisResult( + file.Name, + file.FullName, + AnalysisState.Succeeded, + parser.Parse(file.OpenText()).ToList(), + null, + workerId + ); } catch (Exception ex) { // Save exception message to result - throw new NotImplementedException("TODO: T2.2"); + result = new AnalysisResult( + file.Name, + file.FullName, + AnalysisState.Failed, + new List(), + ex.Message, + 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; + } } } } diff --git a/src/LogAnalyzer/WorkQueue.cs b/src/LogAnalyzer/WorkQueue.cs index 23055a5..26d9c31 100644 --- a/src/LogAnalyzer/WorkQueue.cs +++ b/src/LogAnalyzer/WorkQueue.cs @@ -20,17 +20,55 @@ public bool IsCompleted public void Enqueue(T item) { - throw new NotImplementedException("TODO: T2.1"); + lock (_items) + { + _isCompleted = false; + if (item == null) + throw new ArgumentNullException(nameof(item)); + _items.Enqueue(item); + } } public bool TryDequeue([NotNullWhen(true)] out T? item) { - throw new NotImplementedException("TODO: T2.1"); + lock (_items) + { + if (_items.Count > 0) + { + item = _items.Dequeue(); + if (item != null) + { + return true; + } + } + else + { + while (!_isCompleted) + { + Monitor.Wait(_items); + } + if (_items.Count > 0) + { + item = _items.Dequeue(); + if (item != null) + { + return true; + } + } + } + item = default; + return false; + } + } public void CompleteAdding() { - throw new NotImplementedException("TODO: T2.1"); + lock (_items) + { + _isCompleted = true; + Monitor.PulseAll(_items); + } } } } diff --git a/src/LogAnalyzerAgent/Applications/AgentSession.cs b/src/LogAnalyzerAgent/Applications/AgentSession.cs index 2531f22..f020926 100644 --- a/src/LogAnalyzerAgent/Applications/AgentSession.cs +++ b/src/LogAnalyzerAgent/Applications/AgentSession.cs @@ -24,7 +24,7 @@ private static OperationStatusMessage CreateInternalErrorOperationStatus(Excepti { Success = false, Code = AgentErrorCode.InternalError, - Message = $"An error occurred while retrieving agent status: {ex.Message}", + Message = $"An internal agent error occurred: {ex.Message}", }; } @@ -79,22 +79,121 @@ public Task GetLogFiles(Empty empty, CancellationToken canc public Task ChangeDirectory(ChangeDirectoryRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var response = new ChangeDirectoryResponse(); + try + { + { + if (_analyzer.ChangeDirectory(request.DirectoryPath)) + { + response.Status = CreateNoErrorOperationStatus(); + response.CurrentDirectory = _analyzer.CurrentDirectory ?? ""; + response.FileNames.AddRange(_analyzer.GetLogFiles()); + } + else + { + response.Status = new OperationStatusMessage() + { + Success = false, + Code = AgentErrorCode.DirectoryNotFound, + Message = $"Directory not found: {request.DirectoryPath}.", + }; + } + } + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "An error occurred while changing directory."); + } + return Task.FromResult(response); } public Task AnalyzeAll(AnalyzeAllRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var response = new AnalyzeAllResponse(); + try + { + _analyzer.AnalyzeAll(request.DegreeOfParallelism); + response.Status = CreateNoErrorOperationStatus(); + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "An error occurred while analyzing all log files."); + } + return Task.FromResult(response); } public Task AnalyzeFiles(AnalyzeFilesRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var response = new AnalyzeFilesResponse(); + try + { + _analyzer.AnalyzeFiles(request.DegreeOfParallelism, request.FileNames); + response.Status = CreateNoErrorOperationStatus(); + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "An error occurred while analyzing log files."); + } + return Task.FromResult(response); } public IReadOnlyList GetAnalysisResult(GetAnalysisResultRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var responses = new List(); + try + { + if (_analyzer.TryGetAnalysisResult(request.FileName, out var result) && result is not null) + { + responses.Add(new GetAnalysisResultResponse + { + Header = new AnalysisResultHeaderMessage + { + FileName = result.FileName ?? "Unknown", + FullName = result.FullName ?? "Unknown", + State = GrpcTypeConverter.ConvertToGrpc(result.State), + ErrorMessage = result.ErrorMessage ?? string.Empty, + WorkerId = result.WorkerId, + }, + Status = CreateNoErrorOperationStatus(), + }); + + if (result.State == AnalysisState.Succeeded) + { + foreach (var entry in result.Entries) + { + responses.Add(new GetAnalysisResultResponse + { + LogEntry = GrpcTypeConverter.ConvertToGrpc(entry), + Status = CreateNoErrorOperationStatus(), + }); + } + } + } + else + { + responses.Add(new GetAnalysisResultResponse + { + Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.FileNotFound, + Message = $"File not found: {request.FileName}", + }, + }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while retrieving analysis results."); + responses.Add(new GetAnalysisResultResponse + { + Status = CreateInternalErrorOperationStatus(ex), + }); + } + return responses; } } } diff --git a/src/LogAnalyzerAgent/Services/AgentService.cs b/src/LogAnalyzerAgent/Services/AgentService.cs index 591dcad..1bd81f2 100644 --- a/src/LogAnalyzerAgent/Services/AgentService.cs +++ b/src/LogAnalyzerAgent/Services/AgentService.cs @@ -29,27 +29,31 @@ public override Task GetAgentStatus(Empty empty, ServerC public override Task ChangeDirectory(ChangeDirectoryRequest request, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + return _session.ChangeDirectory(request, context.CancellationToken); } public override Task GetLogFiles(Empty empty, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + return _session.GetLogFiles(empty, context.CancellationToken); } public override Task AnalyzeAll(AnalyzeAllRequest request, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + return _session.AnalyzeAll(request, context.CancellationToken); } public override Task AnalyzeFiles(AnalyzeFilesRequest request, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + return _session.AnalyzeFiles(request, context.CancellationToken); } public override async Task GetAnalysisResult(GetAnalysisResultRequest request, IServerStreamWriter responseStream, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + var results = _session.GetAnalysisResult(request, context.CancellationToken); + foreach (var result in results) + { + await responseStream.WriteAsync(result); + } } } } diff --git a/src/LogAnalyzerClient/LogAnalyzerClient/Models/RemoteModels.cs b/src/LogAnalyzerClient/LogAnalyzerClient/Models/RemoteModels.cs index 2ff1b64..898bc28 100644 --- a/src/LogAnalyzerClient/LogAnalyzerClient/Models/RemoteModels.cs +++ b/src/LogAnalyzerClient/LogAnalyzerClient/Models/RemoteModels.cs @@ -11,7 +11,24 @@ public sealed record LogFileItem(string FileName) public sealed record LogFields(int Index, IReadOnlyList Fields, string? ErrorMessage) { - public string Summary => "TODO: T4.1"; + public string Summary => GetMessage(); + private string GetMessage() => Fields switch + { + null or [] => ErrorMessage ?? "No fields", + _ => Fields.FirstOrDefault(f => f.Key == "Type")?.Value switch + { + "Header" => $"Header: {GetFieldValue("FileName")} - {GetFieldValue("State")}\n{GetFieldValue("ErrorMessage", ErrorMessage ?? string.Empty)}", + "LogEntry" => $"Log Entry: {string.Join(", ", Fields.Where(f => f.Key != "Type").Select(f => $"{f.Key}={f.Value}"))}", + "Error" => $"Error: {GetFieldValue("Code")}\n{GetFieldValue("Message", ErrorMessage ?? "Unknown error")}", + _ => "Unknown type" + } + }; + + private string GetFieldValue(string key, string defaultValue = "N/A") + { + return Fields.FirstOrDefault(f => f.Key == key)?.Value ?? defaultValue; + } + } public sealed record LogFieldItem(string Key, string Value); diff --git a/src/LogAnalyzerClient/LogAnalyzerClient/ViewModels/MainViewModel.cs b/src/LogAnalyzerClient/LogAnalyzerClient/ViewModels/MainViewModel.cs index 91c05a8..c0744ba 100644 --- a/src/LogAnalyzerClient/LogAnalyzerClient/ViewModels/MainViewModel.cs +++ b/src/LogAnalyzerClient/LogAnalyzerClient/ViewModels/MainViewModel.cs @@ -36,7 +36,7 @@ public partial class MainViewModel : ViewModelBase private string _degreeOfParallelismText = "1"; [ObservableProperty] - private string _currentAddress = ""; + private string _currentAddress = "http://localhost:5000"; private static class ConnectStatusString { public const string NOT_CONNECTED = "Not connected."; @@ -108,22 +108,54 @@ await DialogHelper.ShowMessageDialogAsync("Error", } } + private async Task ReadDegreeOfParallelismAsync() + { + if (!int.TryParse(DegreeOfParallelismText, out var degreeOfParallelism) || degreeOfParallelism < 0) + { + await DialogHelper.ShowMessageDialogAsync("Error", "Degree of parallelism must be a non-negative integer."); + return null; + } + + return degreeOfParallelism; + } + + private async Task EnsureSuccessAsync(OperationStatusMessage status) + { + if (status.Success) + { + return true; + } + + await DialogHelper.ShowMessageDialogAsync( + "Error", + $"{status.Code}: {status.Message}"); + return false; + } + [RelayCommand] private async Task ChangeDirectoryAsync() { - await WithClientNotNull(async() => + await WithClientNotNull(async () => { var request = new ChangeDirectoryRequest() { DirectoryPath = DirectoryPath, }; var response = await _client!.ChangeDirectoryAsync(request); - if (!response.Status.Success) + if (!await EnsureSuccessAsync(response.Status)) + { + return; + } + + DirectoryPath = response.CurrentDirectory; + SelectedLogFile = null; + SelectedFiles = Array.Empty(); + ResultEntries.Clear(); + LogFiles.Clear(); + foreach (var fileName in response.FileNames) { - await DialogHelper.ShowMessageDialogAsync("Error", - $"{response.Status.Code}: {response.Status.Message}"); + LogFiles.Add(new LogFileItem(fileName)); } - await RefreshAsync(); }); } @@ -132,31 +164,160 @@ private async Task RefreshAsync() { await WithClientNotNull(async () => { - throw new NotImplementedException("TODO: T4.1"); + var response = await _client!.GetLogFilesAsync(new Empty()); + if (!await EnsureSuccessAsync(response.Status)) + { + return; + } + + LogFiles.Clear(); + SelectedLogFile = null; + SelectedFiles = Array.Empty(); + ResultEntries.Clear(); + foreach (var fileName in response.FileNames) + { + LogFiles.Add(new LogFileItem(fileName)); + } }); } [RelayCommand] private async Task AnalyzeSelectedFilesAsync() { - throw new NotImplementedException("TODO: T4.1"); + await WithClientNotNull(async () => + { + if (SelectedFiles.Count == 0) + { + await DialogHelper.ShowMessageDialogAsync("Error", "Please select at least one log file."); + return; + } + + var degreeOfParallelism = await ReadDegreeOfParallelismAsync(); + if (degreeOfParallelism is null) + { + return; + } + + var response = await _client!.AnalyzeFilesAsync(new AnalyzeFilesRequest + { + DegreeOfParallelism = degreeOfParallelism.Value, + FileNames = { SelectedFiles }, + }); + await EnsureSuccessAsync(response.Status); + }); } - /* - * TODO: T4.1 - * Add AnalyzeAllAsync ReplayCommand - */ + [RelayCommand] + private async Task AnalyzeAllAsync() + { + await WithClientNotNull(async () => + { + var degreeOfParallelism = await ReadDegreeOfParallelismAsync(); + if (degreeOfParallelism is null) + { + return; + } + + var response = await _client!.AnalyzeAllAsync(new AnalyzeAllRequest + { + DegreeOfParallelism = degreeOfParallelism.Value, + }); + await EnsureSuccessAsync(response.Status); + }); + } [RelayCommand] private async Task AnalyzeRightClickedFileAsync() { - throw new NotImplementedException("TODO: T4.1"); + await WithClientNotNull(async () => + { + if (SelectedLogFile is null) + { + await DialogHelper.ShowMessageDialogAsync("Error", "No log file selected."); + return; + } + + var degreeOfParallelism = await ReadDegreeOfParallelismAsync(); + if (degreeOfParallelism is null) + { + return; + } + + var response = await _client!.AnalyzeFilesAsync(new AnalyzeFilesRequest + { + DegreeOfParallelism = degreeOfParallelism.Value, + FileNames = { SelectedLogFile.FileName }, + }); + await EnsureSuccessAsync(response.Status); + }); } [RelayCommand] private async Task GetAnalysisResultAsync() { - throw new NotImplementedException("TODO: T4.1"); + await WithClientNotNull(async () => + { + var request = new GetAnalysisResultRequest() + { + FileName = SelectedLogFile?.FileName ?? string.Empty, + }; + if (string.IsNullOrWhiteSpace(request.FileName)) + { + await DialogHelper.ShowMessageDialogAsync("Error", "Please select a log file."); + return; + } + + using var call = _client!.GetAnalysisResult(request, cancellationToken: default); + ResultEntries.Clear(); + int idx = 1; + await foreach (var response in call.ResponseStream.ReadAllAsync()) + { + if (!response.Status.Success) + { + ResultEntries.Add(new LogFields(idx++, new List + { + new("Type", "Error"), + new("Code", response.Status.Code.ToString()), + new("Message", response.Status.Message), + }, response.Status.Message)); + continue; + } + + switch (response.PayloadCase) + { + case GetAnalysisResultResponse.PayloadOneofCase.Header: + ResultEntries.Add(new LogFields(idx++, new List + { + new("Type", "Header"), + new("FileName", response.Header.FileName), + new("FullName", response.Header.FullName), + new("State", response.Header.State.ToString()), + new("ErrorMessage", response.Header.ErrorMessage ?? string.Empty), + new("WorkerId", response.Header.WorkerId.ToString()), + }, response.Header.ErrorMessage)); + break; + case GetAnalysisResultResponse.PayloadOneofCase.LogEntry: + var entry = GrpcTypeConverter.ConvertFromGrpc(response.LogEntry); + var fields = new KeyValueVisitor().Dump(entry) + .Select(pair => new LogFieldItem(pair.Key, pair.Value)) + .Prepend(new LogFieldItem("Type", "LogEntry")) + .ToList(); + ResultEntries.Add(new LogFields(idx++, fields, null)); + break; + default: + ResultEntries.Add(new LogFields( + idx++, + new List + { + new("Type", "Error"), + new("Message", "The agent returned an empty response."), + }, + "The agent returned an empty response.")); + break; + } + } + } + ); } [RelayCommand] diff --git a/src/LogAnalyzerClient/LogAnalyzerClient/Views/MainView.axaml b/src/LogAnalyzerClient/LogAnalyzerClient/Views/MainView.axaml index fffef7e..32c0360 100644 --- a/src/LogAnalyzerClient/LogAnalyzerClient/Views/MainView.axaml +++ b/src/LogAnalyzerClient/LogAnalyzerClient/Views/MainView.axaml @@ -1,15 +1,15 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:vm="using:LogAnalyzerClient.ViewModels" + xmlns:models="using:LogAnalyzerClient.Models" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:Class="LogAnalyzerClient.Views.MainView" + x:DataType="vm:MainViewModel"> + to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) --> @@ -49,7 +49,7 @@ to set the actual DataContext for runtime, set the DataContext property in code Grid.Row="0" Grid.Column="0" Spacing="4" - > + > @@ -58,7 +58,7 @@ to set the actual DataContext for runtime, set the DataContext property in code Grid.Row="1" Grid.Column="0" Classes="Card" - > + > - + +