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));
}
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 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()
{
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
});
}
}