Files
desktop-plugin/lib/print/pavo/pavo_backend.dart
T
dzc e00086c01c fix: Pavo 后端 isolate 闭包捕获实例链导致 Windows 状态查询必炸(unsendable)
用户 Windows 实测:未装驱动点跳过后页面报"状态读取失败 Illegal argument in
isolate message: object is unsendable - _Timer"。堆栈定位:

Isolate.run(_statusSync) 的方法 tear-off 会连带 this(PavoPrintBackend)→
_log(agent.log.write tear-off)→ AgentLog → StreamController → UI 日志订阅链
→ 向导轮询 Timer——Timer 不可跨 isolate 发送,消息序列化直接抛错。mac 上
CUPS 后端不走 Isolate.run,从未暴露。

修复:isolate 入口全部改为顶层函数(pavoStatusSync / pavoPrintSync),不捕获
任何实例状态;printOneCard 先解出纯字符串(data URL/份数)再进闭包,最小化
捕获面。新增守护测试 pavo_isolate_test:在活跃日志订阅下调 getStatus,
回归捕获 this 时会立即复现 unsendable。14 测试全过、analyze 零问题。
2026-09-21 16:15:27 +08:00

138 lines
4.7 KiB
Dart
Raw 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.
import 'dart:isolate';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import '../../image/card_image.dart';
import '../../server/protocol.dart';
import '../backend.dart';
import '../cups_backend.dart' show noopLog;
import 'pavo_ffi.dart';
import 'pavo_printer.dart';
/// Windows 打印后端:HiTi PavoApi 直打(BGR/K 分层 BITMAP)。
///
/// 所有 ffi 调用(含像素转换的 CPU 密集计算)都放进 `Isolate.run`:
/// PavoApi 的同步调用最长可阻塞数秒(发送 2MB 像素)、打印轮询最长 3 分钟,
/// 放独立 isolate 保证主 isolate 的 HTTP 服务(任务状态轮询)不被卡住。
///
/// **重要**:传给 Isolate.run 的闭包/函数不得捕获任何实例状态(this 链会经
/// onLog→AgentLog→StreamController→UI 订阅链一路拽到 Timer 等 unsendable 对象,
/// isolate 消息序列化直接抛 "Illegal argument in isolate message")——
/// 因此 isolate 入口全部是顶层函数,只以纯数据(字符串)进出。
class PavoPrintBackend implements PrintBackend {
PavoPrintBackend({void Function(String line)? onLog}) : _log = onLog ?? noopLog;
final void Function(String line) _log;
@override
String get name => 'pavo';
@override
Future<PrinterStatus> getStatus() => Isolate.run(pavoStatusSync);
@override
Future<void> printOneCard(PrintRequest request) {
_log('Pavo 直打任务开始(份数 ${request.copies},后端在子 isolate 执行)');
final front = request.front!;
final back = request.back;
// 先解出纯数据再进闭包:闭包只捕获字符串/整数(最小捕获面,杜绝实例链)
final frontColor = front.color;
final frontK = front.k;
final backColor = back?.color;
final backK = back?.k;
final copies = request.copies;
return Isolate.run(() => pavoPrintSync(
frontColor: frontColor,
frontK: frontK,
backColor: backColor,
backK: backK,
copies: copies,
));
}
}
/// isolate 入口:查询打印机状态(顶层函数,不捕获任何实例状态)。
PrinterStatus pavoStatusSync() {
try {
final api = PavoApi();
final printers = PavoPrinter.enumUsbPrinters(api);
if (printers.isEmpty) {
return PrinterStatus.offline('打印机未连接');
}
final printer = printers.first.toNativeUtf16();
try {
final status = PavoPrinter.getStatus(api, printer);
final (ribbonType, remainCount) = PavoPrinter.getRibbonInfo(api, printer);
final dto = PrinterStatus(
online: true,
model: PavoPrinter.getDeviceInfoText(api, printer, PavoApi.devinfoModelName),
firmware: PavoPrinter.getDeviceInfoText(api, printer, PavoApi.devinfoFirmwareVersion),
connection: 'USB',
ribbon: RibbonInfo(
type: PavoPrinter.ribbonTypeName(ribbonType),
// 设备返回剩余张数,按常见 100 张装 YMCKO 色带折算百分比
remain: ribbonType == PavoApi.ribbonMissing ? -1 : (remainCount > 100 ? 100 : remainCount),
remainCount: remainCount,
),
status: status,
statusText: PavoPrinter.statusTextOf(status),
);
if (status == PavoApi.dsCardLow || status == PavoApi.dsCardOut) {
dto.cardLow = true;
}
return dto;
} finally {
calloc.free(printer);
}
} catch (e) {
return PrinterStatus.offline('状态读取失败:$e');
}
}
/// isolate 入口:转换图层并直打(顶层函数,参数全部为可发送的纯数据)。
Future<void> pavoPrintSync({
required String frontColor,
String? frontK,
String? backColor,
String? backK,
required int copies,
}) async {
final api = PavoApi();
final printers = PavoPrinter.enumUsbPrinters(api);
if (printers.isEmpty) {
throw StateError('未找到 USB 连接的 HiTi 证卡打印机');
}
final printer = printers.first.toNativeUtf16();
try {
final frontImg = CardImage.fromDataUrl(frontColor);
final frontBgr = frontImg.toBgr24();
final Uint8List? frontKGray =
frontK != null && frontK.isNotEmpty ? CardImage.fromDataUrl(frontK).toGray8() : null;
final Uint8List? backBgr;
final Uint8List? backKGray;
if (backColor != null && backColor.isNotEmpty) {
final backImg = CardImage.fromDataUrl(backColor);
backBgr = backImg.toBgr24();
backKGray =
backK != null && backK.isNotEmpty ? CardImage.fromDataUrl(backK).toGray8() : null;
} else {
backBgr = null;
backKGray = null;
}
await PavoPrinter.printOneCard(
api,
printer,
frontBgr: frontBgr,
frontK: frontKGray,
backBgr: backBgr,
backK: backKGray,
isLandscape: frontImg.isLandscape,
copies: copies,
);
} finally {
calloc.free(printer);
}
}