为什么我的Windows服务在重新启动服务之前不会记录

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么我的Windows服务在重新启动服务之前不会记录相关的知识,希望对你有一定的参考价值。

tl;dr

我使用TopShelf创建了一个Windows服务,使用Log4Net添加了日志记录,然后构建了项目,安装了服务并启动了服务。然后我的服务运行正常,但它没有记录。将显示TopShelf日志,但不会显示我添加到Windows服务的日志。更奇怪的是,如果我重新启动Windows服务,则日志记录开始工作。

我已经创建了一个小型项目的GitHub repo,如果你想克隆它并自己重现问题,那么它就会重现这个问题。


How to tell if it's working

该服务应创建两个文件,一个只显示“Hello World”,另一个包含所有日志。如果日志文件已成功记录该行,它将起作用:Why is this line not logged?

如果该行没有出现在log.txt文件中,那么我的问题就没有解决了。

注意:如果单击Visual Studio中的“开始”按钮,将显示此行,但我希望在安装服务并启动服务时它能够正常工作。如果服务启动然后重新启动它也会起作用,但这看起来更像是一个黑客而不是修复。


Project description

这就是我设置服务的方式。我使用.Net Framework 4.6.1创建了一个新的C#控制台应用程序并安装了3个NuGet包:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="log4net" version="2.0.8" targetFramework="net461" />
  <package id="Topshelf" version="4.0.4" targetFramework="net461" />
  <package id="Topshelf.Log4Net" version="4.0.4" targetFramework="net461" />
</packages>

然后我创建了Windows服务:

using log4net.Config;
using System.IO;
using Topshelf;
using Topshelf.HostConfigurators;
using Topshelf.Logging;
using Topshelf.ServiceConfigurators;

namespace LogIssue
{
    public class Program
    {
        public const string Name = "LogIssue";

        public static void Main(string[] args)
        {
            XmlConfigurator.Configure();
            HostFactory.Run(ConfigureHost);
        }

        private static void ConfigureHost(HostConfigurator x)
        {
            x.UseLog4Net();
            x.Service<WindowsService>(ConfigureService);

            x.SetServiceName(Name);
            x.SetDisplayName(Name);
            x.SetDescription(Name);

            x.RunAsLocalSystem();
            x.StartAutomatically();
            x.OnException(ex => HostLogger.Get(Name).Error(ex));
        }

        private static void ConfigureSystemRecovery(ServiceRecoveryConfigurator serviceRecoveryConfigurator) =>
            serviceRecoveryConfigurator.RestartService(delayInMinutes: 1);

        private static void ConfigureService(ServiceConfigurator<WindowsService> serviceConfigurator)
        {
            serviceConfigurator.ConstructUsing(() => new WindowsService(HostLogger.Get(Name)));
            serviceConfigurator.WhenStarted(service => service.OnStart());
            serviceConfigurator.WhenStopped(service => service.OnStop());
        }
    }

    internal class WindowsService
    {
        private LogWriter _logWriter;

        public WindowsService(LogWriter logWriter)
        {
            _logWriter = logWriter;
        }

        internal bool OnStart() {
            new Worker(_logWriter).DoWork();
            return true;
        }

        internal bool OnStop() => true;
    }

    internal class Worker
    {
        private LogWriter _logWriter;

        public Worker(LogWriter logWriter)
        {
            _logWriter = logWriter;
        }

        public async void DoWork() {
            _logWriter.Info("Why is this line not logged?");
            File.WriteAllText("D:file.txt", "Hello, World!");
        }
    }
}

我在app.config中添加了Log4Net配置:

  <log4net>

    <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
      <file value="D:log.txt" />
      <appendToFile value="true" />
      <rollingStyle value="Size" />
      <maxSizeRollBackups value="10" />
      <maximumFileSize value="100KB" />
      <staticLogFileName value="true" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
      </layout>
    </appender>

    <appender name="TraceAppender" type="log4net.Appender.TraceAppender">
      <layout type="log4net.Layout.SimpleLayout" />
    </appender>

    <appender name="ColoredConsoleAppender" type="log4net.Appender.ColoredConsoleAppender">
      <mapping>
        <level value="FATAL" />
        <foreColor value="Purple, HighIntensity" />
      </mapping>
      <mapping>
        <level value="ERROR" />
        <foreColor value="Red, HighIntensity" />
      </mapping>
      <mapping>
        <level value="WARN" />
        <foreColor value="Yellow, HighIntensity" />
      </mapping>
      <mapping>
        <level value="INFO" />
        <foreColor value="Green, HighIntensity" />
      </mapping>
      <mapping>
        <level value="DEBUG" />
        <foreColor value="Cyan, HighIntensity" />
      </mapping>
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%message%newline" />
      </layout>
    </appender>

    <root>
      <appender-ref ref="RollingFileAppender" />
      <appender-ref ref="TraceAppender" />
      <appender-ref ref="ColoredConsoleAppender" />
    </root>

  </log4net>

