65.9K
CodeProject 正在变化。 阅读更多。
Home

从 Windows 服务监控桌面窗口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (17投票s)

2007 年 11 月 28 日

CPOL

4分钟阅读

viewsIcon

87474

downloadIcon

7267

捕获并从 Windows 服务保存桌面窗口。

Screenshot - history.png

引言

有时,您可能希望在不一直盯着的情况下监视计算机的桌面。可能是因为您在监视孩子上网,或者只是检查正在运行的应用程序的状态(该应用程序没有良好的日志)。如果您对该应用程序的工作原理不感兴趣,只需安装提供的 MSI 文件,然后跳至“使用应用程序”部分。如果您对从服务中获取窗口快照的内部工作原理感兴趣,则必须查看代码并了解使其实现的技巧。本文有三个程序集:一个 Windows 服务,一个 UI 查看器/管理器,以及一个执行实际捕获的共享库。如果您选择从源代码构建 Windows 服务,则必须通过运行 installserv.bat 来自行注册它。

背景

在我以前的一篇文章中,我曾将窗口捕获到图像文件中。其中一个缺点是必须弹出特定的窗口才能捕获它。后来,我发现可以在不将窗口置于前景的情况下捕获窗口,甚至可以在从图标恢复时隐藏它。您可能会丢失某些控件(例如复选列表框)的一些细节,但这付出的代价很小。

使 Windows 服务能够与桌面交互

由于 Windows 服务通常没有 UI,因此您必须允许它与桌面交互。您可以在服务控制台中手动完成此操作(在注册表中),并冒着与服务控制器不同步的风险,或者更好地使用 WMI 在 AfterInstall 事件中执行此操作,如下所示:

private void ServiceMonitorInstaller_AfterInstall(object sender, InstallEventArgs e)
{
    ManagementObject wmiService = null;
    ManagementBaseObject InParam = null;
    try
    {
        wmiService = new ManagementObject(string.Format("Win32_Service.Name='{0}'", 
                         ServiceMonitorInstaller.ServiceName));
        InParam = wmiService.GetMethodParameters("Change");
        InParam["DesktopInteract"] = true;
        wmiService.InvokeMethod("Change", InParam, null);
    }
    finally
    {
        if (InParam != null)
            InParam.Dispose();
        if (wmiService != null)
            wmiService.Dispose();
    }
}

有时,使用系统帐户允许桌面交互是不可能的,您可能需要通过模拟交互式用户来以编程方式实现。

与桌面交互的另一种方式

为了方便使用默认桌面,我创建了 ImpersonateInteractiveUser 类,该类使用其构造函数和 Dispose 方法来切换 UI 上下文,并可选地切换 Windows 模拟。Windows 模拟只是一个附加功能,您可以通过将 bimpersonate 参数设置为 true 来使用它,并且当前用户的安全令牌会从现有的 UI 进程(如 Explorer)中检索。实现此目的的代码如下所示,但您可能需要检查整个类,因为代码相当复杂。

//.ctor
public ImpersonateInteractiveUser(Process proc, bool bimpersonate)
{
    _bimpersonate = bimpersonate;
    ImpersonateUsingProcess(proc);
}

