feat: 饭卡打印助手 Flutter 版(Phase 1/2:协议服务器 + macOS CUPS 真机打印)

- 协议与 C# 版 card-printer-agent 逐字段对齐(38200-38202 顺延、CORS *、串行任务队列、30 分钟保留)
- macOS CUPS 后端:lpoptions 队列发现 + lp 参数列表提交(LC_ALL=C 防本地化粘连坑)
- --mock 模拟模式 + bin/agent.dart headless 入口 + 测试卡生成工具
- 真机验收:HiTi CS-200e status 就绪 + 真实出卡;macOS 通用二进制构建通过
- App Sandbox 已关闭(服务需绑回环端口并调用 lp 子进程);zaxprint URL scheme 已注册
This commit is contained in:
dzc committed 2026-09-21 12:03:27 +08:00
commit 689a07ca44
46 files changed
+2986

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
/// 外部命令执行失败(非零退出/超时/无法启动)。
class CommandException implements Exception {
CommandException(this.message);
final String message;
@override
String toString() => message;
}
/// 运行外部命令(lp/lpoptions/lpstat)并返回标准输出。
///
/// 一律用参数列表传参不经过 shell,杜绝命令注入;
/// 强制 LC_ALL=C 英文输出(macOS 会把 lpstat/lp 本地化,中文系统队列名与状态词粘连无法按空格解析)。
Future<String> runCommand(
String executable,
List<String> arguments, {
int timeoutMs = 15000,
bool tolerateExit = false,
}) async {
final Process process;
try {
process = await Process.start(
executable,
arguments,
environment: const {'LC_ALL': 'C', 'LANG': 'C'},
);
} on ProcessException catch (e) {
throw CommandException('无法启动 $executable:${e.message}');
}
// stdout/stderr 立即挂上消费 Future,避免管道缓冲区写满导致死锁
final stdoutFuture = process.stdout.transform(utf8.decoder).join();
final stderrFuture = process.stderr.transform(utf8.decoder).join();
final int exitCode;
try {
exitCode = await process.exitCode.timeout(Duration(milliseconds: timeoutMs));
} on TimeoutException {
process.kill();
throw CommandException('$executable 执行超时');
}
final out = await stdoutFuture;
if (exitCode != 0 && !tolerateExit) {
final err = await stderrFuture;
throw CommandException('$executable 返回 $exitCode:${err.trim()}');
}
return out;
}