mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-20 14:53:42 +08:00
feat: add USB/PC/SC discovery support and an automated deployment installation script
This commit is contained in:
@@ -216,7 +216,8 @@ VoCat uses `qmicli` to verify that a QMI control channel is ready and
|
||||
`qmi-network` to manage packet-data sessions. The one-click installer installs
|
||||
and verifies the corresponding utilities automatically. For manual deployment,
|
||||
Debian/Ubuntu uses `apt install libqmi-utils`; Arch Linux uses
|
||||
`pacman -S libqmi`, and Alpine uses `apk add qmi-utils`.
|
||||
`pacman -S libqmi`, Alpine uses `apk add qmi-utils`, and OpenWrt uses
|
||||
`opkg install qmi-utils`.
|
||||
|
||||
`vocat doctor --repair-dji-qmi` checks for `qmicli` before changing any USB
|
||||
driver binding or asserting DTR. If the utility is unavailable, the command
|
||||
|
||||
@@ -192,7 +192,7 @@ VoCat 会继续在添加设备窗口显示该硬件,并明确提示缺少服
|
||||
VoCat 使用 `qmicli` 验证 QMI 控制通道是否就绪,并使用 `qmi-network` 管理
|
||||
分组数据会话。一键安装脚本会自动安装并验证对应工具。手动部署时,
|
||||
Debian/Ubuntu 使用 `apt install libqmi-utils`;Arch Linux 使用
|
||||
`pacman -S libqmi`,Alpine 使用 `apk add qmi-utils`。
|
||||
`pacman -S libqmi`,Alpine 使用 `apk add qmi-utils`,OpenWrt 使用 `opkg install qmi-utils`。
|
||||
|
||||
`vocat doctor --repair-dji-qmi` 会在修改 USB 驱动绑定或触发 DTR 之前检查
|
||||
`qmicli`。如果工具不可用,命令会给出安装提示并停止,保持设备当前状态不变。
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -44,9 +45,107 @@ func (backend *nativeBackend) dial(ctx context.Context) (*pcscdClient, error) {
|
||||
return nil, fmt.Errorf("%w: pcscd socket is not reachable: %w", ErrUnavailable, errors.Join(failures...))
|
||||
}
|
||||
|
||||
func ensurePCSCDService(ctx context.Context) {
|
||||
if os.Geteuid() != 0 {
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
||||
_ = exec.CommandContext(ctx, "systemctl", "start", "pcscd.socket").Run()
|
||||
_ = exec.CommandContext(ctx, "systemctl", "start", "pcscd").Run()
|
||||
} else if _, err := os.Stat("/etc/init.d/pcscd"); err == nil {
|
||||
_ = exec.CommandContext(ctx, "/etc/init.d/pcscd", "start").Run()
|
||||
} else if path, err := exec.LookPath("pcscd"); err == nil {
|
||||
_ = exec.CommandContext(ctx, path).Start()
|
||||
}
|
||||
}
|
||||
|
||||
func reauthorizeUSBDevice(sysRoot, usbPath string) {
|
||||
if strings.Contains(usbPath, "..") || strings.Contains(usbPath, "/") || strings.Contains(usbPath, "\\") {
|
||||
return
|
||||
}
|
||||
authPath := filepath.Join(filepath.Clean(sysRoot), "bus", "usb", "devices", usbPath, "authorized")
|
||||
if _, err := os.Stat(authPath); err != nil {
|
||||
return
|
||||
}
|
||||
_ = os.WriteFile(authPath, []byte("0\n"), 0o644)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_ = os.WriteFile(authPath, []byte("1\n"), 0o644)
|
||||
}
|
||||
|
||||
func (backend *nativeBackend) waitForPCSCReaders(ctx context.Context, client *pcscdClient, physical []Reader, states []pcscdReaderState) []pcscdReaderState {
|
||||
// First pass: wait up to 2 seconds for active driver negotiation.
|
||||
pollDeadline := time.Now().Add(2 * time.Second)
|
||||
if dl, ok := ctx.Deadline(); ok && dl.Before(pollDeadline) {
|
||||
pollDeadline = dl
|
||||
}
|
||||
for len(states) < len(physical) && time.Now().Before(pollDeadline) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return states
|
||||
case <-time.After(250 * time.Millisecond):
|
||||
}
|
||||
if updated, err := client.readers(ctx); err == nil {
|
||||
states = updated
|
||||
if len(states) >= len(physical) {
|
||||
return states
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(states) >= len(physical) {
|
||||
return states
|
||||
}
|
||||
|
||||
// Second pass: if readers are still missing from pcscd, trigger a USB re-authorization
|
||||
// on the physical devices in sysfs to reset any stalled CCID endpoints, then poll briefly.
|
||||
reauthorized := false
|
||||
for _, phys := range physical {
|
||||
if phys.USBPath != "" {
|
||||
reauthorizeUSBDevice(backend.sysRoot, phys.USBPath)
|
||||
reauthorized = true
|
||||
}
|
||||
}
|
||||
if !reauthorized {
|
||||
return states
|
||||
}
|
||||
|
||||
retryDeadline := time.Now().Add(2 * time.Second)
|
||||
if dl, ok := ctx.Deadline(); ok && dl.Before(retryDeadline) {
|
||||
retryDeadline = dl
|
||||
}
|
||||
for len(states) < len(physical) && time.Now().Before(retryDeadline) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return states
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
}
|
||||
if updated, err := client.readers(ctx); err == nil {
|
||||
states = updated
|
||||
if len(states) >= len(physical) {
|
||||
return states
|
||||
}
|
||||
}
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
func (backend *nativeBackend) Readers(ctx context.Context) ([]Reader, error) {
|
||||
physical := discoverUSBSmartCardReaders(backend.sysRoot, "pcsc_driver_missing")
|
||||
client, err := backend.dial(ctx)
|
||||
if err != nil && len(physical) > 0 {
|
||||
ensurePCSCDService(ctx)
|
||||
dialDeadline := time.Now().Add(1500 * time.Millisecond)
|
||||
for time.Now().Before(dialDeadline) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
}
|
||||
if c, dialErr := backend.dial(ctx); dialErr == nil {
|
||||
client, err = c, nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if len(physical) > 0 {
|
||||
for index := range physical {
|
||||
@@ -61,6 +160,9 @@ func (backend *nativeBackend) Readers(ctx context.Context) ([]Reader, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(physical) > 0 && len(states) < len(physical) {
|
||||
states = backend.waitForPCSCReaders(ctx, client, physical, states)
|
||||
}
|
||||
readers := make([]Reader, 0, len(states))
|
||||
for _, state := range states {
|
||||
reader := Reader{
|
||||
|
||||
@@ -67,17 +67,33 @@ func mergePCSCAndUSBReaders(readers, physical []Reader) []Reader {
|
||||
readers[0] = enrichPCSCReader(readers[0], physical[0])
|
||||
return readers
|
||||
}
|
||||
seen := make(map[string]bool, len(readers))
|
||||
matchedPhysical := make(map[string]bool, len(physical))
|
||||
for i := range readers {
|
||||
seen[readers[i].USBPath] = true
|
||||
for _, usbReader := range physical {
|
||||
if readers[i].USBPath == usbReader.USBPath {
|
||||
readers[i] = enrichPCSCReader(readers[i], usbReader)
|
||||
matchedPhysical[usbReader.USBPath] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Secondary pass: if any pcsc reader is still prefixed with pcsc: (unresolved sysfs USB path),
|
||||
// match with unmatched physical readers by VendorID/ProductID or if 1:1 remaining.
|
||||
var remainingPhysical []Reader
|
||||
for _, p := range physical {
|
||||
if !matchedPhysical[p.USBPath] {
|
||||
remainingPhysical = append(remainingPhysical, p)
|
||||
}
|
||||
}
|
||||
for i := range readers {
|
||||
if strings.HasPrefix(readers[i].USBPath, "pcsc:") && len(remainingPhysical) == 1 {
|
||||
readers[i] = enrichPCSCReader(readers[i], remainingPhysical[0])
|
||||
matchedPhysical[remainingPhysical[0].USBPath] = true
|
||||
remainingPhysical = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, usbReader := range physical {
|
||||
if !seen[usbReader.USBPath] {
|
||||
if !matchedPhysical[usbReader.USBPath] {
|
||||
readers = append(readers, usbReader)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,27 @@ func TestMergePCSCAndSingleUSBReaderEnrichesFallbackPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergePCSCAndMultipleUSBReadersWithFallbackPath(t *testing.T) {
|
||||
readers := mergePCSCAndUSBReaders(
|
||||
[]Reader{
|
||||
{Name: "Identiv uTrust 00 00", USBPath: "1-2", CardPresent: true},
|
||||
{Name: "Generic Smart Card Reader 00 00", USBPath: "pcsc:Generic Smart Card Reader 00 00", CardPresent: true},
|
||||
},
|
||||
[]Reader{
|
||||
{Name: "uTrust", USBPath: "1-2", VendorID: "04e6", ProductID: "5810", DiscoveryIssue: "pcsc_driver_missing"},
|
||||
{Name: "ESTKme-RED", USBPath: "1-1", VendorID: "0bda", ProductID: "0165", DiscoveryIssue: "pcsc_driver_missing"},
|
||||
},
|
||||
)
|
||||
if len(readers) != 2 {
|
||||
t.Fatalf("len(readers) = %d, want 2", len(readers))
|
||||
}
|
||||
for _, r := range readers {
|
||||
if r.DiscoveryIssue != "" {
|
||||
t.Errorf("reader %#v still has discovery issue %q", r, r.DiscoveryIssue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeUSBTestFile(t *testing.T, path, value string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
|
||||
+23
-7
@@ -203,9 +203,14 @@ install_qmi_support() {
|
||||
|
||||
if is_openwrt && command -v opkg >/dev/null 2>&1; then
|
||||
opkg update >/dev/null 2>&1 || true
|
||||
if opkg_has_package libqmi; then
|
||||
opkg install libqmi >/dev/null 2>&1 || true
|
||||
local pkgs=""
|
||||
opkg_has_package qmi-utils && pkgs="$pkgs qmi-utils"
|
||||
opkg_has_package libqmi && pkgs="$pkgs libqmi"
|
||||
if [ -z "$pkgs" ]; then
|
||||
pkgs="qmi-utils libqmi"
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
opkg install $pkgs >/dev/null 2>&1 || true
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update -qq || true
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y libqmi-utils || true
|
||||
@@ -236,6 +241,7 @@ install_pcsc_support() {
|
||||
local packages=""
|
||||
opkg_has_package pcscd && packages="$packages pcscd"
|
||||
opkg_has_package ccid && packages="$packages ccid"
|
||||
opkg_has_package libccid && packages="$packages libccid"
|
||||
if [ -n "$packages" ]; then
|
||||
# shellcheck disable=SC2086
|
||||
opkg install $packages >/dev/null 2>&1 && installed=1 || true
|
||||
@@ -363,7 +369,7 @@ download_and_verify() {
|
||||
[ "$actual" = "$expected" ] || die "SHA-256 校验失败。" "SHA-256 verification failed."
|
||||
chmod 0755 "${VOCAT_TMP}/vocat"
|
||||
"${VOCAT_TMP}/vocat" version >/dev/null 2>&1 || die \
|
||||
"Downloaded binary cannot run on this system; keeping the installed version." \
|
||||
"下载的二进制文件无法在此系统上运行;未更改当前安装的版本。" \
|
||||
"The downloaded binary cannot run on this host; the installed version was not changed."
|
||||
}
|
||||
|
||||
@@ -392,8 +398,18 @@ INITIAL_ADMIN_PASSWORD=""
|
||||
bootstrap_admin() {
|
||||
local candidate="${1:-$BINARY_PATH}"
|
||||
local secret result
|
||||
secret=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n')
|
||||
[ -n "$secret" ] || die "Failed to generate a random secret." "Failed to generate a random secret."
|
||||
if command -v od >/dev/null 2>&1; then
|
||||
secret=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n')
|
||||
elif command -v hexdump >/dev/null 2>&1; then
|
||||
secret=$(hexdump -n 16 -e '16/1 "%02x"' /dev/urandom)
|
||||
elif command -v openssl >/dev/null 2>&1; then
|
||||
secret=$(openssl rand -hex 16 2>/dev/null || true)
|
||||
elif command -v sha256sum >/dev/null 2>&1; then
|
||||
secret=$(head -c 32 /dev/urandom | sha256sum | awk '{print substr($1, 1, 32)}')
|
||||
else
|
||||
secret=$(tr -dc 'a-f0-9' < /dev/urandom | head -c 32)
|
||||
fi
|
||||
[ -n "$secret" ] || die "生成随机密钥失败。" "Failed to generate a random secret."
|
||||
result=$(printf '%s\n' "$secret" | "$candidate" bootstrap-admin --database /opt/vocat/data/vocat.db --username admin) || \
|
||||
die \
|
||||
"待安装版本无法读取或升级现有数据库;当前程序尚未被替换,请检查数据库与版本兼容性。" \
|
||||
@@ -505,7 +521,7 @@ write_service() {
|
||||
write_openwrt_init
|
||||
return
|
||||
fi
|
||||
die "Unsupported service manager." "Neither systemd nor OpenWrt procd was detected."
|
||||
die "不支持的服务管理器。" "Neither systemd nor OpenWrt procd was detected."
|
||||
}
|
||||
|
||||
enable_and_start() {
|
||||
@@ -547,7 +563,7 @@ enable_and_start() {
|
||||
cp -a "${BINARY_PATH}.bak" "$BINARY_PATH"
|
||||
"$OPENWRT_INIT_PATH" restart || true
|
||||
fi
|
||||
die "OpenWrt vocat service failed to start." "The OpenWrt vocat service failed to start."
|
||||
die "OpenWrt vocat 服务启动失败。" "The OpenWrt vocat service failed to start."
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
systemctl enable vocat
|
||||
|
||||
Reference in New Issue
Block a user