Windows Vista 交互式服務編程

Windows Vista 對快速用戶切換,用戶賬戶權限,以及服務程序所運行的會話空間都作了很大的改動,致使一些原本可以工作的程序不再能夠正常工作了,我們不得不進行一些改進以跟上 Vista 的步伐。
我 們的軟件在Windows NT/2000/XP/Vista 系統中安裝了一個系統服務,這個服務負責以 SYSTEM 權限啓動我們的主程序。我們的主程序啓動後會在系統托盤添加一個圖標,點擊此圖標可以彈出控制菜單,通過這個菜單也可以激活配置程序首選項的對話框。在 Windows NT/2000/XP 下我們的程序都可以正常工作。哦不,當 XP 具備了快速用戶切換功能的時候我們的問題已經出現了。XP 啓動後我們以用戶 A 登錄,我們的圖標出現在系統托盤,一切工作都正常,可當我們使用快速用戶切換,切換到用戶B後(用戶A此時也是已登錄狀態,並沒有註銷),雖然用戶B已經 是本地控制檯會話(Session 屬性爲 Console)但我們的圖標已經無法出現了,自然菜單和對話框更無從談起了。我們的程序是和本機控制檯桌面相關的,這種情況無疑是個缺陷。再來看一下在 Vista 平臺是怎麼樣吧,系統啓動後以用戶A登錄,我們的圖標更本就沒有出現,查看進程管理器中的進程列表發現我們的程序已經啓動了,當我們從遠端檢查我們的服 務,發現已經正常工作,嘗試遠程登錄我們的服務,Vista 會在本機控制檯彈出一個消息框,提示有交互式服務消息,是否查看這個消息,點擊立刻查看發現切換到另外一個桌面去了。
於是開始分析這種情況發生的 原因。在 Windows NT/2000 中系統服務進程和本機控制檯交互式登錄的用戶都運行於Session0 中,默認用戶桌面運行於 WinSta0 窗口站,所以我們的程序由服務程序啓動時依然是和本機用戶處於同一個Session中,即使在某些情況下出現不能彈出對話框或者無法添加系統托盤圖標的情 況也只需要修改一下進程桌面到 WinSta0/Default 就可以了(可以參考 MSDN 中 OpenInputDesktop, SetThreadDesktop 等API的說明)。
XP爲我們帶來了快速用戶切換,也讓我們所採用的軟件架構問題浮現出來。當我們快速切換到 用戶B的時候,用戶A仍然在會話中(Session0),而用戶B則處於新啓動的會話中(Session1或者其他),此時服務程序和本機控制檯程序就不 在處於同一會話了,OpenInputDesktop,SetThreadDesktop 等API的工作範圍僅限於本Session,用戶A沒有退出,Session0也依然存在但是已經是 Disconnected 狀態,當進程所處的Session是 Disconnected 狀態的時候調用 OpenInputDesktop 會返回錯誤“無效的API”。進程及線程所屬的Session 是由他們的Token 結構中的 TokenSessionId 決定的(參見MSDN中SetTokenInformation 和 TOKEN_INFORMATION_CLASS的說明),我嘗試以微軟提供的相關API修改運行中的進程和線程的TokenSessionId 信息從而達到修改桌面環境的目的,到目前還沒有成功過(或許可以嘗試參考RootKit 技術,不過即使修改成功到底能不能實現我們的需求也不確定)。我們的進程無法跨越Session的界限,自然無法與當前活動的另外一個Session中的 桌面交互了,L 。
Vista中又是如何的一番景象呢?處於安全方面及其他因素的考慮,Vista以及將所有的服務程序置於Session0中, 而爲本機第一個交互登錄的用戶創建了Session1,快速切換到用戶B後則是 Session2,無論是本機登錄的用戶,快速切換後的用戶,還是遠程桌面登錄的用戶再也沒有誰和服務進程處於同一個Session中了,我們的程序還運 行在Session0中,自然我們的托盤圖標是沒有用戶能看到了。事實上這個圖標還是可以出現的。Session0因爲不是一個交互式會話所以沒有象其他 用戶環境初始化的時候一樣啓動Explorer程序,但是我們開始可以手工啓動他,在Session0中啓動 Explorer 後任務欄出現後我們還是看到了我們的圖標(具體啓動Explorer的方法我們不在此文中討論),菜單、對話框也可以使用。
既然我們的程序必須運 行在Session0而我們又沒有辦法把我們的圖標、對話框一下子就拋到隔壁Session的用戶桌面上去,只能想其他的辦法了。微軟也不提倡我們這種服 務程序直接提供GUI與用戶直接交互的方式,而他們建議使用C/S架構,Client/Server之間用Socket/Pipe/RPC等方式通訊,這 樣我們只要把Client整個進程放到用戶Session去和用戶交互,然後將配置信息等內容通過上述途徑傳遞給Server,服務端在作出相應的響應即 可。
把GUI分離出來並不是那麼困難,然後在以前直接調用的地方加上一個通過Pipe通訊的接口,這樣GUI(Client)的運行就可以靈活的掌握了。
最初我想把用戶界面程序放到 Startup(啓動)中隨用戶登錄自動啓動。這樣當用戶A和B都登錄後將有兩個用戶界面程序在運行,而我們的服務只是和當前活動的控制檯登錄用戶交互,所以這樣並不符合需求。
接下來我們需要看看如何判定當前的活動Session是哪個,然後如何在這個活動Session中啓動我們的用戶界面程序了。
微軟從XP/2003開始爲我們提供了一套Windows Terminal Service 的相關API,這些API都以WTS開頭(請安裝MSDN2005以查閱相關說明),要獲得活動Session也不止一個途徑,最簡單的就是直接使用
DWORD WTSGetActiveConsoleSessionId(void);
來 獲得活動Session Id 。要在程序中使用這些API需要最新的Platform SDK(如果你正在使用Visual Studio 2005那麼它已經具備了相關頭文件和庫文件可以直接使用了),如果你在使用VC++ 6.0 你也沒有或者不打算安裝最新的SDK那麼你可以直接使用LoadLibrary() 裝載wtsapi32.dll然後使用GetProcAddress()獲得相關函數的地址以調用它們。我們獲得了活動SessionId後就可以使用
BOOL WTSQueryUserToken(
 ULONG SessionId,
 PHANDLE phToken
);
來獲取當前活動Session中的用戶令牌(Token),有了這個Token我們的就可以在活動Session中創建新進程了,
BOOL CreateProcessAsUser(
 HANDLE hToken,
 LPCTSTR lpApplicationName,
 LPTSTR lpCommandLine,
 LPSECURITY_ATTRIBUTES lpProcessAttributes,
 LPSECURITY_ATTRIBUTES lpThreadAttributes,
 BOOL bInheritHandles,
 DWORD dwCreationFlags,
 LPVOID lpEnvironment,
 LPCTSTR lpCurrentDirectory,
 LPSTARTUPINFO lpStartupInfo,
 LPPROCESS_INFORMATION lpProcessInformation
);
將 我們獲得的Token作爲此API的第一個參數即可,你可以先嚐試一下運行一個notepad.exe看看,怎麼樣?你可以在控制檯桌面上看到新進程了。 再查看一下進程列表,該進程的用戶名是當前控制檯登錄的用戶。可是這裏我們又遇到一個問題,我們需要收集當前交本機互式登錄用戶的一些信息,而有些操作需 要很高的權限才能完成,而Vista下即使是Administraotrs用戶組成員默認也是以Users權限啓動進程的,所以我們創建的新進程只有 Users權限,無法完成一些操作,當然我們可以使用Vista所提供的UI來詢問用戶以提升至管理員權限,可有些操作甚至是管理員Token也無法完成 的,而且需要用戶確認實在在易用性上大打折扣,所以我決定在活動Session中以SYSTEM權限啓動我們的用戶交互程序。顯然 WTSQueryUserToken() 是不好用了。
之前,我們提到過進程所屬的Session是由進程Token中的 TokenSessionId來決定的,那麼我們是不是可以複製服務進程的Token然後修改其中的TokenSessionId,從而在用戶桌面上創建 一個具有SYSTEM權限的新進程呢?答案是肯定的。一下是實現這個操作的代碼,爲了縮小篇幅我刪除了異常處理代碼
HANDLE hTokenThis = NULL;
HANDLE hTokenDup = NULL;
HANDLE hThisProcess = GetCurrentProcess();
OpenProcessToken(hThisProcess, TOKEN_ALL_ACCESS, &hTokenThis);
DuplicateTokenEx(hTokenThis, MAXIMUM_ALLOWED,NULL, SecurityIdentification, TokenPrimary, &hTokenDup);
DWORDdwSessionId = WTSGetActiveConsoleSessionId();
SetTokenInformation(hTokenDup, TokenSessionId, &dwSessionId, sizeof(DWORD));
 
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(STARTUPINFO));
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
si.cb = sizeof(STARTUPINFO);
si.lpDesktop = "WinSta0//Default";
 
LPVOID pEnv = NULL;
DWORD dwCreationFlag = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;
 
CreateEnvironmentBlock(&pEnv, hTokenDup, FALSE);
 
CreateProcessAsUser(
              hTokenDup,
              NULL,
              (char *)"notepad",
              NULL,
              NULL,
              FALSE,
              dwCreationFlag,
              pEnv,
              NULL,
              &si,
              &pi);

 
到這裏我們的大部分工作已經完成了,我們還需要做的就是監控活動Session的變化,就是用戶的登錄、註銷、快速切換。WTS系列API以及爲我們提供了具備這些能力的API了,大致可以用一下幾種方法實現:
1.              設置一個定時器,使用WTSGetActiveConsoleSessionId()輪詢活動桌面id,當檢測到變化的時候讓用戶交互程序的前一個實例退出,在新活動Session中創建新進程。
2.              使用WTSRegisterSessionNotification()函數註冊一個窗口來接收WTSSESSION_NOTIFICATION消息,來判斷Session變化。
3.              使用 WTSEnumerateSessions枚舉所有Session然後根據返回的WTS_SESSION_INFO結構中的State成員來判斷Session狀態,找到處於 Active狀態的Session.
結合你的其他需求選擇其中之一,然後作出響應就可以了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章