- 协议与 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 已注册
55 lines
1.6 KiB
Dart
55 lines
1.6 KiB
Dart
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;
|
||
}
|