From 9bf0b5fe52f9c219322ccc3404b483fa3cd8a567 Mon Sep 17 00:00:00 2001 From: Shiqvlizi <3164363204@qq.com> Date: Sun, 12 Apr 2026 15:41:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9F=BA=E7=A1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 4 + Worker.cs | 193 +++++++++++++++++++++++++++++++++++- appsettings.json | 8 ++ cloudPrintWinAgent.csproj | 9 ++ config/CloudPrintOptions.cs | 10 ++ 5 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 appsettings.json create mode 100644 config/CloudPrintOptions.cs diff --git a/Program.cs b/Program.cs index 51b14c9..2d40f28 100644 --- a/Program.cs +++ b/Program.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using cloudPrintWinAgent; +using Microsoft.Extensions.Configuration; var builder = Host.CreateApplicationBuilder(args); @@ -14,6 +15,9 @@ builder.Services.AddWindowsService(options => builder.Services.AddHostedService(); +builder.Services.Configure( + builder.Configuration.GetSection("CloudPrint")); + builder.Logging.ClearProviders(); if (Environment.UserInteractive) diff --git a/Worker.cs b/Worker.cs index 32350c7..7b26d1d 100644 --- a/Worker.cs +++ b/Worker.cs @@ -4,29 +4,212 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Cloudprint; +using Grpc.Core; +using Grpc.Net.Client; + +using Microsoft.Extensions.Options; + namespace cloudPrintWinAgent { public sealed class Worker : BackgroundService { private readonly ILogger _logger; - public Worker(ILogger logger) + + private readonly CloudPrintOptions _opt; + + public Worker(ILogger logger, IOptions cloudPrintOptions) { _logger = logger; + _opt = cloudPrintOptions.Value; + ValidateOptions(); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("WinAgent started at: {Time}", DateTimeOffset.Now); - while (!stoppingToken.IsCancellationRequested) + + // Go 后端是明文 h2c(http://),需要打开这个开关 + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + + + using var channel = GrpcChannel.ForAddress(_opt.ServerAddress); + var client = new PrintService.PrintServiceClient(channel); + + + try { - // TODO: 后续放心跳、订阅任务、状态回传 - _logger.LogInformation("WinAgent heartbeat tick at: {Time}", DateTimeOffset.Now); - await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); + var (printerId, token) = await RegisterPrinterAsync(client, stoppingToken); + + var heartbeatTask = HeartbeatLoopAsync(client, printerId, token, stoppingToken); + var subscribeTask = SubscribeJobsLoopAsync(client, printerId, token, stoppingToken); + + await Task.WhenAll(heartbeatTask, subscribeTask); } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Worker fatal error"); + } + _logger.LogInformation("WinAgent stopping at: {Time}", DateTimeOffset.Now); } + + private async Task<(int printerId, string token)> RegisterPrinterAsync( + PrintService.PrintServiceClient client, + CancellationToken ct) + { + var resp = await client.RegisterPrinterAsync(new RegisterPrinterRequest + { + Name = _opt.PrinterName, + DisplayName = _opt.PrinterDisplayName, + ApiKey = _opt.ApiKey + }, cancellationToken: ct); + + _logger.LogInformation("Registered printer. PrinterId={PrinterId}", resp.PrinterId); + return (resp.PrinterId, resp.Token); + } + + // 0: 空闲 1: 打印中 2: 离线 + private async Task HeartbeatLoopAsync( + PrintService.PrintServiceClient client, + int printerId, + string token, + CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + try + { + await client.PrinterHeartbeatAsync(new PrinterHeartbeatRequest + { + PrinterId = printerId, + Status = 0, + Token = token + }, cancellationToken: ct); + + _logger.LogInformation("Heartbeat ok: {Time}", DateTimeOffset.Now); + } + catch (RpcException ex) + { + _logger.LogWarning(ex, "Heartbeat rpc failed"); + } + + await Task.Delay(TimeSpan.FromSeconds(10), ct); + } + } + + private async Task SubscribeJobsLoopAsync( + PrintService.PrintServiceClient client, + int printerId, + string token, + CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + try + { + using var call = client.SubscribeJobs(new SubscribeJobsRequest + { + PrinterId = printerId, + Token = token + }, cancellationToken: ct); + + await foreach (var job in call.ResponseStream.ReadAllAsync(ct)) + { + await HandleJobAsync(client, job, ct); + } + } + catch (OperationCanceledException) + { + throw; + } + catch (RpcException ex) + { + _logger.LogWarning(ex, "Subscribe stream disconnected, retry in 3s"); + await Task.Delay(TimeSpan.FromSeconds(3), ct); + } + catch (Exception ex) + { + _logger.LogError(ex, "Subscribe loop error, retry in 3s"); + await Task.Delay(TimeSpan.FromSeconds(3), ct); + } + } + } + + private async Task HandleJobAsync( + PrintService.PrintServiceClient client, + PrintJob job, + CancellationToken ct) + { + _logger.LogInformation("Received job: JobNo={JobNo}, FilePath={FilePath}", + job.JobNo, job.OrderItem?.File?.FilePath); + + try + { + await ReportStatusAsync(client, job.JobNo, "printing", 10, "", ct); + + // 模拟打印,后续替换成真实 WinAPI 打印 + await Task.Delay(TimeSpan.FromSeconds(2), ct); + + await ReportStatusAsync(client, job.JobNo, "completed", 100, "", ct); + _logger.LogInformation("Job completed: {JobNo}", job.JobNo); + } + catch (OperationCanceledException) + { + // 服务停止/任务取消 + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Job failed: {JobNo}", job.JobNo); + await ReportStatusAsync(client, job.JobNo, "failed", 0, ex.Message, ct); + } + } + + private async Task ReportStatusAsync( + PrintService.PrintServiceClient client, + string jobNo, + string status, + int progress, + string errorMsg, + CancellationToken ct) + { + await client.ReportJobStatusAsync(new ReportJobStatusRequest + { + JobNo = jobNo, + Status = status, + Progress = progress, + ErrorMsg = errorMsg ?? string.Empty + }, cancellationToken: ct); + } + + + + + + + + private void ValidateOptions() + { + if (string.IsNullOrWhiteSpace(_opt.ServerAddress)) + throw new InvalidOperationException("CloudPrint:ServerAddress 不能为空"); + + if (string.IsNullOrWhiteSpace(_opt.PrinterName)) + throw new InvalidOperationException("CloudPrint:PrinterName 不能为空"); + + if (string.IsNullOrWhiteSpace(_opt.PrinterDisplayName)) + throw new InvalidOperationException("CloudPrint:PrinterDisplayName 不能为空"); + + if (string.IsNullOrWhiteSpace(_opt.ApiKey)) + throw new InvalidOperationException("CloudPrint:ApiKey 不能为空"); + } } } diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..be7e3d1 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,8 @@ +{ + "CloudPrint": { + "ServerAddress": "http://127.0.0.1:8080", + "PrinterName": "win-printer-001", + "PrinterDisplayName": "Windows Printer 001", + "ApiKey": "replace-with-your-api-key" + } +} \ No newline at end of file diff --git a/cloudPrintWinAgent.csproj b/cloudPrintWinAgent.csproj index bb9ae5e..606a353 100644 --- a/cloudPrintWinAgent.csproj +++ b/cloudPrintWinAgent.csproj @@ -18,4 +18,13 @@ + + + PreserveNewest + PreserveNewest + + + + + \ No newline at end of file diff --git a/config/CloudPrintOptions.cs b/config/CloudPrintOptions.cs new file mode 100644 index 0000000..7d2cb5c --- /dev/null +++ b/config/CloudPrintOptions.cs @@ -0,0 +1,10 @@ +namespace cloudPrintWinAgent +{ + public sealed class CloudPrintOptions + { + public string ServerAddress { get; set; } = "http://127.0.0.1:8080"; + public string PrinterName { get; set; } = "win-printer-001"; + public string PrinterDisplayName { get; set; } = "Windows Printer 001"; + public string ApiKey { get; set; } = ""; + } +} \ No newline at end of file