有了这个,我可以运行应用程序。

Problem description

什么有用?好吧,我可以通过Visual Studio将应用程序作为控制台应用程序运行。这样,一切正常,特别是行:_logWriter.Info("Why is this line not logged?");正确记录。

当我安装服务时:

  • Release模式构建项目
  • 在管理员命令提示符下运行Path/To/Service.exe install
  • 运行Path/To/Service.exe start

应用程序正确启动并创建了两个日志文件(D:file.txtD:log.txt)但是当我查看D:log.txt文件时,我看不到"Why is this line not logged?"的日志并使其更奇怪 - 重新启动服务(服务>右键单击LogIssue>重新启动)导致所有日志记录再次完美地开始工作。

此外,它不像日志记录不完全。日志文件中充满了TopShelf日志,而不是我从应用程序中记录的内容。

我做错了什么,导致它无法正确记录?

如果您想尝试重现这一点,可以按照上面列出的步骤进行操作,或者如果您愿意,可以克隆项目:https://github.com/jamietwells/log-issue.git

Further Info

经过进一步检查,这比我想象的更令人困惑。我确信这个问题与XmlConfigurator.Configure()呼叫在错误的地方有关,但是在测试时我发现:

  • 安装Windows服务时,调用如下所示: 主要 ConfigureHost
  • 启动Windows服务时,调用如下所示: 主要 ConfigureHost 主要 ConfigureHost ConstructUsing WhenStarted 的OnStart 做工作

所以Main肯定被称为(事实上它似乎被称为两次!)。一个可能的问题是从一个不同的线程调用OnStartMain,但是甚至将XmlConfigurator.Configure()调用复制到OnStart,以便从新线程调用它会导致日志记录无效。

在这一点上,我想知道是否有人让Log4Net与TopShelf合作?

Example logs

以下是我在安装服务时生成的日志文件示例:

2018-06-12 11:55:20,595 [1] INFO  Topshelf.HostFactory [(null)] - Configuration Result:
[Success] Name LogIssue
[Success] ServiceName LogIssue
2018-06-12 11:55:20,618 [1] INFO  Topshelf.HostConfigurators.HostConfiguratorImpl [(null)] - Topshelf v4.0.0.0, .NET Framework v4.0.30319.42000
2018-06-12 11:55:20,627 [1] DEBUG Topshelf.Hosts.InstallHost [(null)] - Attempting to install 'LogIssue'
2018-06-12 11:55:20,636 [1] INFO  Topshelf.Runtime.Windows.HostInstaller [(null)] - Installing LogIssue service
2018-06-12 11:55:20,642 [1] DEBUG Topshelf.Runtime.Windows.HostInstaller [(null)] - Opening Registry
2018-06-12 11:55:20,642 [1] DEBUG Topshelf.Runtime.Windows.HostInstaller [(null)] - Service path: "D:githublog-issueLogIssueinReleaseLogIssue.exe"
2018-06-12 11:55:20,643 [1] DEBUG Topshelf.Runtime.Windows.HostInstaller [(null)] - Image path: "D:githublog-issueLogIssueinReleaseLogIssue.exe"  -displayname "LogIssue" -servicename "LogIssue"
2018-06-12 11:55:20,644 [1] DEBUG Topshelf.Runtime.Windows.HostInstaller [(null)] - Closing Registry
2018-06-12 11:55:22,839 [1] INFO  Topshelf.HostFactory [(null)] - Configuration Result:
[Success] Name LogIssue
[Success] ServiceName LogIssue
2018-06-12 11:55:22,862 [1] INFO  Topshelf.HostConfigurators.HostConfiguratorImpl [(null)] - Topshelf v4.0.0.0, .NET Framework v4.0.30319.42000
2018-06-12 11:55:22,869 [1] DEBUG Topshelf.Hosts.StartHost [(null)] - Starting LogIssue
2018-06-12 11:55:23,300 [1] INFO  Topshelf.Hosts.StartHost [(null)] - The LogIssue service was started.

此时在日志中,我重新启动了Windows服务,您可以看到日志记录随后开始工作。具体来说,这次记录了Why is this line not logged?行,但不是最后一次。

