惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
The Exploit Database - CXSecurity.com
N
News and Events Feed by Topic
Latest news
Latest news
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
CXSECURITY Database RSS Feed - CXSecurity.com
IT之家
IT之家
V
V2EX
WordPress大学
WordPress大学
Apple Machine Learning Research
Apple Machine Learning Research
Cisco Talos Blog
Cisco Talos Blog
K
Kaspersky official blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
S
SegmentFault 最新的问题
小众软件
小众软件
A
Arctic Wolf
酷 壳 – CoolShell
酷 壳 – CoolShell
腾讯CDC
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
G
GRAHAM CLULEY
罗磊的独立博客
T
Tor Project blog
C
Cisco Blogs
美团技术团队
博客园 - Franky
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
T
Threat Research - Cisco Blogs
Cyberwarzone
Cyberwarzone
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
有赞技术团队
有赞技术团队
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Security Latest
Security Latest
博客园 - 司徒正美
Hugging Face - Blog
Hugging Face - Blog
Spread Privacy
Spread Privacy
J
Java Code Geeks
C
CERT Recently Published Vulnerability Notes
大猫的无限游戏
大猫的无限游戏
S
Securelist
The Cloudflare Blog
博客园 - 叶小钗
D
Darknet – Hacking Tools, Hacker News & Cyber Security
阮一峰的网络日志
阮一峰的网络日志
雷峰网
雷峰网
Project Zero
Project Zero

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日 mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日 WSL9x —— 在 Windows 9x 里跑 Linux 内核 6.19 用 ox.el 做你想做的事 —— org-export 高级编程指南 读:Hot-wiring the Lisp Machine —— 用纯 Elisp 构建零依赖的 Org 静态站点生成器 Elisp 性能优化的六个实战教训 - 暗无天日 fcitx5 下 Emacs 无法切换输入法的排查 - 暗无天日 ERT 测试交互命令的三种方式 - 暗无天日 SEM Assistant: 当 Elisp 守护进程遇上 LLM 用 dmsg 给 Elisp 加上结构化调试日志 用 org-habit 追踪非每日习惯 - 暗无天日 Clojure X-Men:当编程语言特性变成超能力 - 暗无天日 TIL: 用 diff-hl 在 fringe 中显示 git 变更 读:llm-test —— 用 LLM agent 驱动 Emacs 测试 TIL: AI 时代的橡皮鸭调试 - 暗无天日 fcitx 启动后键盘输入卡顿的排查 - 暗无天日 TIL: 早期网页的图片热区导航 - 暗无天日 读 Seeing the Whole System 用 Emacs 自动生成每周链接推荐 - 暗无天日 读:ASCII control characters in my terminal 读 What to learn - 暗无天日 Lisp 的括号之痛——一个愚人节玩笑揭开的老伤疤 - 暗无天日 一本书该"线性读"还是"并行读" - 暗无天日 读 How to Monetize a Blog:一篇伪装成变现指南的讽刺文 Python Mock 第三方依赖的四种策略 - 暗无天日 Emacs Lisp 热重载实用指南 - 暗无天日 Prot 的 Emacs 配置哲学 - 暗无天日 TIL: 从直播对谈中学到的三个 Emacs 技巧 - 暗无天日 TIL: 自动使用项目虚拟环境的 Python - 暗无天日 TIL: 让 Help buffer 自动获得焦点 一条命令让本地开发用上 HTTPS —— slim 工具介绍 用 fsck 检查和修复 Linux 文件系统 排查Linux进程"卡死"实战:从strace到gdb全流程 - 暗无天日 PostgreSQL 索引:从基础到你可能不知道的高级用法 - 暗无天日 用 .pdbrc 自定义 Python 调试器 ANSI 转义码的标准化现状 - 暗无天日 终端程序的潜规则 - 暗无天日 PARA Org-mode 测试配置 - 暗无天日 AI越强越辣鸡?控制论说这是必然的 - 暗无天日 AI 越强越需要你盯着——反馈循环实操指南 - 暗无天日 你的AI代理正在偷你的密钥——四种你没想到的泄露通道 - 暗无天日 LLM 在 DevOps 中的三种角色 - 暗无天日 写作风格的反建议 - 暗无天日 反驳本质复杂性——Dan Luu 论为什么《没有银弹》错了 - 暗无天日 文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱 - 暗无天日 AI 时代的 PARA 方法:用 Org-mode 和 AI 打造个人知识管理系统 Linux 数据去重学习笔记 - 暗无天日 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field - 暗无天日 X11 Forwarding 排障指南 - 暗无天日 IP欺骗端口扫描:当别人冒充你去扫描别人 - 暗无天日 Linux 输入栈全景解析:从硬件按键到屏幕响应 - 暗无天日 Unix 系统中那些被埋没的配置开关——以 FontConfig 为例 - 暗无天日 在Linux上限制儿童使用电脑 - 暗无天日 GIF不仅仅是一种图片格式——用GIF流做些奇怪的事 - 暗无天日 Leiningen 学习笔记:Clojure 项目构建与管理从入门到实战配置 - 暗无天日 Google SRE Book 读书笔记 - 暗无天日 yes 管道 head 发生了什么 - 暗无天日 为什么 nohup 在 crontab 中不起作用 Bash中的Indirection与Nameref - 暗无天日 Linux PAM 简介 - 暗无天日 从Linux ISO文件启动计算机 - 暗无天日 用 Bash 打造一个Screen Locker 用GitHub Actions自动构建EGO博客 - 暗无天日 blocking I/O 的作用 - 暗无天日 mobileog 手机端同步提示Error:2 No such file 的解决方法 回收 WSL2 VHDX 文件占用空间 使用 org-mode columnview 生成任务列表 - 暗无天日 Emacs 作为 MPD 客户端 - 暗无天日 移动文件路径却不破坏org file link的方法 - 暗无天日 如何合理的导出help link 成HTML - 暗无天日 笑话理解之Biology - 暗无天日
浏览器文件选择框弹不出来——xdg-desktop-portal 启动时序陷阱 - 暗无天日
lujun9972, Claude · 2026-06-22 · via 暗无天日