private void ImpersonateUsingProcess(Process proc)
{
    IntPtr hToken = IntPtr.Zero;
    Win32API.RevertToSelf();
    if (Win32API.OpenProcessToken(proc.Handle, 
             TokenPrivilege.TOKEN_ALL_ACCESS, ref hToken) != 0)
    {
        try
        {
            SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
            sa.Length = Marshal.SizeOf(sa);
            bool result = Win32API.DuplicateTokenEx(hToken, 
              Win32API.GENERIC_ALL_ACCESS, ref sa,
             (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, 
             (int)TOKEN_TYPE.TokenPrimary, ref _userTokenHandle);
            if (IntPtr.Zero == _userTokenHandle)
            {

                Win32Exception ex = new Win32Exception(Marshal.GetLastWin32Error());
                throw new ApplicationException(string.Format("Can't duplicate" + 
                      " the token for {0}:\n{1}", proc.ProcessName, ex.Message), ex);
            }

            if (!ImpersonateDesktop())
            {
                Win32Exception ex = new Win32Exception(Marshal.GetLastWin32Error());
                throw new ApplicationException(ex.Message, ex);
            }
        }
        finally
        {
            Win32API.CloseHandle(hToken);
        }
    }
    else
    {
        string s = String.Format("OpenProcess Failed {0}, privilege not held", 
                                 Marshal.GetLastWin32Error());
        throw new Exception(s);
    }
}

bool ImpersonateDesktop()
{
    _hSaveWinSta = Win32API.GetProcessWindowStation();
    if (_hSaveWinSta == IntPtr.Zero)
        return false;
    _hSaveDesktop = Win32API.GetThreadDesktop(Win32API.GetCurrentThreadId());
    if (_hSaveDesktop == IntPtr.Zero)
        return false;
    if (_bimpersonate)
    {
        WindowsIdentity newId = new WindowsIdentity(_userTokenHandle);
        _impersonatedUser = newId.Impersonate();
    }
    _hWinSta = Win32API.OpenWindowStation("WinSta0", false, 
                                          Win32API.MAXIMUM_ALLOWED);
    if (_hWinSta == IntPtr.Zero)
        return false;
    if (!Win32API.SetProcessWindowStation(_hWinSta))
        return false;
    _hDesktop = Win32API.OpenDesktop("Default", 0, true, Win32API.MAXIMUM_ALLOWED);
    if (_hDesktop == IntPtr.Zero)
    {
        Win32API.SetProcessWindowStation(_hSaveWinSta);
        Win32API.CloseWindowStation(_hWinSta);
        return false;
    }
    if (!Win32API.SetThreadDesktop(_hDesktop))
        return false;
    return true;
}

不幸的是,有时某些 UI API(例如 SetWindowLong)在从服务运行时不起作用,但在从常规用户进程运行时则可以正常工作。我不知道这是 Windows 错误还是“按设计”的功能,但上述方法无济于事。为了绕过此限制,我必须确保它们仅在必要时(即需要弹出图标窗口并使其透明,以免用户注意到闪烁)从由服务生成的进程中调用。

将服务作为常规进程运行

在生成用户进程时,我使用与服务相同的程序集,但带有窗口句柄作为参数。args.Length 实际上决定了它是作为服务还是作为常规进程运行。您可以看到在 using 语句中使用了 ImpersonateInteractiveUser 类。

static void Main(string[] args)
{
    if (args.Length > 0)
    {
        WndList lst = new WndList(args.Length);
        foreach (string txt in args)
        {
            lst.Add(new IntPtr(Convert.ToInt32(txt)));
        }
        try
        {
            string folder = System.IO.Path.GetDirectoryName(
                               Assembly.GetExecutingAssembly().Location);
            ScreenMonitorLib.SnapShot snp = 
              new ScreenMonitorLib.SnapShot(folder, ScreenMonitor._interval);
            Process proc = Process.GetProcessesByName("explorer")[0];
            using (ImpersonateInteractiveUser imptst = 
                    new ImpersonateInteractiveUser(proc, false))
            {
                snp.SaveSnapShots(lst);
            }
        }
        catch (Exception ex)
        {

            EventLog.WriteEntry("Screen Monitor", 
              string.Format("exception in user proc:{0}\n at {1}", 
              ex.Message,ex.StackTrace), EventLogEntryType.Error, 1, 1);
        }
        return;
    }
    else
    {
        ScreenMonitor sm = new ScreenMonitor();
        ServiceBase[] ServicesToRun;
        ServicesToRun = new ServiceBase[] { sm };
        ServiceBase.Run(ServicesToRun);
    }
}

存储图像文件的哈希值

由于窗口捕获的保存已在我之前的文章中进行了介绍,因此我必须找到一种为图像文件编制索引的方法。我利用了 MD5CryptoServiceProvider,它提供了一个 16 字节数组,可以存储在数据集中 GUID 中。

private void PersistCapture(IntPtr hWnd, Bitmap bitmap, 
             bool isIconic, SnapShotDS.WndSettingsRow rowSettings)
{
    using (MemoryStream ms = new MemoryStream())
    {
        bitmap.Save(ms, ImageFormat.Jpeg);
        MD5 md5 = new MD5CryptoServiceProvider();
        md5.Initialize();
        ms.Position = 0;
        byte[] result = md5.ComputeHash(ms);
        Guid guid = new Guid(result);
        int len = _tblSnapShots.Select(string.Format("{0} = '{1}'", 
                  _tblSnapShots.FileNameColumn.ColumnName, guid.ToString())).Length;
        string path = System.IO.Path.Combine(_folder, guid.ToString() + ".jpg");
        if (len == 0 || !File.Exists(path))
        {
            using (FileStream fs = File.OpenWrite(path))
            {
                ms.WriteTo(fs);
            }
        }

// code ommited for brevity…..

}

管理服务事件

为了管理 OnSessionChangeOnStop 的事件,我添加了两个 Windows 事件:_terminate 用于检查服务停止并创建超时间隔,_desktopUnLocked 用于处理会话更改,如下所示:

try
{
    StartNewDesktopSession();
    string folder = System.IO.Path.GetDirectoryName(
             Assembly.GetExecutingAssembly().Location) + "\\";
    ScreenMonitorLib.SnapShot snp = new ScreenMonitorLib.SnapShot(folder, 
                                          _interval, this.ServiceHandle);
    bool bexit = false;
    do
    {
        bexit = _terminate.WaitOne(_interval, false);
        _desktopUnLocked.WaitOne();
        if(_blocked)
        {
            _blocked = false;
            continue;
        }
        WndList lst = snp.GetDesktopWindows(_imptst.HDesktop);
        snp.SaveAllSnapShots(lst);
    }
    while (!bexit);
}
catch (ApplicationException ex)
{
    EventLog.WriteEntry("Screen Monitor", 
       string.Format("ApplicationException: {0}\n at {1}", 
       ex.Message, ex.InnerException.TargetSite), 
       EventLogEntryType.Error, 1, 1);
}
catch (Exception ex)
{
    EventLog.WriteEntry("Screen Monitor", 
             string.Format("exception in thread at: {0}:{1}", 
             ex.TargetSite.Name, ex.Message), 
             EventLogEntryType.Error, 1, 1);
}
finally
{
    if (_imptst != null)
        _imptst.Dispose();
}

设置服务启动模式和其他参数

该服务将存储在 data.xml 表中的历史信息持久化,并从 SnapShotmanager.exe 创建的 settings.xml 加载一些设置。另一个值得注意的技巧是设置服务启动模式并使用 WMI 启动它,如下所示:

//code omitted for brevity….

ManagementObject wmiService = null;
ManagementBaseObject InParam = null;
try
{
    ConnectionOptions coOptions = new ConnectionOptions();
    coOptions.Impersonation = ImpersonationLevel.Impersonate;
    ManagementScope mgmtScope = 
      new System.Management.ManagementScope(@"root\CIMV2", coOptions);
    mgmtScope.Connect();
    // Query WMI for additional information about this service.



    wmiService = new ManagementObject("Win32_Service.Name='Screen Monitor'");
    wmiService.Get();
    object o = wmiService["StartMode"];//"Auto" or "Disabled"



    InParam = wmiService.GetMethodParameters("ChangeStartMode");
    string start = _cbxStartupType.Text;
    InParam["StartMode"] = start;
    ManagementBaseObject outParams = 
      wmiService.InvokeMethod("ChangeStartMode", InParam, null);
      uint ret = (uint)(outParams.Properties["ReturnValue"].Value);
    if (ret != 0)
        MessageBox.Show(this, "Error", 
                   string.Format("Failed to set the Start mode, error code: {0}", ret), 
                   MessageBoxButtons.OK, MessageBoxIcon.Warning);

    //bad parent process

    if (_ckStartService.Checked)
    {
        // Start service



        outParams = wmiService.InvokeMethod("StartService", null, null);
        ret = (uint)(outParams.Properties["ReturnValue"].Value);
        if (ret != 0)
            MessageBox.Show(this, "Error", 
                 string.Format("Failed to start the service with error code: {0}", ret), 
                 MessageBoxButtons.OK, MessageBoxIcon.Warning);
    }
}
catch (System.Management.ManagementException ex)
{
    MessageBox.Show(this, "The service might not be installed on this computer. or\n" + 
                    ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
catch (Exception ex)
{
    MessageBox.Show(this, ex.Message, "Error", 
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
    if (InParam != null)
        InParam.Dispose();
    if (wmiService != null)
        wmiService.Dispose();
}

使用应用程序

Screenshot - settings.png

在“设置”选项卡上,您必须选择要跟踪的应用程序以及何时进行捕获。您还可以指定捕获之间的时间间隔和服务启动类型。服务可以从服务控制台启动,或者更方便地从 Snapshot Manager 启动。在“查看器”选项卡(参见本页顶部)上,您可以查看和删除历史记录以及关联的图像文件。

历史

这是 1.0.0.0 版本,已在 Windows XP 上进行测试。它处理锁定/解锁桌面、登录/注销,但不支持快速会话切换功能。您可能会在此应用程序中发现其他一些很酷的功能,例如将数据绑定到嵌入在数据网格单元中的组合框,但这超出了当前主题的范围。正如有人指出的那样,您必须在管理员帐户下运行才能安装服务或 MSI。尽情享受吧!

© . All rights reserved.