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; } } /// /// 真实 gRPC 通信服务,负责连接后端、注册打印机、订阅任务、下载文件和状态上报。 /// 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? PrintJobReceived; public event EventHandler? 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)); } _apiKey = apiKey; if (address.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && AllowInsecureGrpc) { AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); } _channel?.Dispose(); _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}"); } public async Task 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 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() { _subscriptionCts?.Cancel(); IsConnected = false; _channel?.Dispose(); _channel = null; _printClient = null; _libraryClient = null; _fileClient = null; _adminClient = null; 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}"); IsConnected = false; } } 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 }); } }