目录

  • 故障现象
  • 排查过程
    • 第一步:排除代码问题
    • 第二步:检查 portal 服务状态
    • 第三步:为什么找不到 display?
    • 第四步:为什么 portal-gtk 会这么早启动?
    • 第五步:D-Bus 激活绕过了时序保护
    • 第六步:发现 service unit 的设计缺陷
  • 根因分析(5 Whys)
    • Why #1:为什么文件选择框弹不出来?
    • Why #2:为什么 portal-gtk 启动失败?
    • Why #3:为什么 systemd user session 里没有 DISPLAY?
    • Why #4:为什么 portal-gtk 会在没有 DISPLAY 的时候被启动?
    • Why #5:为什么 unit 设计没有阻止这种情况?
    • 识别出的根因
  • 解决方案
    • 已采用的修复
    • 理想的 upstream 修复

故障现象

我在自己写的打字练习网站后台管理页面(AdminPage)上加了一个文件上传功能——点击按钮选择本地视频文件。代码写好后,点击按钮……什么也没发生。没有文件选择框,没有报错,没有反应。

不只是 Chrome ,Firefox 也一样——点击按钮毫无反应。两个浏览器都弹不出文件选择框。

排查过程

第一步:排除代码问题

我当时还没意识到问题的严重性,以为是前端代码的问题,先后尝试了四种不同的 CSS + JS 方案来触发 <input type"file">= :

  1. display:none + JS .click() → 不弹
  2. clip:rect(0,0,0,0) + JS .click() → 不弹
  3. <label> + 离屏 input → 不弹
  4. 透明 overlay 直接盖在按钮上(用户点的是真正的 <input> ) → 还是不弹!

