2025/10/22

ESP32-DevKitC V4 與 ESP32 系列產品的關係


產品分層架構

ESP32 的產品線可以分為三個層級:晶片(SoC)、模組(Module)、開發板(Development Board)。這三層形成了從矽晶片到最終使用者產品的完整架構。

第一層:晶片(SoC - System on Chip)

這是最底層的矽晶片元件,是所有後續產品的核心。常見的 ESP32 晶片包括:

第二層:模組(Module)

模組是將晶片與必要的周邊元件(如 Flash 記憶體、晶振、天線等)整合在小型 PCB 上的產品。這些模組已通過必要的認證,可以直接用於產品設計。常見的 ESP32 模組包括:

第三層:開發板(Development Board / EVB)

開發板是將模組焊接在一個完整的電路板上,並提供 USB 連接、電源管理、GPIO 引腳等便利開發的功能。開發板適合開發者和創客使用。常見的 ESP32 開發板包括:

ESP32-DevKitC V4 在產品線中的位置

所屬系列

ESP32-DevKitC V4 屬於 ESP32 原始系列(第一代 ESP32,使用 Xtensa LX6 雙核或單核處理器)。它不是以下變體系列:

與 ESP32 系列的技術關係

ESP32-DevKitC V4 使用的是標準的 ESP32 晶片,因此它在技術規格上與原始的 ESP32 系列完全相同。具體來說:

硬體構成

  • 預設搭載 ESP32-WROOM-32E 模組
  • 該模組內部採用 ESP32-D0WD-V3 晶片

核心技術規格

  • 處理器:Xtensa 雙核 32 位元 LX6,最高時脈 240 MHz
  • 指令集架構:Xtensa ISA
  • 記憶體:520 KB SRAM(快速記憶體),448 KB ROM(唯讀記憶體)
  • GPIO 引腳:34 個
  • 類比轉換器(ADC):2 個,各 12 位元
  • 數位轉換器(DAC):2 個,各 8 位元
  • SPI 介面:4 個
  • I2C 介面:2 個
  • UART 介面:3 個

差異

雖然核心規格相同,但使用方式有所不同:

  • 晶片或模組:需要設計客製 PCB、處理電源管理、連接 USB 轉換晶片等,適合產品設計工程師
  • 開發板:已整合所有必要電路,可直接透過 USB 連接電腦進行開發,適合開發者和創客使用

產品線對照

ESP32 原始系列的各層級產品及其適用對象:

產品層級產品名稱說明適用對象
晶片(SoC)ESP32-D0WD-V3雙核 Xtensa LX6 裸晶片晶片設計廠商
模組(Module)ESP32-WROOM-32E晶片 + Flash + 天線整合模組產品設計工程師
模組(Module)ESP32-WROVER-E晶片 + Flash + PSRAM + 天線產品設計工程師
模組(Module)ESP32-SOLO-1單核版本模組產品設計工程師
開發板(EVB)ESP32-DevKitC V4模組 + USB + 電源 + GPIO 引腳開發者、創客、學生
開發板(EVB)ESP32-DevKitM-1搭載 ESP32-MINI-1 小型模組開發者
開發板(EVB)ESP32-PICO-KIT-1搭載 ESP32-PICO-D4 模組開發者

實際應用情境

開發者和創客

選擇使用 ESP32-DevKitC V4 這類開發板的主要優點:

  • 開箱即用,透過 USB 直接連接電腦
  • 已整合電源管理和 USB 轉 UART 晶片
  • 所有 GPIO 引腳引出到排針,方便連接其他元件
  • 價格低廉,約 5 到 10 美元

產品設計工程師

選擇使用 ESP32-WROOM-32E 這類模組的主要優點:

  • 已通過 FCCCESRRC 等國際認證
  • 只需設計簡單的底板 PCB
  • 降低產品認證成本
  • 享受穩定可靠的供應鏈

大量生產廠商

直接使用 ESP32 晶片的主要優點:

  • 成本最低
  • 產品設計完全客製化
  • 可將產品體積最小化

總結

ESP32-DevKitC V4 與最初列出的 ESP32 系列的關係可以總結如下:

  • 核心架構相同:DevKitC V4 搭載的模組使用的正是最初列出的 ESP32 系列晶片(ESP32-D0WD-V3)
  • 技術規格一致:處理器、指令集架構、時脈、記憶體、周邊介面等技術規格與最初列出的 ESP32 系列完全相同
  • 產品形式不同:最初列出的是晶片和模組層級的技術規格,而 ESP32-DevKitC V4 是開發板層級的最終產品,但核心都是同一顆 ESP32 晶片
  • 官方開發板:ESP32-DevKitC V4 就是 Espressif 公司推出的官方開發板,代表了最受歡迎的通用 ESP32 開發解決方案

