feat: 基础
This commit is contained in:
@@ -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
193
Worker.cs
@@ -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 后端是明文 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: 后续放心跳、订阅任务、状态回传
|
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
8
appsettings.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
10
config/CloudPrintOptions.cs
Normal file
10
config/CloudPrintOptions.cs
Normal 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; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user