结果四种方案全部失败,而且两个浏览器都不行。

  • 只凭"多浏览器不行"→ 可能是代码 bug ,也可能是系统问题
  • 只凭"多方案不行"→ 可能是某个浏览器对所有方案都不兼容
  • 两者叠加 → 如果是代码 bug ,不可能四种方案全写错;如果是浏览器兼容性,不可能两个浏览器同时排斥所有方案。问题一定在更底层

小知识:Linux 下浏览器的文件选择框是谁弹出来的?

在 Windows 和 macOS 上,浏览器直接调用操作系统的原生文件对话框 API ,简单直接。

但在 Linux 上,浏览器不自己画文件对话框。Chrome 和 Firefox 都通过一个叫 xdg-desktop-portal 的中间层来请求桌面环境弹出对话框。这个 portal 的设计初衷是让沙盒应用(Flatpak、Snap)能安全地访问系统资源,后来主流浏览器都采用了这套机制。

流程是:

浏览器需要 file dialog
  → 通过 D-Bus 询问 xdg-desktop-portal
    → portal 找到当前桌面环境的后端(如 portal-gtk)
      → 后端调用 GTK/Qt 弹出原生文件对话框
        → 用户选择文件,结果传回浏览器

如果 portal 或它的后端挂了,浏览器需要"弹文件对话框"时就什么都做不了——而且不会报错,因为从浏览器的角度看,它只是发了一个 D-Bus 请求,没收到回复而已。

第二步:检查 portal 服务状态

systemctl --user status xdg-desktop-portal-gtk
× xdg-desktop-portal-gtk.service - Portal service (GTK/GNOME implementation)
     Loaded: loaded (/usr/lib/systemd/user/xdg-desktop-portal-gtk.service; static)
     Active: failed (Result: exit-code)
     Process: 1277 ExecStart=/usr/lib/xdg-desktop-portal-gtk (code=exited, status=1/FAILURE)
Main PID: 1277 (code=exited, status=1/FAILURE)

6月 06 04:30 systemd[1277]: Started Portal service (GTK/GNOME implementation).
6月 06 04:30 xdg-desktop-portal-gtk[1277]: cannot open display:
6月 06 04:30 systemd[1277]: xdg-desktop-portal-gtk.service: Failed with result 'exit-code'.

BINGO! 服务失败了,错误信息是 cannot open display: ——它找不到显示服务器!这也解释了为什么 Chrome 和 Firefox 都弹不出文件选择框——它们依赖的是同一个已死掉的 portal 后端。

第三步:为什么找不到 display?

这个服务在开机时就尝试启动了(PID 1277 ,非常小的数字,说明是系统启动早期)。但我的系统是通过 startx 启动图形界面的:

时间线:
─────────────────────────────────────────────────────────→

T1: 在 tty 输入用户名密码登录
    │
    ├─ systemd --user 启动(PID ~1277)
    │   └─ xdg-desktop-portal-gtk.service 尝试启动
    │       → 报错 "cannot open display:"
    │       → 此时还没有 X11,DISPLAY 不存在!
    │
T2: 在 tty 里输入 startx
    │
    ├─ Xorg 启动 → DISPLAY=:0 出现
    │   但只存在于 startx 的子进程环境中
    │   systemd user session 仍然没有 DISPLAY
    │
T3: awesome WM 启动,桌面出现
    │
T4: 打开浏览器 → 需要 portal → portal 后端已挂 → 文件对话框弹不出来

问题出在 启动时序 :portal 服务在 X11 启动之前就尝试运行了,自然打不开 display 。

第四步:为什么 portal-gtk 会这么早启动?

查看它的 service unit :

systemctl --user cat xdg-desktop-portal-gtk.service
[Unit]
Description=Portal service (GTK/GNOME implementation)
PartOf=graphical-session.target
After=graphical-session.target

[Service]
Type=dbus
BusName=org.freedesktop.impl.portal.desktop.gtk
ExecStart=/usr/lib/xdg-desktop-portal-gtk

它声明了 After=graphical-session.target ,意思是"应该在图形会话准备好之后再启动"。但关键是—— graphical-session.target 在我的系统上 从来没被激活过