因此,ESP32-DevKitC V4 是 ESP32 原始系列的官方開發板實現,讓開發者能夠方便地使用和學習 ESP32 晶片的所有功能特性。

2025/10/14

GNOME Terminal 啟動延遲問題再次發生

 

問題描述

在執行 apt update 和 apt upgrade 之後,gnome-terminal 啟動延遲的問題再次出現。終端機在系統開機後需要 20-30 秒才能啟動,儘管先前的修正(停用 im-config 自動啟動 fcitx5 並建立 systemd 使用者服務)仍然有效。

症狀

  • GNOME Terminal 在系統開機後首次啟動時會有明顯延遲(約 26 秒)
  • 問題只發生在開機後的第一次啟動
  • 後續的啟動正常,沒有延遲
  • 先前的設定(im-config 設為 "none" 及自訂的 fcitx5.service)仍然完整

診斷過程

步驟 1:驗證先前的設定

確認先前的修正仍然有效:

$ im-config -l
 ibus fcitx5 xim

$ cat ~/.xinputrc
# im-config(8) generated on Fri, 10 Oct 2025 13:01:31 +0800
run_im none
# im-config signature: 0a0fbfefedad5558a9e9e2919b262f89  -

$ systemctl --user status fcitx5.service
● fcitx5.service - Fcitx5 Input Method
     Loaded: loaded (/home/charles-chang/.config/systemd/user/fcitx5.service; enabled; preset: enabled)
     Active: inactive (dead) since Tue 2025-10-14 08:32:41 CST

設定完整保留

步驟 2:檢查開機日誌的時序問題

$ journalctl --user -b 0 --no-pager | grep -E "(fcitx5|gnome-terminal|graphical-session)"

從開機序列中發現的關鍵資訊:

08:31:46 - graphical-session.target 尚未就緒
08:31:49 - 達到目標 graphical-session.target
08:31:49 - fcitx5.service 啟動
08:31:55 - 請求啟動 gnome-terminal
08:31:59 - fcitx5: DBus call error: org.freedesktop.DBus.Error.NoReply Method call timed out
08:32:04 - fcitx5: DBus call error: org.freedesktop.DBus.Error.NoReply Method call timed out
08:32:09 - fcitx5: DBus call error: org.freedesktop.DBus.Error.NoReply Method call timed out
08:32:14 - fcitx5: DBus call error: org.freedesktop.DBus.Error.NoReply Method call timed out
08:32:14 - fcitx5: Query portal value reaches retry limit
08:32:21 - gnome-terminal-server.service 啟動(請求後 26 秒!)

步驟 3:找出 Portal 服務問題

fcitx5 的日誌顯示 portalsettingmonitor.cpp 重複嘗試查詢桌面 portal 設定時發生錯誤。進一步檢查 portal 服務:

$ journalctl --user -b 0 --no-pager | grep -E "portal"

關鍵發現:

08:31:46 - 啟動 xdg-desktop-portal.service
08:31:46 - 啟用 org.freedesktop.impl.portal.desktop.gnome
08:31:46 - xdg-desktop-portal-gnome.service: Job failed with result 'dependency'
08:31:49 - fcitx5 啟動(portal 尚未就緒!)
08:32:36 - xdg-desktop-portal-gtk.service 以備用方案啟動(延遲 50 秒!)

根本原因分析

這個問題是從先前問題演變而來的:

先前的問題(已解決):

  • fcitx5 啟動太早(在 graphical-session.target 之前)
  • 透過讓 fcitx5 等待 graphical-session.target 來修正

目前的問題(apt upgrade 之後):

  1. xdg-desktop-portal-gnome.service 在 08:31:46 嘗試啟動,早於 graphical-session.target 完全就緒的時間(08:31:49)
  2. 由於 Requisite=graphical-session.target 相依性,GNOME portal 啟動失敗
  3. fcitx5 在 08:31:49 正確啟動(在 graphical-session.target 之後),但立即嘗試查詢 portal 設定
  4. portal 尚未就緒,導致 fcitx5 被阻塞並重試 4 次(每次逾時 5 秒 = 總共 20 秒)
  5. gnome-terminal 的啟動被阻塞,等待 fcitx5 完成初始化
  6. 在 fcitx5 放棄後,xdg-desktop-portal-gtk 以備用方案啟動,gnome-terminal 終於能夠啟動

