Files
client/Services/GrpcService.cs
2026-05-28 23:26:37 +08:00

386 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CloudPrint.Client.Models;
using CloudPrint.Client.Services.Grpc;
using CloudPrint.Client.Services.Abstractions;
using Google.Protobuf;
using Grpc.Core;
using Grpc.Net.Client;
using System.IO;
namespace CloudPrint.Client.Services;
public sealed class PrintJobReceivedEventArgs : EventArgs
{
public PrintJobReceivedEventArgs(PrintJob job)
{
Job = job;
}
public PrintJob Job { get; }
}
/// <summary>
/// 真实 gRPC 通信服务,负责连接后端、注册打印机、订阅任务、下载文件和状态上报。
/// </summary>
public sealed class GrpcService : IPrintBackendClient
{
private CancellationTokenSource? _subscriptionCts;
private GrpcChannel? _channel;
private Cloudprint.PrintService.PrintServiceClient? _printClient;
private Cloudprint.LibraryService.LibraryServiceClient? _libraryClient;
private Cloudprint.FileService.FileServiceClient? _fileClient;
private Cloudprint.AdminService.AdminServiceClient? _adminClient;
private string _apiKey = string.Empty;
public event EventHandler<PrintJobReceivedEventArgs>? PrintJobReceived;
public event EventHandler<AppLogEntry>? LogReceived;
public bool IsConnected { get; private set; }
public int PrinterId { get; private set; }
public string Token { get; private set; } = string.Empty;
public AdminSession AdminSession { get; private set; } = new();
public bool AllowInsecureGrpc { get; set; } = true;
public async Task ConnectAsync(string address, string apiKey, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(address))
{
throw new ArgumentException("服务器地址不能为空", nameof(address));
}
if (string.IsNullOrWhiteSpace(apiKey))
{
throw new ArgumentException("API Key 不能为空", nameof(apiKey));
}
if (address.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && AllowInsecureGrpc)
{
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
}
ResetConnectionState(clearCredentials: true);
try
{
_apiKey = apiKey;
_channel = GrpcChannel.ForAddress(address);
_printClient = new Cloudprint.PrintService.PrintServiceClient(_channel);
_libraryClient = new Cloudprint.LibraryService.LibraryServiceClient(_channel);
_fileClient = new Cloudprint.FileService.FileServiceClient(_channel);
_adminClient = new Cloudprint.AdminService.AdminServiceClient(_channel);
IsConnected = true;
Log(AppLogLevel.Success, $"已连接 gRPC 服务:{address}");
}
catch
{
ResetConnectionState(clearCredentials: true);
throw;
}
}
public async Task<AdminSession> LoginAdminAsync(string username, string password, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(username))
{
throw new ArgumentException("管理员用户名不能为空", nameof(username));
}
if (string.IsNullOrWhiteSpace(password))
{
throw new ArgumentException("管理员密码不能为空", nameof(password));
}
var response = await EnsureAdminClient().LoginAsync(new Cloudprint.AdminLoginRequest
{
Username = username,
Password = password
}, headers: CreateMetadata(), cancellationToken: cancellationToken).ResponseAsync.ConfigureAwait(true);
AdminSession = new AdminSession
{
AdminId = response.Admin?.Id ?? 0,
Username = response.Admin?.Username ?? username,
Nickname = response.Admin?.Nickname ?? string.Empty,
Role = response.Admin?.Role ?? 0,
AccessToken = response.AccessToken
};
Log(AppLogLevel.Success, $"管理员登录成功:{AdminSession.DisplayName}");
return AdminSession;
}
public void SetAdminToken(string username, string accessToken)
{
AdminSession = new AdminSession
{
Username = username,
Nickname = username,
AccessToken = accessToken
};
}
public void LogoutAdmin()
{
AdminSession = new AdminSession();
Log(AppLogLevel.Warning, "管理员已退出登录");
}
public async Task RegisterPrinterAsync(PrinterInfo printer, string apiKey, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(printer);
var client = EnsurePrintClient();
var response = await client.RegisterPrinterAsync(new Cloudprint.RegisterPrinterRequest
{
Name = printer.Name,
DisplayName = printer.DisplayName,
ApiKey = apiKey
}, headers: CreateMetadata(), cancellationToken: cancellationToken).ResponseAsync.ConfigureAwait(true);
PrinterId = response.PrinterId;
Token = response.Token;
Log(AppLogLevel.Success, $"已注册打印机:{printer.DisplayName}PrinterId={PrinterId}");
}
public async Task SubscribePrintJobsAsync(PrinterInfo printer, CancellationToken cancellationToken = default)
{
if (!IsConnected)
{
throw new InvalidOperationException("请先连接 gRPC 服务");
}
_subscriptionCts?.Cancel();
_subscriptionCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var token = _subscriptionCts.Token;
Log(AppLogLevel.Info, $"开始订阅打印任务流:{printer.DisplayName}");
_ = Task.Run(async () => await ReadPrintJobStreamAsync(token).ConfigureAwait(false), CancellationToken.None);
}
public async Task<string> DownloadPrintFileAsync(PrintJob job, string cacheRoot, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(job);
if (job.FileId <= 0)
{
throw new InvalidOperationException("打印任务缺少 file_id无法下载文件");
}
var root = string.IsNullOrWhiteSpace(cacheRoot)
? Path.Combine(Path.GetTempPath(), "CloudPrint.Client", "print-cache")
: cacheRoot;
Directory.CreateDirectory(root);
var fileName = string.IsNullOrWhiteSpace(job.FileName) ? $"{job.JobNo}.bin" : job.FileName;
var targetPath = PathHelper.CreateUniqueFilePath(root, fileName);
using var call = EnsureFileClient().GetFile(new Cloudprint.GetFileRequest { FileId = job.FileId }, headers: CreateMetadata(), cancellationToken: cancellationToken);
await using var output = File.Create(targetPath);
await foreach (var chunk in call.ResponseStream.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
if (chunk.Data.Length > 0)
{
chunk.Data.WriteTo(output);
}
}
Log(AppLogLevel.Success, $"打印文件已下载:{targetPath}");
return targetPath;
}
public async Task UploadLibraryDocumentAsync(CloudPrintDocument document, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(document);
if (!IsConnected)
{
throw new InvalidOperationException("请先连接 gRPC 服务");
}
await UploadLibraryDocumentByGrpcAsync(document, cancellationToken).ConfigureAwait(true);
}
public async Task CancelPrintJobAsync(PrintJob job, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(job);
if (!IsConnected)
{
job.MarkCancelled();
return;
}
await EnsurePrintClient().CancelPrintJobAsync(new Cloudprint.CancelPrintJobRequest
{
JobNo = job.JobNo
}, headers: CreateMetadata(), cancellationToken: cancellationToken).ResponseAsync.ConfigureAwait(true);
job.MarkCancelled();
Log(AppLogLevel.Warning, $"已取消打印任务:{job.JobNo}");
}
public async Task SendStatusUpdateAsync(PrintJob job, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(job);
if (IsConnected)
{
await EnsurePrintClient().ReportJobStatusAsync(new Cloudprint.ReportJobStatusRequest
{
JobNo = job.JobNo,
Status = CloudPrintGrpcMapper.ToBackendStatus(job.Status),
Progress = job.Progress,
ErrorMsg = job.ErrorMessage
}, headers: CreateMetadata(), cancellationToken: cancellationToken).ResponseAsync.ConfigureAwait(true);
}
Log(AppLogLevel.Info, $"上报任务状态:{job.JobNo} / {job.StatusText} / {job.Progress}%");
}
public async Task SendHeartbeatAsync(PrinterInfo printer, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(printer);
if (IsConnected)
{
await EnsurePrintClient().PrinterHeartbeatAsync(new Cloudprint.PrinterHeartbeatRequest
{
PrinterId = PrinterId == 0 ? printer.Id : PrinterId,
Status = (int)printer.Status,
Token = Token
}, headers: CreateMetadata(), cancellationToken: cancellationToken).ResponseAsync.ConfigureAwait(true);
}
Log(AppLogLevel.Info, $"心跳:{printer.DisplayName} / {printer.StatusText}");
}
public void Disconnect()
{
ResetConnectionState(clearCredentials: true);
Log(AppLogLevel.Warning, "已断开 gRPC 连接");
}
public ValueTask DisposeAsync()
{
_subscriptionCts?.Cancel();
_subscriptionCts?.Dispose();
_channel?.Dispose();
return ValueTask.CompletedTask;
}
private async Task ReadPrintJobStreamAsync(CancellationToken cancellationToken)
{
try
{
using var call = EnsurePrintClient().SubscribeJobs(new Cloudprint.SubscribeJobsRequest
{
PrinterId = PrinterId,
Token = Token
}, headers: CreateMetadata(), cancellationToken: cancellationToken);
await foreach (var backendJob in call.ResponseStream.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
var job = CloudPrintGrpcMapper.ToPrintJob(backendJob);
PrintJobReceived?.Invoke(this, new PrintJobReceivedEventArgs(job));
Log(AppLogLevel.Info, $"收到打印任务:{job.JobNo}");
}
}
catch (OperationCanceledException)
{
Log(AppLogLevel.Warning, "打印任务订阅已取消");
}
catch (Exception ex)
{
Log(AppLogLevel.Error, $"打印任务订阅中断:{ex.Message}");
ResetConnectionState(clearCredentials: false);
}
}
private void ResetConnectionState(bool clearCredentials)
{
_subscriptionCts?.Cancel();
_subscriptionCts?.Dispose();
_subscriptionCts = null;
_channel?.Dispose();
_channel = null;
_printClient = null;
_libraryClient = null;
_fileClient = null;
_adminClient = null;
IsConnected = false;
PrinterId = 0;
Token = string.Empty;
if (clearCredentials)
{
_apiKey = string.Empty;
}
}
private async Task UploadLibraryDocumentByGrpcAsync(CloudPrintDocument document, CancellationToken cancellationToken)
{
if (!File.Exists(document.FilePath))
{
throw new FileNotFoundException("文库文件不存在", document.FilePath);
}
document.SyncStatus = "上传中";
using var call = EnsureLibraryClient().UploadFile(headers: CreateMetadata(), cancellationToken: cancellationToken);
const int bufferSize = 64 * 1024;
var buffer = new byte[bufferSize];
await using var stream = File.OpenRead(document.FilePath);
while (true)
{
var read = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(true);
if (read == 0)
{
break;
}
await call.RequestStream.WriteAsync(new Cloudprint.UploadLibraryFileRequest
{
CategoryId = document.CategoryId,
Title = document.Title,
Description = document.Description,
FileName = document.FileName,
FileType = document.FileType,
Data = ByteString.CopyFrom(buffer, 0, read)
}, cancellationToken).ConfigureAwait(true);
}
await call.RequestStream.CompleteAsync().ConfigureAwait(true);
_ = await call.ResponseAsync.ConfigureAwait(true);
document.IsUploaded = true;
document.SyncStatus = "已同步";
Log(AppLogLevel.Success, $"文库文件已上传:{document.Title}");
}
private Cloudprint.PrintService.PrintServiceClient EnsurePrintClient()
{
return _printClient ?? throw new InvalidOperationException("gRPC 打印服务未连接");
}
private Cloudprint.LibraryService.LibraryServiceClient EnsureLibraryClient()
{
return _libraryClient ?? throw new InvalidOperationException("gRPC 文库服务未连接");
}
private Cloudprint.FileService.FileServiceClient EnsureFileClient()
{
return _fileClient ?? throw new InvalidOperationException("gRPC 文件服务未连接");
}
private Cloudprint.AdminService.AdminServiceClient EnsureAdminClient()
{
return _adminClient ?? throw new InvalidOperationException("gRPC 管理员服务未连接");
}
private global::Grpc.Core.Metadata CreateMetadata()
{
return GrpcMetadataFactory.Create(_apiKey, Token, AdminSession);
}
private void Log(AppLogLevel level, string message)
{
LogReceived?.Invoke(this, new AppLogEntry
{
Level = level,
Message = message
});
}
}