2018-06-12 12:09:43,525 [6] INFO  Topshelf.Runtime.Windows.WindowsServiceHost [(null)] - [Topshelf] Stopping
2018-06-12 12:09:43,542 [6] INFO  Topshelf.Runtime.Windows.WindowsServiceHost [(null)] - [Topshelf] Stopped
2018-06-12 12:09:45,033 [1] INFO  Topshelf.HostFactory [(null)] - Configuration Result:
[Success] Name LogIssue
[Success] ServiceName LogIssue
2018-06-12 12:09:45,055 [1] INFO  Topshelf.HostConfigurators.HostConfiguratorImpl [(null)] - Topshelf v4.0.0.0, .NET Framework v4.0.30319.42000
2018-06-12 12:09:45,071 [1] DEBUG Topshelf.Runtime.Windows.WindowsHostEnvironment [(null)] - Started by the Windows services process
2018-06-12 12:09:45,071 [1] DEBUG Topshelf.Builders.RunBuilder [(null)] - Running as a service, creating service host.
2018-06-12 12:09:45,072 [1] INFO  Topshelf.Runtime.Windows.WindowsServiceHost [(null)] - Starting as a Windows service
2018-06-12 12:09:45,074 [1] DEBUG Topshelf.Runtime.Windows.WindowsServiceHost [(null)] - [Topshelf] Starting up as a windows service application
2018-06-12 12:09:45,076 [5] INFO  Topshelf.Runtime.Windows.WindowsServiceHost [(null)] - [Topshelf] Starting
2018-06-12 12:09:45,076 [5] DEBUG Topshelf.Runtime.Windows.WindowsServiceHost [(null)] - [Topshelf] Current Directory: D:githublog-issueLogIssueinRelease
2018-06-12 12:09:45,076 [5] DEBUG Topshelf.Runtime.Windows.WindowsServiceHost [(null)] - [Topshelf] Arguments: 
2018-06-12 12:09:45,078 [5] INFO  LogIssue.Worker [(null)] - Why is this line not logged?
2018-06-12 12:09:45,083 [5] INFO  Topshelf.Runtime.Windows.WindowsServiceHost [(null)] - [Topshelf] Started
答案

为清楚起见,这里按文件名列出所有代码:

assemblyinfo.cs(将其添加到已存在的代码中):

[assembly: log4net.Config.XmlConfigurator(ConfigFile = "Log4Net.config", Watch = true)]

app.config(将此添加到框架生成的默认配置):

  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
  </configSections>
  <log4net configSource="Log4Net.config"/>

Log4Net.config(这里有更多但我删除它,因为它与这里的问题无关):

<log4net>
  <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
    <file value="D:log.txt" />
    <appendToFile value="true" />
    <rollingStyle value="Size" />
    <maxSizeRollBackups value="10" />
    <maximumFileSize value="100KB" />
    <staticLogFileName value="true" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
    </layout>
  </appender>
  <root>
    <appender-ref ref="RollingFileAppender" />
  </root>
</log4net>

Program.cs中:

using Topshelf;
using Topshelf.HostConfigurators;
using Topshelf.Logging;
using Topshelf.ServiceConfigurators;

namespace LogIssue
{
    public class Program
    {
        public const string Name = "LogIssue";

        public static void Main(string[] args)
        {
            HostFactory.Run(ConfigureHost);
        }

        private static void ConfigureHost(HostConfigurator x)
        {
            x.Service<WindowsService>(ConfigureService);

            x.SetServiceName(Name);
            x.SetDisplayName(Name);
            x.SetDescription(Name);

            x.RunAsLocalSystem();
            x.StartAutomatically();
            x.OnException(ex => HostLogger.Get(Name).Error(ex));
        }

        private static void ConfigureSystemRecovery(ServiceRecoveryConfigurator serviceRecoveryConfigurator) =>
            serviceRecoveryConfigurator.RestartService(delayInMinutes: 1);

        private static void ConfigureService(ServiceConfigurator<WindowsService> serviceConfigurator)
        {
            serviceConfigurator.ConstructUsing(() => new WindowsService());
            serviceConfigurator.WhenStarted(service => service.OnStart());
            serviceConfigurator.WhenStopped(service => service.OnStop());
        }
    }
}

WindowsService.cs:

using log4net;

namespace LogIssue
{
    internal class WindowsService
    {
        static ILog _log = LogManager.GetLogger(typeof(WindowsService));

        internal bool OnStart() {
            new Worker().DoWork();
            return true;
        }

        internal bool OnStop() => true;
    }
}

Worker.cs:

using log4net;
using System.IO;

namespace LogIssue
{
    internal class Worker
    {
        static ILog _log = LogManager.GetLogger(typeof(Worker));

        public void DoWork() {
            _log.Info("Why is this line not logged?");
            File.WriteAllText("D:file.txt", "Hello, World!");
        }
    }
}

编辑:

说明:

  1. 其中任何一个...... 从github(https://github.com/jamietwells/log-issue)下载 创建一个新项目...... 添加topshelf和log4net的引用 将上面的代码复制到相关文件中
  2. F5构建并运行。
  3. 注意在D驱动器的根目录上创建的2个文件
  4. 停止运行,删除2个文件
  5. 以管理员身份打开命令行
  6. 输入如下所示的命令转到目录并告诉它安装服务
  7. 转到服务管理器“services.msc”并注意列出“LogIssue”服务
  8. 单击以启动该服务
  9. 注释文件已重新创建,打开以下结果

这是我的结果(点击图片放大)...

enter image description here

此时值得注意的是,在worker.cs中进行的日志调用可能不会立即输出到日志,主要是因为在收集了一定数量的日志语句或日志后,log4net定期执行的文件“刷新”容器已超出范围,并且将被拆除。

这可能导致在将代码部署到服务器时似乎没有进行日志记录调用。

我们可以通过修改上面的服务来测试这个,定期“处理”工人类并构建一个新的工作类......

using log4net;
using System.Timers;

namespace LogIssue
{
    internal class WindowsService
    {
        static ILog _log = LogManager.GetLogger(typeof(WindowsService));
        readonly Timer _timer = new Timer(1000);

        public WindowsService() =>  _timer.Elapsed += (s, e) => new Worker().DoWork();

        internal void OnStart() =>  _timer.Start();

        internal void OnStop() => _timer.Stop();
    }
}
另一答案

我已经解决了这个问题。或者更确切地说,一个名叫Kvarv的人在一年前解决了这个问题:https://github.com/Topshelf/Topshelf/issues/206#issuecomment-312581963

The problem

基本上,当在命令提示符窗口中运行path/to/exe start时,TopShelf将启动应用程序的两个实例。

第一个实例是进行一些设置和配置,第二个实例将是我们想要启动并继续运行的实际Windows服务。

因为两者都是同时运行的,所以可以为任何可以访问日志文件并首先锁定它的人引入竞争条件。这意味着TopShelf将记录或您的应用程序将记录,具体取决于谁先锁定文件。

How this explains what we're seeing

如果TopShelf首先锁定日志文件,则应用程序不会记录。

我意识到如果我在启动服务之前放置1秒延迟,我可以修复日志记录,但直到现在才意识到为什么。第一个实例已经完成了它的配置,完成了日志文件并且锁定到期,然后我的应用程序可以出现并配置其日志记录并写入文件。

我还意识到我们可以重启服务并让它突然开始工作和记录。我不知道是这种情况,但我愿意打赌当重启被称为TopShelf表现不同,并没有启动程序的第二个实例,它只是调用OnStop,然后OnStart。如果有人在重新启动服务时移动了TopShelf行为的信息,我很想知道。

它还解释了问题似乎并没有为每个人重现。竞争条件在不同硬件上给出不同的结果。

The solution

有几种解决方案可以解决这个问题。

  1. 在上面链接的TopShelf问题上,第一个建议是使用PowerShell模块安装服务: Start-Service <serviceName> 如果在命令提示符而不是PowerShell中,我们也可以使用sc start <serviceName> 这似乎不会启动多个实例并锁定文件,与通过执行以下操作启动服务的其他方法相比,可以获得更一致且可预测的体验: path/to/exe start
  2. 我们可以确保日志记录将文件锁定的时间尽可能短,以减少死锁的可能性。这在使用日志记录时会产生性能影响,但它可以解决问题。我们可以简单地添加: <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> App.config的RollingFileAppender。
  3. 我们还可以在OnStart方法中添加一秒延迟,以便为第一个实例提供完成时间。
  4. 我们还可以更改Log4Net的配置方式,以便它们不会争夺文件。这是我的解决方案。在App.config文件的log4net部分中,我添加了: <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender"> <file value="D:log.txt" /> 但只需将其更改为: <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender"> <file type="log4net.Util.PatternString" value="D:Logs\%processid.log" /> 将导致使用当前正在运行的进程的ID命名日志文件。这样每个实例都会获得自己的日志文件,并且锁定问题不再存在。

Note

似乎包括XmlConfigurator.Configure();作为Main()的第一行之一在某种程度上是重要的。我仍然没有完全理解为什么这很重要,但可能是因为据我所知:x.UseLog4Net();并没有打电话给XmlConfigurator.Configure();,但HostLogger.Get(Name))确实如此。这可以在TopShelf源(函数CreateLogWriterFactory)中看到。

以上是关于为什么我的Windows服务在重新启动服务之前不会记录的主要内容,如果未能解决你的问题,请参考以下文章

事件中心检查点在服务器重新启动之前不获取事件

Windows Server 2016 怎么重新启动远程服务器

Windows Server 2016 怎么重新启动远程服务器

iOS 为啥我的应用程序在重新启动之前不会将数据加载到 tableview 中?

在服务器进程完成重新启动之前重新加载 gulp-livereload

如何使用任务计划程序重新启动 Windows 服务