【问题标题】:Starting application from service running as SYSTEM that can interact with the user从可以与用户交互的 SYSTEM 服务启动应用程序
【发布时间】:2011-11-24 20:35:28
【问题描述】:

我目前有一个应用程序需要从我在 .net 3.5 中编码的 Windows 服务启动。此应用程序当前以运行服务的用户身份运行,在我的情况下为 SYSTEM 用户。如果以 SYSTEM 用户身份运行,它不会向用户桌面显示应用程序。想法?建议?

//constructor
    private Process ETCHNotify = new Process();

//StartService()     
    ETCHNotify.StartInfo.FileName = baseDir + "\\EtchNotify.exe";
    ETCHNotify.StartInfo.UseShellExecute = false;

//BackgroundWorkerThread_DoWork()
    if (!systemData.GetUserName().Equals(""))
    {
        // start ETCHNotify
        try {
            ETCHNotify.Start();
        }
            catch (Exception ex)
        {
            systemData.Run("ERR: Notify can't start: " + ex.Message);
        }
    }

如果我编写的函数 GetUserName()(确定运行 explorer.exe 的用户的用户名)不为空,我只会执行 try/catch

再次重申:所需的功能是启动 ETCHNotify 的状态允许它与当前登录的用户进行交互,由 GetUserName() 确定

【问题讨论】:

标签: c# .net windows service


【解决方案1】:

一些帖子的拼贴(thisthis

请注意,从 Windows Vista 开始,服务被严格禁止直接与用户交互

重要提示:从 Windows 开始,服务无法直接与用户交互 远景。因此,标题为“使用”的部分中提到的技术 不应在新代码中使用交互式服务。

这个“功能”被破坏了,传统观点认为无论如何你都不应该依赖它。服务并非旨在提供 UI 或允许任何类型的直接用户交互。由于可能存在安全风险,Microsoft 自 Windows NT 早期以来一直警告应避免使用此功能。

但是,如果您绝对必须拥有此功能,则有一些可能的解决方法。但我强烈建议您仔细考虑其必要性,并为您的服务探索替代设计。

使用WTSEnumerateSessions 找到正确的桌面,然后使用CreateProcessAsUser 在该桌面上启动应用程序(您将桌面句柄作为STARTUPINFO 结构的一部分传递给它)是正确的。

但是,我强烈建议不要这样做。在某些环境中,例如具有许多活动用户的终端服务器主机,确定哪个桌面是“活动”桌面并不容易,甚至不可能。

更传统的方法是在全球启动组中为您的服务放置一个小型客户端应用程序的快捷方式。然后,此应用程序将与每个用户会话一起启动,并可用于启动其他应用程序(如果需要),无需对用户凭据、会话和/或桌面进行任何处理。

【讨论】:

  • 听马可的话,你不想这样做。编写一个小应用程序来显示该服务的进展情况,并通过电子邮件将链接发送给他们。给他们发短信,写桌面小部件,监控服务。除了这个,即使你让它工作,我怀疑你的用户会热情
  • @tony-hopkinson > 只是为了清楚,我不想让用户看到该服务。要求是启动并确保程序在用户可见的机器上运行......如果程序通过任务管理器关闭或崩溃,请重新启动它。无法篡改程序的执行。这就是为什么我有一项服务来确保程序在用户桌面上运行。
【解决方案2】:

我不打算回答这个问题,因为你已经回答了,(哦,什么?现在已经 2.5 岁了!?)答案...

为了让我的服务与桌面交互,无论 WHAT 桌面,也无论有多少桌面,甚至该服务是否在与桌面应用程序相同的计算机上运行!这与我得到的东西无关......我不会让你厌烦细节,我只会给你肉和土豆,如果你想看更多,请告诉我......

好的。我做的第一件事是创建一个广告服务。这是服务运行的一个线程,它打开一个 UDP 套接字来监听网络上的广播。然后,使用相同的代码,我与客户端应用程序共享它,但它调用 Advertise.CLIENT,而不是 Advertise.SERVER...客户端打开我希望服务打开的端口,并广播一条消息,“你好……有人在外面吗??”,询问他们是否有任何服务器在监听,如果有,回复这个 IP 地址您的计算机名称、IP 地址和端口号,我可以在哪里找到 .NET 远程处理服务...”然后它会等待一小段超时时间,收集它得到的响应,如果不止一个,它会显示用户有一个对话框和一个响应的服务列表......然后客户端选择一个,或者,如果只有一个响应,它将调用 Connect((TServerResponse) res); 对此,建立连接。此时,服务器正在使用带有 WellKnownClientType 和 WellKnownServerType 的 Remoting Services 将自己放在那里...

我认为您对我的“自动服务定位器”不太感兴趣,因为很多人不喜欢 UDP,当您的应用开始在大型网络上广播时更是如此。所以,我假设你会对我的 RemotingHelper 更感兴趣,它可以让客户端连接到服务器。它看起来像这样:

    public static Object GetObject(Type type) 
    {
        try {
            if(_wellKnownTypes == null) {
                InitTypeCache();
            }
            WellKnownClientTypeEntry entr = (WellKnownClientTypeEntry)_wellKnownTypes[type];

            if(entr == null) {
                throw new RemotingException("Type not found!");
            }
            return System.Activator.GetObject(entr.ObjectType, entr.ObjectUrl);
        } catch(System.Net.Sockets.SocketException sex) {
            DebugHelper.Debug.OutputDebugString("SocketException occured in RemotingHelper::GetObject().  Error: {0}.", sex.Message);
            Disconnect();
            if(Connect()) {
                return GetObject(type);
            }
        }
        return null;
    }

    private static void InitTypeCache() 
    {
        if(m_AdvertiseServer == null) {
            throw new RemotingException("AdvertisementServer cannot be null when connecting to a server.");
        }

        _wellKnownTypes = new Dictionary<Type, WellKnownClientTypeEntry>();

        Dictionary<string, object> channelProperties = new Dictionary<string, object>();
        channelProperties["port"] = 0;
        channelProperties["name"] = m_AdvertiseServer.ChannelName;

        Dictionary<string, object> binFormatterProperties = new Dictionary<string, object>();
        binFormatterProperties["typeFilterLevel"] = "Full";

        if(Environment.UserInteractive) {
            BinaryServerFormatterSinkProvider binFormatterProvider = new BinaryServerFormatterSinkProvider(binFormatterProperties, null); 
            _serverChannel = new TcpServerChannel(channelProperties, binFormatterProvider);
            // LEF: Only if we are coming form OUTSIDE the SERVICE do we want to register the channel, since the SERVICE already has this
            //      channel registered in this AppDomain.
            ChannelServices.RegisterChannel(_serverChannel, false);
        }

        System.Diagnostics.Debug.Write(string.Format("Registering: {0}...\n", typeof(IPawnStatServiceStatus)));
        RegisterType(typeof(IPawnStatServiceStatus),m_AdvertiseServer.RunningStatusURL.ToString());
        System.Diagnostics.Debug.Write(string.Format("Registering: {0}...\n", typeof(IPawnStatService)));
        RegisterType(typeof(IPawnStatService),      m_AdvertiseServer.RunningServerURL.ToString());
        System.Diagnostics.Debug.Write(string.Format("Registering: {0}...\n", typeof(IServiceConfiguration)));
        RegisterType(typeof(IServiceConfiguration), m_AdvertiseServer.RunningConfigURL.ToString());
    }

    [SecurityPermission(SecurityAction.Demand, Flags=SecurityPermissionFlag.RemotingConfiguration, RemotingConfiguration=true)]
    public static void RegisterType(Type type, string serviceUrl)
    {
        WellKnownClientTypeEntry clientType = new WellKnownClientTypeEntry(type, serviceUrl);
        if(clientType != RemotingConfiguration.IsWellKnownClientType(type)) {
            RemotingConfiguration.RegisterWellKnownClientType(clientType);
        }
        _wellKnownTypes[type] = clientType;
    }

    public static bool Connect()
    {
        // Init the Advertisement Service, and Locate any listening services out there...
        m_AdvertiseServer.InitClient();
        if(m_AdvertiseServer.LocateServices(iTimeout)) {
            if(!Connected) {
                bConnected = true;
            }
        } else {
            bConnected = false;
        }
        return Connected;
    }

    public static void Disconnect()
    {
        if(_wellKnownTypes != null) {
            _wellKnownTypes.Clear();
        }
        _wellKnownTypes = null;
        if(_serverChannel != null) {
            if(Environment.UserInteractive) {
                // LEF: Don't unregister the channel, because we are running from the service, and we don't want to unregister the channel...
                ChannelServices.UnregisterChannel(_serverChannel);
                // LEF: If we are coming from the SERVICE, we do *NOT* want to unregister the channel, since it is already registered!
                _serverChannel = null;
            }
        }
        bConnected = false;
    }
}

