如何从系统服务启动屏幕保护程序
我有几种启动屏幕保护程序的变体。我最喜欢的是
[DllImport("user32.dll", SetLastError = false)]
private static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
private void startScreensaver()
{
UInt32 WM_SYSCOMMAND = 0x112;
IntPtr SC_SCREENSAVE = new IntPtr(0xf140);
IntPtr hWnd = GetDesktopWindow();
SendMessage(hWnd, WM_SYSCOMMAND, SC_SCREENSAVE, new IntPtr(0));
}
我的问题是我想从系统服务中启动屏幕保护程序。例如,如果我想在会话锁定后立即启动屏幕保护程序(只是为了概念证明),我可以尝试
protected override void OnSessionChange(SessionChangeDescription changeDescription)
{
base.OnSessionChange(changeDescription);
if (changeDescription.Reason == SessionChangeReason.SessionLock)
startScreensaver();
}
这不起作用,我认为原因是该服务是通过
ServiceProcessInstaller.Account 安装的= ServiceAccount.LocalSystem;
它无权访问用户的会话。我可以实现一个在用户会话中运行的小程序,该程序由服务触发以触发屏幕保护程序......但这不是好方法。
有什么建议吗?谢谢。
编辑:显然问题与 GetDesktopWindow();
调用有关,但我仍然不知道如何修复该问题
更新:
根据 Erics 的建议,我现在迭代所有窗口站(使用 OpenWindowStation),然后对于所有这些我迭代所有桌面(使用 EnumDesktops)。然后,我使用 OpenDesktop 打开桌面并将句柄存储到桌面。我的标准 Windows 安装产生以下 windowStation:Desktop:dskHandle
- WinSta0:Default:732
- WinSta0:Disconnect:760
- WinSta0:Winlogon:784
- msswindowstation:mssrestricteddesk:0
列表 我现在启动一个新线程,
[DllImport("user32.dll", SetLastError = true)]
static extern bool SetThreadDesktop(IntPtr hDesktop);
然后在其中调用 startScreensaver上面的()方法。 IntPtr hWnd = GetDesktopWindow()
确实返回合理的结果,但屏幕保护程序尚未启动。在
[DllImport("user32.dll")]
static extern IntPtr OpenDesktop(string lpszDesktop, uint dwFlags, bool fInherit, uint dwDesiredAccess);
我使用GENERIC_ALL = 0x10000000
作为dwDesiredAccess。正如法尔津指出的那样,我检查了
允许服务与桌面交互
我不是 win32 或 pInvoke 专业人士,所以我现在完全迷失了。某人可以解释一下所有这些东西是如何协同工作的吗? sb有更好的建议吗?我想做的就是从系统服务调用屏幕保护程序。
I have several variants to start the screensaver. I favourite is
[DllImport("user32.dll", SetLastError = false)]
private static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
private void startScreensaver()
{
UInt32 WM_SYSCOMMAND = 0x112;
IntPtr SC_SCREENSAVE = new IntPtr(0xf140);
IntPtr hWnd = GetDesktopWindow();
SendMessage(hWnd, WM_SYSCOMMAND, SC_SCREENSAVE, new IntPtr(0));
}
My problem is that i want to start the screensaver out of a system service. If i e.g. want to start the screensaver as soon as the session is locked (just for prove of concept), i could try
protected override void OnSessionChange(SessionChangeDescription changeDescription)
{
base.OnSessionChange(changeDescription);
if (changeDescription.Reason == SessionChangeReason.SessionLock)
startScreensaver();
}
This doesn't work and i think the reason is that the service is installed with the
ServiceProcessInstaller.Account = ServiceAccount.LocalSystem;
which does not have access to the User's session. I could implement a small program that runs in the user session, which is triggered by the service to trigger the screensaver... but that's not the nice way.
Any suggestions? Thanks.
edited: obviously the problem is related to the GetDesktopWindow();
call, still i don't know how to fix that
Update:
According to Erics suggestion, i do now iterate all window station (using OpenWindowStation), then for all of those i iterate all desktops (using EnumDesktops). I then open the desktops using OpenDesktop and store the handle to the desktop. My standard Windows installation yields to the following list of windowStation:Desktop:dskHandle
- WinSta0:Default:732
- WinSta0:Disconnect:760
- WinSta0:Winlogon:784
- msswindowstation:mssrestricteddesk:0
I do now start a new Thread in which i
[DllImport("user32.dll", SetLastError = true)]
static extern bool SetThreadDesktop(IntPtr hDesktop);
and then invoke the startScreensaver() method above. The IntPtr hWnd = GetDesktopWindow()
does return reasonable results, still the screensaver is not started. In the
[DllImport("user32.dll")]
static extern IntPtr OpenDesktop(string lpszDesktop, uint dwFlags, bool fInherit, uint dwDesiredAccess);
i use GENERIC_ALL = 0x10000000
as the dwDesiredAccess. And as Farzin noted, i checked the
Allow service to interact with desktop
I am not a win32 or pInvoke pro, so i am totally lost now. Can sb explain how all the stuff works together? Does sb has a better suggestion? All I want to do is to invoke the screensaver from a system service.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
不要使用
SERVICE_INTERACTIVE_PROCESS
标志,也不依赖交互式服务技术。自 Windows Vista 以来,这种方法已被逐步淘汰。更多内容请此处来自 Microsoft 自己的话:
因此,即使上述任何方法有效,它们也只不过是“黑客”,并且可能在任何新版本甚至 Windows 更新中停止工作。
做你想做的事情的最佳选择是通过你自己提到的,“我可以实现一个在用户会话中运行的小程序,该程序由服务触发以触发屏幕保护程序”。
相信我,我花了无数个小时尝试做你想做的事(以错误的方式),但我失败了。以下是 Microsoft 在其软件中自行执行此操作的方式以及您需要执行此操作的方式:
在您的系统服务中创建一个全局命名的自动重置事件,将其状态设置为非信号。确保调整此事件的安全描述符以供“所有人”读取和同步。更多此处和此处 和 此处介绍如何创建安全描述符。如果您不想稍后处理 ERROR_ACCESS_DENIED 错误,则此步骤非常重要。
制作一个带有隐藏窗口的小型 Win32 GUI 程序。启动时,它会打开由上述服务创建的全局事件。如果我用 C++ 编写它,它看起来像这样: OpenEvent(READ_CONTROL | SYNCHRONIZE, FALSE, _T("Global\Whatever_name_you_use"));
然后创建一个工作线程,仅使用 同步函数。当然,请确保工作线程能够处理这个 GUI 小程序关闭时的情况。
当全局命名自动重置事件发出信号时,从工作线程运行以下代码。使用 wParam = SC_SCREENSAVE 和 lParam = 0 将 WM_SYSCOMMAND 通知发送到主 GUI 线程中其自己的窗口,或者通过从主 GUI 线程调用
DefWindowProc()
API 来完成此操作。这应该为运行 GUI 程序的用户启动当前设置的屏幕保护程序。如果您想启动特定的屏幕保护程序,则只需使用 ShellExecute 和 GUI 程序中的 /s 参数来运行它即可。 (当然,当发出全局命名自动重置事件信号时,从工作线程执行此操作。)所有屏幕保护程序通常都放置在“%WINDIR%\System32”文件夹中。它们具有 .scr 扩展名。
好的,现在如何从系统服务激活它。
当您需要运行屏幕保护程序时,您需要确保您的小型 GUI 程序正在当前活动的用户会话中运行。主动部分很重要。这里有两种方法。第一的。您可以在每次用户会话变为活动状态时启动 GUI 程序(当然,可以通过为停止活动的会话关闭该 GUI 程序的副本。您可以通过全局命名事件向其发出命令来关闭它。可以通过捕获 SERVICE_CONTROL_SESSIONCHANGE 通知来跟踪系统服务和
ServiceHandlerEx()
处理程序中的用户会话更改。)您还可以在需要激活屏幕保护程序并立即关闭它时运行此 GUI 程序。我将由您决定选择哪种方法。要点是,您必须以某种方式在活动用户会话中运行 GUI 程序,并使用全局命名事件与其进行通信。 (当然,您可以合并任何其他方式在我的书中,全局事件最简单地传达布尔值或“是和否”类型的命令。)我需要立即告诉您,在另一个用户会话中启动进程是最简单的。这里的劳动密集型部分记录很少,并且很难调试。简而言之,您需要使用来自系统服务的 CreateProcessAsUser()
API,但困难的部分是准备调用该 API。不幸的是,对于如何调用它没有明确的共识,并且有一个 网上提供的一堆建议都有些不同。对我有用的步骤如下:将 GUI 程序放置在通常可访问的位置(即使对于权限最低的用户也是如此)。由于它是系统服务的一部分,因此您可以使用“%WINDIR%\System32”,但请确保在不再需要时将其从那里删除!
通过调用 WTSEnumerateSessions() 获取当前活动会话,并查看具有 WTSActive 状态的会话。
WTSQueryUserToken() 获取活动用户会话令牌
DuplicateTokenEx(, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &);
通过调用 CreateEnvironmentBlock() 创建环境字符串块
通过调用 LoadUserProfile() 加载用户配置文件。您可以使用以下 API 收集所有必要的信息:NetUserGetInfo() 用于获取配置文件路径,WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, , WTSUserName, &, &) 用于获取会话用户名。
并通过调用 ImpersonateLoggedOnUser() 来模拟该用户
此时,在放置 GUI 程序的位置上调用 CreateProcessAsUser()。让我再说一遍,您必须从您刚刚模拟的用户可访问的位置运行它!这里常见的错误是从这样的位置运行它:“C:\Users\SomeUserName\AppData\Roaming”。这个调用可能看起来像这样:
CreateProcessAsUser(hToken2, NULL, pNonConstOrStaticBufferWithPathToGUIProgram, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, pEnvironmentBlock, NULL, &pSTARTUPINFO, &pPROCESS_INFORMATION);
始终恢复模拟:RevertToSelf();
WaitForInputIdle() 以确保您的 GUI 进程启动并到达消息泵。
通过调用UnloadUserProfile()、DestroyEnvironmentBlock()、WTSFreeMemory()、CloseHandle()等进行清理
现在,您可以通过调用 SetEvent() 来设置全局命名自动重置事件,以通知 GUI 进程启动屏幕保护程序。你就完成了!您可能还想启用 GUI 程序的某种向后反馈,以确保屏幕保护程序实际启动,但我将把它留给您。再次参考IPC的方法 了解实现方法。
作为结论,我要说的是,上述方法是通过无数论坛帖子和多次网络搜索收集的。是的,我知道这种方法有多么庞大和繁琐,但是,嘿,这就是 Windows,不是吗:) 如果您想要简单性,请使用 OS X 或 iOS。这就是我最终所做的......
Do not use
SERVICE_INTERACTIVE_PROCESS
flag, nor rely on Interactive Services techniques. This approach has been phased out since Windows Vista. More hereFrom Microsoft's own words:
So even if any of the mentioned approaches above work they would not be anything than a "hack" and may stop working in any new version or even an update to Windows.
Your best bet to do what you want is through what you mentioned yourself, "I could implement a small program that runs in the user session, which is triggered by the service to trigger the screensaver".
Trust me, I spent countless hours trying to do what you want (the wrong way) and I failed. Here's how Microsoft do it themselves in their software and how you need to do it:
In your system service create a global named auto-reset event, set its state to non-sginaled. Make sure to adjust the security descriptor for this event to be read and synchronized by "Everyone". More here and here and here on creating a security descriptor. This step is important if you don't want to deal with ERROR_ACCESS_DENIED errors later.
Make a small Win32 GUI program that has a hidden window. Upon the start it opens the global event created by the service above. If I was to write it in C++ it'd look like this: OpenEvent(READ_CONTROL | SYNCHRONIZE, FALSE, _T("Global\Whatever_name_you_use"));
Then create a worker thread that simply waits for this event to become signaled using one of the WaitFor*Object APIs from the synchronization functions. Of course, make sure that the worker thread handles the situation when this small GUI program closes.
From the worker thread run the following code when the global named auto-reset event becomes signaled. Send the WM_SYSCOMMAND notification to its own window in the main GUI thread with wParam = SC_SCREENSAVE, and lParam = 0, or do it via a call to
DefWindowProc()
API from the main GUI thread. This should start the currently set up screen saver for the user where the GUI program is running.In case you want to start a specific screensaver, then you can simply run it using ShellExecute with the /s parameter from your GUI program. (Of course, do it from the worker thread when the global named auto-reset event is signaled.) All screensavers are normally placed into the "%WINDIR%\System32" folder. They have the .scr extension.
OK, now how to activate it from the system service.
When you need to run your screensaver you need to ensure that your small GUI program is running in a user session that is currently active. The active part is important. There are two approaches here. First. You can start your GUI program every time a user session becomes active (of course by closing a copy of this GUI program for a session that stops being active. You can close it by issuing a command to it via a global named event. And you can track the user session changes from your system service and the
ServiceHandlerEx()
handler by trapping SERVICE_CONTROL_SESSIONCHANGE notifications.) You can also run this GUI program right when you need to activate the screensaver and then close it immediately. I'll leave it up to you which approach you choose. The main point is that you have to somehow run your GUI program in an active user session and use global named events to communicate with it. (Of course, you can incorporate any other means of the IPC. In my book global events are simplest to convey a boolean, or "yes and no" type command.) I need to tell you right off the bat that starting a process in another user session is the most labor intensive part here, is poorly documented and is hard to debug. In a nutshell, you need to use theCreateProcessAsUser()
API from your system service, but the hard part is prepping for the call to that API. Unfortunately there's no clear-cut consensus on how to call it and there's a bunch of advice available on the web that is all somewhat different. The steps that worked for me are as follows:Place your GUI program into a commonly accessible place (even for the least privileged users). Since it's a part of the system service, you can use "%WINDIR%\System32" but make sure to remove it from there when it is no longer needed!
Get the current active session by calling WTSEnumerateSessions() and look a session with the WTSActive state.
WTSQueryUserToken() to get the active user session token
DuplicateTokenEx(, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &);
Create environment strings block with a call to CreateEnvironmentBlock()
Load user profile by calling LoadUserProfile(). You can collect all necessary info before with the following APIs: NetUserGetInfo() for the profile path, and WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, , WTSUserName, &, &) to get a session user name.
And impersonate that user with a call to ImpersonateLoggedOnUser()
At this point call CreateProcessAsUser() on the location of your GUI program where you placed it. Let me repeat that you must run it from the location accessible for the user that you've just impersonated! The common mistake here is running it from a location like this: "C:\Users\SomeUserName\AppData\Roaming". This call may look like this:
CreateProcessAsUser(hToken2, NULL, pNonConstOrStaticBufferWithPathToGUIProgram, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, pEnvironmentBlock, NULL, &pSTARTUPINFO, &pPROCESS_INFORMATION);
Always revert imporsonation: RevertToSelf();
WaitForInputIdle() to make sure your GUI process started and reached the message pump.
Clean up by calling UnloadUserProfile(), DestroyEnvironmentBlock(), WTSFreeMemory(), CloseHandle(), etc.
Now you can set your global named auto-reset event by calling SetEvent() to signal your GUI process to start the screensaver. And you're done! You may also want to enable some sort of backwards feedback from a GUI program to ensure that the screensaver is actually started, but I'll leave it up to you. Again refer to the means of the IPC for the ways to do it.
As a conclusion, let me say, that the above approach has been collected through a countless forum postings and by gleaning off multiple web searches. And, yes, I understand how bulky and cumbersome this approach is, but, hey, that's what Windows is, isn't it :) If you want simplicity, go OS X or iOS. That's what I eventually did...
转到您的服务,右键单击服务,然后在登录选项卡中将以下项目设置为 true :
如果您想在安装时执行此操作,
:参考:http:// www.codeproject.com/KB/install/cswindowsservicedesktop.aspx
go to your services, right click on service and in the LogOn tab set the item bellow to true :
if you want to do this on install :
Reference : http://www.codeproject.com/KB/install/cswindowsservicedesktop.aspx
它可以在安装时允许服务与桌面交互(将
SERVICE_INTERACTIVE_PROCESS
传递给CreateService
)。否则(可能会出现访问问题 - 我没有尝试过)您需要从 Window Station 和桌面功能。您需要做的是找到登录用户的窗口站(
EnumWindowStations
、OpenWindowStation
)、桌面(EnumDesktops
、OpenDesktop)
),创建一个线程和SetThreadDesktop
,然后最后使用GetDesktopWindow
。It may work to allow the service to interact with the desktop when installing (Pass
SERVICE_INTERACTIVE_PROCESS
toCreateService
). Otherwise (there may be problems with access - I haven't tried this) you'll need to start with Window Station and Desktop Functions.What you need to do is to find the logged on users window station (
EnumWindowStations
,OpenWindowStation
), the Desktop (EnumDesktops
,OpenDesktop
), create a thread andSetThreadDesktop
, then finally useGetDesktopWindow
.