為什麼在 apt upgrade 後發生:

套件更新可能修改了 xdg-desktop-portal 服務的時序或相依性,導致 GNOME portal 在開機序列中更早失敗。這造成了 fcitx5 和 portal 服務之間的新競爭條件。

解決方案

更新 fcitx5 systemd 服務,明確等待桌面 portal 服務:

檔案: ~/.config/systemd/user/fcitx5.service

[Unit]
Description=Fcitx5 Input Method
Documentation=man:fcitx5(1)
PartOf=graphical-session.target
After=graphical-session.target xdg-desktop-portal.service
Wants=graphical-session.target xdg-desktop-portal.service

[Service]
Type=simple
ExecStart=/usr/bin/fcitx5
ExecStartPost=/usr/bin/dbus-update-activation-environment --systemd GTK_IM_MODULE=fcitx QT_IM_MODULE=fcitx XMODIFIERS=@im=fcitx
Restart=on-failure
RestartSec=3

[Install]
WantedBy=graphical-session.target

變更內容:

  • 在 After= 指令中加入 xdg-desktop-portal.service:fcitx5 會等到 portal 就緒後才啟動
  • 在 Wants= 指令中加入 xdg-desktop-portal.service:確保 portal 服務被請求/啟動

套用修正:

$ systemctl --user daemon-reload
$ systemctl --user show fcitx5.service | grep -E "^(After|Wants)="
Wants=graphical-session.target xdg-desktop-portal.service
After=graphical-session.target basic.target xdg-desktop-portal.service app.slice

驗證結果

重新開機系統後:

  • gnome-terminal 立即啟動,沒有延遲
  • fcitx5 日誌中沒有 DBus 逾時錯誤
  • 正確的啟動序列:graphical-session.target → xdg-desktop-portal → fcitx5 → gnome-terminal

關鍵要點

  1. 服務相依性很重要:即使修正了最初的 im-config 競爭條件,其他相依性(如桌面 portal)仍可能引入新的時序問題
  2. 系統更新可能改變時序:套件更新可能改變服務啟動順序或相依性,暴露出先前隱藏的競爭條件
  3. 監控所有元件:問題不在於 fcitx5 相對於 graphical-session.target 的時序(這部分是正確的),而是它與 portal 服務的互動
  4. 適當的相依鏈:同時使用 After= 和 Wants= 確保服務以正確順序啟動,並且所需的服務確實被啟動

參考資料

2025/10/11

MCP Weather Server:SSE Transport

 

SSE (Server-Sent Events) transport,這是一個基於 HTTP 的傳輸方式,讓 MCP server 可以作為獨立的網路服務運行。

SSE Transport

SSE transport 有以下特點:

  • 透過 HTTP 通訊,而非標準輸入/輸出
  • server 作為獨立的 HTTP 服務運行
  • client 透過網路連線到 server
  • 使用 Server-Sent Events 實作雙向通訊
  • 可同時服務多個 client

架構

SSE server 的核心檔案位於 src/weather_server/sse_server.py,包含以下組件:

實作

1. 建立 MCP Server

建立 MCP server 並註冊:

from mcp.server import Server
from mcp.server.sse import SseServerTransport

mcp_server = Server("weather-server-sse")

@mcp_server.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="get_weather",
            description="Get current weather information for a city.",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "Name of the city to get weather for",
                    }
                },
                "required": ["city"],
            },
        )
    ]

2. 設定 SSE Transport

建立 SSE transport ,指定訊息處理的路徑:

sse = SseServerTransport("/messages")

3. 實作 HTTP Endpoints

SSE transport 需要兩個 HTTP endpoints:

GET /sse - 建立 SSE 連線

async def handle_sse(request: Request):
    logger.info(f"New SSE connection from {request.client.host}")

    async with sse.connect_sse(
        request.scope,
        request.receive,
        request._send,
    ) as streams:
        await mcp_server.run(
            streams[0],
            streams[1],
            mcp_server.create_initialization_options(),
        )

此 endpoint 接收來自 client 的連線請求,並建立持久的 SSE 連線。

POST /messages - 接收訊息

async def handle_messages(request: Request):
    await sse.handle_post_message(request.scope, request.receive, request._send)

此 endpoint 處理 client 發送的 POST 請求,實作雙向通訊的「上行」部分。

4. 建立 Starlette Application

from starlette.applications import Starlette
from starlette.routing import Route