所以,这就是我的远程代码的核心,它允许我编写一个客户端,而不必知道服务安装在哪里,或者网络上运行了多少服务。这使我可以通过网络或本地计算机与它进行通信。让两个或两个以上的人运行该应用程序不是问题,但是,你的可能。现在,我有一些复杂的回调代码,我在其中注册事件以通过远程处理通道,所以我必须有代码检查客户端是否仍然连接,然后再向客户端发送发生事件的通知.另外,如果您为多个用户运行,您可能不想使用 Singleton 对象。这对我来说很好,因为服务器拥有对象,它们是服务器所说的任何东西。因此,例如,我的 STATS 对象是一个 Singleton。当每个人都将看到相同的数据时,没有理由为每个连接创建它的实例,对吧?

如有必要,我可以提供更多代码块。当然,这只是完成这项工作的整体情况的一小部分……更不用说订阅提供商等等了。

为了完整起见,我包含了代码块,以使您的服务在整个流程的生命周期内保持连接。

public override object InitializeLifetimeService() 
{
    ILease lease = (ILease)base.InitializeLifetimeService();

    if(lease.CurrentState == LeaseState.Initial) {
        lease.InitialLeaseTime = TimeSpan.FromHours(24);
        lease.SponsorshipTimeout = TimeSpan.FromSeconds(30);
        lease.RenewOnCallTime = TimeSpan.FromHours(1);
    }
    return lease;
}

#region ISponsor Members

[SecurityPermissionAttribute(SecurityAction.LinkDemand, Flags=SecurityPermissionFlag.Infrastructure)]
public TimeSpan Renewal(ILease lease) 
{
    return TimeSpan.FromHours(12);
}
#endregion

如果你将 ISponsor 接口作为你的服务器对象的一部分,你可以实现上面的代码。

希望其中的一些有用。

【讨论】:

    【解决方案3】:

    当您注册您的服务时,您可以告诉它允许与桌面交互。你可以阅读这个老链接http://www.codeproject.com/KB/install/cswindowsservicedesktop.aspx

    另外,不要忘记您可以让多个用户同时登录。

    显然,在 Windows Vista 和更新版本上,与桌面交互变得更加困难。阅读此内容以获得潜在的解决方案:http://www.codeproject.com/KB/cs/ServiceDesktopInteraction.aspx

    【讨论】:

    • 据我所知,从 Windows Vista 上的服务无法与桌面交互。我错了吗?
    • 是的,我试过了,我确实选择了它。如果我没有选择它,它就不会执行程序。现在我已经选择了该框并允许服务与桌面交互,它会启动程序,但用户无法访问它。我认为是因为它将应用程序作为系统启动。用户在进程资源管理器中看到可执行文件,但在桌面上看不到。
    • 好点@marco,这是在Vista上。但是那里有一个复选框,它确实会影响执行能力的结果。
    • 多用户登录时,GetUserName() 函数将返回运行 explorer.exe 的用户的第一个实例
    • 哦,自Vista以来发生了变化,我已经过时了。阅读此内容:codeproject.com/KB/cs/ServiceDesktopInteraction.aspx 以获得可能的解决方案。
    【解决方案4】:

    最终为了解决这个问题,我接受了@marco 的建议和他提到的帖子。我创建的服务完全独立于与用户交互的托盘应用程序。然而,我确实通过注册表“启动”方法安装了托盘应用程序。服务安装程序现在将安装与用户交互的应用程序......这是最安全和最完整的方法。

    感谢大家的帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-11-15
      • 2017-11-01
      相关资源
      最近更新 更多