一个Service问题的求解历程

一个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,重启ServiceServer端发送指令,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函数是获取窗口句柄最方便的,可是我并不知道explorerClassName啊。

MSDN中FindWindow函数原型:

HWND FindWindow(LPCTSTR lpClassName,LPCTSTR lpWindowName);

Baidu一下,看到的尽是某个也叫“explorer.exe”的病毒信息;Google一下,也是“explorer.exe”病毒的查杀。算了算了,我可没空与这个李鬼纠缠。不就是获得explorerHandle么!大不了我枚举系统中所有进程,获得了它的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已经获取的

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

5、  再试试程序中设置权限

前面绕了一圈,也没有找到解决的办法,还是回归到问题的本身吧!不能录音是因为没有足够的权限!那么,如果我用就在启动的时候赋给它足够的权限那又该如何呢?!想到这里,不由眼前一亮!

赶紧Google + MSDN一回,答案一会儿就找到了:可以用OpenProcessToken来打开一个Token(权限管理的令牌),然后DuplicateTokenEx这个Token。如果我打开了explorer.exeToken,然后复制它的令牌,不就搞定了么!当然,启动的时候也不能用ShellExecuteCreateProcess,而是改用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

再看一下结果——yeahsndrec32终于可以录音了!

梳理一下思路:Service在启动其他程序的时候,需要以Local System权限启动,这样启动了的程序才具有与桌面交互的功能;可是,由Service启动的程序(如果能与桌面交互)再去启动其他程序,将不再限制为Local System权限了;于是,在已经启动的程序中,提升权限,并且在设置了桌面对象的指针si.lpDesktop := PChar('winsta0/default'); 之后,启动的程序将具有完全正常的当前用户权限。


7、  尾声

到现在,客户们抱怨咱们的系统很有革命精神“嫌富爱贫”,就容易真相大白了。旧电脑中,一般用的是AC97声卡,而现在的新机器普遍使用HD声卡,所以造成了新机器不能用咱们的系统。那么为什么HD声卡在LocalSystem权限下不能用呢?估计是它的驱动调用的某个WinApi要求权限所造成的。至于到底是哪个Api,还是留待读者来研究吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值