feat: 基础

This commit is contained in:
2026-04-12 15:41:48 +08:00
parent a886baebdd
commit 9bf0b5fe52
5 changed files with 219 additions and 5 deletions

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using cloudPrintWinAgent; using cloudPrintWinAgent;
using Microsoft.Extensions.Configuration;
var builder = Host.CreateApplicationBuilder(args); var builder = Host.CreateApplicationBuilder(args);
@@ -14,6 +15,9 @@ builder.Services.AddWindowsService(options =>
builder.Services.AddHostedService<Worker>(); builder.Services.AddHostedService<Worker>();
builder.Services.Configure<CloudPrintOptions>(
builder.Configuration.GetSection("CloudPrint"));
builder.Logging.ClearProviders(); builder.Logging.ClearProviders();
if (Environment.UserInteractive) if (Environment.UserInteractive)

193
Worker.cs
View File

@@ -4,29 +4,212 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Cloudprint;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.Extensions.Options;
namespace cloudPrintWinAgent namespace cloudPrintWinAgent
{ {
public sealed class Worker : BackgroundService public sealed class Worker : BackgroundService
{ {
private readonly ILogger<Worker> _logger; private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
private readonly CloudPrintOptions _opt;
public Worker(ILogger<Worker> logger, IOptions<CloudPrintOptions> cloudPrintOptions)
{ {
_logger = logger; _logger = logger;
_opt = cloudPrintOptions.Value;
ValidateOptions();
} }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
_logger.LogInformation("WinAgent started at: {Time}", DateTimeOffset.Now); _logger.LogInformation("WinAgent started at: {Time}", DateTimeOffset.Now);
while (!stoppingToken.IsCancellationRequested)
// Go 后端是明文 h2chttp://),需要打开这个开关
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
using var channel = GrpcChannel.ForAddress(_opt.ServerAddress);
var client = new PrintService.PrintServiceClient(channel);
try
{ {
// TODO: 后续放心跳、订阅任务、状态回传 var (printerId, token) = await RegisterPrinterAsync(client, stoppingToken);
_logger.LogInformation("WinAgent heartbeat tick at: {Time}", DateTimeOffset.Now);
await Task.Delay(TimeSpan.FromSeconds(10), 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); _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 不能为空");
}
} }
} }

8
appsettings.json Normal file
View File

@@ -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"
}
}

View File

@@ -18,4 +18,13 @@
<Protobuf Include=".\proto\cloudprint.proto" GrpcServices="Client" /> <Protobuf Include=".\proto\cloudprint.proto" GrpcServices="Client" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project> </Project>

View File

@@ -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; } = "";
}
}