我是否正在作为一个服务运行?

53

我目前正在编写一些Bootstrap代码,用于在控制台中运行的服务。基本上就是调用OnStart()方法,而不是使用ServiceBase启动和停止服务(因为如果未将其安装为服务,则无法运行应用程序,这使得调试变得非常困难)。

现在我正在使用Debugger.IsAttached来确定是否应该使用ServiceBase.Run或[service].OnStart,但我知道那不是最好的想法,因为有时最终用户希望在控制台中运行服务(以实时查看输出等)。

有什么想法可以确定Windows服务控制器是否启动了“我”,还是用户在控制台中启动了“我”?显然Environment.IsUserInteractive不是答案。我考虑过使用命令行参数,但那似乎很“糟糕”。

我总是可以尝试在ServiceBase.Run周围使用try-catch语句,但那似乎也很“糟糕”。编辑:try-catch无效。

我有一个解决方案:在此处公布给其他感兴趣的人:

    public void Run()
    {
        if (Debugger.IsAttached || Environment.GetCommandLineArgs().Contains<string>("-console"))
        {
            RunAllServices();
        }
        else
        {
            try
            {
                string temp = Console.Title;
                ServiceBase.Run((ServiceBase[])ComponentsToRun);
            }
            catch
            {
                RunAllServices();
            }
        }
    } // void Run

    private void RunAllServices()
    {
        foreach (ConsoleService component in ComponentsToRun)
        {
            component.Start();
        }
        WaitForCTRLC();
        foreach (ConsoleService component in ComponentsToRun)
        {
            component.Stop();
        }
    }

编辑:StackOverflow上还有一个问题,其中一位使用者遇到了Environment.CurrentDirectory为"C:\Windows\System32" 的问题,看起来这可能是答案。我今天会进行测试。


感谢您添加解决方案,这将是一个有用的参考。 - Ash
2
请注意,IsUserInteractive不会像您在上面提供的链接中所示那样对控制台应用程序返回false - 至少通常不会。我正在将其用于此目的,并且从未遇到任何问题。 - Christian.K
这里是关于C++的同样问题:https://dev59.com/BEvSa4cB1Zd3GeqPbArA - M.M
这个回答解决了你的问题吗?检测代码是否作为服务运行 - MarkovskI
1
@MarkSchultheiss 如果我理解正确的话,我已经回答了这个问题,但它被删除了 :) - MarkovskI
显示剩余2条评论
13个回答

27

另一种解决方法...可以作为WinForm或Windows服务运行

var backend = new Backend();

if (Environment.UserInteractive)
{
     backend.OnStart();
     Application.EnableVisualStyles();
     Application.SetCompatibleTextRenderingDefault(false);
     Application.Run(new Fronend(backend));
     backend.OnStop();
}
else
{
     var ServicesToRun = new ServiceBase[] {backend};
     ServiceBase.Run(ServicesToRun);
}

5
我喜欢这个解决方案,它似乎是 Environment.UserInteractive 设计的初衷。 - Josh M.
1
我想知道如果你已经为该服务勾选了“允许服务与桌面交互”,会发生什么。据我所知,这将允许服务拥有GUI。那么UserInteractive属性不应该返回true吗? [MSDN:对于没有用户界面的Windows进程或IIS之类的服务,UserInteractive属性报告false。] - Mircea Ion
4
我已经测试过:当你勾选“允许服务与桌面交互”时,UserInteractive为真。 - csname1910
5
我尝试在 Windows Docker 容器中运行我的进程,但在容器中 UserInteractive 也是 false... 然而我绝对没有以服务的形式运行。 - 9Rune5
1
危险:存在几种情况,即使不是服务,Environment.UserInteractive == false也会发生。其中之一是当应用程序以LocalSystem运行时。 - marsh-wiggle

22
通常我会将我的Windows服务标记为控制台应用程序,并使用命令行参数“-console”来运行控制台,否则它将作为服务运行。为了调试,您只需在项目选项中设置命令行参数为“-console”,然后就可以开始调试了!
这样做使得调试变得简单易行,并且意味着该应用程序默认情况下作为服务运行,这也是您想要的。

4
我也是这样做的。效果非常好;调试时唯一需要注意的是安全性(哪个帐户)和工作文件夹 - 可以更轻松地编写代码来解决。 - Marc Gravell

17

我的解决方法:

  • 负责服务工作的类在单独的线程中运行。
  • 该线程由 OnStart() 方法启动,由 OnStop() 方法停止。
  • 服务模式和控制台模式之间的区分依赖于 Environment.UserInteractive

示例代码:

class MyService : ServiceBase
{
    private static void Main()
    {
        if (Environment.UserInteractive)
        {
            startWorkerThread();
            Console.WriteLine ("======  Press ENTER to stop threads  ======");
            Console.ReadLine();
            stopWorkerThread() ;
            Console.WriteLine ("======  Press ENTER to quit  ======");
            Console.ReadLine();
        }
        else
        {
            Run (this) ;
        }
    }

    protected override void OnStart(string[] args)
    {
        startWorkerThread();
    }

    protected override void OnStop()
    {
        stopWorkerThread() ;
    }
}

感谢您的提示gyrolf,但不幸的是Environment.UserInteractive仅适用于Windows Forms应用程序 :(. - Jonathan C Dickinson
7
据我理解文档和其中的示例代码,没有限制于 Windows Forms 应用程序。我已经在普通控制台应用程序中成功使用它。 - gyrolf
4
我可以确认这是正确的。当在控制台运行时,Environment.UserInteractive 为True,而在作为服务运行时为False。 - voithos
2
这段代码在某些情况下会失败 - 如果您将此应用程序作为任务从Windows计划程序中运行,则即使它不是服务,Environment.UserInteractive也会被设置为false。如果您计划从计划程序中运行应用程序,请考虑更健壮的解决方案。 - pg0xC
@pg0xC 上面的类派生自ServiceBase。我不认为有人会尝试将其作为任务运行。原始问题是关于一个可以在控制台或服务中运行的应用程序。我同意voithos的观点,Environment.UserInteractive在所需情况下起作用。如果您将应用程序作为任务运行,则应返回false。 - Derek Johnson

16

和Ash一样,我把所有实际处理代码写在一个单独的类库程序集中,然后由Windows服务可执行文件和控制台应用程序引用。

然而,在某些情况下,知道类库是在服务可执行文件还是控制台应用程序的上下文中运行是很有用的。我做法是反射托管应用程序的基类。(抱歉使用VB,但我想以下内容可以很容易地转换为C#):

Public Class ExecutionContext
    ''' <summary>
    ''' Gets a value indicating whether the application is a windows service.
    ''' </summary>
    ''' <value>
    ''' <c>true</c> if this instance is service; otherwise, <c>false</c>.
    ''' </value>
    Public Shared ReadOnly Property IsService() As Boolean
        Get
            ' Determining whether or not the host application is a service is
            ' an expensive operation (it uses reflection), so we cache the
            ' result of the first call to this method so that we don't have to
            ' recalculate it every call.

            ' If we have not already determined whether or not the application
            ' is running as a service...
            If IsNothing(_isService) Then

                ' Get details of the host assembly.
                Dim entryAssembly As Reflection.Assembly = Reflection.Assembly.GetEntryAssembly

                ' Get the method that was called to enter the host assembly.
                Dim entryPoint As System.Reflection.MethodInfo = entryAssembly.EntryPoint

                ' If the base type of the host assembly inherits from the
                ' "ServiceBase" class, it must be a windows service. We store
                ' the result ready for the next caller of this method.
                _isService = (entryPoint.ReflectedType.BaseType.FullName = "System.ServiceProcess.ServiceBase")

            End If

            ' Return the cached result.
            Return CBool(_isService)
        End Get
    End Property

    Private Shared _isService As Nullable(Of Boolean) = Nothing
#End Region
End Class

3
如果同一个程序集能够作为控制台应用程序或Windows服务运行,我不明白这怎么能实现...在这两种情况下,Assembly.GetEntryAssembly()和Assembly.EntryPoint返回相同的值。我猜只有在两种情况下运行不同的程序集才能起作用。 - Dan Ports
@DanPorts:我从未尝试过将相同的汇编文件同时作为控制台应用程序和Windows服务运行。然而,有时将相同的类编译成每种应用程序是很有用的,这种情况下上面的类可以确定它被用于哪种上下文中。 - Kramii
我在使用..ReflectedType.BaseType.FullName时,返回值是“System.Object”而不是“System.ServiceProcess.ServiceBase”(是的,我是从服务窗口运行代码)。 - Emre Guldogan

10

Jonathan,这不完全是对你问题的回答,但我刚刚完成了一个Windows服务并注意到调试和测试的困难。

通过简单地将所有实际处理代码编写到一个单独的类库程序集中,然后由Windows服务可执行文件、控制台应用程序和测试工具引用它来解决了这个问题。

除了基本的计时器逻辑,所有更复杂的处理都在公共程序集中发生,并且可以非常容易地进行测试/运行。


这是非常有用的信息,我猜那就是“正确”的做法。但愿你能接受两个答案 :)。 - Jonathan C Dickinson
没问题,乔纳森,很高兴它有用。现在我尝试为所有应用程序遵循这种方法(分离应用逻辑组件)。这样,Windows服务可以被视为应用程序的另一种类型的视图。我想这就是模型-视图-控制器模式。 - Ash

10
我已经修改了ProjectInstaller以在安装为服务时附加命令行参数/ service:
static class Program
{
    static void Main(string[] args)
    {
        if (Array.Exists(args, delegate(string arg) { return arg == "/install"; }))
        {
            System.Configuration.Install.TransactedInstaller ti = null;
            ti = new System.Configuration.Install.TransactedInstaller();
            ti.Installers.Add(new ProjectInstaller());
            ti.Context = new System.Configuration.Install.InstallContext("", null);
            string path = System.Reflection.Assembly.GetExecutingAssembly().Location;
            ti.Context.Parameters["assemblypath"] = path;
            ti.Install(new System.Collections.Hashtable());
            return;
        }

        if (Array.Exists(args, delegate(string arg) { return arg == "/uninstall"; }))
        {
            System.Configuration.Install.TransactedInstaller ti = null;
            ti = new System.Configuration.Install.TransactedInstaller();
            ti.Installers.Add(new ProjectInstaller());
            ti.Context = new System.Configuration.Install.InstallContext("", null);
            string path = System.Reflection.Assembly.GetExecutingAssembly().Location;
            ti.Context.Parameters["assemblypath"] = path;
            ti.Uninstall(null);
            return;
        }

        if (Array.Exists(args, delegate(string arg) { return arg == "/service"; }))
        {
            ServiceBase[] ServicesToRun;

            ServicesToRun = new ServiceBase[] { new MyService() };
            ServiceBase.Run(ServicesToRun);
        }
        else
        {
            Console.ReadKey();
        }
    }
}

然后,修改ProjectInstaller.cs以重写OnBeforeInstall()和OnBeforeUninstall()方法。

[RunInstaller(true)]
public partial class ProjectInstaller : Installer
{
    public ProjectInstaller()
    {
        InitializeComponent();
    }

    protected virtual string AppendPathParameter(string path, string parameter)
    {
        if (path.Length > 0 && path[0] != '"')
        {
            path = "\"" + path + "\"";
        }
        path += " " + parameter;
        return path;
    }

    protected override void OnBeforeInstall(System.Collections.IDictionary savedState)
    {
        Context.Parameters["assemblypath"] = AppendPathParameter(Context.Parameters["assemblypath"], "/service");
        base.OnBeforeInstall(savedState);
    }

    protected override void OnBeforeUninstall(System.Collections.IDictionary savedState)
    {
        Context.Parameters["assemblypath"] = AppendPathParameter(Context.Parameters["assemblypath"], "/service");
        base.OnBeforeUninstall(savedState);
    }
}

1
上面的例子没有正确处理引号,请查看以下链接以获取更好的解决方案:https://dev59.com/TW445IYBdhLWcg3wcJ2O - Palani
改进了路径周围引号的处理 - Rolf Kristensen

4
这个线程非常老,但我想提供我的解决方案。简单来说,为了处理这种情况,我建立了一个"服务测试器",在控制台和Windows服务案例中都使用。像上面所说,大部分逻辑都包含在一个单独的库中,但这更多是为了测试和"可连接性"。
附加的代码并不代表解决此问题的"最佳可能"方法,只是我自己的方法。在这里,当控制台应用程序处于"控制台模式"时,服务测试器由控制台应用程序调用;当它作为服务运行时,由同一应用程序的"启动服务"逻辑调用。通过这种方式,您现在可以从代码中的任何地方调用 ServiceHost.Instance.RunningAsAService(布尔值),以检查应用程序是作为服务运行还是仅作为控制台。
以下是代码:
public class ServiceHost
{
    private static Logger log = LogManager.GetLogger(typeof(ServiceHost).Name);

    private static ServiceHost mInstance = null;
    private static object mSyncRoot = new object();

    #region Singleton and Static Properties

    public static ServiceHost Instance
    {
        get
        {
            if (mInstance == null)
            {
                lock (mSyncRoot)
                {
                    if (mInstance == null)
                    {
                        mInstance = new ServiceHost();
                    }
                }
            }

            return (mInstance);
        }
    }

    public static Logger Log
    {
        get { return log; }
    }

    public static void Close()
    {
        lock (mSyncRoot)
        {
            if (mInstance.mEngine != null)
                mInstance.mEngine.Dispose();
        }
    }

    #endregion

    private ReconciliationEngine mEngine;
    private ServiceBase windowsServiceHost;
    private UnhandledExceptionEventHandler threadExceptionHanlder = new UnhandledExceptionEventHandler(ThreadExceptionHandler);

    public bool HostHealthy { get; private set; }
    public bool RunningAsService {get; private set;}

    private ServiceHost()
    {
        HostHealthy = false;
        RunningAsService = false;
        AppDomain.CurrentDomain.UnhandledException += threadExceptionHandler;

        try
        {
            mEngine = new ReconciliationEngine();
            HostHealthy = true;
        }
        catch (Exception ex)
        {
            log.FatalException("Could not initialize components.", ex);
        }
    }

    public void StartService()
    {
        if (!HostHealthy)
            throw new ApplicationException("Did not initialize components.");

        try
        {
            mEngine.Start();
        }
        catch (Exception ex)
        {
            log.FatalException("Could not start service components.", ex);
            HostHealthy = false;
        }
    }

    public void StartService(ServiceBase serviceHost)
    {
        if (!HostHealthy)
            throw new ApplicationException("Did not initialize components.");

        if (serviceHost == null)
            throw new ArgumentNullException("serviceHost");

        windowsServiceHost = serviceHost;
        RunningAsService = true;

        try
        {
            mEngine.Start();
        }
        catch (Exception ex)
        {
            log.FatalException("Could not start service components.", ex);
            HostHealthy = false;
        }
    }

    public void RestartService()
    {
        if (!HostHealthy)
            throw new ApplicationException("Did not initialize components.");         

        try
        {
            log.Info("Stopping service components...");
            mEngine.Stop();
            mEngine.Dispose();

            log.Info("Starting service components...");
            mEngine = new ReconciliationEngine();
            mEngine.Start();
        }
        catch (Exception ex)
        {
            log.FatalException("Could not restart components.", ex);
            HostHealthy = false;
        }
    }

    public void StopService()
    {
        try
        {
            if (mEngine != null)
                mEngine.Stop();
        }
        catch (Exception ex)
        {
            log.FatalException("Error stopping components.", ex);
            HostHealthy = false;
        }
        finally
        {
            if (windowsServiceHost != null)
                windowsServiceHost.Stop();

            if (RunningAsService)
            {
                AppDomain.CurrentDomain.UnhandledException -= threadExceptionHanlder;
            }
        }
    }

    private void HandleExceptionBasedOnExecution(object ex)
    {
        if (RunningAsService)
        {
            windowsServiceHost.Stop();
        }
        else
        {
            throw (Exception)ex;
        }
    }

    protected static void ThreadExceptionHandler(object sender, UnhandledExceptionEventArgs e)
    {
        log.FatalException("Unexpected error occurred. System is shutting down.", (Exception)e.ExceptionObject);
        ServiceHost.Instance.HandleExceptionBasedOnExecution((Exception)e.ExceptionObject);
    }
}

在这里,你所需要做的就是将那个看起来令人不安的 ReconcilationEngine 引用替换为你的逻辑引导方法。然后在你的应用程序中,无论是在控制台模式下还是作为服务运行,都要使用 ServiceHost.Instance.Start()ServiceHost.Instance.Stop() 方法。


3
也许需要检查进程的父级是否为C:\Windows\system32\services.exe。

2
我发现实现这个的唯一方法是首先检查进程是否连接了控制台,通过在try/catch块中访问任何Console对象属性(例如Title)来实现。
如果服务由SCM启动,则没有控制台,访问该属性将抛出System.IO.IOError。
然而,由于这感觉有点像依赖于特定于实现的细节(如果某些平台或某天SCM决定为它启动的进程提供控制台会怎么样?),因此我总是在生产应用程序中使用命令行开关(-console)...

1
这是对chksr回答.NET并避免无法识别交互服务错误的翻译:
using System.Security.Principal;

var wi = WindowsIdentity.GetCurrent();
var wp = new WindowsPrincipal(wi);
var serviceSid = new SecurityIdentifier(WellKnownSidType.ServiceSid, null);
var localSystemSid = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null);
var interactiveSid = new SecurityIdentifier(WellKnownSidType.InteractiveSid, null);
// maybe check LocalServiceSid, and NetworkServiceSid also

bool isServiceRunningAsUser = wp.IsInRole(serviceSid);
bool isSystem = wp.IsInRole(localSystemSid);
bool isInteractive = wp.IsInRole(interactiveSid);

bool isAnyService = isServiceRunningAsUser || isSystem || !isInteractive;

即使不是服务,进程也可以作为LocalSystem运行:psexec -s MyApp.exe。在这种情况下,默认情况下也不是交互式的。 - marsh-wiggle
@marsh-wiggle:在我看来,如果一个服务将一些工作放在进程外执行,那么生成的子进程仍然是“服务的一部分”。但是如果它们不是服务的根进程,他们确实不想调用StartServiceCtrlDispatcher - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接