AutoHotkey 實戰:RDP 斷線與鎖定 Session 下的 Windows UI 自動化突破(AutoWorklistUpdate 背景排程開發紀錄)

在臨床醫療自動化工作流中,為了隨時掌握放射科資訊系統 (Radiology Information System, RIS) 的待打報告工作清單 (Worklist) 狀態,我設計了一套整合自動化方案:

  1. 背景自動化推送 (Background Automation):透過 Windows 工作排程器 (Task Scheduler) 定時執行獨立的 AutoHotkey Utility 腳本,自動讀取並抓取 RIS 工作清單的未報告筆數。
  2. 資料庫儲存與整合 (Database Integration):將抓取到的工作清單狀態推送到 n8n Webhook,並利用 n8n Data Table 建立一個輕量級的小型資料庫來儲存最新數據。
  3. 隨時手動查詢 (On-demand Query):隨時透過 Telegram Bot 發送指令,手動讀取該 n8n 資料庫中的最新工作清單狀態。

設計此方案的初衷與痛點:
在日常臨床工作中,雖然可以直接透過 VPN 連回醫院主機並開啟 RDP 去查看工作清單,但醫院的 VPN 並非隨時隨地都方便連線;此外,每次開啟電腦、連線 VPN、開 RDP 登入查看的過程也相當耗時且繁瑣。因此,我希望能設計一個更輕量、能快速取得 Worklist 狀態的方案,讓我僅需透過手機 Telegram 發個訊息,就能在幾秒內掌握當前科內報告的累積狀況。

然而,在 Windows 環境下,當試圖在後台自動化 UI 時,遇到一個經典的棘手問題:本機運作完全正常,但只要透過遠端桌面 (Remote Desktop Protocol, RDP) 連線後直接關閉視窗(中斷連線),自動化 UI 便完全卡死或失效。

本文將以完整的時間軸與技術演進紀錄,回顧當初在 RisController 裡開發此功能的過程,深入分析 RDP 斷線後 UI 自動化失效的底層原因,並探討如何透過獨立運作的 AutoWorklistUpdate.v2.ahk 腳本與 UI Automation (UIA) 技術優化,完美突破此限制,實現 24/7 的穩定背景更新。


1. 發展歷程:從內置常駐到獨立腳本 (Development History)

1.1 階段一:RisController 內置常駐 Timer (舊版作法)

在最早的實作中,我在主要的前端 AHK 腳本 RisController 中開發了 GetWorklistJson 函數,並設定了一個每 60 秒觸發一次的背景計時器 (SetTimer)。

  • 定位與抓取:完全依賴 UIA 框架,藉由元件的 AutomationId 進行樹狀遞迴搜尋 (FindElement),並遍歷表格 (DataGridView) 抓取資料。
  • 環境限制與「主動放棄」設計
    在 Timer 檢查邏輯中,加入了 User32\OpenInputDesktop 的防護檢測。一旦 RDP 斷線或螢幕鎖定,該 API 回傳為假(無活動輸入桌面),程式會主動跳過更新。
    當時加入此設計的核心考量在於
    • 防止主程式執行緒卡死 (UI Freezing):AutoHotkey 預設為單執行緒 (Single-threaded) 架構。在 RDP 斷線無 GUI 渲染時,常駐 Timer 執行 UIA FindElement 會陷入長達數秒的阻塞式等待(Blocking Timeout),直接造成使用者在前台操作時發生嚴重的鍵盤與畫面卡頓。
    • 避免彈出錯誤對話框:無渲染時 UIA 頻繁拋出未處理異常,會彈出 AHK 的錯誤對話框(Runtime Error Dialog),直接中斷常駐程式的運作。

1.2 階段二:獨立腳本與工作排程器重構 (新版作法)

為了解決 RDP 斷線後無法自動更新的痛點,我進行了架構重構:

  • 邏輯剝離:將工作清單自動更新的所有邏輯從主程式中徹底移除。
  • 獨立腳本:建立 Utilities/AutoWorklistUpdate.v2.ahk,專門供 Windows 工作排程器 (Task Scheduler) 定時調用,即使後台卡死也與主程式完全隔離。
  • 多重備援:重新設計控制項定位與資料擷取邏輯,引入「HWND 快取」+「Win32 座標 Fallback」+「剪貼簿 ControlSend 備援」的多重防禦機制。

2. 核心痛點:為何 RDP 斷線後 UI 自動化會失效? (Core Problem)