app = Starlette(
    debug=True,
    routes=[
        Route("/sse", endpoint=handle_sse, methods=["GET"]),
        Route("/messages", endpoint=handle_messages, methods=["POST"]),
    ],
)

5. 啟動 Server

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(
        app,
        host="localhost",
        port=3000,
        log_level="info"
    )

執行 SSE Server

1. 直接執行

python -m weather_server.sse_server

server 將在 http://localhost:3000 啟動。

2. 使用 uv run(推薦)

uv run weather-server-sse
提示:確保已在 pyproject.toml 中定義了 script entry point。

配置 Claude Code

在 Claude Code 中使用 SSE server,需要在配置檔案中加入以下設定:

{
  "mcpServers": {
    "weather": {
      "url": "http://localhost:3000/sse"
    }
  }
}

stdio transport 不同,這裡使用 url 而非 command

SSE vs Stdio Transport 比較

特性Stdio TransportSSE Transport
通訊方式標準輸入/輸出HTTP (SSE + POST)
啟動方式由 client 啟動 process獨立運行的 HTTP server
生命週期與 client 綁定獨立於 client
多 client 支援否(一對一)是(一對多)
適用場景本地開發、簡單整合生產環境、多用戶服務

除錯技巧

查看 Server 日誌

SSE server 的日誌會直接輸出到啟動 server 的終端機:

INFO:     Started server process [12345]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://localhost:3000

測試連線

可以使用 curl 測試 SSE endpoint 是否正常運作:

curl -N http://localhost:3000/sse
注意:確保 server 已啟動且沒有其他程式佔用 port 3000。

結語

相關檔案:src/weather_server/sse_server.py:1

MCP 伺服器:stdio 傳輸實作

 

MCP

Model Context Protocol (MCP) 是 Anthropic 推出的協定,讓 AI 助理可以呼叫外部工具。透過 MCP,我們可以擴展 Claude 的能力,讓它存取資料或執行特定功能。

MCP 支援兩種傳輸方式:

  • stdio:伺服器作為子程序,透過標準輸入輸出溝通
  • SSE:伺服器獨立運行,透過 HTTP 溝通

本文介紹 stdio 方式,它比較簡單適合入門。

專案

建立一個簡單的天氣查詢 MCP 伺服器:

  • 提供 5 個城市的預設天氣資料
  • 定義 get_weather 工具讓 Claude 呼叫
  • 當使用者詢問天氣時,Claude 自動使用這個工具

環境準備

# 建立專案
mkdir weather-mcp-server
cd weather-mcp-server

# 初始化並安裝套件
uv init --python 3.11
uv add mcp

結構

weather-mcp-server/
├── .mcp.json                    # Claude Code 設定
├── src/
│   └── weather_server/
│       ├── __init__.py
│       ├── weather_data.py      # 天氣資料
│       └── stdio_server.py      # MCP 伺服器
└── pyproject.toml

步驟 1:定義天氣資料

檔案:src/weather_server/weather_data.py

from typing import TypedDict

class WeatherData(TypedDict):
    temperature: float
    condition: str
    humidity: int

WEATHER_DATA: dict[str, WeatherData] = {
    "New York": {"temperature": 22.0, "condition": "Sunny", "humidity": 60},
    "London": {"temperature": 12.0, "condition": "Rainy", "humidity": 85},
    "Tokyo": {"temperature": 24.0, "condition": "Sunny", "humidity": 65},
    "Sydney": {"temperature": 18.0, "condition": "Cloudy", "humidity": 70},
    "Paris": {"temperature": 15.0, "condition": "Foggy", "humidity": 80},
}

def get_weather(city: str) -> WeatherData | None:
    for city_name, data in WEATHER_DATA.items():
        if city_name.lower() == city.lower():
            return data
    return None

def get_supported_cities() -> list[str]:
    return list(WEATHER_DATA.keys())

步驟 2:實作 MCP 伺服器

檔案:src/weather_server/stdio_server.py

import asyncio
import logging
from typing import Any
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from .weather_data import get_weather, get_supported_cities

# 設定日誌
logging.basicConfig(
    level=logging.INFO,
    handlers=[
        logging.StreamHandler(),  # stderr
        logging.FileHandler('/tmp/weather-mcp-server.log')
    ]
)
logger = logging.getLogger("weather-server")

app = Server("weather-server")