systemctl --user is-active graphical-session.target

小知识:=graphical-session.target= 是谁激活的?

在 Linux 桌面的生态中,=graphical-session.target= 是一个 systemd target ,代表"图形用户会话已就绪"。它不是一个普通服务,而是一个 里程碑标记 ,告诉其他依赖图形环境的服务"现在可以安全启动了"。

正常情况下,谁负责激活它?

  • GDM(GNOME Display Manager) :用户在图形登录界面输入密码后,GDM 启动 X11/Wayland ,然后通过 PAM 模块激活 graphical-session.target
  • SDDM(KDE) 、*LightDM* 等:同理,显示管理器负责整个启动链路

关键点:显示管理器保证 先有图形环境,再激活 target 。服务单元只需要声明 After=graphical-session.target 就能确保启动时 DISPLAY 已经存在。

但如果你用 startx 启动桌面,没有显示管理器来激活这个 target ,它就一直是 inactive

第五步:D-Bus 激活绕过了时序保护

那 portal-gtk 是怎么被启动的呢?它是通过 D-Bus 自动激活的:

cat /usr/share/dbus-1/services/org.freedesktop.impl.portal.desktop.gtk.service
[D-BUS Service]
Name=org.freedesktop.impl.portal.desktop.gtk
Exec=/usr/lib/xdg-desktop-portal-gtk
SystemdService=xdg-desktop-portal-gtk.service

当主 portal 服务(=xdg-desktop-portal=)尝试通过 D-Bus 访问 GTK 后端时,D-Bus 守护进程发现这个服务没运行,就自动让 systemd 启动它。这个激活过程 不受 After=graphical-session.target 约束 ,因为 systemd 认为这个 target 已经处于终态(inactive ),不需要等待。

小知识:D-Bus 自动激活(D-Bus Activation)

D-Bus 有一个很方便的机制:当某个 D-Bus 服务名被请求但没有人提供时,D-Bus 守护进程会查找 /usr/share/dbus-1/services/ 下的配置文件,找到对应的 SystemdService ,然后让 systemd 启动它。

这意味着:即使一个服务没有被 enable ,只要它的 D-Bus 配置文件在,有其他程序请求它提供的服务时,它就会被自动拉起。这就是为什么 portal-gtk 虽然没有被手动 enable ,却在开机时就被启动了。

第六步:发现 service unit 的设计缺陷

对比 GNOME 版的 portal service :

systemctl --user cat xdg-desktop-portal-gnome.service
[Unit]
Description=Portal service (GNOME implementation)
After=graphical-session.target
Requisite=graphical-session.target    ← 注意这行!
PartOf=graphical-session.target

[Service]
Type=dbus
BusName=org.freedesktop.impl.portal.desktop.gnome
ExecStart=/usr/lib/xdg-desktop-portal-gnome

GNOME 版多了一行 Requisite=graphical-session.target ,意思是"如果 graphical-session.target 没激活,就 拒绝启动 "。这是正确的防御措施——在纯 tty 环境下,GNOME portal 不会尝试启动,避免了无意义的失败。

小知识:=After= vs Requisite vs PartOf 的区别

systemd 的依赖指令很多,容易混淆:

指令 含义
After 排序约束:如果两者同时要启动,先启动目标单元
Requires 硬依赖:目标失败时,自己也跟着停
Requisite 门卫:目标没启动就直接拒绝启动自己(比 Requires 更严格)
PartOf 跟随:目标的 stop/restart 会传播给自己
Wants 软依赖:建议启动目标,但目标失败了不影响自己

在本案中,GTK portal 只有 AfterPartOf ,缺少 Requisite 。这导致在 graphical-session.target 未激活的情况下,D-Bus 激活仍然能把它拉起来,然后它去打开一个不存在的 display ,就崩溃了。

根因分析(5 Whys)

Why #1:为什么文件选择框弹不出来?