當 RDP 連線中斷(例如直接關閉遠端桌面視窗)時,Windows 基於安全與效能考量,會將該會話轉為鎖定狀態 (Locked Session)。此時:

  1. 停止渲染 (No GUI Rendering):OS 停止為該 Session 繪製任何圖形介面,DllCall("User32\OpenInputDesktop", ...) 回傳 0 (失敗),代表系統不再接受物理的滑鼠與鍵盤輸入。
  2. 物理事件失效:無畫面渲染下,試圖使用物理點擊(如 elBtn.Click() 或是 AHK 的 Click)是無效的,因為此時沒有可供滑鼠游標活動的坐標空間。
  3. UIA 遞迴搜尋崩潰:UI Automation 框架是建立在「操作系統有繪製 GUI」的假設上。當 Windows 停止圖形渲染時,主視窗的 UIA 樹狀階層 (Accessibility Tree) 無法生成或更新。此時呼叫 elWindow.FindElement() 遞迴尋找控制項,將會頻繁拋出 “Element not found”“Operation timeout” 的異常。

3. 尋求出路:曾嘗試與放棄的替代方案 (Alternative Approaches Tried)

在探尋當前這套「純 AHK/Win32 底層重構」的方案之前,我也曾研究過其他幾種常見的 Windows RDP 自動化繞過作法,但最終均因醫療工作站特定的環境限制而被迫放棄:

方案 A:使用 tscon 將 Session 歸還給本機實體 Console

  • 原理:當 RDP 斷線時,透過批次檔或腳本執行類似以下指令:
    tscon %sessionname% /dest:console 或是 tscon 1 /dest:console
    強制 Windows 將遠端連線 Session 切換回本機實體螢幕顯示,從而保持螢幕解鎖與畫面渲染。
  • 受限原因權限不足 (Insufficient Privilege)。 執行 tscon 指令需要系統管理員權限 (Administrator privileges)。由於醫療臨床工作站出於資訊安全管理,登入的使用者帳戶均無 Admin 權限,因此此路不通。

方案 B:連接擬真/虛擬顯示器輸出 (Pseudo / Dummy Video Output)

  • 原理:在主機接上 HDMI 騙壓插頭 (HDMI Dummy Plug) 或是安裝虛擬顯示卡驅動程式 (Virtual Display Driver),讓顯示卡認為始終有螢幕連接,防止 Windows 關閉 Session 渲染或進入鎖定模式。
  • 受限原因:醫療工作站主機通常沒有多餘的實體視訊輸出埠 (Ports) 可供插接 Dummy Plug;且在沒有 Admin 權限的環境下,亦無法自行安裝虛擬顯示卡驅動程式。

結論:
因為上述方案皆被權限與物理安全機制卡死,我才被迫走向 「在不改變 Windows Session 鎖定狀態下,完全靠底層 Win32 API 與記憶體直達來完成自動化」 的純軟體重構之路。


4. 技術突破:獨立腳本的後台自動化設計 (Technical Breakthrough)

全新設計的獨立腳本 AutoWorklistUpdate.v2.ahk 藉由以下底層技術,徹底擺脫了對 GUI 渲染與 UIA 樹狀搜尋的絕對依賴:

4.1 控制項定位的「多重防禦機制」(Multi-layered Resolve)

ResolveWorklistControls 函數中,程式設計了三層定位防線:

  1. 第一防線:HWND 快取驗證 (Fast Path)
    • 當用戶有畫面連線時,將工作清單視窗與核心控制項的 HWND 寫入快取 config/worklist-controls.ini
    • 背景執行時,透過 DllCall("IsWindow", "Ptr", hCtrl) 驗證快取中的 HWND 是否有效,確認有效即直接套用,完全不需經過搜尋
  2. 第二防線:UIA 動態定位 (UIA Resolve)
    • 若快取失效,利用 UIA 物件動態定位以重建快取(初期版本限制僅在 Active Session 下啟用)。
  3. 第三防線:座標幾何排序 Fallback (最後備援)
    • 若 UIA 失效且無快取,退回到 Win32 API,調用 WinGetControls 獲取視窗內所有的子控制項,並依據座標位置排序(ER -> ADM -> OPD)。

4.2 後台非同步訊息點擊 (PostMessage + BM_CLICK)

在定位到重新整理按鈕的 HWND 後,腳本使用 Windows 訊息機制發送點擊指令:

PostMessage(0x00F5, 0, 0, , "ahk_id " . btnHwnd) ; BM_CLICK = 0x00F5
  • 原理:直接向按鈕控制項的訊息佇列投遞點擊訊息。不需要視窗被啟動 (Active)、不需要視窗位於最上層,更不需要任何畫面渲染,按鈕即可在後台被觸發更新。