@app.list_tools()
async def list_tools() -> list[Tool]:
    """定義可用的工具"""
    return [
        Tool(
            name="get_weather",
            description=f"查詢城市天氣。支援:{', '.join(get_supported_cities())}",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名稱"}
                },
                "required": ["city"],
            },
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
    """處理工具呼叫"""
    if name != "get_weather":
        raise ValueError(f"未知的工具:{name}")

    city = arguments.get("city")
    if not city:
        raise ValueError("缺少參數:city")

    logger.info(f"查詢:{city}")
    weather = get_weather(city)

    if weather is None:
        supported = get_supported_cities()
        text = f"沒有 '{city}' 的資料。支援:{', '.join(supported)}"
    else:
        text = (
            f"{city} 的天氣:\n"
            f"溫度:{weather['temperature']}°C\n"
            f"狀況:{weather['condition']}\n"
            f"濕度:{weather['humidity']}%"
        )

    return [TextContent(type="text", text=text)]

async def main():
    logger.info("啟動天氣 MCP 伺服器")
    async with stdio_server() as (read_stream, write_stream):
        await app.run(read_stream, write_stream, app.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())

步驟 3:設定 Claude Code

檔案:.mcp.json

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": ["run", "python", "-m", "weather_server.stdio_server"],
      "env": {"PYTHONPATH": "src"}
    }
  }
}

關鍵概念

為什麼要用 -m 參數?

因為程式碼使用相對引入(from .weather_data import ...),必須作為模組執行。

# 錯誤
python src/weather_server/stdio_server.py

# 正確
PYTHONPATH=src python -m weather_server.stdio_server

stdio 傳輸運作原理

Claude Code 啟動時:
  1. 讀取 .mcp.json
  2. 執行指定指令(建立子程序)
  3. 透過 stdin/stdout 溝通(JSON-RPC)
  4. 日誌輸出到 stderr(被捕捉但不顯示)

為什麼需要檔案日誌?

因為 stderr 被 Claude Code 捕捉後不會顯示,所以同時寫到檔案。監看日誌:

tail -f /tmp/weather-mcp-server.log

測試

手動測試伺服器

PYTHONPATH=src uv run python -m weather_server.stdio_server

應該看到啟動訊息,按 Ctrl+C 停止。

在 Claude Code 中測試

  1. 重新啟動 Claude Code
  2. 執行 /mcp 確認 weather 伺服器已連接
  3. 詢問 Claude:"東京現在的天氣如何?"
  4. 另開終端機監看日誌:tail -f /tmp/weather-mcp-server.log

Tool 定義說明

Tool(
    name="get_weather",           # 工具名稱
    description="...",            # 告訴 Claude 這個工具的用途
    inputSchema={                 # JSON Schema 定義參數
        "type": "object",
        "properties": {
            "city": {"type": "string"}
        },
        "required": ["city"]
    }
)

Claude 會根據 description 和 inputSchema,在適當時機自動呼叫工具。

常見問題

伺服器連接失敗?

確認 .mcp.json 有設定:

  • 使用 -m 參數
  • 設定 PYTHONPATH=src

如何看日誌?

tail -f /tmp/weather-mcp-server.log

修改程式碼後如何重新載入?

重新啟動 Claude Code 即可。

結語

完整程式碼:https://github.com/checko/hellomcpserver

mcp servers for mediawiki

 

Feature/Project NameProfessionalWiki/MediaWiki-MCP-Servershiquda/mediawiki-mcp-serverharugon/mediawiki-mcp-serverlucamauri/MediaWiki-MCP-adapterRowik (Roshan George)wikipedia-mcp
Main LanguageTypeScriptPythonJavaScript (Node.js)Python/Node (custom)JavaScript/TypeScriptPython
OAuth 2.0 SupportYesNo (read-only)Optional (login/pass)CustomizableOptionalNo
Read page contentYesYesYesYesYesYes
Edit/create pagesYesNoYesYesYesNo
Search functionalityYesYesYesYesYesYes
File/media retrievalYesNoNoNoNoNo
Multi-wiki supportYesYes (via endpoint)Yes (via config)YesYes (configurable)Yes
Streaming/server-sent eventsNoYesNoNoNoNo
HTTP transport modeYesYesYesYesYesYes
Stdio transport modeYesYesYesYesYesYes
Docker image availableYesNoYesUnknownUnknownNo
Major use caseProduction, enterpriseLightweight, quick readGeneral LLM agent useCustom/enterprisePersonal wikiSearch-focused
LicenseGPL v2MITMITGPL/MIT (varies)MITMIT
Community activityHigh (active, multiple releases)Low (archived, read-only)Moderate (npm, Playbooks)ModerateLowModerate

2025/10/10

GNOME Terminal 逾時問題 (解決)

 14檢查 fcitx5 的啟動機制

🤔 分析:

