From 13e681252b359c960e62bbe8e0ef3e31d81df84b Mon Sep 17 00:00:00 2001 From: fuleyi Date: Mon, 24 Aug 2026 17:03:58 +0800 Subject: [PATCH] fix(inputdevices): fsync udev rule and rebuild on boot for touchpad MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Replace os.WriteFile with os.Create + f.Sync() to ensure the udev rule file is flushed to disk before close 2. On startup, call setTouchpadEnableViaUdev directly instead of setTouchpadEnable to avoid skipped udev rebuild when changed=false due to pre-initialized Enable field 3. Prevents touchpad disable state loss after forced power-off Log: fix touchpad disabled state lost after forced power-off because udev rule file write was not fsynced and startup rebuild was skipped by changed=false check Influence: 1. Disable touchpad, force power off, reboot and verify it stays disabled 2. Disable touchpad, normal reboot and verify it stays disabled 3. Enable touchpad, force power off, reboot and verify it stays enabled fix(inputdevices): 修复强制关机后触控板禁用状态丢失 1. 将 os.WriteFile 改为 os.Create + f.Sync() 确保写入 udev 规则文件时强制刷盘,防止断电丢失 2. 启动恢复时直接调用 setTouchpadEnableViaUdev 重建 udev 规 则,避免 changed=false 导致跳过写入 3. 修复强制关机后触控板禁用失效的问题 Log: 修复强制关机后触控板禁用状态丢失,原因是 udev 规则文件 写入未调用 fsync 导致断电丢失,且启动恢复逻辑因 changed=false 跳过了 udev 规则重建 Influence: 1. 禁用触控板后强制关机,重启后确认触控板仍为禁用状态 2. 禁用触控板后正常重启,确认触控板仍为禁用状态 3. 启用触控板后强制关机,重启后确认触控板仍为启用状态 PMS: BUG-374789 --- go.mod | 2 +- go.sum | 4 +- system/inputdevices1/inputdevices.go | 7 +- system/inputdevices1/touchpad.go | 105 +++++++++++++++++++++++---- 4 files changed, 100 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 576009df6..61f2a17d8 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/godbus/dbus/v5 v5.1.0 github.com/jouyouyun/hardware v0.1.8 github.com/linuxdeepin/dde-api v0.0.0-20260511093853-07ca3f2f1232 - github.com/linuxdeepin/go-dbus-factory v0.0.0-20260604114052-5398d61b0886 + github.com/linuxdeepin/go-dbus-factory v0.0.0-20260804093636-709f57a20d4a github.com/linuxdeepin/go-gir v0.0.0-20251204113853-1873b5530f50 github.com/linuxdeepin/go-lib v0.0.0-20260205120541-a1f572ce1442 github.com/linuxdeepin/go-x11-client v0.0.0-20240415051504-c8e43d028ff9 diff --git a/go.sum b/go.sum index 81738e4f2..6663c1c8d 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/linuxdeepin/dde-api v0.0.0-20260511093853-07ca3f2f1232 h1:1jSUHQ4FlFGhhhLiCzbXvcH+JIW92gGLWOQExbl/6SI= github.com/linuxdeepin/dde-api v0.0.0-20260511093853-07ca3f2f1232/go.mod h1:etFJ3bz0/U3wklOLPLn7A4DM5VBQJ4yHhHaWpCEzzV8= -github.com/linuxdeepin/go-dbus-factory v0.0.0-20260604114052-5398d61b0886 h1:5RFVYFt5HjQ4P/Q/zIK1cfmbuoNX+T1F3WbXn1Sh/6E= -github.com/linuxdeepin/go-dbus-factory v0.0.0-20260604114052-5398d61b0886/go.mod h1:dfpixHkqiijg3P7w5QArkMyC7+vlHcutN5R6zHIn8is= +github.com/linuxdeepin/go-dbus-factory v0.0.0-20260804093636-709f57a20d4a h1:OLlPMMm/LdxpSjGuZhmK0hkM6ovwzYuXVB2EIFmMofY= +github.com/linuxdeepin/go-dbus-factory v0.0.0-20260804093636-709f57a20d4a/go.mod h1:dfpixHkqiijg3P7w5QArkMyC7+vlHcutN5R6zHIn8is= github.com/linuxdeepin/go-gir v0.0.0-20250812023606-b28aaee32ac9/go.mod h1:a0tox5vepTQu5iO6rdKc4diGT+fkyXZlRROM8ULEvaI= github.com/linuxdeepin/go-gir v0.0.0-20251204113853-1873b5530f50 h1:CLVQdE+YgfvHD2EadN/0hPIhCbbu5cdGyDu4brW9pyw= github.com/linuxdeepin/go-gir v0.0.0-20251204113853-1873b5530f50/go.mod h1:a0tox5vepTQu5iO6rdKc4diGT+fkyXZlRROM8ULEvaI= diff --git a/system/inputdevices1/inputdevices.go b/system/inputdevices1/inputdevices.go index 423941795..bf342384c 100644 --- a/system/inputdevices1/inputdevices.go +++ b/system/inputdevices1/inputdevices.go @@ -93,7 +93,12 @@ func (m *InputDevices) init() { logger.Warning(err) return } - err = m.touchpad.setTouchpadEnable(v.Value().(bool)) + enabled := v.Value().(bool) + // 启动恢复:直接根据 dconfig 重建 udev 规则文件。 + // 不调用 setTouchpadEnable,因为 newTouchpad 已将 Enable 设为 dconfig 值, + // setPropEnable 会判定 changed=false 从而跳过 udev 写入, + // 导致强制关机后 udev 规则文件丢失但无法重建。 + err = m.touchpad.setTouchpadEnableViaUdev(enabled) if err != nil { logger.Warning(err) } diff --git a/system/inputdevices1/touchpad.go b/system/inputdevices1/touchpad.go index 6f1a77993..5aab5fee6 100644 --- a/system/inputdevices1/touchpad.go +++ b/system/inputdevices1/touchpad.go @@ -129,25 +129,102 @@ func (t *Touchpad) setTouchpadEnableViaUdev(enabled bool) error { return err } logger.Info("removed udev rule file:", udevRuleFile) - } else { - // 禁用:生成动态的 udev 规则内容 - udevRuleContent := generateUdevRuleContent() - - // 检查文件是否已存在且内容相同 - existingContent, err := os.ReadFile(udevRuleFile) - if err == nil && string(existingContent) == udevRuleContent { - logger.Debug("udev rule file already exists with correct content, skip writing") - return nil + return t.refreshTouchpadDevices() + } + + // 禁用:写入 udev 规则文件;若内容未变则无需刷新设备 + changed, err := writeUdevRuleFile(generateUdevRuleContent()) + if err != nil { + return err + } + if !changed { + return nil + } + return t.refreshTouchpadDevices() +} + +// writeUdevRuleFile 以 0644 权限原子地写入 udev 规则文件,并确保数据与目录项落盘。 +// 返回 changed 表示文件内容是否发生变化(true=已写入或新建,false=内容未变跳过写入)。 +// 安全要点: +// - fast path 内容已正确时仍收窄权限,防止外部改宽的 0666 等遗留 +// - 用 O_EXCL 原子探测是否为首次新建,避免 Stat/OpenFile TOCTOU 竞态 +// - 先 Chmod 成功再 Truncate,避免权限修改失败时 O_TRUNC 已清空原内容 +// - 文件数据 fsync 后,首次创建还需 fsync 父目录以保证目录项断电不丢 +func writeUdevRuleFile(udevRuleContent string) (changed bool, err error) { + // 内容已正确时,仅做权限兜底与父目录 best-effort sync 后直接返回 + if existingContent, rerr := os.ReadFile(udevRuleFile); rerr == nil && + string(existingContent) == udevRuleContent { + if err := os.Chmod(udevRuleFile, 0644); err != nil { + return false, err } + syncDirBestEffort(filepath.Dir(udevRuleFile)) + logger.Debug("udev rule file already exists with correct content, skip writing") + return false, nil + } - // 创建或覆盖 udev 规则文件 - if err := os.WriteFile(udevRuleFile, []byte(udevRuleContent), 0644); err != nil { - return err + // 尝试以 O_EXCL 独占创建,原子判断是否为首次新建, + // 避免 Stat/OpenFile 之间因并发删除导致的 TOCTOU 竞态使父目录漏 sync + f, err := os.OpenFile(udevRuleFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) + isNew := err == nil + if err != nil && !os.IsExist(err) { + return false, err + } + if os.IsExist(err) { + // 文件已存在,以 O_WRONLY 打开但不立即截断 + f, err = os.OpenFile(udevRuleFile, os.O_WRONLY, 0644) + if err != nil { + return false, err + } + } + // 先收窄权限,成功后再截断写入,避免 Chmod 失败时 O_TRUNC 已清空原内容导致规则丢失 + if err := f.Chmod(0644); err != nil { + f.Close() + return false, err + } + if err := f.Truncate(0); err != nil { + f.Close() + return false, err + } + if _, err := f.Write([]byte(udevRuleContent)); err != nil { + f.Close() + return false, err + } + // fsync 确保文件数据落盘, + // 防止强制关机(断电)时 page cache 丢失导致规则文件丢失 + if err := f.Sync(); err != nil { + f.Close() + return false, err + } + if err := f.Close(); err != nil { + return false, err + } + // 首次创建文件时,还需 fsync 父目录以确保目录项落盘, + // 否则断电后文件数据虽已持久但目录项丢失,规则文件仍会缺失 + if isNew { + dir, err := os.Open(filepath.Dir(udevRuleFile)) + if err != nil { + return false, err + } + err = dir.Sync() + dir.Close() + if err != nil { + return false, err } - logger.Info("created udev rule file:", udevRuleFile) } + logger.Info("created udev rule file:", udevRuleFile) + return true, nil +} - return t.refreshTouchpadDevices() +// syncDirBestEffort 尽力同步目录元数据,失败仅记录不返回错误。 +func syncDirBestEffort(dir string) { + d, err := os.Open(dir) + if err != nil { + return + } + defer d.Close() + if err := d.Sync(); err != nil { + logger.Warning("failed to sync parent directory:", err) + } } // reloadUdevRules 重新加载 udev 规则