【问题标题】:How to start screensaver from system service如何从系统服务启动屏幕保护程序
【发布时间】:2011-04-20 09:11:01
【问题描述】:

我有几个变体来启动屏幕保护程序。我最喜欢的是

[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:默认值:732
  • WinSta0:断开连接: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。正如 Farzin 所说,我检查了

允许服务与桌面交互

我不是 win32 或 pInvoke 专业人士,所以我现在完全迷失了。某人能解释一下所有的东西是如何一起工作的吗? sb 有更好的建议吗?我要做的就是从系统服务调用屏幕保护程序。

【问题讨论】:

    标签: .net windows windows-services screensaver


    【解决方案1】:

    不要使用SERVICE_INTERACTIVE_PROCESS 标志,也不要依赖交互式服务技术。自 Windows Vista 以来,此方法已被逐步淘汰。更多here

    用微软自己的话来说:

    重要服务不能直接与 Windows Vista 中的用户交互。因此,不应在新代码中使用标题为“使用交互式服务”一节中提到的技术。

    因此,即使上述任何一种方法有效,它们也只不过是“黑客”,并且可能会在任何新版本甚至 Windows 更新中停止工作。

    做你想做的最好的选择是通过你自己提到的,“我可以实现一个在用户会话中运行的小程序,它由服务触发以触发屏幕保护程序” .

    相信我,我花了无数个小时试图做你想做的事(错误的方式)但我失败了。以下是 Microsoft 在他们的软件中自己做的事情以及您需要做的事情:

    1. 在您的系统服务中创建一个全局命名的自动重置事件,将其状态设置为 non-sginaled。确保调整此事件的安全描述符以供“所有人”读取和同步。更多 hereherehere 创建安全描述符。如果您不想稍后处理 ERROR_ACCESS_DENIED 错误,此步骤很重要。

    2. 制作一个带有隐藏窗口的小型 Win32 GUI 程序。启动时,它会打开由上述服务创建的全局事件。如果我用 C++ 编写它,它看起来像这样: OpenEvent(READ_CONTROL | SYNCHRONIZE, FALSE, _T("Global\Whatever_name_you_use")); 然后创建一个工作线程,使用来自synchronization functions 的WaitFor*Object API 之一简单地等待此事件发出信号。当然,请确保工作线程处理这个小 GUI 程序关闭时的情况。

    3. 当全局命名的自动重置事件发出信号时,从工作线程运行以下代码。使用 wParam = SC_SCREENSAVE 和 lParam = 0 将 WM_SYSCOMMAND 通知发送到主 GUI 线程中它自己的窗口,或者通过从主 GUI 线程调用 DefWindowProc() API 来完成。这应该会为运行 GUI 程序的用户启动当前设置的屏幕保护程序。

    4. 如果你想启动一个特定的屏幕保护程序,那么你可以简单地使用 ShellExecute 和 GUI 程序中的 /s 参数来运行它。 (当然,当全局命名的自动重置事件发出信号时,从工作线程执行此操作。)所有屏幕保护程序通常都放在“%WINDIR%\System32”文件夹中。它们具有 .scr 扩展名。

    5. 好的,现在如何从系统服务中激活它。

    6. 当您需要运行屏幕保护程序时,您需要确保您的小型 GUI 程序在当前处于活动状态的用户会话中运行。活跃的部分很重要。这里有两种方法。第一的。每次用户会话激活时,您都可以启动您的 GUI 程序(当然,通过关闭该 GUI 程序的副本来停止活动的会话。您可以通过全局命名事件向它发出命令来关闭它。而且您可以通过捕获 SERVICE_CONTROL_SESSIONCHANGE 通知来跟踪系统服务和ServiceHandlerEx() 处理程序的用户会话更改。)您也可以在需要激活屏幕保护程序时立即运行此 GUI 程序,然后立即关闭它。我将由您决定选择哪种方法。要点是您必须以某种方式在活动用户会话中运行您的 GUI 程序并使用全局命名事件与其通信。 (当然,您可以合并任何other means of the IPC。在我的书中,全局事件最简单地传达布尔值或“是和否”类型的命令。)我需要立即告诉您在另一个用户中启动一个进程session 是这里最费力的部分,文档记录很差,很难调试。简而言之,您需要使用系统服务中的CreateProcessAsUser() API,但困难的部分是准备调用该 API。不幸的是,对于如何称呼它并没有明确的共识,并且有一个bunch of advice available on the web,这一切都有些不同。对我有用的步骤如下:

    • 将您的 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 程序的某种向后反馈,以确保屏幕保护程序实际启动,但我将由您决定。再次参考means of the IPC 的方法。

    作为结论,让我说,上述方法是通过无数论坛帖子和多次网络搜索收集的。而且,是的,我知道这种方法有多么庞大和繁琐,但是,嘿,这就是 Windows,不是吗 :) 如果您想要简单,请使用 OS X 或 iOS。这就是我最终所做的......

    【讨论】:

    • 很棒的帖子,但是绕道“createProcessAsUser”对于我的目的来说太复杂了。我现在只是假设我的“守护程序”正在用户会话中运行。这个小程序通过 Windows Remoting 公开了一个公共接口。从那里我可以启动屏幕保护程序。以后我可能会写一篇博文来总结一下……
    【解决方案2】:

    转到您的服务,右键单击服务并在登录选项卡中将以下项目设置为 true:

    允许服务与桌面交互

    如果您想在安装时执行此操作:

    public WindowsServiceInstaller()
    {
      // This call is required by the Designer.
    
      InitializeComponent();
      ServiceInstaller si = new ServiceInstaller();
      si.ServiceName = "WindowsService1"; 
      si.DisplayName = "WindowsService1";
      si.StartType = ServiceStartMode.Manual;
      this.Installers.Add(si);
      ServiceProcessInstaller spi = new ServiceProcessInstaller();
      spi.Account = System.ServiceProcess.ServiceAccount.LocalSystem; 
      spi.Password = null;
      spi.Username = null;
      this.Installers.Add(spi);
    
      // Here is where we set the bit on the value in the registry.
    
      // Grab the subkey to our service
    
      RegistryKey ckey = Registry.LocalMachine.OpenSubKey(
        @"SYSTEM\CurrentControlSet\Services\WindowsService1", true);
      // Good to always do error checking!
    
      if(ckey != null)
      {
        // Ok now lets make sure the "Type" value is there, 
    
        //and then do our bitwise operation on it.
    
        if(ckey.GetValue("Type") != null)
        {
          ckey.SetValue("Type", ((int)ckey.GetValue("Type") | 256));
        }
      }
    }
    

    参考:http://www.codeproject.com/KB/install/cswindowsservicedesktop.aspx

    【讨论】:

    • 这一点是正确的,我需要“允许服务与桌面交互”,但是我仍然需要计算登录用户的桌面。因此我现在探索 Erik 提供的解决方案
    【解决方案3】:

    安装时允许服务与桌面交互可能会起作用(将SERVICE_INTERACTIVE_PROCESS 传递给CreateService)。否则(访问可能有问题 - 我没有尝试过)你需要从Window Station and Desktop Functions开始。

    你需要做的是找到已登录的用户窗口站(EnumWindowStationsOpenWindowStation),桌面(EnumDesktopsOpenDesktop),创建线程和SetThreadDesktop,然后最后使用GetDesktopWindow.

    【讨论】:

    • 嗯,听起来是一种合理的方法。我可以使用 pInvoke 进行 EnumWindowStation,但我不知道如何使用 OpenWindowStation。你能详细说明一下吗?也许你有时间给一些代码片段?!
    • @Martin:对不起,我不真正做 .NET,不知道。这就是我从语言中立的角度来处理它的方式
    猜你喜欢
    • 2010-09-21
    • 1970-01-01
    • 1970-01-01
    • 2016-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-29
    相关资源
    最近更新 更多