我們發現 fcitx5 在 10:33:17 過早啟動,但需要確認:是什麼機制啟動了 fcitx5?

default
fcitx5
ibus

ibus
$ cat ~/.xinputrc
# im-config(8) generated on Mon, 22 Sep 2025 10:25:39 +0800
run_im fcitx5
# im-config signature: d0dc02817325a9a408600bd547799708  -
🎯 發現

~/.xinputrc 設定為 run_im fcitx5,這表示 im-config 框架會在 X session 初始化時自動啟動 fcitx5!

這解釋了為什麼 fcitx5 會在 graphical-session.target 就緒前就啟動。

15檢查是否有 systemd 服務也在管理 fcitx5

$ systemctl --user list-units --all | grep fcitx
fcitx5.service loaded inactive dead Fcitx5 Input Method
$ systemctl --user cat fcitx5.service
[Unit]
Description=Fcitx5 Input Method
PartOf=graphical-session.target
After=graphical-session.target
Wants=graphical-session.target

[Service]
ExecStart=/usr/bin/fcitx5
...
🎯🎯🎯 真正的根本原因

系統中存在兩種啟動 fcitx5 的機制:

  1. im-config(傳統方式):透過 ~/.xinputrc 在 X session 早期啟動
  2. systemd 服務(現代方式):透過 fcitx5.service 在 graphical-session.target 後啟動

問題:im-config 的啟動時機太早(10:33:17),在 graphical-session.target 就緒前(10:33:19),導致 fcitx5 請求 portal 時觸發競態條件!

16驗證修復方案

$ im-config -n none
設定 im-config 不自動啟動任何輸入法
$ cat ~/.xinputrc
# im-config(8) generated on Fri, 10 Oct 2025 13:01:31 +0800
run_im none
# im-config signature: 0a0fbfefedad5558a9e9e2919b262f89  -
🤔 修復邏輯:

禁用 im-config 自動啟動後,fcitx5 只會透過 systemd 服務在正確時機(graphical-session.target 之後)啟動,避免競態條件。

17系統重啟後驗證

$ journalctl --user -b 0 | grep -E "fcitx5|graphical-session.target|xdg-desktop-portal" | head -15
Oct 10 13:03:34 systemd: Reached target graphical-session.target  ⬅️ 先就緒
Oct 10 13:03:34 systemd: Starting fcitx5.service                  ⬅️ 後啟動
Oct 10 13:03:34 systemd: Started fcitx5.service
Oct 10 13:03:34 dbus-daemon: Activating via systemd: xdg-desktop-portal
Oct 10 13:03:34 systemd: Starting xdg-desktop-portal-gnome.service
Oct 10 13:03:34 systemd: Started xdg-desktop-portal-gnome.service  ⬅️ 成功!
Oct 10 13:03:34 systemd: Started xdg-desktop-portal.service
$ journalctl --user -b 0 | grep -i "gnome-terminal\|timeout"
Oct 10 13:03:39 dbus-daemon: Activating via systemd: org.gnome.Terminal
Oct 10 13:03:39 systemd: Starting gnome-terminal-server.service
Oct 10 13:03:39 systemd: Started gnome-terminal-server.service  ⬅️ 立即啟動,無逾時!
✅ 修復成功驗證!

正確的啟動順序:

  1. 13:03:34 - graphical-session.target 就緒
  2. 13:03:34 - fcitx5.service 啟動(在 target 之後)
  3. 13:03:34 - xdg-desktop-portal-gnome 成功啟動
  4. 13:03:39 - gnome-terminal 立即啟動(無逾時)

問題完全解決!

完整的解決方案

永久修復步驟

步驟 1:禁用 im-config 自動啟動

$ im-config -n none

步驟 2:建立/更新 systemd 使用者服務

mkdir -p ~/.config/systemd/user

cat > ~/.config/systemd/user/fcitx5.service << 'EOF'
[Unit]
Description=Fcitx5 Input Method
Documentation=man:fcitx5(1)
PartOf=graphical-session.target
After=graphical-session.target
Wants=graphical-session.target

[Service]
Type=simple
ExecStart=/usr/bin/fcitx5
ExecStartPost=/usr/bin/dbus-update-activation-environment --systemd GTK_IM_MODULE=fcitx QT_IM_MODULE=fcitx XMODIFIERS=@im=fcitx
Restart=on-failure
RestartSec=3

[Install]
WantedBy=graphical-session.target
EOF

systemctl --user daemon-reload
systemctl --user enable fcitx5.service

步驟 3:重新啟動系統

