Files
desktop-plugin/lib/setup/driver_install.dart
T
dzc 170e9ddc5e feat(windows): 向导内置 HiTi Windows 驱动一键安装(对齐 mac 的"装 App 即用"体验)
用户实测定论:CS-2xx 的 USB 是自定义设备(驱动包内含 CS2xxhcd.inf 内核驱动),
PavoApi 直打依赖驱动建立的 USB 通道——无驱动时枚举到设备但状态返回离线,
"免驱动"假设不成立。产品要求两平台一致:驱动随 App 内置、向导一键安装。

- assets/drivers/CS2xx_Windows_Driver.zip:HiTi 官网 CS-2xx Windows 驱动
  (2.6.25.48)整目录 zip(含 CS2xx/Setup64.exe),33MB,不入 git
- DriverInstall Windows 分支:Expand-Archive 解压到临时目录 → 递归定位
  Setup64.exe(兜底 setup/autorun)→ PowerShell Start-Process -Verb RunAs -Wait
  提权安装(UAC 一次)→ 写安装标记(HKCU\Software\zax\desktop-plugin,
  系统驱动安装状态无统一可查标志)→ finally 清理临时目录
- 向导第 2 步统一双平台状态机:驱动未装(异步查标记/目录)→"一键安装驱动
  (内置安装包)";轮询 3 秒自动刷新安装/连接状态
- bundledInstallerPath 双平台资产路径推导(mac .dmg / win .zip);
  .gitignore 增 *.zip;assets/drivers/README 更新双平台放置约定
2026-09-21 16:38:11 +08:00

164 lines
6.5 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:io';
import '../utils/command_runner.dart';
/// HiTi 打印驱动内置安装:App 资源携带驱动安装包,向导一键安装。
///
/// - macOS:CUPS 过滤器 pkg(dmg 内置),已装判断 = /Library/Printers/HiTi 存在,
/// 安装 = 挂载 dmg → osascript 提权 installer → 卸载卷
/// - Windows:HiTi 官方驱动 exe(assets/drivers/*.exe 内置),UAC 提权运行;
/// 已装判断 = 我们自己的注册表标记(HKCU\Software\zax\desktop-plugin,
/// 系统级驱动安装状态无统一可查标志,安装成功后写标记最可靠)
class DriverInstall {
static const installedMarker = '/Library/Printers/HiTi';
static const _winMarkKey = r'HKCU\Software\zax\desktop-plugin';
static const _winMarkValue = 'HiTiDriverInstalled';
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:异步查询安装标记。
static Future<bool> queryInstalled() async {
if (!Platform.isWindows) return isInstalled();
final out = await runCommand(
'reg',
['query', _winMarkKey, '/v', _winMarkValue],
tolerateExit: true,
);
return out.contains('0x1');
}
/// 内置驱动安装包的文件系统路径;未打包携带返回 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 弹窗中确认,安装向导可能需要点几次"下一步")…');
// -Verb RunAs 触发 UAC;-Wait 等安装程序退出(用户取消 UAC 时 PowerShell 报错)
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. 写安装标记(我们自己的注册表键;0x1 = 已装)
await runCommand('reg',
['add', _winMarkKey, '/v', _winMarkValue, '/t', 'REG_DWORD', '/d', '1', '/f']);
log('已记录驱动安装状态');
}
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;
}
}