4.3 雙軌資料擷取 (UIA -> ControlSend 雙軌機制)

在讀取 DataGridView 表格內容時,腳本實作了高度容錯的雙軌制:

  1. 軌道一 (UIA 直讀):優先使用 UIA 的 TreeWalker 直接從 HWND 讀取資料。這種方式免除了剪貼簿的介入,最為乾淨。
  2. 軌道二 (剪貼簿複製 Fallback):當 UIA 讀取失敗時,退回到剪貼簿複製流程。使用 ControlSend("^a^c", hwnd) 向目標 HWND 發送按鍵訊息,強制觸發其內部的 Copy 事件,將表格資料寫入 Windows 剪貼簿,再藉由 AHK 的 A_Clipboard 進行字串解析。

5. 實戰遭遇戰:2026/05 Locked Session 座標歸零 Bug 與 UIA 動態定位的解放 (Production Bug & Fix)

5.1 問題現象與 Log 診斷 (Symptom & Diagnostic)

在實戰部署後,我們遇到一個奇特現象:在 RDP 鎖定(Locked Session)後,背景排程腳本雖照常觸發,但抓取上傳至 n8n 的統計數據全數為 0(手動執行則正常)。
調閱日誌發現:

2026-05-22 21:58:38 [機制 - Fallback] DataGridView 排序成功 - ER: 0x30B62 (x:0), ADM: 0x10B70 (x:0), OPD: 0x10B72 (x:0)

原因
當會話進入鎖定狀態時,Windows 視窗管理器 (Window Manager) 會將所有子控制項的幾何位置坐標重設為 0。這導致依賴「X 軸座標由小到大排序」的第三防線(座標 Fallback)發生錯配。系統將 ERADMOPD 的識別綁定到了錯誤的 DataGridView HWND,導致隨後的資料讀取發生錯配,擷取到全為 0 的空數據。

5.2 技術突破:解放 UIA 定位限制 (Session-Agnostic UIA Resolve)

針對此問題,我們對 UIA 定位機制進行了重新驗證,有了重大突破:

  • 事實真相:雖然物理點擊與大型 UIA 樹遍歷在無畫面下不可靠,但藉由 HWND 直達UIA.ElementFromHandle(winHwnd),直接去尋找具有特定 AutomationId(如 dgvClassifyOPDE 等)的子控制項 HWND,在鎖定 Session 下依然能夠完美運作且極其快速
  • 修正對策
    移除定位邏輯中對於 Session 是否 Active 的限制,將 UIA 定位調整為無差別優先啟用。只要快取失效,均先嘗試透過 UIA 獲取精確的 AutomationId 綁定,只有在 UIA 徹底拋出異常時,才降級至座標 Fallback 的第三防線。

5.3 修復後實測 (Verification)

當腳本移除 Session 限制後,實測於 Locked Session 背景定時執行,其定位與讀取日誌如下:

2026-05-22 22:11:01 嘗試使用 UI Automation (UIA) 進行定位 (Session: Locked) (第二防線)...
2026-05-22 22:11:01 ✅ [機制 - UIA] UIA 定位成功,快取已更新 (第二防線)
2026-05-22 22:11:01 控制項定位結果 - RefreshButton: 0x10B78, ER: 0x10B8C, ADM: 0x10B8A, OPD: 0x10B88
2026-05-22 22:11:03 正在擷取 [ER] 表格資料 (HWND: 0x10B8C)...
2026-05-22 22:11:03   [ER] 嘗試使用 UIA 從 HWND (0x10B8C) 讀取資料...
2026-05-22 22:11:03   [ER] UIA 尋獲 12 個 Row 元素,UIA 讀取成功

透過此項優化,即使快取失效,排程程式依然能在鎖定狀態下透過 UIA 直接更新快取,徹底杜絕了統計數據被置零的 Bug。


6. 深度技術解析:為什麼在 RDP 斷線後,UIA 直達控制項 HWND 依然能成功讀取資料?

根據實測 Log,在 RDP 鎖定的後台環境下,獨立腳本在資料擷取階段依然能成功透過軌道一 (UIA 讀取) 獲取資料。核心差異在於 「存取視窗控制項的入口方式不同」

6.1 「UIA 樹遞迴走訪 (FindElement)」的硬傷

在舊版 RisController 中,定位方式是從主視窗節點向下遞迴搜尋。當 RDP 連線關閉、Session 鎖定後,Windows 停止為該會話渲染圖形介面 (GUI),作業系統亦不再更新整個視窗的 UIA 樹狀階層。因此,調用 FindElement 會因為樹狀結構不全而拋出「元件不存在」的錯誤。