關鍵說明:

  • After=graphical-session.target 確保在圖形工作階段就緒後才啟動
  • ExecStartPost 設定環境變數,讓應用程式能使用 fcitx5
  • 移除 -d --replace 參數,因為 systemd 會管理程序

GNOME Terminal 逾時問題追查過程

 結果沒有解決問題...

第一階段:理解問題現象

1觀察錯誤訊息

錯誤訊息:
Error constructing proxy for org.gnome.Terminal:/org/gnome/Terminal/Factory0:
Error calling StartServiceByName for org.gnome.Terminal:
Timeout was reached
🤔 分析思路:
  • 錯誤提到 org.gnome.Terminal - 這是 D-Bus 服務名稱
  • StartServiceByName - D-Bus 正在嘗試啟動服務
  • Timeout was reached - 服務啟動時間過長,超過逾時限制
  • 但 terminal 最終有啟動 - 表示服務最終成功了,只是太慢
✅ 初步結論:

不是完全失敗,而是啟動太慢。需要找出是什麼在拖慢啟動速度。

2檢查當前系統狀態

$ echo $DBUS_SESSION_BUS_ADDRESS
unix:path=/run/user/1000/bus
$ ps aux | grep dbus-daemon | grep -v grep
charles+ 3481 /usr/bin/dbus-daemon --session ...
$ gnome-terminal --version
🤔 分析:
  • D-Bus 正常運作 - 排除 D-Bus 本身的問題
  • gnome-terminal 已安裝且可執行 - 排除安裝問題
  • 問題應該在啟動機制或相依服務

第二階段:追查服務啟動時間

3檢查 systemd 服務狀態

$ systemctl --user status gnome-terminal-server.service
● gnome-terminal-server.service - GNOME Terminal Server
   Active: active (running) since Fri 2025-10-10 10:34:43
Main PID: 4791
🤔 分析:
  • Terminal 是透過 systemd 的 D-Bus 啟動機制運作
  • 服務目前正在運行 - 所以之前的啟動最終成功了
  • 需要查看啟動歷史,找出為何第一次啟動很慢

4分析 systemd 服務啟動時間

$ systemd-analyze --user blame | head -20
1min 30.058s xdg-desktop-portal.service
     25.121s gnome-terminal-server.service  ⬅️ 發現問題!
      2.264s snap.snapd-desktop-integration.service
      2.139s org.gnome.Shell@x11.service
🎯 發現:

gnome-terminal-server.service 啟動花了 25.121 秒

另外 xdg-desktop-portal.service 更慢,花了 90 秒。這兩者可能有關聯。

5檢查服務啟動日誌

$ journalctl --user -u gnome-terminal-server.service --no-pager
Oct 10 10:34:43 systemd[3444]: Starting gnome-terminal-server.service
Oct 10 10:35:08 systemd[3444]: Started gnome-terminal-server.service  ⬅️ 25秒後!
🤔 分析:
  • 10:34:43 開始啟動
  • 10:35:08 完成啟動(25 秒間隔)
  • 這 25 秒發生了什麼?需要更深入的追蹤

第三階段:分析相依性與等待原因

6檢查服務相依性鏈

$ systemd-analyze --user critical-chain gnome-terminal-server.service
gnome-terminal-server.service +25.121s
└─basic.target @146ms
  └─sockets.target @146ms
    └─gpg-agent-ssh.socket @129ms +17ms
🤔 分析:
  • 所有相依服務在 146ms 就準備好了
  • 但 terminal-server 本身花了 25 秒
  • 結論:延遲不是因為等待相依性,而是服務本身初始化慢

7查看完整的系統日誌(關鍵步驟)

$ journalctl --user --since "10:34:40" --until "10:35:10" --no-pager
Oct 10 10:34:43 dbus-daemon: Activating via systemd: org.gnome.Terminal
Oct 10 10:34:43 systemd: Starting gnome-terminal-server.service
Oct 10 10:34:45 systemd: xdg-desktop-portal.service: start operation timed out ⬅️ 重要!
Oct 10 10:34:45 systemd: Failed to start xdg-desktop-portal.service
Oct 10 10:35:08 dbus-daemon: Successfully activated service org.gnome.Terminal
🎯 重大突破:

在 terminal 啟動期間,xdg-desktop-portal.service 發生逾時失敗!

時間點很可疑:portal 在 10:34:45 失敗,terminal 在 10:35:08 完成(23 秒差異)。

第四階段:追查 Portal 與 Terminal 的關聯

8檢查更早的日誌,了解 Portal 為何失敗

