用户实测:一键安装日志显示 12 秒"装完"但打印机仍离线。根因:HiTi 的 Setup64.exe 是自解压壳(InstallShield 类),Start-Process -Wait 等的是壳进程, 内层安装向导未真正完成即返回;且旧的注册表标记机制把这次假安装记成了"已装"。 修复: - 安装流程尾部增加 pnputil /enum-drivers 校验(匹配 cs2xxhcd/htitahcd/hiti), 未发现驱动即报"校验未通过"(引导重试或手动运行 Setup64),不再盲写标记 - queryInstalled 改为 pnputil 实时校验驱动商店(唯一真源,废弃注册表标记—— 假安装误写后向导会错误地不再显示安装按钮) - 成功日志改为提示"拔插 USB 重新枚举设备后再检测"
171 lines
7.4 KiB
Dart
171 lines
7.4 KiB
Dart
import 'dart:io';
|
||
|
||
import '../utils/command_runner.dart';
|
||
|
||
/// HiTi 打印驱动内置安装:App 资源携带驱动安装包,向导一键安装。
|
||
///
|
||
/// - macOS:CUPS 过滤器 pkg(dmg 内置),已装判断 = /Library/Printers/HiTi 存在,
|
||
/// 安装 = 挂载 dmg → osascript 提权 installer → 卸载卷
|
||
/// - Windows:HiTi 官方驱动整目录 zip 内置,已装判断 = pnputil 枚举驱动商店
|
||
/// (实时校验,安装器壳提前退出不会误判),安装 = 解压 → UAC 提权 Setup64 →
|
||
/// pnputil 校验通过才算成功
|
||
class DriverInstall {
|
||
static const installedMarker = '/Library/Printers/HiTi';
|
||
|
||
static bool get isSupported => Platform.isMacOS || Platform.isWindows;
|
||
|
||
/// 驱动是否已安装(macOS:驱动目录;Windows:安装标记,同步场景请用 queryInstalled)。
|
||
static bool isInstalled() {
|
||
if (Platform.isMacOS) return Directory(installedMarker).existsSync();
|
||
return false; // Windows 走异步 queryInstalled(注册表查询需要子进程)
|
||
}
|
||
|
||
/// Windows:pnputil 实时校验驱动商店(安装器是自解压壳、可能假成功,
|
||
/// 注册表标记会被失败的安装误写,故以系统驱动商店为唯一真源)。
|
||
static Future<bool> queryInstalled() async {
|
||
if (!Platform.isWindows) return isInstalled();
|
||
final drivers = await runCommand('pnputil', ['/enum-drivers'],
|
||
tolerateExit: true, timeoutMs: 60000);
|
||
final lower = drivers.toLowerCase();
|
||
return lower.contains('cs2xxhcd') || lower.contains('htitahcd') || lower.contains('hiti');
|
||
}
|
||
|
||
/// 内置驱动安装包的文件系统路径;未打包携带返回 null。
|
||
///
|
||
/// Flutter assets 在包内的真实路径(两平台结构不同):
|
||
/// - macOS:[exe]/../Frameworks/App.framework/Resources/flutter_assets
|
||
/// - Windows:[exe]/data/flutter_assets
|
||
static String? bundledInstallerPath() {
|
||
if (Platform.isMacOS) {
|
||
final exeDir = File(Platform.resolvedExecutable).parent.path;
|
||
return _firstFile(
|
||
'$exeDir/../Frameworks/App.framework/Resources/flutter_assets/assets/drivers',
|
||
'.dmg');
|
||
}
|
||
if (Platform.isWindows) {
|
||
final exeDir = File(Platform.resolvedExecutable).parent.path;
|
||
return _firstFile('$exeDir/data/flutter_assets/assets/drivers', '.zip');
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static String? _firstFile(String dirPath, String ext) {
|
||
final dir = Directory(dirPath);
|
||
if (!dir.existsSync()) return null;
|
||
final files = dir
|
||
.listSync()
|
||
.whereType<File>()
|
||
.where((f) => f.path.toLowerCase().endsWith(ext))
|
||
.toList();
|
||
return files.isEmpty ? null : files.first.path;
|
||
}
|
||
|
||
/// 静默安装(macOS)/提权安装(Windows,UAC 确认一次)。
|
||
static Future<void> install(String installerPath, {void Function(String line)? onLog}) async {
|
||
final log = onLog ?? (_) {};
|
||
if (Platform.isMacOS) {
|
||
await _installMac(installerPath, log);
|
||
return;
|
||
}
|
||
if (Platform.isWindows) {
|
||
await _installWindows(installerPath, log);
|
||
return;
|
||
}
|
||
throw UnsupportedError('当前平台不支持驱动安装');
|
||
}
|
||
|
||
// ---------------- macOS:dmg → pkg → osascript 提权 ----------------
|
||
|
||
static Future<void> _installMac(String dmgPath, void Function(String) log) async {
|
||
// 挂载点带随机序号,必须从 attach 输出解析,不能拼名字
|
||
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 {
|
||
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;
|
||
|
||
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 {
|
||
try {
|
||
await runCommand('hdiutil', ['detach', mountPoint, '-force'], tolerateExit: true);
|
||
} catch (_) {}
|
||
}
|
||
}
|
||
|
||
// ---------------- Windows:解压 zip → UAC 提权 Setup64 → 写安装标记 ----------------
|
||
|
||
static Future<void> _installWindows(String zipPath, void Function(String) log) async {
|
||
// 1. 解压到临时目录(驱动是"安装器 + 数据文件"的多文件结构,内置为 zip)
|
||
final extractDir =
|
||
'${Directory.systemTemp.path}/zax-hiti-driver-${DateTime.now().millisecondsSinceEpoch}';
|
||
log('解压驱动安装包…');
|
||
await runCommand('powershell', [
|
||
'-Command',
|
||
'Expand-Archive -Path "$zipPath" -DestinationPath "$extractDir" -Force'
|
||
], timeoutMs: 120000);
|
||
|
||
try {
|
||
// 2. 递归找安装器(优先 64 位 Setup64.exe,兜底 autorun.exe)
|
||
final setup = _findFileDeep(extractDir, 'setup64.exe') ??
|
||
_findFileDeep(extractDir, 'setup.exe') ??
|
||
_findFileDeep(extractDir, 'autorun.exe');
|
||
if (setup == null) {
|
||
throw StateError('驱动包内未找到安装程序(Setup64.exe)');
|
||
}
|
||
log('开始安装 HiTi 打印机驱动(请在 UAC 弹窗中确认,安装向导可能需要点几次"下一步")…');
|
||
// 注意:HiTi 的 Setup64 是自解压壳(InstallShield 类),-Wait 等的是壳进程,
|
||
// 内层安装向导可能仍在运行——安装是否成功必须以后续的驱动校验为准
|
||
final script = 'Start-Process -FilePath "$setup" -Verb RunAs -Wait';
|
||
await runCommand('powershell', ['-Command', script], timeoutMs: 600000);
|
||
log('安装程序已退出,正在校验驱动是否装入系统…');
|
||
} finally {
|
||
// 3. 清理临时目录(失败不影响安装结果)
|
||
try {
|
||
await Directory(extractDir).delete(recursive: true);
|
||
} catch (_) {}
|
||
}
|
||
|
||
// 4. 校验:驱动是否真的进了驱动商店(pnputil 枚举,匹配 htitahcd/CS2xx)
|
||
await Future<void>.delayed(const Duration(seconds: 3)); // 给安装收尾一点时间
|
||
final drivers = await runCommand('pnputil', ['/enum-drivers'],
|
||
tolerateExit: true, timeoutMs: 60000);
|
||
final lower = drivers.toLowerCase();
|
||
final installed = lower.contains('cs2xxhcd') || lower.contains('htitahcd') || lower.contains('hiti');
|
||
if (!installed) {
|
||
throw StateError(
|
||
'驱动校验未通过:系统中未发现 HiTi 驱动(安装器可能未真正完成,请重试,'
|
||
'或手动运行驱动包 CS2xx 目录内的 Setup64.exe 完成安装)');
|
||
}
|
||
log('校验通过:HiTi 驱动已装入系统');
|
||
log('驱动安装完成——请拔掉打印机 USB 重新插上(驱动绑定需要设备重新枚举),然后重新检测');
|
||
}
|
||
|
||
static String? _findFileDeep(String root, String lowerName) {
|
||
final hits = Directory(root)
|
||
.listSync(recursive: true, followLinks: false)
|
||
.whereType<File>()
|
||
.where((f) => f.uri.pathSegments.last.toLowerCase() == lowerName)
|
||
.toList();
|
||
return hits.isEmpty ? null : hits.first.path;
|
||
}
|
||
}
|