6.2 「直達控制項 HWND (ElementFromHandle)」的優勢

新版獨立腳本在定位階段已經直接鎖定了三個 DataGridView 子控制項的具體 HWND(例如 0x181590)。在讀取資料時,程式直接呼叫:

elGrid := UIA.ElementFromHandle(hwnd)  ; hwnd 直接傳入 0x181590
  • 原理UIA.ElementFromHandle(hwnd) 是 Windows 提供的直達通道。它不需走訪主視窗的 UIA 階層樹,而是直接向特定的 HWND(由 .NET WinForms 的 DataGridView 元件實作的 IRawElementProviderSimple 介面)發送查詢。
  • 只要該控制項所屬的視窗尚未關閉,雖然「圖形介面沒有被渲染繪製」,但該控制項底層的記憶體模型與資料結構依然完好存在。因此,直接向該子 HWND 請求 UIA 元素時,UIA 依然能成功訪問記憶體中的數據模型並讀出 Row,完全不依賴主視窗 UIA 樹的動態生成與更新。

7. 方案對比總覽 (Comparison Table)

特性 / 機制舊版作法 (RisController 常駐 Timer)新版獨立腳本 (AutoWorklistUpdate.v2.ahk)
執行載體AHK 主程式執行緒內以 SetTimer 執行獨立腳本,由 Windows 工作排程器 (Task Scheduler) 定時調用
RDP 斷線檢測偵測到 RDP 斷線即主動退出忽略中斷限制,做為是否啟用 UIA 備援定位的判斷依據
控制項定位完全依賴 UIA 遞迴搜尋 (FindElement)快取 HWND 驗證 + Session-Agnostic UIA 動態定位 (座標 Fallback 保底)
點擊重新整理呼叫 UIA 的 elBtn.Invoke() 或實體點擊使用 PostMessage 發送 BM_CLICK 訊息至 HWND (後台靜默觸發)
表格資料讀取單軌依賴 UIA (TreeWalker)雙軌制:優先 UIA 直讀,失敗自動 Fallback 至 ControlSend 剪貼簿複製
資源與穩定性常駐背景佔用執行緒,若卡死會影響主程式運作每次執行完即關閉,不佔用常駐資源,且有獨立排程防卡死機制

8. 關鍵反思:架構隔離與技術重構的雙重奏 (Retrospective)

要實現 RDP 斷線與鎖定 Session 下的完美自動化,必須是 「架構上的執行緒隔離」「技術上的低階機制重構」 雙管齊下,兩者缺一不可:

  1. 獨立腳本運作 (架構隔離) -> 解決「流暢度與穩定性」:為自動更新建立了一個沙盒化的隔離執行緒環境。即使更新過程在後台卡死、報錯或超時,也完全不影響前台主程式的正常運作,避免前台醫師遇到嚴重的鍵盤與畫面卡頓 (UI Freezing)。
  2. 低階機制重構 (技術突破) -> 解決「可行性」:引進了 HWND 快取直達UIA 直讀,以及非實體後台觸發的 PostMessage (BM_CLICK)ControlSend (剪貼簿複製備援),完全繞過了作業系統停止渲染圖形介面的物理限制。

9. 總結與部署建議 (Deployment Suggestion)

透過這次的架構重構與最近對 Locked Session 幾何定位 Bug 的修正,我們成功實現了「在 RDP 斷線且螢幕鎖定」的後台環境下,穩定自動化 UI 的目標。這對於需要長期在醫療工作站後台執行監控、上傳數據的場景提供了極其強韌的技術方案。

部署指南:

  1. 初次執行:在有畫面連線的狀態下,手動執行一次 AutoWorklistUpdate.v2.ahk,或讓主程式 RisController 運作一次,以便在 config/worklist-controls.ini 中建立正確的控制項 HWND 快取與座標資訊。
  2. 設定工作排程器 (Task Scheduler)
    • 建立一個新工作,設定觸發程序為每 30 分鐘執行一次。
    • 動作:啟動程式,設定為 D:\USER\Documents\ahk-nycuh\Utilities\AutoWorklistUpdate.v2.ahk (或透過 AutoHotkey 執行檔載入此腳本)。
    • 關鍵設定:在安全性選項中,勾選 「不管使用者登入與否均執行」 (Run whether user is logged on or not),這能確保排程器在背景以 Non-interactive Session 執行,或者在 RDP 斷線後依然能夠穩定喚醒並執行此腳本。


Leave a Reply