$ journalctl --user --since "10:34:00" --until "10:35:15" | grep -i "portal\|timeout"
Oct 10 10:34:07 xdg-desktop-por: Failed to create settings proxy:
                 Error calling StartServiceByName for org.freedesktop.impl.portal.desktop.gnome:
                 Timeout was reached  ⬅️ GNOME portal backend 逾時!

Oct 10 10:34:30 xdg-desktop-por: Failed to create file chooser proxy:
                 Timeout was reached

Oct 10 10:34:45 systemd: xdg-desktop-portal.service: start operation timed out
🎯 找到連鎖反應:
  1. 10:34:07 - portal 嘗試啟動 GNOME 後端,逾時
  2. 10:34:30 - portal 嘗試建立 file chooser,又逾時
  3. 10:34:45 - portal 服務整個逾時失敗

GTK 應用程式(如 gnome-terminal)會嘗試連接 portal,這導致等待。

9檢查 GNOME Portal 服務狀態與相依性

$ 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
🤔 分析:
  • Requisite=graphical-session.target 是嚴格要求
  • 如果 graphical-session.target 未就緒,服務會立即失敗
  • 需要確認 graphical-session.target 何時就緒

第五階段:追查工作階段初始化時序

10檢查 graphical-session.target 啟動時間

$ systemd-analyze --user critical-chain graphical-session.target
graphical-session.target @4.160s  ⬅️ 工作階段開始後 4.16 秒才就緒
└─basic.target @146ms
$ journalctl --user --since "10:33:15" --until "10:33:25" | grep "graphical-session.target"
Oct 10 10:33:17 systemd: graphical-session.target is inactive  ⬅️ 還未就緒
Oct 10 10:33:19 systemd: Reached target graphical-session.target  ⬅️ 2秒後就緒
🎯 發現時序問題:
  • 10:33:15 - 使用者工作階段啟動
  • 10:33:17 - graphical-session.target 還未就緒
  • 10:33:19 - graphical-session.target 就緒

11檢查 Portal 何時嘗試啟動(關鍵發現)

$ journalctl --user --since "10:33:15" --until "10:33:20" | grep -E "desktop-portal|graphical-session"
Oct 10 10:33:17 dbus-daemon: Activating: org.freedesktop.impl.portal.desktop.gnome
                requested by fcitx5  ⬅️ fcitx5 觸發 portal 啟動!

Oct 10 10:33:17 systemd: graphical-session.target is inactive  ⬅️ 同時間 target 未就緒
Oct 10 10:33:17 systemd: Dependency failed for xdg-desktop-portal-gnome.service  ⬅️ 失敗!

Oct 10 10:33:19 systemd: Reached target graphical-session.target  ⬅️ 2秒後才就緒
🎯🎯🎯 根本原因找到了!

競態條件(Race Condition):

  1. 10:33:17 - fcitx5(輸入法)啟動並請求 portal
  2. 10:33:17 - portal 嘗試啟動但 graphical-session.target 未就緒
  3. 10:33:17 - portal 啟動失敗(相依性未滿足)
  4. 10:33:19 - graphical-session.target 就緒(但 portal 已失敗,不會重試)
  5. 10:34:43 - 使用者啟動 terminal,嘗試連接已失敗的 portal
  6. 10:35:08 - 25 秒逾時後放棄,terminal 繼續啟動

第六階段:驗證根本原因

12確認是誰觸發了過早的 Portal 啟動

$ journalctl --user --since "10:33:17" | grep "fcitx" | head -5
Oct 10 10:33:17 dbus-update-activation-environment: setting GTK_IM_MODULE=fcitx
Oct 10 10:33:17 dbus-daemon: requested by ':1.18' (uid=1000 pid=3699 comm="/usr/bin/fcitx5 -d")
$ ps aux | grep fcitx5
charles+ 3699 /usr/bin/fcitx5 -d ⬅️ 確認 fcitx5 正在運行
✅ 驗證完成:

fcitx5 在工作階段早期(10:33:17)就啟動了,比 graphical-session.target(10:33:19)早 2 秒。

當 fcitx5 啟動時,它作為 GTK 應用程式會嘗試連接 xdg-desktop-portal,觸發了過早的啟動嘗試。

13測試修復後的行為

$ systemctl --user stop gnome-terminal-server.service
$ time gnome-terminal --version
real    0m0.045s  ⬅️ 現在很快!(之前是 25 秒)
🤔 分析思路:

如果手動重啟服務,它啟動很快。這證實問題只發生在工作階段早期的特定時間窗口。