362 lines
13 KiB
C#
362 lines
13 KiB
C#
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));
|
||
}
|
||
|
||
_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<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()
|
||
{
|
||
_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
|
||
});
|
||
}
|
||
} |