您的位置:首页 > 其它

一个Service问题的求解历程

2008-04-29 13:02 309 查看
一个Service问题的求解历程
Asiafly( asiafly@gmail.com
作者版权所有,允许自由拷贝,转载请注明来自http://blog.csdn.net/Asiafly/
1、 楔子
我们开发的一套C/ S架构的应用系统(以下简称“P系统”),Client端有两种启动方式:其一, Server端通过网络给Client端发送指令,Client端机器中有个Windows Service服务,在接收到指令后,启动Client程序,我们称之为“网络模式”。其二,Client端还有一个管理程序,可以直接用它启动Client程序,我们称之为“单机模式”。这套系统一项运行良好,不知过了几个月还是几十个月,终于,好日子慢慢地走到了尽头……
先是客户报告:老机器上明明运行得好好的,自己一高兴,换了新的笔记本就运行不正常了呢?!客户的报告没头没脑,既没有告诉我们当前的系统配置,也没有把我们的系统日志给发过来,然后就猛然这么一问,确实让人疑窦丛生。私底下,我们不禁低估:难道咱们的系统也欺生?……
时间滑到2008,奥运年啊。趁着只涨了米价、油价,电脑价格没涨的大好时机,客户们纷纷都打起换笔记本的注意来。于是,这下子好家伙,更多的客户都打电话给我们了:只听说有“嫌贫爱富”,可咱们的系统却咋“嫌富爱贫”起来了?换了1G的内存、150G的硬盘,换新机子,怎么系统就罢起工来了呢!
正巧,还有几个新需求,于是公司就英明地指出:P系统应该出一个改进版,把这些问题统统搞定!领导都发话了,项目组人马当然不敢怠慢。立项,开工!不是说咱系统“嫌富爱贫”么,我也乘机向公司里头搜刮了几个新家伙:一律是高内存、大硬盘,而且还是HD声卡(High Definition Audio),据说这种声卡用来听音乐很不错哦!

2、 问题的发现

书要从简,分析、设计、评审、Coding都没啥好说的,转眼间第一个版本构建出来了,我正捧一杯水,等着测试人员说“冒烟通过”呢,可测试结果左等不出来,右等不出来,一问才知道原来测试人员碰到更诡异的问题了:明明都跑得好好的,可是怎么“单机模式”下,啥事儿也没有,“网络模式”下却恁是录不了音呢!这几天Coding的时候回顾了一下Client端的代码,在录音这一块的处理,明明是不分单机版还是网络版的啊,怎么这就不行了?!
又试了几回,果真如此。没办法,赶紧跳到代码找找原因吧。先是把Client程序中跟语音相关的部分全文档搜了一番,没看到哪儿跟“网络模式”、“单机模式”不一样的啊。还不死心,在IDE环境里两种模式又debug了一回。结果是再次验证这个诡异的bug是客观存在的。
既然代码中没有什么差异,那么只能是外部环境的不一样了。那么用Service启动和管理程序分别启动一下Windows的“录音机”(sndrec32.exe)试试!结果正如所料:“网络模式”中,即用Service启动的sndrec32.exe,也不能录音!原来如此!……

3、 改权限试试
猛然想到:Service是在Windows登录之前就已启动了,那么莫非是权限问题?赶紧打开“控制面板”,找到“服务”——已经是“本地系统帐号Local System”权限了啊(如图1)。



“系统”帐户,看起名字来,好像权限不低。打开其他的“服务”看看,糟糕,原来大多是“系统帐户”,Windows那群牛人不会不知道“最小权限原则”(就是只给程序分配能运行的最小权限)的,恐怕Local System只会是盛名之下,其实难副。不过这难不倒我,自己的机子还能没有Administrator密码么?赶紧换Administrator试试。
用sndrec32.exe代替Client程序(反正是测试录音功能,用录音机比咱们的Client程序方便多了),并且重启了一下Service,再试试。Server端发出指令:“启动Client”。咦,没有反应。再发!还没有反应,不过好像闪了一下。我发,我发,我发发发——就是不见动静。Service一向相当听话的啊,怎么回事?找来Service的日志看看,日志里白纸黑字地记载着“收到Server端的指令***,启动Client***”。
难道是没有显示?打开“任务管理器”,好家伙,一大堆sndrec32.exe排在那儿!怎么系统自带的录音机也有害羞地躲在后面的癖好?!!肯定是那儿搞错了!再仔细地看一下Service的属性——哦,原来默认的“本地系统帐户”下面还勾着“允许服务与桌面交互”这么个选项。找帮助看看:(如图2)



原来MS已经明白地告诉我们:只有在LocalSystem帐户下才能与桌面交互的哦!看来直接修改权限是不行的!

4、 改变进程的启动关系
既然Services直接启动Client不行,那么由它调用管理程序,再由管理程序来调用Client是不是就可以了呢?Service调用管理程序有很多地方要修改的,也不用这么麻烦,直接写一个批处理文件,让Service调用批处理文件,应该能过得到同样的结果。赶紧试试吧!将Service的登录方式改会默认的Local System,重启Service,Server端发送指令,sndrec32.exe熟悉地出现了。那就录音吧——不好,还是录不了!
只好拿出法宝——“拍脑袋法”,拍呀拍,想啊想,终于记起来了:进程的启动应该是继承关系。Service启动的程序都应该是Local System权限的,只要是Service启动的,不管中间经过几次程序启动程序的过程,都应该是Local System权限。
找到程序员百宝箱中,查看进程的最牛叉工具Process Explorer来看看。结果是一目了然(如图3):



Service的启动关系是smss.exe/Winlogon.exe/Services.exe/<my service>/sndrec32.exe,而直接双击启动的话就简单多了explorer.exe/sndrec32.exe。
这样看起来,只要父进程是explorer.exe的,应该就可以了!
立马想到:ShellExecute可以用来启动程序,差一下MSDN:
HINSTANCE ShellExecute(
HWND hwnd,
LPCTSTR lpOperation,
LPCTSTR lpFile,
LPCTSTR lpParameters,
LPCTSTR lpDirectory,
INT nShowCmd
);

它的第一个参数是窗口的句柄,那么我如果获得了explorer.exe的句柄不就可以了么?!FindWindow函数是获取窗口句柄最方便的,可是我并不知道explorer的ClassName啊。
MSDN中FindWindow函数原型:
HWND FindWindow(LPCTSTR lpClassName,LPCTSTR lpWindowName);
Baidu一下,看到的尽是某个也叫“explorer.exe”的病毒信息;Google一下,也是“explorer.exe”病毒的查杀。算了算了,我可没空与这个李鬼纠缠。不就是获得explorer的Handle么!大不了我枚举系统中所有进程,获得了它的ProcessID,再找它的Handle不就So Easy了么!
function TLaunchApp.getCurUserTokenProcessID: DWORD;
var
processName : string;
b : boolean;
snapshotHandle : THandle;
processEntry32 : TProcessEntry32;
begin
result := LA_ERRORPROCESSID;

processName := lowercase(LA_CURUSERTOKENPROCESSNAME);
snapshotHandle := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
try
processEntry32.dwSize := Sizeof(TProcessEntry32);
b := Process32First(snapshotHandle, processEntry32);
while b do
begin
if processName = lowercase(processEntry32.szExeFile) then
begin
result := processEntry32.th32ProcessID;
break;
end;
b := Process32Next(snapshotHandle, processEntry32);
end;
finally
CloseHandle(snapshotHandle);
end;
end;

哈哈,得到了Handle,赶紧用ShellExecute试一下。咦,还是不能录音!这又为何呢?!打开进程的属性看看:(如图4)



哦,虽然用explorer作为父进程,但并没有改变它的继承关系。这好比儿子改了姓,但他的爹是谁还是谁。不过可惜的是,Client已经获取的
这样的结果挺让人悲观的。难道要我用Service与explorer通讯,再让explorer来启动Client么?与explorer通讯,并用它来启动程序(注意,不是直接调用explorer来启动程序),这好像只有微软知道怎么办,我能做的可以是这样——写一个自启动程序,随系统启动的时候运行,当Service要启动Client的时候,就通过Windows Message与它通讯,告诉自启动程序把Client跑起来。不过,这又要增加程序,并且它是自启动的,随时都有可能给用户毙掉,杀毒软件或许也会过问自启动程序是不是非法的。这种方式太猥琐了,应该继续调研!

5、 再试试程序中设置权限
前面绕了一圈,也没有找到解决的办法,还是回归到问题的本身吧!不能录音是因为没有足够的权限!那么,如果我用就在启动的时候赋给它足够的权限那又该如何呢?!想到这里,不由眼前一亮!
赶紧Google + MSDN一回,答案一会儿就找到了:可以用OpenProcessToken来打开一个Token(权限管理的令牌),然后DuplicateTokenEx这个Token。如果我打开了explorer.exe的Token,然后复制它的令牌,不就搞定了么!当然,启动的时候也不能用ShellExecute或CreateProcess,而是改用CreateProcessAsUser。
function TLaunchApp.Launch(aCommandLine: string): boolean;
var
parentProcessID : DWORD;
parentHandle : THandle;

hExistToken,
hNewToken : THandle;

si : STARTUPINFO;
pi : TProcessInformation;
begin
Result := false;
// 取得系统进程的ID
parentProcessID := getCurUserTokenProcessID;
if parentProcessID = LA_ERRORPROCESSID then
exit;

// 取得系统进程的Handle
parentHandle := OpenProcess(PROCESS_ALL_ACCESS, true, parentProcessID);
if parentHandle = 0 then
exit;
try
//将系统进程的用户Token复制,作为当前将启动的进程Token.
if OpenProcessToken(parentHandle, TOKEN_ALL_ACCESS, hExistToken) then
begin
DuplicateTokenEx(hExistToken, TOKEN_ALL_ACCESS, nil, SecurityDelegation,
TokenPrimary, hNewToken);
end;

si.cb := Sizeof(si);
si.lpDesktop := PChar('winsta0/default');
si.dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
si.wShowWindow := SW_SHOW;

Result := CreateProcessAsUser(
hNewToken,
nil, //PAnsiChar(aExeFileName)
PAnsiChar(aCommandLine),
nil,
nil,
false,
NORMAL_PRIORITY_CLASS, //CREATE_NO_WINDOW, //CREATE_UNICODE_ENVIRONMENT or DETACHED_PROCESS, //
nil,
nil,
si,
pi //当前启动的Process信息
);
finally
CloseHandle(parentHandle);
end;
end;

将代码加入Service试试!只听见硬盘“吱”的一声,接着就没有反应了——咋空闻其声,不见其人了?我的Service用的还是Local System啊!打开Process Explorer一看,果然sndrec32已经启动了,只是没有显示!不死心,看看它的进程属性:哈哈,现在sndrec32的权限和explorer的终于一摸一样啦!(如图5)



问题没有解决,但已经很近了!
6、 终极解决方案
既然Service直接启动Client,并在CreateProcess时提升了权限,将不能显示桌面窗口,那么如果不在Service中提升权限,改在普通程序中提升权限,会不会有所不同呢?说干就干,马上写了一个控制台程序LaunchAs.exe;在Service中直接用ShellExcute启动LaunchAs.exe,再由LaunchAs.exe启动Client。
再看一下结果——yeah!sndrec32终于可以录音了!
梳理一下思路:Service在启动其他程序的时候,需要以Local System权限启动,这样启动了的程序才具有与桌面交互的功能;可是,由Service启动的程序(如果能与桌面交互)再去启动其他程序,将不再限制为Local System权限了;于是,在已经启动的程序中,提升权限,并且在设置了桌面对象的指针si.lpDesktop := PChar('winsta0/default'); 之后,启动的程序将具有完全正常的当前用户权限。

7、 尾声
到现在,客户们抱怨咱们的系统很有革命精神“嫌富爱贫”,就容易真相大白了。旧电脑中,一般用的是AC97声卡,而现在的新机器普遍使用HD声卡,所以造成了新机器不能用咱们的系统。那么为什么HD声卡在LocalSystem权限下不能用呢?估计是它的驱动调用的某个WinApi要求权限所造成的。至于到底是哪个Api,还是留待读者来研究吧!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