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/docs/05-advanced/images/1.png b/docs/05-advanced/images/1.png new file mode 100644 index 0000000..b0e3b9f Binary files /dev/null and b/docs/05-advanced/images/1.png differ diff --git a/docs/05-advanced/report.md b/docs/05-advanced/report.md new file mode 100644 index 0000000..7520863 --- /dev/null +++ b/docs/05-advanced/report.md @@ -0,0 +1,25 @@ +# 项目功能介绍 +* 写在最前:我选择的功能性题目是a.b和a.c,美观性题目是b.a,自由发挥选择a.d。 +## 1.项目简介 +本项目是一个日志解析客户端,在验证用户身份之后,可以对日志文件进行解析并显示。最终由一个常驻的agent(gRPC)服务端和一个客户端交互组成。 +### 1.1前后端架构 +* `LogAnalyzerClient`是前端,负责用户交互,并且向后端发送请求。 +* `LogAnalyzerAgent`是云服务客户端,和前端通过gRPC异步通信,解析日志内容,并生成token验证身份。 +### 1.2编译运行步骤 +* `LogAnalyzerAgent`和`LogAnalyzerClient`发出请求,调用`LogAnalyzerRpc`的接口,然后在调用`LogAnalyze`,进一步用最底层的`LogParser`进行日志解析。 +## 2.功能 +![功能](./images/1.png) +### 2.1基础功能——日志解析 +* 日志解析:留置读取文件,进行逐行解析(简单工厂和访问者) +* 并行日志分析:多线程解析 +* gRPC:传输数据到服务端,在流式返回,尤其注意捕获异常输入 +* 图形客户端:特别注意异步,避免阻塞 +### 2.2进阶功能 +* Token验证:管理员、普通权限不同 +* 日志查询:可以按照类型、等级等清晰观看 +* 美观显示:表格显示 +* 拓补图展示 +## 3.AI +* 相较于前几讲,本节ai借助较多,在分析报错的基础上,利用AI帮助设计了代码框架,自己再进行填充修改,同时,哎帮助分析出了一些由于包版本不同导致运行错误的问题,这些问题我似乎自己难以发现原因。 +## 4.总结和经验 +* 本节使得我对前后端、本地和云端的交互有了更深的理解,之前在硬设比赛大作业中接触过简单的图形化编程,那时候了解到前后端的协调,每一个图形化的按钮都要有i那个管逻辑支撑,本次作业让我理解更深入,同时,关于异步的理解加深了。 \ No newline at end of file diff --git a/src/ConsoleTest/ConsoleTest.csproj b/src/ConsoleTest/ConsoleTest.csproj index 5d4fe2b..9894a7f 100644 --- a/src/ConsoleTest/ConsoleTest.csproj +++ b/src/ConsoleTest/ConsoleTest.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,10 +9,12 @@ + + dataset\%(RecursiveDir)%(Filename)%(Extension) diff --git a/src/ConsoleTest/Program.cs b/src/ConsoleTest/Program.cs index 576af4e..4bac9b9 100644 --- a/src/ConsoleTest/Program.cs +++ b/src/ConsoleTest/Program.cs @@ -1,41 +1,142 @@ -using CsvHelper; -using CsvHelper.Configuration; -using System.Globalization; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; - +using Grpc.Core; +using Grpc.Net.Client; +using Google.Protobuf.WellKnownTypes; +using LogAnalyzerRpc; +using LogAnalyzerRpc.Protos; +using LogParser.Models; using LogParser.Parser; -using LogParser.Visitors; -using LogAnalyzer; -Console.WriteLine("Hello, ConsoleTest of dotnet-workshop!"); -Console.WriteLine(); +Console.WriteLine("=== Advanced Chapter 5: query & topology verification ===\n"); + +var entries = new LogFileParser().Parse(new StreamReader("dataset/basic.log")).ToList(); +Console.WriteLine($"Parsed {entries.Count} log entries from dataset/basic.log"); + +// ---------- 1. 过滤:按事件类型 ---------- +var callFilter = new LogFilter { EventType = LogEventTypeEnum.Call }; +var calls = LogAnalysisQuery.FilterAndSort(entries, callFilter, null); +Console.WriteLine($"\n[filter event=Call] -> {calls.Count} entries"); + +var errFilter = new LogFilter { Severity = LogSeverityEnum.Error }; +var errors = LogAnalysisQuery.FilterAndSort(entries, errFilter, null); +Console.WriteLine($"[filter severity=Error] -> {errors.Count} entries"); + +var gwFilter = new LogFilter { ServiceName = "gateway" }; +var gateway = LogAnalysisQuery.FilterAndSort(entries, gwFilter, null); +Console.WriteLine($"[filter service=gateway] -> {gateway.Count} entries"); +// ---------- 2. 排序 ---------- +var sortBySeverity = new LogSortOptions { SortBy = "Severity", IsDescending = true }; +var sorted = LogAnalysisQuery.FilterAndSort(entries, null, sortBySeverity); +Console.WriteLine($"\n[sort severity desc] first 5 severities:"); +foreach (var e in sorted.Take(5)) { - var parser = new LogFileParser(); - var results = parser.Parse(new StreamReader("dataset/basic.log")).ToList(); - var kvdumper = new KeyValueVisitor(); - var kvresults = results.Select(entry => kvdumper.Dump(entry)).ToList(); + Console.WriteLine($" line={e.LineNo,-4} severity={e.Severity}"); +} + +// ---------- 3. 服务名提取 ---------- +Console.WriteLine($"\n[GetServiceName] gateway-0 -> {LogAnalysisQuery.GetServiceName("gateway-0")}"); +Console.WriteLine($"[GetServiceName] userservice-12 -> {LogAnalysisQuery.GetServiceName("userservice-12")}"); - Console.WriteLine(string.Join("\n", kvresults.Select(kvresult => string.Join(", ", kvresult.Select(kv => $"{kv.Key}: {kv.Value}"))))); - Console.WriteLine(); +// ---------- 4. 拓扑推断 ---------- +var topology = TopologyBuilder.Build(entries); +Console.WriteLine($"\n[topology] {topology.Nodes.Count} nodes:"); +Console.WriteLine(" " + string.Join(", ", topology.Nodes)); +Console.WriteLine($"[topology] {topology.Edges.Count} edges:"); +foreach (var edge in topology.Edges) +{ + Console.WriteLine($" {edge.SourceService} -> {edge.TargetService} ({edge.CallCount} calls)"); } +// ---------- 5. 拓扑边 -> 日志(request_ids 过滤) ---------- +var firstEdge = topology.Edges.FirstOrDefault(); +if (firstEdge is not null) { - var analyzer = new LogFileAnalyzer("dataset"); - Console.WriteLine(analyzer.CurrentDirectory); - Console.WriteLine(string.Join(", ", analyzer.GetLogFiles())); - analyzer.AnalyzeAll(4); - analyzer.TryGetAnalysisResult("basic.log", out var analysisResults); - var kvdumper = new KeyValueVisitor(); - var kvresults = analysisResults!.Entries.Select(entry => kvdumper.Dump(entry)).ToList(); - Console.WriteLine(string.Join("\n", kvresults.Select(kvresult => string.Join(", ", kvresult.Select(kv => $"{kv.Key}: {kv.Value}"))))); - Console.WriteLine(); + var edgeFilter = new LogFilter(); + edgeFilter.RequestIds.AddRange(firstEdge.RequestIds); + var edgeLogs = LogAnalysisQuery.FilterAndSort(entries, edgeFilter, null); + Console.WriteLine($"\n[edge {firstEdge.SourceService}->{firstEdge.TargetService}] " + + $"request_ids={firstEdge.RequestIds.Count}, matched logs={edgeLogs.Count}"); } +// ---------- 6. gRPC 端到端流程测试(需要先启动 Agent 并传入 admin token) ---------- +if (args.Length > 0 && !string.IsNullOrWhiteSpace(args[0])) +{ + Console.WriteLine("\n=== gRPC end-to-end flow test ==="); + var token = args[0]; + using var channel = GrpcChannel.ForAddress("http://localhost:5000"); + var client = new LogAnalyzerAgentService.LogAnalyzerAgentServiceClient(channel); + var headers = new Metadata { { "x-agent-token", token } }; + + // 无 token -> 应被拒绝 + try + { + await client.PingAsync(new Empty()); + Console.WriteLine("[FAIL] ping without token should be rejected"); + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Unauthenticated) + { + Console.WriteLine("[PASS] ping without token -> Unauthenticated"); + } + + // 带 token -> Ping + await client.PingAsync(new Empty(), headers); + Console.WriteLine("[PASS] ping with token -> OK"); + + // ChangeDirectory + var dir = @"D:\THUsummerlearning\THU\src\dataset"; + var cdResp = await client.ChangeDirectoryAsync(new ChangeDirectoryRequest { DirectoryPath = dir }, headers); + Console.WriteLine(cdResp.Status.Success + ? $"[PASS] ChangeDirectory -> {cdResp.CurrentDirectory} ({cdResp.FileNames.Count} files)" + : $"[FAIL] ChangeDirectory: {cdResp.Status.Message}"); + + // GetLogFiles + var filesResp = await client.GetLogFilesAsync(new Empty(), headers); + Console.WriteLine($"[PASS] GetLogFiles -> {filesResp.FileNames.Count} files: {string.Join(", ", filesResp.FileNames)}"); + + // AnalyzeAll + var analyzeResp = await client.AnalyzeAllAsync(new AnalyzeAllRequest { DegreeOfParallelism = 4 }, headers); + Console.WriteLine(analyzeResp.Status.Success ? "[PASS] AnalyzeAll -> OK" : $"[FAIL] AnalyzeAll: {analyzeResp.Status.Message}"); + + // GetAnalysisResult + int entryCount = 0; + using (var call = client.GetAnalysisResult(new GetAnalysisResultRequest { FileName = "basic.log" }, headers)) + { + await foreach (var r in call.ResponseStream.ReadAllAsync()) + { + if (r.PayloadCase == GetAnalysisResultResponse.PayloadOneofCase.LogEntry) entryCount++; + } + } + Console.WriteLine($"[PASS] GetAnalysisResult basic.log -> {entryCount} entries"); + + // Query(按服务名过滤) + int qCount = 0; + using (var qCall = client.QueryAnalysisResult(new QueryAnalysisResultRequest + { + FileName = "basic.log", + Filter = new LogFilter { ServiceName = "gateway" } + }, headers)) + { + await foreach (var r in qCall.ResponseStream.ReadAllAsync()) + { + if (r.PayloadCase == GetAnalysisResultResponse.PayloadOneofCase.LogEntry) qCount++; + } + } + Console.WriteLine($"[PASS] Query service=gateway -> {qCount} entries"); + + // Topology + var topoResp = await client.GetTopologyAsync(new GetTopologyRequest { FileName = "basic.log" }, headers); + Console.WriteLine(topoResp.Status.Success + ? $"[PASS] Topology -> {topoResp.Nodes.Count} nodes, {topoResp.Edges.Count} edges" + : $"[FAIL] Topology: {topoResp.Status.Message}"); + + // ListTokens(管理员接口) + var list = await client.ListTokensAsync(new Empty(), headers); + Console.WriteLine($"[PASS] ListTokens -> {list.Tokens.Count} token(s): " + + string.Join(", ", list.Tokens.Select(t => $"{t.Role}"))); +} +else { - var strs = (IEnumerable)new List() { "a", "b", "c" }; - Console.WriteLine(strs.FirstOrDefault()); - Console.WriteLine(strs.ToList().Count); + Console.WriteLine("\n=== gRPC end-to-end flow test skipped (pass admin token as arg) ==="); } + +Console.WriteLine("\n=== Done ==="); diff --git a/src/LocalCli/LocalCli.csproj b/src/LocalCli/LocalCli.csproj index 0ad60d9..aa0cd49 100644 --- a/src/LocalCli/LocalCli.csproj +++ b/src/LocalCli/LocalCli.csproj @@ -11,7 +11,6 @@ - dataset\%(RecursiveDir)%(Filename)%(Extension) PreserveNewest 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..d013f76 100644 --- a/src/LogAnalyzerAgent/Applications/AgentSession.cs +++ b/src/LogAnalyzerAgent/Applications/AgentSession.cs @@ -1,16 +1,22 @@ -using Google.Protobuf.WellKnownTypes; +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.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; namespace LogAnalyzerAgent.Applications { public class AgentSession { private readonly LogFileAnalyzer _analyzer; - private readonly ILogger _logger; + private readonly ILogger _logger; public AgentSession(LogFileAnalyzer analyzer, ILoggerFactory loggerFactory) { @@ -20,21 +26,21 @@ public AgentSession(LogFileAnalyzer analyzer, ILoggerFactory loggerFactory) private static OperationStatusMessage CreateInternalErrorOperationStatus(Exception ex) { - return new OperationStatusMessage() + return new OperationStatusMessage { Success = false, Code = AgentErrorCode.InternalError, - Message = $"An error occurred while retrieving agent status: {ex.Message}", + Message = $"An internal error occurred: {ex.Message}", }; } private static OperationStatusMessage CreateNoErrorOperationStatus() { - return new OperationStatusMessage() + return new OperationStatusMessage { Success = true, Code = AgentErrorCode.NoAgentError, - Message = "", + Message = string.Empty, }; } @@ -49,14 +55,14 @@ public Task GetAgentStatus(Empty empty, CancellationToke try { response.HasDirectory = _analyzer.HasDirectory; - response.CurrentDirectory = _analyzer.CurrentDirectory ?? ""; + response.CurrentDirectory = _analyzer.CurrentDirectory ?? string.Empty; response.IsAnalyzing = _analyzer.IsAnalyzing; response.Status = CreateNoErrorOperationStatus(); } catch (Exception ex) { response.Status = CreateInternalErrorOperationStatus(ex); - _logger.LogError(ex, "An error occurred while retrieving agent status."); + _logger.LogError(ex, "Error occurred while retrieving agent status."); } return Task.FromResult(response); } @@ -72,29 +78,347 @@ public Task GetLogFiles(Empty empty, CancellationToken canc catch (Exception ex) { response.Status = CreateInternalErrorOperationStatus(ex); - _logger.LogError(ex, "An error occurred while retrieving log files."); + _logger.LogError(ex, "Error occurred while retrieving log files."); } return Task.FromResult(response); } 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 or is currently analyzing." + }; + return Task.FromResult(response); + } + + response.CurrentDirectory = _analyzer.CurrentDirectory ?? request.DirectoryPath; + response.FileNames.AddRange(_analyzer.GetLogFiles()); + response.Status = CreateNoErrorOperationStatus(); + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "Error occurred while changing directory."); + } + return Task.FromResult(response); + } + + public async Task AnalyzeAllAsync(AnalyzeAllRequest request, CancellationToken cancellationToken) + { + var response = new AnalyzeAllResponse(); + try + { + if (!_analyzer.HasDirectory) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = "Directory not set." + }; + return response; + } + + if (_analyzer.IsAnalyzing) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = "Agent is currently analyzing logs." + }; + return response; + } + + // 在后台线程运行密集型 CPU 任务,避免阻塞 gRPC 主处理线程 + await Task.Run(() => _analyzer.AnalyzeAll(request.DegreeOfParallelism), cancellationToken); + response.Status = CreateNoErrorOperationStatus(); + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "Error occurred while analyzing all files."); + } + return response; + } + + public async Task AnalyzeFilesAsync(AnalyzeFilesRequest request, CancellationToken cancellationToken) + { + var response = new AnalyzeFilesResponse(); + try + { + if (!_analyzer.HasDirectory) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = "Directory not set." + }; + return response; + } + + if (_analyzer.IsAnalyzing) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = "Agent is currently analyzing logs." + }; + return 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 response; + } + + // 异步后台线程执行 + await Task.Run(() => _analyzer.AnalyzeFiles(request.DegreeOfParallelism, request.FileNames), cancellationToken); + 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, "Error occurred while analyzing specified files."); + } + return response; + } + + // 优化为延迟生成器 (Yield),提高大日志文件下传输响应速度 + public async IAsyncEnumerable GetAnalysisResultStreamAsync( + GetAnalysisResultRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(request.FileName)) + { + yield return new GetAnalysisResultResponse + { + Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidArgument, + Message = "File name cannot be empty." + } + }; + yield break; + } + + if (!_analyzer.TryGetAnalysisResult(request.FileName, out var result) || result is null) + { + yield return new GetAnalysisResultResponse + { + Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.FileNotFound, + Message = $"File '{request.FileName}' was not found or has no analysis result." + } + }; + yield break; + } + + // 1. 先推送 Header + var headerMessage = new AnalysisResultHeaderMessage + { + FileName = request.FileName, + FullName = result.FullName ?? string.Empty, + State = GrpcTypeConverter.ConvertToGrpc(result.State), + ErrorMessage = result.ErrorMessage ?? string.Empty, + WorkerId = result.WorkerId + }; + + yield return new GetAnalysisResultResponse + { + Header = headerMessage, + Status = CreateNoErrorOperationStatus() + }; + + // 2. 逐条推送 Log Entries + if (result.State.ToString() == "Succeeded" && result.Entries != null) + { + foreach (var entry in result.Entries) + { + if (cancellationToken.IsCancellationRequested) yield break; + + yield return new GetAnalysisResultResponse + { + LogEntry = GrpcTypeConverter.ConvertToGrpc(entry), + Status = CreateNoErrorOperationStatus() + }; + } + } + + await Task.CompletedTask; } - public Task AnalyzeAll(AnalyzeAllRequest request, CancellationToken cancellationToken) + // 查询 + 排序(T5.1.a.c):对已分析文件的日志按条件过滤、排序并流式返回。 + public async IAsyncEnumerable QueryAnalysisResultStreamAsync( + QueryAnalysisResultRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + if (string.IsNullOrWhiteSpace(request.FileName)) + { + yield return ErrorResponse(AgentErrorCode.InvalidArgument, "File name cannot be empty."); + yield break; + } + + if (!_analyzer.TryGetAnalysisResult(request.FileName, out var result) || result is null) + { + yield return ErrorResponse(AgentErrorCode.FileNotFound, + $"File '{request.FileName}' was not found or has no analysis result."); + yield break; + } + + // 1. 先推送 Header(与 GetAnalysisResult 保持一致的协议形状) + var headerMessage = new AnalysisResultHeaderMessage + { + FileName = request.FileName, + FullName = result.FullName ?? string.Empty, + State = GrpcTypeConverter.ConvertToGrpc(result.State), + ErrorMessage = result.ErrorMessage ?? string.Empty, + WorkerId = result.WorkerId + }; + + yield return new GetAnalysisResultResponse + { + Header = headerMessage, + Status = CreateNoErrorOperationStatus() + }; + + if (result.State.ToString() != "Succeeded" || result.Entries is null) + { + yield break; + } + + // 2. 过滤 + 排序后逐条推送 + var filtered = LogAnalysisQuery.FilterAndSort(result.Entries, request.Filter, request.Sort); + foreach (var entry in filtered) + { + if (cancellationToken.IsCancellationRequested) yield break; + + yield return new GetAnalysisResultResponse + { + LogEntry = GrpcTypeConverter.ConvertToGrpc(entry), + Status = CreateNoErrorOperationStatus() + }; + } + + await Task.CompletedTask; } - public Task AnalyzeFiles(AnalyzeFilesRequest request, CancellationToken cancellationToken) + // 云服务拓扑推断(T5.1.a.d):基于 Call 日志构建有向调用图。 + public Task GetTopology(GetTopologyRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var response = new GetTopologyResponse(); + try + { + if (string.IsNullOrWhiteSpace(request.FileName)) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidArgument, + Message = "File name cannot be empty." + }; + return Task.FromResult(response); + } + + if (!_analyzer.TryGetAnalysisResult(request.FileName, out var result) || result is null) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.FileNotFound, + Message = $"File '{request.FileName}' was not found or has no analysis result." + }; + return Task.FromResult(response); + } + + if (result.State.ToString() != "Succeeded" || result.Entries is null) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = $"File '{request.FileName}' has not been analyzed successfully." + }; + return Task.FromResult(response); + } + + var topology = TopologyBuilder.Build(result.Entries); + foreach (var node in topology.Nodes) + { + response.Nodes.Add(new TopologyNode { ServiceName = node }); + } + foreach (var edge in topology.Edges) + { + var edgeMessage = new TopologyEdge + { + SourceService = edge.SourceService, + TargetService = edge.TargetService, + CallCount = edge.CallCount + }; + edgeMessage.RequestIds.AddRange(edge.RequestIds); + response.Edges.Add(edgeMessage); + } + + response.Status = CreateNoErrorOperationStatus(); + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "Error occurred while building service topology."); + } + return Task.FromResult(response); } - public IReadOnlyList GetAnalysisResult(GetAnalysisResultRequest request, CancellationToken cancellationToken) + private static GetAnalysisResultResponse ErrorResponse(AgentErrorCode code, string message) { - throw new NotImplementedException("TODO: T3.1"); + return new GetAnalysisResultResponse + { + Status = new OperationStatusMessage + { + Success = false, + Code = code, + Message = message + } + }; } } -} +} \ No newline at end of file diff --git a/src/LogAnalyzerAgent/Infrastructure/AuthInterceptor.cs b/src/LogAnalyzerAgent/Infrastructure/AuthInterceptor.cs new file mode 100644 index 0000000..59e8086 --- /dev/null +++ b/src/LogAnalyzerAgent/Infrastructure/AuthInterceptor.cs @@ -0,0 +1,63 @@ +using Grpc.Core; +using Grpc.Core.Interceptors; +using LogAnalyzerRpc.Protos; + +namespace LogAnalyzerAgent.Infrastructure +{ + public class AuthInterceptor : Interceptor + { + private readonly TokenManager _tokenManager; + + // Proto 对应的完整路径:/log_analyzer.v1.LogAnalyzerAgentService/方法名 + private static readonly HashSet AdminOnlyMethods = new() + { + "/log_analyzer.v1.LogAnalyzerAgentService/CreateToken", + "/log_analyzer.v1.LogAnalyzerAgentService/RevokeToken", + "/log_analyzer.v1.LogAnalyzerAgentService/ListTokens" + }; + + public AuthInterceptor(TokenManager tokenManager) + { + _tokenManager = tokenManager; + } + + public override async Task UnaryServerHandler( + TRequest request, ServerCallContext context, UnaryServerMethod continuation) + { + ValidateAndContextualize(context); + return await continuation(request, context); + } + + public override async Task ServerStreamingServerHandler( + TRequest request, IServerStreamWriter responseStream, ServerCallContext context, ServerStreamingServerMethod continuation) + { + ValidateAndContextualize(context); + await continuation(request, responseStream, context); + } + + private void ValidateAndContextualize(ServerCallContext context) + { + var token = context.RequestHeaders.GetValue("x-agent-token"); + if (string.IsNullOrEmpty(token)) + { + var authHeader = context.RequestHeaders.GetValue("authorization"); + if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + token = authHeader.Substring("Bearer ".Length).Trim(); + } + } + + if (string.IsNullOrEmpty(token) || !_tokenManager.ValidateToken(token, out var role)) + { + throw new RpcException(new Status(StatusCode.Unauthenticated, "Invalid or missing Agent Authentication Token.")); + } + + if (AdminOnlyMethods.Contains(context.Method) && role != TokenRole.RoleAdmin) + { + throw new RpcException(new Status(StatusCode.PermissionDenied, "Permission denied. Administrator role required.")); + } + + context.UserState["Token"] = token; + } + } +} \ No newline at end of file diff --git a/src/LogAnalyzerAgent/Infrastructure/SessionManager.cs b/src/LogAnalyzerAgent/Infrastructure/SessionManager.cs new file mode 100644 index 0000000..f8772ac --- /dev/null +++ b/src/LogAnalyzerAgent/Infrastructure/SessionManager.cs @@ -0,0 +1,31 @@ +using System.Collections.Concurrent; +using LogAnalyzer; +using LogAnalyzerAgent.Applications; + +namespace LogAnalyzerAgent.Infrastructure +{ + public class SessionManager + { + private readonly ConcurrentDictionary _sessions = new(); + private readonly ILoggerFactory _loggerFactory; + + public SessionManager(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + } + + public AgentSession GetOrCreateSession(string token) + { + return _sessions.GetOrAdd(token, t => + { + var analyzer = new LogFileAnalyzer(null); + return new AgentSession(analyzer, _loggerFactory); + }); + } + + public void RemoveSession(string token) + { + _sessions.TryRemove(token, out _); + } + } +} \ No newline at end of file diff --git a/src/LogAnalyzerAgent/Infrastructure/TokenManager.cs b/src/LogAnalyzerAgent/Infrastructure/TokenManager.cs new file mode 100644 index 0000000..423be37 --- /dev/null +++ b/src/LogAnalyzerAgent/Infrastructure/TokenManager.cs @@ -0,0 +1,70 @@ +using System.Collections.Concurrent; +using Google.Protobuf.WellKnownTypes; +using LogAnalyzerRpc.Protos; + +namespace LogAnalyzerAgent.Infrastructure +{ + public class TokenInfoInternal + { + public string Token { get; set; } = string.Empty; + public TokenRole Role { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + } + + public class TokenManager + { + private readonly ConcurrentDictionary _tokens = new(); + private readonly ILogger _logger; + + public TokenManager(ILogger logger) + { + _logger = logger; + + // 1. 启动时生成初始 Admin Token 并通过 _logger 输出 + var initialAdminToken = Guid.NewGuid().ToString("N"); + _tokens[initialAdminToken] = new TokenInfoInternal + { + Token = initialAdminToken, + Role = TokenRole.RoleAdmin, + CreatedAt = DateTime.UtcNow + }; + + _logger.LogWarning("=================================================="); + _logger.LogWarning("[Admin Token Generated] Initial Admin Token: {Token}", initialAdminToken); + _logger.LogWarning("=================================================="); + } + + public bool ValidateToken(string token, out TokenRole role) + { + if (!string.IsNullOrEmpty(token) && _tokens.TryGetValue(token, out var info)) + { + role = info.Role; + return true; + } + role = TokenRole.RoleUnspecified; + return false; + } + + public TokenInfoInternal CreateToken(TokenRole role) + { + var token = Guid.NewGuid().ToString("N"); + var info = new TokenInfoInternal + { + Token = token, + Role = role, + CreatedAt = DateTime.UtcNow + }; + _tokens[token] = info; + return info; + } + + public bool RevokeToken(string token, string currentActiveToken) + { + // 禁止注销当前正在使用的 Token,防止管理员误把自己锁出 + if (token == currentActiveToken) return false; + return _tokens.TryRemove(token, out _); + } + + public IEnumerable ListTokens() => _tokens.Values; + } +} \ No newline at end of file diff --git a/src/LogAnalyzerAgent/Program.cs b/src/LogAnalyzerAgent/Program.cs index 4e247ad..f6d6fd3 100644 --- a/src/LogAnalyzerAgent/Program.cs +++ b/src/LogAnalyzerAgent/Program.cs @@ -1,29 +1,43 @@ -using LogAnalyzer; +using LogAnalyzer; using LogAnalyzerAgent.Applications; +using LogAnalyzerAgent.Infrastructure; using LogAnalyzerAgent.Services; using Microsoft.AspNetCore.Server.Kestrel.Core; var builder = WebApplication.CreateBuilder(args); +// 仅使用控制台日志,确保初始 Admin Token 一定打印在控制台,并避免 EventLog 写入权限问题。 +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); + builder.WebHost.ConfigureKestrel(options => { options.ConfigureEndpointDefaults(listenOptions => { - listenOptions.Protocols = HttpProtocols.Http2; // for Desktop - // listenOptions.Protocols = HttpProtocols.Http1AndHttp2; // for Browser + listenOptions.Protocols = HttpProtocols.Http2; // 兼顾 Desktop & Web }); }); -builder.Services.AddGrpc(); -builder.Services.AddCors(); +// 1. 注册多用户 Token 管理器与 Session 隔离管理器 +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// 2. 注册 gRPC 服务并启用 Auth 拦截器 +builder.Services.AddGrpc(options => +{ + options.Interceptors.Add(); +}); -// 依赖注入,且有状态服务需要单例 -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddCors(); builder.Services.AddSingleton(); var app = builder.Build(); +// 3. 显式触发 TokenManager 初始化,自动生成并打印初始 Admin Token +app.Services.GetRequiredService(); + +// 跨域白名单配置 var whiteList = new HashSet() { "http://localhost:5235", @@ -35,21 +49,17 @@ "http://127.0.0.1:57814", "https://127.0.0.1:57815", }; + app.UseCors(policy => { policy - .SetIsOriginAllowed(origin => - string.IsNullOrEmpty(origin) || whiteList.Contains(origin)) + .SetIsOriginAllowed(origin => string.IsNullOrEmpty(origin) || whiteList.Contains(origin)) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); -// for Browser app.UseGrpcWeb(); +app.MapGrpcService().EnableGrpcWeb(); -// for Browser -app.MapGrpcService() - .EnableGrpcWeb(); - -app.Run(); +app.Run(); \ No newline at end of file diff --git a/src/LogAnalyzerAgent/Services/AgentService.cs b/src/LogAnalyzerAgent/Services/AgentService.cs index 591dcad..1cc5590 100644 --- a/src/LogAnalyzerAgent/Services/AgentService.cs +++ b/src/LogAnalyzerAgent/Services/AgentService.cs @@ -1,55 +1,126 @@ -using Google.Protobuf.WellKnownTypes; +using Google.Protobuf.WellKnownTypes; using Grpc.Core; -using LogAnalyzer; using LogAnalyzerRpc.Protos; -using LogAnalyzerRpc; -using LogParser.Visitors; +using LogAnalyzerAgent.Infrastructure; using LogAnalyzerAgent.Applications; namespace LogAnalyzerAgent.Services { public class AgentService : LogAnalyzerAgentService.LogAnalyzerAgentServiceBase { - private readonly AgentSession _session; + private readonly SessionManager _sessionManager; + private readonly TokenManager _tokenManager; - public AgentService(AgentSession session) + public AgentService(SessionManager sessionManager, TokenManager tokenManager) { - _session = session; + _sessionManager = sessionManager; + _tokenManager = tokenManager; } - public override Task Ping(Empty empty, ServerCallContext context) + private AgentSession GetSession(ServerCallContext context) { - return _session.Ping(empty, context.CancellationToken); + var token = context.UserState["Token"] as string ?? string.Empty; + return _sessionManager.GetOrCreateSession(token); } + // ==================== 1. 日志分析路由 (按 Token 隔离) ==================== + + public override Task Ping(Empty empty, ServerCallContext context) + => GetSession(context).Ping(empty, context.CancellationToken); + public override Task GetAgentStatus(Empty empty, ServerCallContext context) - { - return _session.GetAgentStatus(empty, context.CancellationToken); - } + => GetSession(context).GetAgentStatus(empty, context.CancellationToken); public override Task ChangeDirectory(ChangeDirectoryRequest request, ServerCallContext context) + => GetSession(context).ChangeDirectory(request, context.CancellationToken); + + public override Task GetLogFiles(Empty empty, ServerCallContext context) + => GetSession(context).GetLogFiles(empty, context.CancellationToken); + + public override Task AnalyzeAll(AnalyzeAllRequest request, ServerCallContext context) + => GetSession(context).AnalyzeAllAsync(request, context.CancellationToken); + + public override Task AnalyzeFiles(AnalyzeFilesRequest request, ServerCallContext context) + => GetSession(context).AnalyzeFilesAsync(request, context.CancellationToken); + + public override async Task GetAnalysisResult(GetAnalysisResultRequest request, IServerStreamWriter responseStream, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + var session = GetSession(context); + await foreach (var response in session.GetAnalysisResultStreamAsync(request, context.CancellationToken)) + { + await responseStream.WriteAsync(response, context.CancellationToken); + } } - public override Task GetLogFiles(Empty empty, ServerCallContext context) + // ==================== 3. 查询 / 排序 / 拓扑 ==================== + + public override async Task QueryAnalysisResult(QueryAnalysisResultRequest request, IServerStreamWriter responseStream, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + var session = GetSession(context); + await foreach (var response in session.QueryAnalysisResultStreamAsync(request, context.CancellationToken)) + { + await responseStream.WriteAsync(response, context.CancellationToken); + } } - public override Task AnalyzeAll(AnalyzeAllRequest request, ServerCallContext context) + public override Task GetTopology(GetTopologyRequest request, ServerCallContext context) + => GetSession(context).GetTopology(request, context.CancellationToken); + + // ==================== 2. 管理员 Token 接口 ==================== + + public override Task CreateToken(CreateTokenRequest request, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + var created = _tokenManager.CreateToken(request.Role); + return Task.FromResult(new CreateTokenResponse + { + Status = new OperationStatusMessage { Success = true, Code = AgentErrorCode.NoAgentError }, + TokenInfo = new TokenInfo + { + Token = created.Token, + Role = created.Role, + CreatedAt = Timestamp.FromDateTime(created.CreatedAt.ToUniversalTime()) + } + }); } - public override Task AnalyzeFiles(AnalyzeFilesRequest request, ServerCallContext context) + public override Task RevokeToken(RevokeTokenRequest request, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + var currentActiveToken = context.UserState["Token"] as string ?? string.Empty; + bool success = _tokenManager.RevokeToken(request.Token, currentActiveToken); + if (success) + { + _sessionManager.RemoveSession(request.Token); + } + + return Task.FromResult(new RevokeTokenResponse + { + Status = new OperationStatusMessage + { + Success = success, + Code = success ? AgentErrorCode.NoAgentError : AgentErrorCode.InvalidArgument, + Message = success ? string.Empty : "Cannot revoke active current token or target token not found." + } + }); } - public override async Task GetAnalysisResult(GetAnalysisResultRequest request, IServerStreamWriter responseStream, ServerCallContext context) + public override Task ListTokens(Empty empty, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + var response = new ListTokensResponse + { + Status = new OperationStatusMessage { Success = true, Code = AgentErrorCode.NoAgentError } + }; + + foreach (var item in _tokenManager.ListTokens()) + { + response.Tokens.Add(new TokenInfo + { + Token = item.Token, + Role = item.Role, + CreatedAt = Timestamp.FromDateTime(item.CreatedAt.ToUniversalTime()) + }); + } + + return Task.FromResult(response); } } -} +} \ No newline at end of file diff --git a/src/LogAnalyzerClient/Directory.Packages.props b/src/LogAnalyzerClient/Directory.Packages.props index 8c9efe7..a17e6bb 100644 --- a/src/LogAnalyzerClient/Directory.Packages.props +++ b/src/LogAnalyzerClient/Directory.Packages.props @@ -6,14 +6,15 @@ - - - - - + + + + + + - - + + \ No newline at end of file diff --git a/src/LogAnalyzerClient/LogAnalyzerClient/App.axaml b/src/LogAnalyzerClient/LogAnalyzerClient/App.axaml index 1e08725..4ef8172 100644 --- a/src/LogAnalyzerClient/LogAnalyzerClient/App.axaml +++ b/src/LogAnalyzerClient/LogAnalyzerClient/App.axaml @@ -1,16 +1,11 @@ - - - - - + \ No newline at end of file diff --git a/src/LogAnalyzerClient/LogAnalyzerClient/App.axaml.cs b/src/LogAnalyzerClient/LogAnalyzerClient/App.axaml.cs index d0ee7a2..cb11b92 100644 --- a/src/LogAnalyzerClient/LogAnalyzerClient/App.axaml.cs +++ b/src/LogAnalyzerClient/LogAnalyzerClient/App.axaml.cs @@ -1,43 +1,36 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Data.Core; -using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using LogAnalyzerClient.ViewModels; using LogAnalyzerClient.Views; -using System.Linq; -namespace LogAnalyzerClient +namespace LogAnalyzerClient; + +public partial class App : Application { - public partial class App : Application + public override void Initialize() { - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } + AvaloniaXamlLoader.Load(this); + } - public override void OnFrameworkInitializationCompleted() + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + // 关键:必须给主窗口设置 DataContext,否则所有 Command 绑定都会失效(按钮变灰)。 + desktop.MainWindow = new MainWindow { - desktop.MainWindow = new MainWindow - { - DataContext = new MainViewModel() - }; - } - else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) - { - singleViewFactoryApplicationLifetime.MainViewFactory = () => new MainView { DataContext = new MainViewModel() }; - } - else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + DataContext = new MainViewModel() + }; + } + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + singleViewPlatform.MainView = new MainView { - singleViewPlatform.MainView = new MainView - { - DataContext = new MainViewModel() - }; - } - - base.OnFrameworkInitializationCompleted(); + DataContext = new MainViewModel() + }; } + + base.OnFrameworkInitializationCompleted(); } -} \ No newline at end of file +} diff --git a/src/LogAnalyzerClient/LogAnalyzerClient/Dialogs/ConnectDialog.axaml b/src/LogAnalyzerClient/LogAnalyzerClient/Dialogs/ConnectDialog.axaml index 4f44da3..fc14e4c 100644 --- a/src/LogAnalyzerClient/LogAnalyzerClient/Dialogs/ConnectDialog.axaml +++ b/src/LogAnalyzerClient/LogAnalyzerClient/Dialogs/ConnectDialog.axaml @@ -4,17 +4,23 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="LogAnalyzerClient.ConnectDialog" - d:DesignWidth="440" d:DesignHeight="180" - Width="440" Height="180" + d:DesignWidth="480" d:DesignHeight="260" + Width="480" Height="280" CanResize="False" WindowStartupLocation="CenterOwner" Title="Connect..."> - - + + + + + + + Spacing="5" + Margin="0,6,0,0">