diff --git a/docs/04-avalonia/images/analysis-results.png b/docs/04-avalonia/images/analysis-results.png new file mode 100644 index 0000000..84248bd Binary files /dev/null and b/docs/04-avalonia/images/analysis-results.png differ diff --git a/docs/04-avalonia/images/error-invalid-path.png b/docs/04-avalonia/images/error-invalid-path.png new file mode 100644 index 0000000..80c1cf2 Binary files /dev/null and b/docs/04-avalonia/images/error-invalid-path.png differ diff --git a/docs/04-avalonia/images/error-unconnected.png b/docs/04-avalonia/images/error-unconnected.png new file mode 100644 index 0000000..037889e Binary files /dev/null and b/docs/04-avalonia/images/error-unconnected.png differ diff --git a/docs/04-avalonia/report.md b/docs/04-avalonia/report.md new file mode 100644 index 0000000..17f7267 --- /dev/null +++ b/docs/04-avalonia/report.md @@ -0,0 +1,54 @@ +# 04-avalonia + +## 一、 功能实现说明 + +实现了一个图形化客户端,并通过和后端进行异步通信来实现功能。 + +1. **查看是否连接** + - 通过 `ConnectCommand` 调用 `DesktopDialogHelper` 弹窗引导用户输入 Agent 地址与服务器连接。 + - 使用异步方法 `_client.PingAsync()` 校验连接有效性,实时更新底部状态栏(如 `Connected.`、`Connecting...`、`Not connected.`)。 + +2. **选定分析的目录** + - **切换目录 (`ChangeDirectoryCommand`)**:发送 `ChangeDirectoryRequest` 并异步等待响应,更新服务端当前工作路径。 + - **刷新文件 (`RefreshCommand`)**:请求服务端日志列表并清空填充至 `ObservableCollection`,实现 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完成部分代码的复用。 \ No newline at end of file diff --git a/src/LocalCli/Program.cs b/src/LocalCli/Program.cs index 17b30db..9bb775e 100644 --- a/src/LocalCli/Program.cs +++ b/src/LocalCli/Program.cs @@ -1,5 +1,6 @@ using LogAnalyzer; using LogParser.Visitors; +using System; namespace LocalCli { @@ -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; + } } } -} +} \ No newline at end of file diff --git a/src/LogAnalyzer/LogFileAnalyzer.cs b/src/LogAnalyzer/LogFileAnalyzer.cs index c3e7691..9867568 100644 --- a/src/LogAnalyzer/LogFileAnalyzer.cs +++ b/src/LogAnalyzer/LogFileAnalyzer.cs @@ -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 { @@ -138,10 +143,8 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) } fileList = fileNameList.Select(fileName => _logFiles[fileName]).ToList(); - /* - * Set _isAnalyzing - */ - // TODO: T2.2 + //设置正在分析状态为true + _isAnalyzing = true; } try @@ -150,11 +153,11 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) } finally { - /* - * Unset _isAnalyzing - * Remember to lock _syncRoot to prevent data race - */ - // TODO: T2.2 + //分析结束,设置一个锁保护,在其中重置状态 + lock (_syncRoot) + { + _isAnalyzing = false; + } } } @@ -165,11 +168,15 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList 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); + } } } @@ -180,10 +187,11 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis var queue = new WorkQueue(); - /* - * 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]; @@ -191,16 +199,17 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis { 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 queue) @@ -212,21 +221,35 @@ private void WorkerMain(int workerId, WorkQueue 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(), + 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; + } } } } -} +} \ No newline at end of file diff --git a/src/LogAnalyzer/WorkQueue.cs b/src/LogAnalyzer/WorkQueue.cs index 23055a5..160051c 100644 --- a/src/LogAnalyzer/WorkQueue.cs +++ b/src/LogAnalyzer/WorkQueue.cs @@ -1,4 +1,7 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; namespace LogAnalyzer { @@ -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); + } } } } diff --git a/src/LogAnalyzerAgent/Applications/AgentSession.cs b/src/LogAnalyzerAgent/Applications/AgentSession.cs index 2531f22..d6af145 100644 --- a/src/LogAnalyzerAgent/Applications/AgentSession.cs +++ b/src/LogAnalyzerAgent/Applications/AgentSession.cs @@ -1,9 +1,13 @@ using Google.Protobuf.WellKnownTypes; using Grpc.Core; using LogAnalyzer; -using LogAnalyzerRpc.Protos; using LogAnalyzerRpc; +using LogAnalyzerRpc.Protos; using LogParser.Visitors; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; namespace LogAnalyzerAgent.Applications { @@ -79,22 +83,219 @@ 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 (string.IsNullOrWhiteSpace(request.DirectoryPath)) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidArgument, + Message = "Directory path cannot be empty." + }; + return Task.FromResult(response); + } + + if (!_analyzer.ChangeDirectory(request.DirectoryPath)) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.DirectoryNotFound, + Message = $"Directory '{request.DirectoryPath}' does not exist." + }; + return Task.FromResult(response); + } + + response.CurrentDirectory = _analyzer.CurrentDirectory ?? request.DirectoryPath; + response.FileNames.AddRange(_analyzer.GetLogFiles()); + response.Status = CreateNoErrorOperationStatus(); + } + catch (ArgumentException ex) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidArgument, + Message = ex.Message + }; + } + 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 + { + if (!_analyzer.HasDirectory) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = "Directory not set." + }; + return Task.FromResult(response); + } + + if (_analyzer.IsAnalyzing) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = "Agent is currently analyzing logs." + }; + return Task.FromResult(response); + } + + _analyzer.AnalyzeAll(request.DegreeOfParallelism); + response.Status = CreateNoErrorOperationStatus(); + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "An error occurred while analyzing all files."); + } + return Task.FromResult(response); } public Task AnalyzeFiles(AnalyzeFilesRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var response = new AnalyzeFilesResponse(); + try + { + if (!_analyzer.HasDirectory) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = "Directory not set." + }; + return Task.FromResult(response); + } + + if (_analyzer.IsAnalyzing) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = "Agent is currently analyzing logs." + }; + return Task.FromResult(response); + } + + if (request.FileNames == null || request.FileNames.Count == 0) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidArgument, + Message = "No files specified for analysis." + }; + return Task.FromResult(response); + } + + _analyzer.AnalyzeFiles(request.DegreeOfParallelism, request.FileNames); + response.Status = CreateNoErrorOperationStatus(); + } + catch (FileNotFoundException ex) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.FileNotFound, + Message = ex.Message + }; + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "An error occurred while analyzing specified files."); + } + return Task.FromResult(response); } public IReadOnlyList GetAnalysisResult(GetAnalysisResultRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var resultsList = new List(); + try + { + if (string.IsNullOrWhiteSpace(request.FileName)) + { + resultsList.Add(new GetAnalysisResultResponse + { + Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidArgument, + Message = "File name cannot be empty." + } + }); + return resultsList; + } + + if (!_analyzer.TryGetAnalysisResult(request.FileName, out var result) || result is null) + { + resultsList.Add(new GetAnalysisResultResponse + { + Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.FileNotFound, + Message = $"File '{request.FileName}' was not found or has no analysis result." + } + }); + return resultsList; + } + + var headerMessage = new AnalysisResultHeaderMessage + { + FileName = request.FileName, + FullName = result.FullName ?? "", + State = GrpcTypeConverter.ConvertToGrpc(result.State), + ErrorMessage = result.ErrorMessage ?? "", + WorkerId = result.WorkerId + }; + + resultsList.Add(new GetAnalysisResultResponse + { + Header = headerMessage, + Status = CreateNoErrorOperationStatus() + }); + + if (result.State == AnalysisState.Succeeded && result.Entries != null) + { + foreach (var entry in result.Entries) + { + resultsList.Add(new GetAnalysisResultResponse + { + LogEntry = GrpcTypeConverter.ConvertToGrpc(entry), + Status = CreateNoErrorOperationStatus() + }); + } + } + } + catch (Exception ex) + { + resultsList.Clear(); + resultsList.Add(new GetAnalysisResultResponse + { + Status = CreateInternalErrorOperationStatus(ex) + }); + _logger.LogError(ex, "An error occurred while getting analysis result."); + } + + return resultsList; } } } diff --git a/src/LogAnalyzerAgent/Services/AgentService.cs b/src/LogAnalyzerAgent/Services/AgentService.cs index 591dcad..bc0034e 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 responses = _session.GetAnalysisResult(request, context.CancellationToken); + foreach (var response in responses) + { + await responseStream.WriteAsync(response, context.CancellationToken); + } } } -} +} \ No newline at end of file diff --git a/src/LogAnalyzerClient/LogAnalyzerClient/Models/RemoteModels.cs b/src/LogAnalyzerClient/LogAnalyzerClient/Models/RemoteModels.cs index 2ff1b64..f96a81c 100644 --- a/src/LogAnalyzerClient/LogAnalyzerClient/Models/RemoteModels.cs +++ b/src/LogAnalyzerClient/LogAnalyzerClient/Models/RemoteModels.cs @@ -11,7 +11,7 @@ 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 => ErrorMessage ?? string.Join(" ", Fields?.Select(f => $"{f.Key}: {f.Value}") ?? []); } 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..9b9e996 100644 --- a/src/LogAnalyzerClient/LogAnalyzerClient/ViewModels/MainViewModel.cs +++ b/src/LogAnalyzerClient/LogAnalyzerClient/ViewModels/MainViewModel.cs @@ -111,7 +111,7 @@ await DialogHelper.ShowMessageDialogAsync("Error", [RelayCommand] private async Task ChangeDirectoryAsync() { - await WithClientNotNull(async() => + await WithClientNotNull(async () => { var request = new ChangeDirectoryRequest() { @@ -132,31 +132,162 @@ private async Task RefreshAsync() { await WithClientNotNull(async () => { - throw new NotImplementedException("TODO: T4.1"); + var response = await _client!.GetLogFilesAsync(new Empty()); + if (!response.Status.Success) + { + await DialogHelper.ShowMessageDialogAsync("Error", $"{response.Status.Code}: {response.Status.Message}"); + return; + } + + LogFiles.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 == null || SelectedFiles.Count == 0) + { + await DialogHelper.ShowMessageDialogAsync("Warning", "No log files selected."); + return; + } + + if (!int.TryParse(DegreeOfParallelismText, out int parallelism) || parallelism < 0) + { + await DialogHelper.ShowMessageDialogAsync("Error", "Degree of parallelism must be a non-negative integer."); + return; + } + + var request = new AnalyzeFilesRequest + { + DegreeOfParallelism = parallelism + }; + request.FileNames.AddRange(SelectedFiles); + + var response = await _client!.AnalyzeFilesAsync(request); + if (!response.Status.Success) + { + await DialogHelper.ShowMessageDialogAsync("Error", $"{response.Status.Code}: {response.Status.Message}"); + } + }); } - /* - * TODO: T4.1 - * Add AnalyzeAllAsync ReplayCommand - */ + [RelayCommand] + private async Task AnalyzeAllAsync() + { + await WithClientNotNull(async () => + { + if (!int.TryParse(DegreeOfParallelismText, out int parallelism) || parallelism < 0) + { + await DialogHelper.ShowMessageDialogAsync("Error", "Degree of parallelism must be a non-negative integer."); + return; + } + + var request = new AnalyzeAllRequest + { + DegreeOfParallelism = parallelism + }; + + var response = await _client!.AnalyzeAllAsync(request); + if (!response.Status.Success) + { + await DialogHelper.ShowMessageDialogAsync("Error", $"{response.Status.Code}: {response.Status.Message}"); + } + }); + } [RelayCommand] private async Task AnalyzeRightClickedFileAsync() { - throw new NotImplementedException("TODO: T4.1"); + await WithClientNotNull(async () => + { + if (SelectedLogFile is null || string.IsNullOrEmpty(SelectedLogFile.FileName)) + { + await DialogHelper.ShowMessageDialogAsync("Warning", "No log file selected."); + return; + } + + if (!int.TryParse(DegreeOfParallelismText, out int parallelism) || parallelism < 0) + { + await DialogHelper.ShowMessageDialogAsync("Error", "Degree of parallelism must be a non-negative integer."); + return; + } + + var request = new AnalyzeFilesRequest + { + DegreeOfParallelism = parallelism + }; + request.FileNames.Add(SelectedLogFile.FileName); + + var response = await _client!.AnalyzeFilesAsync(request); + if (!response.Status.Success) + { + await DialogHelper.ShowMessageDialogAsync("Error", $"{response.Status.Code}: {response.Status.Message}"); + } + }); } [RelayCommand] private async Task GetAnalysisResultAsync() { - throw new NotImplementedException("TODO: T4.1"); + await WithClientNotNull(async () => + { + if (SelectedLogFile is null || string.IsNullOrEmpty(SelectedLogFile.FileName)) + { + await DialogHelper.ShowMessageDialogAsync("Warning", "Please select a log file to view analysis results."); + return; + } + + var fileName = SelectedLogFile.FileName; + var request = new GetAnalysisResultRequest + { + FileName = fileName + }; + + ResultEntries.Clear(); + + using var call = _client!.GetAnalysisResult(request); + await foreach (var response in call.ResponseStream.ReadAllAsync()) + { + if (!response.Status.Success) + { + await DialogHelper.ShowMessageDialogAsync("Error", $"{response.Status.Code}: {response.Status.Message}"); + return; + } + + switch (response.PayloadCase) + { + case GetAnalysisResultResponse.PayloadOneofCase.Header: + var header = response.Header; + switch (header.State) + { + case AnalysisStateEnum.NotAnalyzed: + ResultEntries.Add(new LogFields(0, Array.Empty(), $"File '{fileName}' has not been analyzed yet.")); + break; + case AnalysisStateEnum.Failed: + ResultEntries.Add(new LogFields(0, Array.Empty(), $"Analysis failed for '{fileName}': {header.ErrorMessage}")); + break; + case AnalysisStateEnum.Succeeded: + break; + } + break; + + case GetAnalysisResultResponse.PayloadOneofCase.LogEntry: + var entry = GrpcTypeConverter.ConvertFromGrpc(response.LogEntry); + if (entry != null) + { + ResultEntries.Add(new LogFields(0, Array.Empty(), entry.ToString())); + } + break; + } + } + }); } [RelayCommand] @@ -171,3 +302,4 @@ EESAST Software Center } } } + diff --git a/src/LogAnalyzerClient/LogAnalyzerClient/Views/MainView.axaml b/src/LogAnalyzerClient/LogAnalyzerClient/Views/MainView.axaml index fffef7e..5ab5881 100644 --- a/src/LogAnalyzerClient/LogAnalyzerClient/Views/MainView.axaml +++ b/src/LogAnalyzerClient/LogAnalyzerClient/Views/MainView.axaml @@ -1,15 +1,13 @@ + 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"> - @@ -45,20 +43,12 @@ to set the actual DataContext for runtime, set the DataContext property in code - + - + - + -