mirror of
https://github.com/shenmintao/aic8800d80.git
synced 2026-11-10 16:49:17 +00:00
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:
8 files changed
+285
-26
No files matched your search
@@ -0,0 +1,25 @@
|
|||||||
|
# 维护原则
|
||||||
|
|
||||||
|
本文件适用于整个仓库。
|
||||||
|
|
||||||
|
## 与上游保持一致
|
||||||
|
|
||||||
|
- 对继承自 Radxa/AIC 上游的驱动、固件、加载协议和运行行为,默认保持一致,优先采用上游已经接受的修复。
|
||||||
|
- 对这些上游组件,除非修复有证据支持的重要 bug,否则不引入会增加下游维护负担的改动。
|
||||||
|
- 重要 bug 包括实际影响设备功能、稳定性、安全性或受支持内核构建的问题。仅凭代码风格、理论风险或“体验更好”不足以认定为重要 bug。
|
||||||
|
- 修复应尽量小且独立,便于审查和同步未来上游版本。避免顺带重构、清理、增加功能或建立新的策略与兼容框架。
|
||||||
|
|
||||||
|
## 本仓库维护的工具
|
||||||
|
|
||||||
|
- 安装脚本、打包、诊断和文档应与上游驱动改动分开评估;不能仅因上游没有同类实现就否决。
|
||||||
|
- 这些改动仍应简单、必要、有验证,避免不必要的默认行为和维护成本。
|
||||||
|
- 试编译应评估失败时能否保住已有安装及额外编译耗时;固件备份应评估恢复价值、重复备份和磁盘占用,避免无限累积。它们本身不构成驱动或固件偏离上游。
|
||||||
|
|
||||||
|
## PR 和 issue 处理
|
||||||
|
|
||||||
|
- 先核实当前分支、上游实现和最新讨论,避免重复处理已合并或仍在等待实机验证的问题。
|
||||||
|
- 不因 PR 使用了 AI、描述完整或声称测试通过而接受或拒绝;以可核实的问题、实际代码、独立验证及维护成本为依据。
|
||||||
|
- 合入前说明问题的触发条件、影响、与上游的差异和验证结果;证据不足时继续保留为待验证项。
|
||||||
|
- 编译通过、模拟测试和硬件测试必须明确区分,不得用前两者替代实机结论。
|
||||||
|
- 涉及两个维护分支的必要修复应分别评估并同步,保留 `main` 与 `legacy-mcu1` 各自匹配的固件和加载配置,不能直接混合固件或整体合并分支。
|
||||||
|
- 用户本次会话的明确指示优先于本文件。
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'"
|
||||||
@@ -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="/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"
|
INSTALL_LOG="/tmp/aic8800d80_install.log"
|
||||||
|
fi
|
||||||
|
|
||||||
read_dkms_conf_value() {
|
read_dkms_conf_value() {
|
||||||
local key="$1"
|
local key="$1"
|
||||||
|
|||||||
+35
-15
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
Reference in new issue
Block a user