Merge pull request #131 from shenmintao/fix/installer-usb-safety-main

fix: preserve existing installs on build failure and scope USB switching
This commit is contained in:
MinQ authored and GitHub committed 2026-11-02 14:38:08 +08:00
commit 2e75f8885a
8 files changed
+286 -27

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
# 维护原则
本文件适用于整个仓库。
## 与上游保持一致
- 对继承自 Radxa/AIC 上游的驱动、固件、加载协议和运行行为,默认保持一致,优先采用上游已经接受的修复。
- 对这些上游组件,除非修复有证据支持的重要 bug,否则不引入会增加下游维护负担的改动。
- 重要 bug 包括实际影响设备功能、稳定性、安全性或受支持内核构建的问题。仅凭代码风格、理论风险或“体验更好”不足以认定为重要 bug。
- 修复应尽量小且独立,便于审查和同步未来上游版本。避免顺带重构、清理、增加功能或建立新的策略与兼容框架。
## 本仓库维护的工具
- 安装脚本、打包、诊断和文档应与上游驱动改动分开评估;不能仅因上游没有同类实现就否决。
- 这些改动仍应简单、必要、有验证,避免不必要的默认行为和维护成本。
- 试编译应评估失败时能否保住已有安装及额外编译耗时;固件备份应评估恢复价值、重复备份和磁盘占用,避免无限累积。它们本身不构成驱动或固件偏离上游。
## PR 和 issue 处理
- 先核实当前分支、上游实现和最新讨论,避免重复处理已合并或仍在等待实机验证的问题。
- 不因 PR 使用了 AI、描述完整或声称测试通过而接受或拒绝;以可核实的问题、实际代码、独立验证及维护成本为依据。
- 合入前说明问题的触发条件、影响、与上游的差异和验证结果;证据不足时继续保留为待验证项。
- 编译通过、模拟测试和硬件测试必须明确区分,不得用前两者替代实机结论。
- 涉及两个维护分支的必要修复应分别评估并同步,保留 `main` 与 `legacy-mcu1` 各自匹配的固件和加载配置,不能直接混合固件或整体合并分支。
- 用户本次会话的明确指示优先于本文件。
+8
View File
@@ -35,6 +35,14 @@ cannot download dependencies because the database is stale, run
`sudo pacman -Syu` yourself, reboot if the kernel was updated, and rerun the `sudo pacman -Syu` yourself, reboot if the kernel was updated, and rerun the
installer with headers matching the running kernel. installer with headers matching the running kernel.
Before replacing installed firmware or DKMS modules, the installer cleans and
test-builds a temporary copy of the driver for the running kernel. A test-build
failure leaves the existing firmware, DKMS registration and installed source
untouched. A successful installation compiles again through DKMS, so this check
adds build time. Logs are written to `/var/log/aic8800-install.log` with mode
`0644` after the root check. This check does not provide automatic rollback if a
later installation step fails.
The USB modules declare the complete firmware set shipped in `fw/`, including The USB modules declare the complete firmware set shipped in `fw/`, including
configuration files and extension patches, so initramfs tools can include those configuration files and extension patches, so initramfs tools can include those
files when they include the modules. Rerun the installer after updating the files when they include the modules. Rerun the installer after updating the
+5
View File
@@ -77,6 +77,11 @@ commands. Existing users must rerun `sudo ./install.sh` to replace the installed
usb_modeswitch configuration, then unplug the adapter completely and plug it usb_modeswitch configuration, then unplug the adapter completely and plug it
back in. back in.
Automatic switching requires the USB product string to start with `88M80`.
Unrelated flash disks also use `1111:1111`, so that USB ID alone is not enough.
If your adapter reports a different product string, verify its identity before
trying the manual command below.
If automatic switching does not occur, try the same sequence manually: If automatic switching does not occur, try the same sequence manually:
```bash ```bash
+15 -11
View File
@@ -1,14 +1,18 @@
# AIC8800 USB mass-storage mode switch rules # AIC8800 USB mass-storage mode switch rules
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5721", SYMLINK+="aicudisk", RUN+="/usr/bin/eject /dev/%k" ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5721", SYMLINK+="aicudisk", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5722", SYMLINK+="aicudisk", RUN+="/usr/bin/eject /dev/%k" ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5722", SYMLINK+="aicudisk", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5723", SYMLINK+="tendaudisk", RUN+="/usr/bin/eject /dev/%k" ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5723", SYMLINK+="tendaudisk", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5724", SYMLINK+="ugreenax900", RUN+="/usr/bin/eject /dev/%k" ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5724", SYMLINK+="ugreenax900", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5725", SYMLINK+="tendaudiskv2", RUN+="/usr/bin/eject /dev/%k" ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5725", SYMLINK+="tendaudiskv2", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5726", SYMLINK+="tendaudiskv3", RUN+="/usr/bin/eject /dev/%k" ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5726", SYMLINK+="tendaudiskv3", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5727", SYMLINK+="tendaudiskv4", RUN+="/usr/bin/eject /dev/%k" ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5727", SYMLINK+="tendaudiskv4", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="572a", SYMLINK+="tendaudiskv5", RUN+="/usr/bin/eject /dev/%k" ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="572a", SYMLINK+="tendaudiskv5", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="572c", SYMLINK+="cudydiskv2", RUN+="/usr/bin/eject /dev/%k" ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="572c", SYMLINK+="cudydiskv2", RUN+="/usr/bin/eject /dev/%k"
KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="572f", SYMLINK+="aicudisk572f", RUN+="/usr/bin/eject /dev/%k" ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd*", ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="572f", SYMLINK+="aicudisk572f", RUN+="/usr/bin/eject /dev/%k"
# AIC8800D80 "Pandora" clone: switch 1111:1111 to its operational USB ID. # AIC8800D80 "Pandora" clone: switch 1111:1111 to its operational USB ID.
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1111", ATTR{idProduct}=="1111", RUN+="/bin/sh -c 'if [ -x /usr/sbin/usb_modeswitch ]; then exec /usr/sbin/usb_modeswitch -c /etc/usb_modeswitch.d/1111:1111; elif [ -x /usr/bin/usb_modeswitch ]; then exec /usr/bin/usb_modeswitch -c /etc/usb_modeswitch.d/1111:1111; fi'" # 1111:1111 is a placeholder ID that other devices also report (some AIC
# dongles expose a separate 1111:1111 "FLASH" driver disk next to an
# already working Wi-Fi function), so only devices whose product string is
# 88M80 are switched, and usb_modeswitch is pointed at this exact device.
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="1111", ATTR{idProduct}=="1111", ATTR{product}=="88M80*", RUN+="/bin/sh -c 'if [ -x /usr/sbin/usb_modeswitch ]; then exec /usr/sbin/usb_modeswitch -b $attr{busnum} -g $attr{devnum} -c /etc/usb_modeswitch.d/1111:1111; elif [ -x /usr/bin/usb_modeswitch ]; then exec /usr/bin/usb_modeswitch -b $attr{busnum} -g $attr{devnum} -c /etc/usb_modeswitch.d/1111:1111; fi'"
+5 -1
View File
@@ -19,7 +19,11 @@ NC='\033[0m'
DRV_NAME="aic8800" DRV_NAME="aic8800"
DRV_VERSION="1.0.0" DRV_VERSION="1.0.0"
INSTALL_LOG="/tmp/aic8800d80_install.log" INSTALL_LOG="/var/log/aic8800-install.log"
# Installers before this change wrote their log to /tmp.
if [ ! -f "$INSTALL_LOG" ] && [ -f /tmp/aic8800d80_install.log ]; then
INSTALL_LOG="/tmp/aic8800d80_install.log"
fi
read_dkms_conf_value() { read_dkms_conf_value() {
local key="$1" local key="$1"
+35 -15
View File
@@ -24,7 +24,7 @@ readonly DRV_NAME="aic8800"
readonly DRV_VERSION="1.0.0" readonly DRV_VERSION="1.0.0"
readonly SRC_DIR="/usr/src/${DRV_NAME}-${DRV_VERSION}" readonly SRC_DIR="/usr/src/${DRV_NAME}-${DRV_VERSION}"
readonly MODULE_NAME="aic8800_fdrv" readonly MODULE_NAME="aic8800_fdrv"
readonly LOG_FILE="/tmp/aic8800d80_install.log" readonly LOG_FILE="/var/log/aic8800-install.log"
# Script directory (where the script is located) # Script directory (where the script is located)
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -36,12 +36,7 @@ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
log_message() { log_message() {
local level="$1" local level="$1"
shift shift
# Garantir que o arquivo de log existe e tem permissões corretas { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" >> "$LOG_FILE"; } 2>/dev/null || true
if [ ! -f "$LOG_FILE" ]; then
touch "$LOG_FILE" 2>/dev/null || true
chmod 666 "$LOG_FILE" 2>/dev/null || true
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" >> "$LOG_FILE" 2>/dev/null || true
} }
print_info() { print_info() {
@@ -504,6 +499,30 @@ EOF
print_success "DKMS configuration created." print_success "DKMS configuration created."
} }
#############################################################################
# Pre-flight Build
#############################################################################
preflight_build() {
local build_root kver
kver="$(uname -r)"
print_step "Test-building the driver for $kver before replacing the existing installation..."
build_root=$(mktemp -d "${TMPDIR:-/tmp}/aic8800-preflight.XXXXXX")
# Use a private copy and clean it so a previous build cannot hide an error.
if cp -r "${SCRIPT_DIR}/drivers" "$build_root/" >> "$LOG_FILE" 2>&1 &&
make -C "$build_root/drivers/aic8800" KVER="$kver" KDIR="/lib/modules/$kver/build" clean >> "$LOG_FILE" 2>&1 &&
make -C "$build_root/drivers/aic8800" KVER="$kver" KDIR="/lib/modules/$kver/build" >> "$LOG_FILE" 2>&1; then
rm -rf "$build_root"
print_success "Test build succeeded."
else
rm -rf "$build_root"
print_error "Test build failed for kernel $kver. Existing firmware and DKMS installation were not changed."
echo "See $LOG_FILE for the compiler output."
return 1
fi
}
############################################################################# #############################################################################
# DKMS Installation # DKMS Installation
############################################################################# #############################################################################
@@ -823,10 +842,13 @@ show_final_instructions() {
############################################################################# #############################################################################
main() { main() {
# Initialize log file # Step 1: Check root privileges (before touching the log, which lives in
rm -f "$LOG_FILE" 2>/dev/null || true # /var/log and is written by root only)
touch "$LOG_FILE" 2>/dev/null || true check_root
chmod 666 "$LOG_FILE" 2>/dev/null || true
# Initialize log file: owned by root, readable by all, never world-writable
rm -f "$LOG_FILE"
install -m 0644 /dev/null "$LOG_FILE"
echo -e "${CYAN}" echo -e "${CYAN}"
echo "╔════════════════════════════════════════════════════════════════╗" echo "╔════════════════════════════════════════════════════════════════╗"
@@ -839,9 +861,6 @@ main() {
log_message "INFO" "Kernel: $(uname -r)" log_message "INFO" "Kernel: $(uname -r)"
log_message "INFO" "Distribution: $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')" log_message "INFO" "Distribution: $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')"
# Step 1: Check root privileges
check_root
# Step 2: Check Secure Boot # Step 2: Check Secure Boot
check_secure_boot check_secure_boot
@@ -854,7 +873,8 @@ main() {
# Step 5: Check kernel build tree # Step 5: Check kernel build tree
check_kernel_build_tree check_kernel_build_tree
# Step 6: Install firmware # Step 6: Test-build before changing installed firmware or DKMS modules
preflight_build
install_firmware install_firmware
# Step 7: Create DKMS configuration # Step 7: Create DKMS configuration
+124
View File
@@ -30,6 +30,8 @@ class InstallerTests(unittest.TestCase):
def run_installer_function(self, name, stubs="", after=""): def run_installer_function(self, name, stubs="", after=""):
# Extract only the functions under test; never execute main(). # Extract only the functions under test; never execute main().
names = ["run_logged", "cleanup_on_error", name] names = ["run_logged", "cleanup_on_error", name]
if name == "main" and re.search(r"(?m)^preflight_build\(\)", SOURCE):
names.insert(-1, "preflight_build")
functions = [] functions = []
for function in names: for function in names:
match = re.search(r"(?ms)^" + function + r"\(\) \{.*?^\}", SOURCE) match = re.search(r"(?ms)^" + function + r"\(\) \{.*?^\}", SOURCE)
@@ -42,6 +44,10 @@ class InstallerTests(unittest.TestCase):
{SHELL_OPTIONS} {SHELL_OPTIONS}
SCRIPT_DIR={shlex.quote(str(self.root))} SCRIPT_DIR={shlex.quote(str(self.root))}
LOG_FILE="$SCRIPT_DIR/install.log" LOG_FILE="$SCRIPT_DIR/install.log"
SRC_DIR="$SCRIPT_DIR/installed-source"
TMPDIR="$SCRIPT_DIR/tmp"
export TMPDIR
mkdir -p "$TMPDIR"
DRV_NAME=aic8800 DRV_NAME=aic8800
DRV_VERSION=1.0.0 DRV_VERSION=1.0.0
MODULE_NAME=aic8800_fdrv MODULE_NAME=aic8800_fdrv
@@ -50,6 +56,7 @@ print_info() {{ echo "$*"; }}
print_success() {{ echo "$*"; }} print_success() {{ echo "$*"; }}
print_warning() {{ echo "$*"; }} print_warning() {{ echo "$*"; }}
print_error() {{ echo "$*"; }} print_error() {{ echo "$*"; }}
log_message() {{ :; }}
dkms() {{ echo 'aic8800/1.0.0: installed'; }} dkms() {{ echo 'aic8800/1.0.0: installed'; }}
lsmod() {{ :; }} lsmod() {{ :; }}
aic_zlp_target_present() {{ return 1; }} aic_zlp_target_present() {{ return 1; }}
@@ -132,6 +139,123 @@ cleanup_legacy_bluetooth_config() {{ echo LEGACY_CLEANUP; }}
self.assertFalse(old.exists()) self.assertFalse(old.exists())
self.assertEqual((old.parent / "new.bin").read_bytes(), b"new") self.assertEqual((old.parent / "new.bin").read_bytes(), b"new")
def prepare_preflight(self):
source = self.root / "drivers" / "aic8800" / "source.c"
source.parent.mkdir(parents=True)
source.write_bytes(b"source")
old = self.firmware / "aic8800D80" / "old.bin"
old.parent.mkdir()
old.write_bytes(b"installed firmware")
installed = self.root / "installed-source" / "revision"
installed.parent.mkdir()
installed.write_bytes(b"installed source")
(self.root / "dkms-state").write_bytes(b"installed modules")
return r'''
uname() { echo test-kernel; }
check_root() { :; }
check_secure_boot() { :; }
detect_package_manager() { DETECTED_PKG_MANAGER=apt; }
install_dependencies() { :; }
check_kernel_build_tree() { :; }
create_dkms_conf() { :; }
refresh_initramfs() { :; }
load_module() { :; }
verify_installation() { :; }
show_final_instructions() { echo FINAL_INSTRUCTIONS; }
make() {
echo "MAKE $*" >> "$SCRIPT_DIR/events"
[ "$(cat "$2/source.c")" = source ] || return 99
case "$*" in
*' clean') return "${MAKE_CLEAN_STATUS:-0}" ;;
esac
echo built > "$2/source.c"
return "${MAKE_BUILD_STATUS:-0}"
}
install_firmware() {
echo FIRMWARE >> "$SCRIPT_DIR/events"
echo changed > "$SCRIPT_DIR/firmware/aic8800D80/old.bin"
}
install_via_dkms() {
echo DKMS >> "$SCRIPT_DIR/events"
echo changed > "$SRC_DIR/revision"
echo changed > "$SCRIPT_DIR/dkms-state"
}
'''
def assert_installation_preserved(self, result):
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual((self.firmware / "aic8800D80/old.bin").read_bytes(), b"installed firmware")
self.assertEqual((self.root / "installed-source/revision").read_bytes(), b"installed source")
self.assertEqual((self.root / "dkms-state").read_bytes(), b"installed modules")
events = self.root / "events"
if events.exists():
self.assertNotIn("FIRMWARE", events.read_text())
self.assertNotIn("DKMS", events.read_text())
self.assertNotIn("FINAL_INSTRUCTIONS", result.stdout)
self.assertEqual(list((self.root / "tmp").iterdir()), [])
def test_failed_preflight_preserves_firmware_source_and_dkms(self):
stubs = self.prepare_preflight() + "\nMAKE_BUILD_STATUS=17\n"
result = self.run_installer_function("main", stubs)
self.assert_installation_preserved(result)
self.assertIn("Test build failed", result.stdout)
self.assertIn("MAKE", (self.root / "events").read_text())
self.assertEqual((self.root / "drivers/aic8800/source.c").read_bytes(), b"source")
def test_preflight_copy_failure_preserves_installation(self):
stubs = self.prepare_preflight() + "\ncp() { return 18; }\n"
result = self.run_installer_function("main", stubs)
self.assert_installation_preserved(result)
self.assertFalse((self.root / "events").exists())
def test_preflight_clean_failure_preserves_installation(self):
stubs = self.prepare_preflight() + "\nMAKE_CLEAN_STATUS=19\n"
result = self.run_installer_function("main", stubs)
self.assert_installation_preserved(result)
self.assertEqual(len((self.root / "events").read_text().splitlines()), 1)
def test_preflight_success_precedes_installation_and_cleans_temp(self):
result = self.run_installer_function("main", self.prepare_preflight())
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
events = (self.root / "events").read_text().splitlines()
self.assertEqual(len(events), 4)
self.assertTrue(events[0].endswith(" clean"), events)
self.assertTrue(events[1].startswith("MAKE "), events)
self.assertEqual(events[2:], ["FIRMWARE", "DKMS"])
for line in events[:2]:
self.assertIn(str(self.root / "tmp"), line)
self.assertIn("KVER=test-kernel KDIR=/lib/modules/test-kernel/build", line)
self.assertEqual((self.root / "drivers/aic8800/source.c").read_bytes(), b"source")
self.assertEqual(list((self.root / "tmp").iterdir()), [])
self.assertIn("FINAL_INSTRUCTIONS", result.stdout)
def test_log_replaces_symlink_without_overwriting_target(self):
stubs = self.prepare_preflight()
target = self.root / "keep.log"
target.write_bytes(b"keep this file")
(self.root / "install.log").symlink_to(target)
result = self.run_installer_function("main", stubs)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(target.read_bytes(), b"keep this file")
log = self.root / "install.log"
self.assertFalse(log.is_symlink())
self.assertEqual(log.stat().st_mode & 0o777, 0o644)
def test_root_check_precedes_log_changes(self):
stubs = self.prepare_preflight() + "\ncheck_root() { return 20; }\n"
log = self.root / "install.log"
log.write_bytes(b"previous log")
result = self.run_installer_function("main", stubs)
self.assert_installation_preserved(result)
self.assertEqual(log.read_bytes(), b"previous log")
self.assertFalse((self.root / "events").exists())
def test_log_creation_failure_stops_before_preflight(self):
stubs = self.prepare_preflight() + "\ninstall() { return 21; }\n"
result = self.run_installer_function("main", stubs)
self.assert_installation_preserved(result)
self.assertFalse((self.root / "events").exists())
if __name__ == "__main__": if __name__ == "__main__":
unittest.main(verbosity=2) unittest.main(verbosity=2)
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Check device/event selection using fixtures; does not switch real devices.
This models the basic equality/glob predicates used by aic.rules. Run
udevadm verify separately to validate syntax with the real udev parser.
"""
from fnmatch import fnmatchcase
from pathlib import Path
import re
import unittest
RULES = (Path(__file__).resolve().parents[2] / "aic.rules").read_text().splitlines()
def matching_rules(device):
result = []
for rule in RULES:
if not rule.strip() or rule.lstrip().startswith("#"):
continue
predicates = re.findall(r'([A-Z]+(?:\{[^}]+\})?)=="([^"]*)"', rule)
if predicates and all(fnmatchcase(device.get(key, ""), value) for key, value in predicates):
result.append(rule)
return result
class UdevTests(unittest.TestCase):
def pandora(self, **changes):
device = {"ACTION": "add", "SUBSYSTEM": "usb", "ENV{DEVTYPE}": "usb_device",
"ATTR{idVendor}": "1111", "ATTR{idProduct}": "1111", "ATTR{product}": "88M80"}
device.update(changes)
return device
def test_88m80_uses_event_bus_and_device_numbers(self):
for product in ("88M80", "88M80 USB WiFi"):
with self.subTest(product=product):
matches = matching_rules(self.pandora(**{"ATTR{product}": product}))
self.assertEqual(len(matches), 1)
# Both possible usb_modeswitch executable locations target this event's device.
self.assertEqual(matches[0].count("-b $attr{busnum} -g $attr{devnum}"), 2)
def test_generic_1111_flash_disk_is_not_switched(self):
for product in ("FLASH UDISK 2.30", "USB DISK", ""):
with self.subTest(product=product):
self.assertEqual(matching_rules(self.pandora(**{"ATTR{product}": product})), [])
def test_wrong_ids_interfaces_and_non_add_events_are_not_switched(self):
for changes in ({"ACTION": "change"}, {"ACTION": "remove"},
{"ENV{DEVTYPE}": "usb_interface"}, {"ATTR{idVendor}": "2222"},
{"ATTR{idProduct}": "8d80"}):
with self.subTest(changes=changes):
self.assertEqual(matching_rules(self.pandora(**changes)), [])
def test_eject_only_on_add_of_a_whole_supported_disk(self):
for product in ("5721", "5722", "5723", "5724", "5725", "5726", "5727", "572a", "572c", "572f"):
disk = {"ACTION": "add", "SUBSYSTEM": "block", "ENV{DEVTYPE}": "disk", "KERNEL": "sdb",
"ATTRS{idVendor}": "a69c", "ATTRS{idProduct}": product}
with self.subTest(product=product):
matches = matching_rules(disk)
self.assertEqual(len(matches), 1)
self.assertIn('/usr/bin/eject /dev/%k', matches[0])
for changes in ({"ACTION": "change"}, {"ACTION": "remove"},
{"ENV{DEVTYPE}": "partition", "KERNEL": "sdb1"}):
self.assertEqual(matching_rules(dict(disk, **changes)), [])
if __name__ == "__main__":
unittest.main(verbosity=2)