昨天用c寫了一個windows服務(服務內部帶一個gui窗口+系統托盤),在windows xp sp3上測試,啟動服務后,系統托盤顯示正常。
但在另一台windows 2003 sp2 上測試(通過遠程桌面登錄),暈了,服務是啟動了(在進程管理器中能看到),但系統托盤看不到,也就是在桌面的右下角看不到系統托盤的圖標。
到網上找原因,找到這么幾篇:
http://blog.s135.com/windows_mstsc/
http://chenjava.blog.51cto.com/374566/80250
http://www.sldd.cn/web/bbsxp/ShowPost.asp?id=45110
http://hi.baidu.com/since2006bitlove/blog/item/81555e4cdc52c5f3d62afc71.html
http://topic.csdn.net/u/20090320/17/84ac2e8e-cdff-4ca8-908a-fe7e6854deb6.html
一開始,我參照:http://topic.csdn.net/u/20090320/17/84ac2e8e-cdff-4ca8-908a-fe7e6854deb6.html 在c的代碼中顯示窗口的前、后面加了這么一段代碼:
HDESK hdeskCurrent;
HDESK hdesk;
HWINSTA hwinstaCurrent;
HWINSTA hwinsta;
hwinstaCurrent = GetProcessWindowStation();
if (hwinstaCurrent == NULL)
{
//LogEvent(_T("get window station err"));
return;
}
hdeskCurrent = GetThreadDesktop(GetCurrentThreadId());
if (hdeskCurrent == NULL)
{
//LogEvent(_T("get window desktop err"));
return;
}
//打開winsta0
hwinsta = OpenWindowStation("winsta0", FALSE,
WINSTA_ACCESSCLIPBOARD |
WINSTA_ACCESSGLOBALATOMS |
WINSTA_CREATEDESKTOP |
WINSTA_ENUMDESKTOPS |
WINSTA_ENUMERATE |
WINSTA_EXITWINDOWS |
WINSTA_READATTRIBUTES |
WINSTA_READSCREEN |
WINSTA_WRITEATTRIBUTES);
if (hwinsta == NULL)
{
//LogEvent(_T("open window station err"));
return;
}
if (!SetProcessWindowStation(hwinsta))
{
//LogEvent(_T("Set window station err"));
return;
}
//打開desktop
hdesk = OpenDesktop("default", 0, FALSE,
DESKTOP_CREATEMENU |
DESKTOP_CREATEWINDOW |
DESKTOP_ENUMERATE |
DESKTOP_HOOKCONTROL |
DESKTOP_JOURNALPLAYBACK |
DESKTOP_JOURNALRECORD |
DESKTOP_READOBJECTS |
DESKTOP_SWITCHDESKTOP |
DESKTOP_WRITEOBJECTS);
if (hdesk == NULL)
{
//LogEvent(_T("Open desktop err"));
return;
}
SetThreadDesktop(hdesk);
//到這一步,我們獲取了和用戶交互(如顯示窗口)的權利
//顯示窗口的代碼寫在這里
.....................................
SetProcessWindowStation(hwinstaCurrent);
SetThreadDesktop(hdeskCurrent);
CloseWindowStation(hwinsta);
CloseDesktop(hdesk);
編譯后,在另一台 windows 2003 sp2 上啟動服務,然后在 windows xp sp3 上,運行 mstsc /console,吊用沒有。
為什么沒效果呢?
接着搜索,找到一篇:
遠程桌面mstsc /console(/admin) 的運用 -> http://lcq225.blog.163.com/blog/static/16978498201171252623326/
原來:如果系統是WINXP SP3,是不支持 /console這個參數的,需要使用mstsc /admin。經測試,vista 也不支持 mstsc /console模式,看了一下幫助,是沒有/console這個參數的。
windows xp升級到sp3后,命令換成mstsc /admin即可實現winXp2中MSTSC /console的功能。
我試着使用 mstsc /admin 登錄,在Java 6 環境,發現系統托盤區還是沒有顯示圖標。
但是,如果手動啟動服務,系統托盤就能顯示圖標了。
我試着使用 mstsc /admin 登錄,在Java 7 環境,發現系統托盤區可以正常顯示圖標。
難道不成,java 6 與 java 7在系統托盤(SystemTray)方面的底層實現有所不同???
我接着把上面的代碼刪除,再啟動服務,系統托盤也會顯示圖標,看來上面的代碼是不需要了。
當然,windows服務需要與桌面交互,還是需要設置一下服務的屬性:
打開控制面板->服務,查看服務的屬性->[登錄]-[允許服務與桌面交互],打上鈎后,系統托盤就能顯示在任務欄。
至於為什么不能顯示系統托盤的原因,看了這個介紹才明白:
我們的軟件在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中啟動我們的用戶界面程序了。
2012-01-21