feat: 向导内置 HiTi Mac 驱动一键安装(补齐计划 §3.2"驱动 dmg 放包内"设计)
- setup/driver_install.dart:检测已装(/Library/Printers/HiTi)+ 内置 dmg 定位 (从 resolvedExecutable 反推 flutter_assets/assets/drivers/,缺失降级官网链接) + 静默安装(hdiutil 挂载→定位 pkg→osascript 提权 installer→finally detach; 挂载点名带随机序号必须从 attach 输出解析) - 向导第 2 步重做状态机:驱动未装 → "一键安装驱动(内置安装包)"按钮 (安装中提示输管理员密码,失败显示原因);驱动已装但无队列 → 连接/添加队列 引导;无内置 dmg 的构建回退官网链接 - assets/drivers/ 收驱动 dmg(.gitignore 不入库,打包脚本检查缺失并警告, 当前从 card-printer-agent/dist 拷入 2.4.2 版,25MB) - 实测:挂载/pkg 定位/detach 免密链路全通过;打包 47MB dmg 内含驱动, 冒烟启动正常(服务已占主端口 38200);一键安装的提权弹窗待用户实测
This commit is contained in:
1 parent
3ecac9a302
commit
d475cac6ab
5 files changed
+173
-4
No files matched your search
@@ -44,3 +44,4 @@ app.*.map.json
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
/dist/
|
||||
assets/drivers/
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'dart:io';
|
||||
|
||||
import '../utils/command_runner.dart';
|
||||
|
||||
/// HiTi Mac 驱动(CUPS 过滤器)内置安装:App 资源携带驱动 dmg,
|
||||
/// 向导一键挂载静默安装(osascript 提权一次),已装判断 = /Library/Printers/HiTi 存在。
|
||||
/// Windows 不适用(PavoApi 直打,官方驱动可选)。
|
||||
class DriverInstall {
|
||||
static const installedMarker = '/Library/Printers/HiTi';
|
||||
|
||||
static bool get isSupported => Platform.isMacOS;
|
||||
|
||||
/// 驱动是否已安装。
|
||||
static bool isInstalled() => Directory(installedMarker).existsSync();
|
||||
|
||||
/// 内置驱动 dmg 的文件系统路径;未打包携带(如开发运行/产物缺失)返回 null。
|
||||
///
|
||||
/// Flutter assets 在 .app 内的真实路径:
|
||||
/// [app]/Contents/Frameworks/App.framework/Resources/flutter_assets/[asset]
|
||||
/// 从可执行文件位置反推(exe 在 [app]/Contents/MacOS/)。
|
||||
static String? bundledDmgPath() {
|
||||
if (!Platform.isMacOS) return null;
|
||||
final exe = Platform.resolvedExecutable;
|
||||
final exeDir = File(exe).parent.path; // .../Contents/MacOS
|
||||
final driversDir =
|
||||
'$exeDir/../Frameworks/App.framework/Resources/flutter_assets/assets/drivers';
|
||||
final dir = Directory(driversDir);
|
||||
if (!dir.existsSync()) return null;
|
||||
final dmgs = dir
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((f) => f.path.toLowerCase().endsWith('.dmg'))
|
||||
.toList();
|
||||
return dmgs.isEmpty ? null : dmgs.first.path;
|
||||
}
|
||||
|
||||
/// 从内置 dmg 静默安装驱动(挂载 → 找 pkg → osascript 提权 installer → 卸载卷)。
|
||||
/// 提权弹窗由系统显示(一次管理员密码),超时 3 分钟。
|
||||
static Future<void> installFromDmg(String dmgPath, {void Function(String line)? onLog}) async {
|
||||
final log = onLog ?? (_) {};
|
||||
|
||||
// 1. 只读挂载(挂载点带随机序号,必须从输出解析,不能拼名字)
|
||||
final attachOut = await runCommand('hdiutil', ['attach', '-nobrowse', '-readonly', dmgPath],
|
||||
timeoutMs: 30000);
|
||||
final match = RegExp(r'/Volumes/[^\t\n]+').allMatches(attachOut);
|
||||
if (match.isEmpty) {
|
||||
throw StateError('驱动 dmg 挂载失败');
|
||||
}
|
||||
final mountPoint = match.last.group(0)!;
|
||||
log('驱动卷已挂载:$mountPoint');
|
||||
|
||||
try {
|
||||
// 2. 找 pkg
|
||||
final pkg = Directory(mountPoint)
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((f) => f.path.toLowerCase().endsWith('.pkg'))
|
||||
.toList();
|
||||
if (pkg.isEmpty) {
|
||||
throw StateError('驱动 dmg 内未找到 .pkg 安装包');
|
||||
}
|
||||
final pkgPath = pkg.first.path;
|
||||
|
||||
// 3. osascript 提权安装(AppleScript 字符串内嵌套引号用 \" 转义)
|
||||
final script =
|
||||
'do shell script "installer -pkg \\"$pkgPath\\" -target /" '
|
||||
'with administrator privileges with prompt "安装 HiTi 打印机驱动"';
|
||||
log('开始安装驱动(请在系统弹窗中输入管理员密码)…');
|
||||
await runCommand('osascript', ['-e', script], timeoutMs: 180000);
|
||||
log('驱动安装完成');
|
||||
} finally {
|
||||
// 4. 卸载卷(失败不影响安装结果)
|
||||
try {
|
||||
await runCommand('hdiutil', ['detach', mountPoint, '-force'], tolerateExit: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import '../agent.dart';
|
||||
import '../server/protocol.dart';
|
||||
import '../setup/autostart.dart';
|
||||
import '../setup/driver_install.dart';
|
||||
import '../setup/first_run.dart';
|
||||
import '../setup/url_scheme.dart';
|
||||
|
||||
@@ -42,6 +43,8 @@ class _SetupWizardState extends State<SetupWizard> {
|
||||
bool _autostart = true;
|
||||
bool _autostartBusy = false;
|
||||
bool _printerChecking = false;
|
||||
bool _driverInstalling = false;
|
||||
String? _driverInstallError;
|
||||
PrinterStatus? _printerStatus;
|
||||
|
||||
Agent get _agent => widget.agent;
|
||||
@@ -173,9 +176,29 @@ class _SetupWizardState extends State<SetupWizard> {
|
||||
|
||||
// ---------------- Step 1:打印机检测 ----------------
|
||||
|
||||
/// 一键安装内置驱动(mac):装完自动重检(队列要等打印机连上/系统识别后才出现)。
|
||||
Future<void> _installDriver() async {
|
||||
final dmg = DriverInstall.bundledDmgPath();
|
||||
if (dmg == null) return;
|
||||
setState(() {
|
||||
_driverInstalling = true;
|
||||
_driverInstallError = null;
|
||||
});
|
||||
try {
|
||||
await DriverInstall.installFromDmg(dmg, onLog: _agent.log.write);
|
||||
await _checkPrinter();
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _driverInstallError = '$e');
|
||||
} finally {
|
||||
if (mounted) setState(() => _driverInstalling = false);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _stepPrinter(ColorScheme scheme) {
|
||||
final s = _printerStatus;
|
||||
final ready = s != null && s.online;
|
||||
final driverMissing = Platform.isMacOS && !DriverInstall.isInstalled();
|
||||
final bundledDmg = Platform.isMacOS ? DriverInstall.bundledDmgPath() : null;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -186,11 +209,69 @@ class _SetupWizardState extends State<SetupWizard> {
|
||||
okText: '打印机就绪${s != null && s.model.isNotEmpty ? "(${s.model})" : ""}',
|
||||
badText: s == null ? '未检测到打印队列' : (s.statusText.isEmpty ? '打印机未就绪' : s.statusText),
|
||||
),
|
||||
if (!ready) ...[
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// mac:驱动未装 → 首选一键安装内置驱动,其次官网链接
|
||||
if (driverMissing) ...[
|
||||
Text(
|
||||
'本机尚未安装 HiTi 打印机驱动(mac 打印必需的系统组件)。',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.6),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: _driverInstalling || bundledDmg == null ? null : _installDriver,
|
||||
icon: _driverInstalling
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.download, size: 16),
|
||||
label: Text(_driverInstalling
|
||||
? '正在安装…(弹窗中输入管理员密码)'
|
||||
: '一键安装驱动(内置安装包)'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton.icon(
|
||||
onPressed: () => launchUrl(Uri.parse(_driverUrl)),
|
||||
icon: const Icon(Icons.open_in_new, size: 14),
|
||||
label: const Text('改为手动下载'),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (bundledDmg == null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
'(当前构建未内置驱动安装包,请用官网链接手动安装)',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
if (_driverInstallError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
'安装失败:$_driverInstallError',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: scheme.error),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'安装完成后:USB 连接打印机并开机,系统识别出现打印队列即完成。',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
]
|
||||
|
||||
// 驱动已装但打印机未就绪 → 引导连接/添加队列
|
||||
else if (!ready) ...[
|
||||
Text(
|
||||
Platform.isMacOS
|
||||
? '安装步骤:① 打开 HiTi 官网下载 macOS 驱动(HiTiCardTypeMacDrivers dmg),双击安装里面的 pkg;② USB 连接打印机并开机;③ 系统设置 → 打印机与扫描仪 确认出现 HiTi 打印机;④ 回到这里点「重新检测」。'
|
||||
? '驱动已安装。请确认:① 打印机 USB 已连接并开机;② 系统设置 → 打印机与扫描仪 里出现 HiTi 打印机(没有的话点下方「+」添加,选 HiTi CS-200e);③ 回到这里重新检测。'
|
||||
: 'Windows 打印站:打开 HiTi 官网下载 CS-2xx Windows 驱动并安装,USB 连接打印机;本应用也支持免驱动的 SDK 直连模式,装完驱动或接好打印机后点「重新检测」。',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.6),
|
||||
),
|
||||
@@ -210,7 +291,6 @@ class _SetupWizardState extends State<SetupWizard> {
|
||||
label: const Text('重新检测'),
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: 12),
|
||||
Text('打印机已连接,可以正常接收管理端提交的卡面打印。',
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
|
||||
@@ -75,6 +75,8 @@ flutter:
|
||||
- assets/tray_icon_mac.png
|
||||
- assets/tray_icon_win.png
|
||||
- assets/logo.png
|
||||
# HiTi Mac 驱动安装包(不进 git:放 assets/drivers/*.dmg 后重新打包;缺失时向导降级为官网链接)
|
||||
- assets/drivers/
|
||||
|
||||
# To add assets to your application, add an assets section to your flutter
|
||||
# configuration like this:
|
||||
|
||||
@@ -12,6 +12,14 @@ DMG="dist/${APP_NAME}-macOS.dmg"
|
||||
echo "==> flutter build macos --release"
|
||||
flutter build macos --release
|
||||
|
||||
echo "==> 检查内置驱动"
|
||||
if ls assets/drivers/*.dmg >/dev/null 2>&1; then
|
||||
echo "已内置:$(ls assets/drivers/*.dmg)"
|
||||
else
|
||||
echo "警告:assets/drivers/ 下没有驱动 dmg,产出的安装包在向导中将只提供官网链接(一键安装不可用)。"
|
||||
echo " 将 HiTiCardTypeMacDrivers_2.4.2.dmg 放入 assets/drivers/ 后重新打包即可内置。"
|
||||
fi
|
||||
|
||||
echo "==> ad-hoc 就地签名(含全部嵌入内容;构建产物位于最终位置,直接在此重签)"
|
||||
# 注意:ad-hoc 签名不要加 --options=runtime——hardened runtime 的库校验(library validation)
|
||||
# 要求所有嵌入库与主程序同 Team ID,ad-hoc 无 Team 会导致 framework 加载被 dyld 拒绝
|
||||
|
||||
Reference in new issue
Block a user