从 Windows 服务监控桌面窗口






4.72/5 (17投票s)
捕获并从 Windows 服务保存桌面窗口。
引言
有时,您可能希望在不一直盯着的情况下监视计算机的桌面。可能是因为您在监视孩子上网,或者只是检查正在运行的应用程序的状态(该应用程序没有良好的日志)。如果您对该应用程序的工作原理不感兴趣,只需安装提供的 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…..
}
管理服务事件
为了管理 OnSessionChange
和 OnStop
的事件,我添加了两个 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();
}
使用应用程序
在“设置”选项卡上,您必须选择要跟踪的应用程序以及何时进行捕获。您还可以指定捕获之间的时间间隔和服务启动类型。服务可以从服务控制台启动,或者更方便地从 Snapshot Manager 启动。在“查看器”选项卡(参见本页顶部)上,您可以查看和删除历史记录以及关联的图像文件。
历史
这是 1.0.0.0 版本,已在 Windows XP 上进行测试。它处理锁定/解锁桌面、登录/注销,但不支持快速会话切换功能。您可能会在此应用程序中发现其他一些很酷的功能,例如将数据绑定到嵌入在数据网格单元中的组合框,但这超出了当前主题的范围。正如有人指出的那样,您必须在管理员帐户下运行才能安装服务或 MSI。尽情享受吧!