天天看點

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中的桌面互動了 。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權限的新程序呢?答案是肯定的。一下是實作這個操作的代碼,為了縮小篇幅我删除了異常處理代碼

HANDLEhTokenThis = NULL;

HANDLEhTokenDup = NULL;

HANDLEhThisProcess = GetCurrentProcess();

OpenProcessToken(hThisProcess, TOKEN_ALL_ACCESS, &hTokenThis);

DuplicateTokenEx(hTokenThis, MAXIMUM_ALLOWED,NULL, SecurityIdentification, TokenPrimary, &hTokenDup);

DWORDdwSessionId = WTSGetActiveConsoleSessionId();

SetTokenInformation(hTokenDup, TokenSessionId, &dwSessionId, sizeof(DWORD));

STARTUPINFOsi;

PROCESS_INFORMATION pi;

ZeroMemory(&si, sizeof(STARTUPINFO));

ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));

si.cb = sizeof(STARTUPINFO);

si.lpDesktop = "WinSta0\\Default";

LPVOIDpEnv = NULL;

DWORDdwCreationFlag = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;

CreateEnvironmentBlock(&pEnv, hTokenDup, FALSE);

CreateProcessAsUser(

              hTokenDup,

              NULL,

              (char *)"notepad",

              FALSE,

              dwCreationFlag,

              pEnv,

              &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.

結合你的其他需求選擇其中之一,然後作出響應就可以了。

繼續閱讀