Windows Vista 對(duì)快速用戶(hù)切換,用戶(hù)賬戶(hù)權(quán)限,以及服務(wù)程序所運(yùn)行的會(huì)話空間都作了很大的改動(dòng),致使一些原本可以工作的程序不再能夠正常工作了,我們不得不進(jìn)行一些改進(jìn)以跟上 Vista 的步伐。
我們的軟件在Windows NT/2000/XP/Vista 系統(tǒng)中安裝了一個(gè)系統(tǒng)服務(wù),這個(gè)服務(wù)負(fù)責(zé)以 SYSTEM 權(quán)限啟動(dòng)我們的主程序。我們的主程序啟動(dòng)后會(huì)在系統(tǒng)托盤(pán)添加一個(gè)圖標(biāo),點(diǎn)擊此圖標(biāo)可以彈出控制菜單,通過(guò)這個(gè)菜單也可以激活配置程序首選項(xiàng)的對(duì)話框。在 Windows NT/2000/XP 下我們的程序都可以正常工作。哦不,當(dāng) XP 具備了快速用戶(hù)切換功能的時(shí)候我們的問(wèn)題已經(jīng)出現(xiàn)了。XP 啟動(dòng)后我們以用戶(hù) A 登錄,我們的圖標(biāo)出現(xiàn)在系統(tǒng)托盤(pán),一切工作都正常,可當(dāng)我們使用快速用戶(hù)切換,切換到用戶(hù)B后(用戶(hù)A此時(shí)也是已登錄狀態(tài),并沒(méi)有注銷(xiāo)),雖然用戶(hù)B已經(jīng)是本地控制臺(tái)會(huì)話(Session 屬性為 Console)但我們的圖標(biāo)已經(jīng)無(wú)法出現(xiàn)了,自然菜單和對(duì)話框更無(wú)從談起了。我們的程序是和本機(jī)控制臺(tái)桌面相關(guān)的,這種情況無(wú)疑是個(gè)缺陷。再來(lái)看一下在 Vista 平臺(tái)是怎么樣吧,系統(tǒng)啟動(dòng)后以用戶(hù)A登錄,我們的圖標(biāo)更本就沒(méi)有出現(xiàn),查看進(jìn)程管理器中的進(jìn)程列表發(fā)現(xiàn)我們的程序已經(jīng)啟動(dòng)了,當(dāng)我們從遠(yuǎn)端檢查我們的服務(wù),發(fā)現(xiàn)已經(jīng)正常工作,嘗試遠(yuǎn)程登錄我們的服務(wù),Vista 會(huì)在本機(jī)控制臺(tái)彈出一個(gè)消息框,提示有交互式服務(wù)消息,是否查看這個(gè)消息,點(diǎn)擊立刻查看發(fā)現(xiàn)切換到另外一個(gè)桌面去了。
于是開(kāi)始分析這種情況發(fā)生的原因。在 Windows NT/2000 中系統(tǒng)服務(wù)進(jìn)程和本機(jī)控制臺(tái)交互式登錄的用戶(hù)都運(yùn)行于Session0 中,默認(rèn)用戶(hù)桌面運(yùn)行于 WinSta0 窗口站,所以我們的程序由服務(wù)程序啟動(dòng)時(shí)依然是和本機(jī)用戶(hù)處于同一個(gè)Session中,即使在某些情況下出現(xiàn)不能彈出對(duì)話框或者無(wú)法添加系統(tǒng)托盤(pán)圖標(biāo)的情況也只需要修改一下進(jìn)程桌面到 WinSta0\Default 就可以了(可以參考 MSDN 中 OpenInputDesktop, SetThreadDesktop 等API的說(shuō)明)。
XP為我們帶來(lái)了快速用戶(hù)切換,也讓我們所采用的軟件架構(gòu)問(wèn)題浮現(xiàn)出來(lái)。當(dāng)我們快速切換到用戶(hù)B的時(shí)候,用戶(hù)A仍然在會(huì)話中(Session0),而用戶(hù)B則處于新啟動(dòng)的會(huì)話中(Session1或者其他),此時(shí)服務(wù)程序和本機(jī)控制臺(tái)程序就不在處于同一會(huì)話了,OpenInputDesktop,SetThreadDesktop 等API的工作范圍僅限于本Session,用戶(hù)A沒(méi)有退出,Session0也依然存在但是已經(jīng)是 Disconnected 狀態(tài),當(dāng)進(jìn)程所處的Session是 Disconnected 狀態(tài)的時(shí)候調(diào)用 OpenInputDesktop 會(huì)返回錯(cuò)誤“無(wú)效的API”。進(jìn)程及線程所屬的Session 是由他們的Token 結(jié)構(gòu)中的 TokenSessionId 決定的(參見(jiàn)MSDN中SetTokenInformation 和 TOKEN_INFORMATION_CLASS的說(shuō)明),我嘗試以微軟提供的相關(guān)API修改運(yùn)行中的進(jìn)程和線程的TokenSessionId 信息從而達(dá)到修改桌面環(huán)境的目的,到目前還沒(méi)有成功過(guò)(或許可以嘗試參考RootKit 技術(shù),不過(guò)即使修改成功到底能不能實(shí)現(xiàn)我們的需求也不確定)。我們的進(jìn)程無(wú)法跨越Session的界限,自然無(wú)法與當(dāng)前活動(dòng)的另外一個(gè)Session中的桌面交互了,L 。
Vista中又是如何的一番景象呢?處于安全方面及其他因素的考慮,Vista以及將所有的服務(wù)程序置于Session0中,而為本機(jī)第一個(gè)交互登錄的用戶(hù)創(chuàng)建了Session1,快速切換到用戶(hù)B后則是 Session2,無(wú)論是本機(jī)登錄的用戶(hù),快速切換后的用戶(hù),還是遠(yuǎn)程桌面登錄的用戶(hù)再也沒(méi)有誰(shuí)和服務(wù)進(jìn)程處于同一個(gè)Session中了,我們的程序還運(yùn)行在Session0中,自然我們的托盤(pán)圖標(biāo)是沒(méi)有用戶(hù)能看到了。事實(shí)上這個(gè)圖標(biāo)還是可以出現(xiàn)的。Session0因?yàn)椴皇且粋€(gè)交互式會(huì)話所以沒(méi)有象其他用戶(hù)環(huán)境初始化的時(shí)候一樣啟動(dòng)Explorer程序,但是我們開(kāi)始可以手工啟動(dòng)他,在Session0中啟動(dòng) Explorer 后任務(wù)欄出現(xiàn)后我們還是看到了我們的圖標(biāo)(具體啟動(dòng)Explorer的方法我們不在此文中討論),菜單、對(duì)話框也可以使用。
既然我們的程序必須運(yùn)行在Session0而我們又沒(méi)有辦法把我們的圖標(biāo)、對(duì)話框一下子就拋到隔壁Session的用戶(hù)桌面上去,只能想其他的辦法了。微軟也不提倡我們這種服務(wù)程序直接提供GUI與用戶(hù)直接交互的方式,而他們建議使用C/S架構(gòu),Client/Server之間用Socket/Pipe/RPC等方式通訊,這樣我們只要把Client整個(gè)進(jìn)程放到用戶(hù)Session去和用戶(hù)交互,然后將配置信息等內(nèi)容通過(guò)上述途徑傳遞給Server,服務(wù)端在作出相應(yīng)的響應(yīng)即可。
把GUI分離出來(lái)并不是那么困難,然后在以前直接調(diào)用的地方加上一個(gè)通過(guò)Pipe通訊的接口,這樣GUI(Client)的運(yùn)行就可以靈活的掌握了。
最初我想把用戶(hù)界面程序放到 Startup(啟動(dòng))中隨用戶(hù)登錄自動(dòng)啟動(dòng)。這樣當(dāng)用戶(hù)A和B都登錄后將有兩個(gè)用戶(hù)界面程序在運(yùn)行,而我們的服務(wù)只是和當(dāng)前活動(dòng)的控制臺(tái)登錄用戶(hù)交互,所以這樣并不符合需求。
接下來(lái)我們需要看看如何判定當(dāng)前的活動(dòng)Session是哪個(gè),然后如何在這個(gè)活動(dòng)Session中啟動(dòng)我們的用戶(hù)界面程序了。
微軟從XP/2003開(kāi)始為我們提供了一套Windows Terminal Service 的相關(guān)API,這些API都以WTS開(kāi)頭(請(qǐng)安裝MSDN2005以查閱相關(guān)說(shuō)明),要獲得活動(dòng)Session也不止一個(gè)途徑,最簡(jiǎn)單的就是直接使用
DWORD WTSGetActiveConsoleSessionId(void);
來(lái)獲得活動(dòng)Session Id 。要在程序中使用這些API需要最新的Platform SDK(如果你正在使用Visual Studio 2005那么它已經(jīng)具備了相關(guān)頭文件和庫(kù)文件可以直接使用了),如果你在使用VC++ 6.0 你也沒(méi)有或者不打算安裝最新的SDK那么你可以直接使用LoadLibrary() 裝載wtsapi32.dll然后使用GetProcAddress()獲得相關(guān)函數(shù)的地址以調(diào)用它們。我們獲得了活動(dòng)SessionId后就可以使用
BOOL WTSQueryUserToken(
ULONG SessionId,
PHANDLE phToken
);
來(lái)獲取當(dāng)前活動(dòng)Session中的用戶(hù)令牌(Token),有了這個(gè)Token我們的就可以在活動(dòng)Session中創(chuàng)建新進(jìn)程了,
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的第一個(gè)參數(shù)即可,你可以先嘗試一下運(yùn)行一個(gè)notepad.exe看看,怎么樣?你可以在控制臺(tái)桌面上看到新進(jìn)程了。再查看一下進(jìn)程列表,該進(jìn)程的用戶(hù)名是當(dāng)前控制臺(tái)登錄的用戶(hù)??墒沁@里我們又遇到一個(gè)問(wèn)題,我們需要收集當(dāng)前交本機(jī)互式登錄用戶(hù)的一些信息,而有些操作需要很高的權(quán)限才能完成,而Vista下即使是Administraotrs用戶(hù)組成員默認(rèn)也是以Users權(quán)限啟動(dòng)進(jìn)程的,所以我們創(chuàng)建的新進(jìn)程只有Users權(quán)限,無(wú)法完成一些操作,當(dāng)然我們可以使用Vista所提供的UI來(lái)詢(xún)問(wèn)用戶(hù)以提升至管理員權(quán)限,可有些操作甚至是管理員Token也無(wú)法完成的,而且需要用戶(hù)確認(rèn)實(shí)在在易用性上大打折扣,所以我決定在活動(dòng)Session中以SYSTEM權(quán)限啟動(dòng)我們的用戶(hù)交互程序。顯然 WTSQueryUserToken() 是不好用了。
之前,我們提到過(guò)進(jìn)程所屬的Session是由進(jìn)程Token中的TokenSessionId來(lái)決定的,那么我們是不是可以復(fù)制服務(wù)進(jìn)程的Token然后修改其中的TokenSessionId,從而在用戶(hù)桌面上創(chuàng)建一個(gè)具有SYSTEM權(quán)限的新進(jìn)程呢?答案是肯定的。一下是實(shí)現(xiàn)這個(gè)操作的代碼,為了縮小篇幅我刪除了異常處理代碼
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", NULL, NULL, FALSE, dwCreationFlag, pEnv, NULL, &si, &pi); |
到這里我們的大部分工作已經(jīng)完成了,我們還需要做的就是監(jiān)控活動(dòng)Session的變化,就是用戶(hù)的登錄、注銷(xiāo)、快速切換。WTS系列API以及為我們提供了具備這些能力的API了,大致可以用一下幾種方法實(shí)現(xiàn):
1. 設(shè)置一個(gè)定時(shí)器,使用WTSGetActiveConsoleSessionId()輪詢(xún)活動(dòng)桌面id,當(dāng)檢測(cè)到變化的時(shí)候讓用戶(hù)交互程序的前一個(gè)實(shí)例退出,在新活動(dòng)Session中創(chuàng)建新進(jìn)程。
2. 使用WTSRegisterSessionNotification()函數(shù)注冊(cè)一個(gè)窗口來(lái)接收WTSSESSION_NOTIFICATION消息,來(lái)判斷Session變化。
3. 使用 WTSEnumerateSessions枚舉所有Session然后根據(jù)返回的WTS_SESSION_INFO結(jié)構(gòu)中的State成員來(lái)判斷Session狀態(tài),找到處于 Active狀態(tài)的Session.
結(jié)合你的其他需求選擇其中之一,然后作出響應(yīng)就可以了。
本文只是淺顯的描述一下我在向Windows Vista轉(zhuǎn)移時(shí)遇到的問(wèn)題和我的解決方案,有疏漏及謬誤指出請(qǐng)讀者不吝指正,你有好的想法和實(shí)現(xiàn)也請(qǐng)賜教.