**回答**:=xdg-desktop-portal-gtk= 服务启动失败(=cannot open display:=),Chrome 和 Firefox 都依赖这个服务来弹出文件选择框。

**证据**:=systemctl --user status xdg-desktop-portal-gtk= 显示 failed (exit-code) ,两个浏览器都无反应。

---

Why #2:为什么 portal-gtk 启动失败?

**回答**:服务启动时 systemd user session 中不存在 DISPLAY 环境变量。没有 DISPLAY ,GTK 程序无法连接显示服务器。

**证据**:错误日志 cannot open display: ;服务启动时间(PID 1277 )早于 startx 执行时间。

---

Why #3:为什么 systemd user session 里没有 DISPLAY?

**回答**:因为用户通过 startx 启动图形界面。=systemd --user= 在 tty 登录时就启动了(那时没有 DISPLAY ),而 startx 在之后才创建 DISPLAY=:0 ,但这个变量只存在于 startx 的子进程环境中,不会自动传递给 systemd user session 。

**证据**:=startx= 不像 GDM/SDDM 那样调用 systemctl --user import-environment

---

Why #4:为什么 portal-gtk 会在没有 DISPLAY 的时候被启动?

**回答**:因为 D-Bus 自动激活机制绕过了 After=graphical-session.target 的排序约束。当 graphical-session.target 处于 inactive 状态时,=After= 指令不产生阻止效果——systemd 认为"不需要等待一个已经处于终态的单元"。

**证据**:=graphical-session.target= 的状态是 inactive (dead) ,而 portal-gtk 仍然被 D-Bus 激活拉起了。

---

Why #5:为什么 unit 设计没有阻止这种情况?

**回答**:=xdg-desktop-portal-gtk.service= 缺少 Requisite=graphical-session.target 。对比 xdg-desktop-portal-gnome.service ,GNOME 版有 Requisite 保护(target 没激活就拒绝启动),而 GTK 版漏掉了这个防御。这是 upstream 的设计疏漏。

**证据**:GNOME 版 unit 文件有 Requisite=graphical-session.target ,GTK 版没有。

---

识别出的根因

层级 根因
直接 portal-gtk 因缺少 DISPLAY 启动失败
系统 startx 不像显示管理器那样将 DISPLAY 导入 systemd user session
设计 xdg-desktop-portal-gtk.service 缺少 Requisite=graphical-session.target

解决方案

已采用的修复

~/.xinitrc 中添加两行,在 X11 启动后、窗口管理器启动前执行:

systemctl --user import-environment DISPLAY 2>/dev/null
systemctl --user restart xdg-desktop-portal-gtk 2>/dev/null

小知识:=systemctl --user import-environment= 是什么?

每个进程都有自己的环境变量副本。=startx= 启动的子进程(包括 awesome WM 、浏览器等)继承了 shell 的 DISPLAY:0 。但 systemd --user 实例是你在 tty 登录时就启动的,那时 DISPLAY 还不存在。

import-environment DISPLAY 的作用是把当前 shell 的 DISPLAY 变量"导入"到 systemd user session 的环境中。这样 systemd 启动的所有用户服务就都能看到 DISPLAY:0 了。

修改后完整的 ~/.xinitrc 相关部分:

systemctl --user import-environment DISPLAY 2>/dev/null
systemctl --user restart xdg-desktop-portal-gtk 2>/dev/null

export GTK_IM_MODULE=fcitx5
export QT_IM_MODULE=fcitx5
export XMODIFIERS="@im=fcitx5"
fcitx5 &

exec awesome

理想的 upstream 修复

xdg-desktop-portal-gtk.service 应该加上 Requisite=graphical-session.target ,与 GNOME 版保持一致:

[Unit]
Description=Portal service (GTK/GNOME implementation)
Requisite=graphical-session.target    ← 加上这一行
PartOf=graphical-session.target
After=graphical-session.target

这样在没有图形环境时,portal-gtk 不会尝试启动。等到 .xinitrcimport-environmentrestart 执行时,它才能正常工作。