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 秒)
🤔 分析思路:

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

GNOME Terminal 啟動逾時問題

 

問題描述

當從 xterm 啟動 gnome-terminal 時,會顯示以下錯誤訊息:

Error constructing proxy for org.gnome.Terminal:/org/gnome/Terminal/Factory0: Error calling StartServiceByName for org.gnome.Terminal: Timeout was reached

雖然最終 gnome-terminal 會成功啟動,但需要等待約 25 秒的逾時時間。

原因:fcitx5 重複啟動導致的競態條件(Race Condition)

這個問題是由 im-configsystemd 雙重啟動 fcitx5 所引起的競態條件。

完整時間軸

10:33:15 - 使用者工作階段啟動(systemd --user)
10:33:17 - im-config 自動啟動 fcitx5(過早!)
10:33:17 - fcitx5 透過 D-Bus 觸發 xdg-desktop-portal
10:33:17 - xdg-desktop-portal 嘗試啟動 xdg-desktop-portal-gnome
10:33:17 - xdg-desktop-portal-gnome 啟動失敗
         - 錯誤:「graphical-session.target is inactive」
         - 錯誤:「Dependency failed for xdg-desktop-portal-gnome.service」

10:33:19 - GNOME Shell 完成啟動(2 秒後)
10:33:19 - graphical-session.target 變為 ACTIVE
10:33:19 - 如果 fcitx5 在此時啟動,portal 就能正常運作

--- 經過 84 秒 ---

10:34:43 - 使用者從 xterm 啟動 gnome-terminal
10:34:43 - gnome-terminal(GTK 應用程式)嘗試存取 xdg-desktop-portal-gnome
10:34:43 - Portal 後端仍未執行(從未從早期失敗中恢復)
10:34:43 - D-Bus 嘗試啟動 portal,但發生逾時
10:35:08 - 25 秒逾時後,terminal 仍然啟動

細節

  1. im-config 框架透過 ~/.xinputrc 設定在 X11 session 初始化時自動啟動 fcitx5
  2. fcitx5 在登入後約 2 秒時過早啟動(在 graphical-session.target 就緒前
  3. fcitx5 立即透過 D-Bus 請求 xdg-desktop-portal
  4. xdg-desktop-portal 嘗試載入 xdg-desktop-portal-gnome 後端
  5. xdg-desktop-portal-gnome.service 有嚴格的相依性:Requisite=graphical-session.target
  6. 在該時刻(10:33:17),graphical-session.target 尚未就緒
  7. 服務因未滿足的相依性而啟動失敗
  8. 2 秒後(10:33:19),graphical-session.target 變為就緒(GNOME Shell 完成載入後)
  9. Portal 服務從未重試 - 保持失敗狀態
  10. 當啟動 gnome-terminal 時,它嘗試存取 portal
  11. D-Bus 啟動在 25 秒後逾時

為什麼會有重複啟動的問題?

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

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

im-config 的啟動時機太早,導致 fcitx5 在 graphical-session.target 就緒前就請求 portal,觸發競態條件。

重點:這是一個時序問題,不是效能問題。解決方案是禁用 im-config 自動啟動,改用 systemd 服務在正確時機啟動 fcitx5。

解決方案

方案 1:正確的永久修復(推薦,已驗證有效)

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

im-config -n none

這會將 ~/.xinputrc 設定為 run_im none,停止過早啟動 fcitx5。

步驟 2:建立 systemd 使用者服務來管理 fcitx5

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

說明:

  • After=graphical-session.target 確保 fcitx5 在圖形工作階段就緒後才啟動
  • ExecStartPost 設定必要的環境變數讓應用程式能使用 fcitx5
  • 這完全避免了競態條件,因為 fcitx5 會在正確的時機啟動

步驟 3:重新啟動系統

systemctl reboot

登入後,fcitx5 會在 graphical-session.target 就緒後自動啟動,gnome-terminal 將立即開啟。

方案 2:繞過 D-Bus Factory(臨時快速修復,不建議長期使用)

gnome-terminal --disable-factory

這會完全避免 D-Bus 啟動機制。可以在 ~/.bashrc 或 ~/.zshrc 中建立別名:

alias gnome-terminal='gnome-terminal --disable-factory'

缺點:會失去 terminal server 的功能,每次都會啟動新的 terminal 實例。

方案 3:等待工作階段初始化完成(暫時性緩解)

登入後等待 5 秒以上再啟動 gnome-terminal。這不是真正的解決方案,只是避開競態條件的時間窗口。

測試

套用方案 1 後(禁用 im-config 並使用 systemd 服務):

  1. 重新啟動系統
  2. 登入後檢查啟動順序:journalctl --user -b 0 | grep -E "fcitx5|graphical-session.target|xdg-desktop-portal"
  3. 應該看到:
    • graphical-session.target 先就緒
    • fcitx5.service 在之後啟動
    • xdg-desktop-portal-gnome 成功啟動
  4. 立即嘗試啟動 gnome-terminal
  5. 應該不會出現 25 秒的逾時,terminal 會立即開啟

修復前後對比

修復前(問題狀態):
10:33:17 - im-config 啟動 fcitx5 (graphical-session.target 未就緒)
10:33:17 - xdg-desktop-portal-gnome 啟動失敗
10:33:19 - graphical-session.target 就緒 (太晚了)
10:34:43 - 啟動 gnome-terminal
10:35:08 - 25 秒逾時後 terminal 才開啟
修復後(正常狀態):
13:03:34 - graphical-session.target 就緒
13:03:34 - systemd 啟動 fcitx5.service
13:03:34 - xdg-desktop-portal-gnome 成功啟動
13:03:39 - gnome-terminal 立即開啟 (無逾時)

結論

這個問題的根本原因是 im-config 和 systemd 雙重啟動機制造成的競態條件。im-config 在 X session 初始化早期就啟動 fcitx5,此時 graphical-session.target 尚未就緒,導致 fcitx5 觸發的 xdg-desktop-portal-gnome 因相依性未滿足而啟動失敗。Portal 服務從未重試,因此當 gnome-terminal 稍後嘗試使用 portal 時,就會發生 25 秒的 D-Bus 逾時。

正確的解決方案是禁用 im-config 自動啟動,改用 systemd 使用者服務在 graphical-session.target 就緒後才啟動 fcitx5。這確保了正確的啟動順序,完全避免競態條件。

系統資訊: