Почему моя служба Windows не регистрируется, пока служба не будет перезапущена

tl;dr

Я создал службу Windows с помощью TopShelf, добавил ведение журнала с помощью Log4Net, а затем создал проект, установил службу и запустил ее. Тогда моя служба работает нормально, но не регистрируется. Появляются журналы TopShelf, но не журналы, которые я добавляю в свою службу Windows. Что делает это более странным, так это то, что если я перезапускаю службу Windows, ведение журнала начинает работать.

Я создал репозиторий GitHub небольшого проекта, который воспроизводит эту проблему, если вы хотите клонировать его и воспроизвести проблему самостоятельно.


Как узнать, работает ли он

Служба должна создать два файла, один из которых просто говорит «Hello World», а другой содержит все журналы. Он будет работать, если файл журнала успешно зарегистрировал строку: Why is this line not logged?

Если эта строка не отображается в файле log.txt, моя проблема не решена.

Примечание. Эта строка появится, если вы нажмете кнопку запуска в Visual Studio, но я хочу, чтобы она работала, когда я устанавливаю службу и запускаю ее. Это также будет работать, если служба запущена, а затем перезапущена, но это больше похоже на взлом, чем на исправление.


Описание Проекта

Вот как я настроил свой сервис. Я создал новое консольное приложение C# с помощью .Net Framework 4.6.1 и установил 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!");
        }
    }
}

и я добавил конфигурацию Log4Net в app.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>

    <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>

С этим я могу запустить приложение.

Описание проблемы

Итак, что работает? Что ж, я могу запустить приложение как консольное через Visual Studio. Таким образом, все работает, особенно строка: _logWriter.Info("Why is this line not logged?"); регистрируется правильно.

Когда я устанавливаю службу:

  • Сборка проекта в режиме Release
  • Запуск Path/To/Service.exe install в командной строке администратора
  • Бег Path/To/Service.exe start

Приложение запускается правильно, и создаются два файла журнала (D:\file.txt и D:\log.txt), но когда я заглядываю внутрь файла D:\log.txt, я не вижу журнала для "Why is this line not logged?" и, что еще более странно, перезапускаю service (Службы > Щелкните правой кнопкой мыши LogIssue > Перезапустить) заставляет все журналы снова начать работать.

Кроме того, это не похоже на то, что ведение журнала не работает полностью. Файл журнала заполнен журналами TopShelf, но не тем, что я записываю из своего приложения.

Что я делаю неправильно, из-за чего он не регистрируется правильно?

Если вы хотите попробовать воспроизвести это, вы можете выполнить шаги, описанные выше, или вы можете клонировать проект, если хотите: https://github.com/jamietwells/log-issue.git

Дополнительная информация

При дальнейшем рассмотрении это еще более запутанно, чем я думал. Я был убежден, что проблема связана с вызовом XmlConfigurator.Configure() в неправильном месте, однако при тестировании я обнаружил:

  • при установке службы Windows вызовы выглядят примерно так:

    1. Main
    2. Настроить хост
  • при запуске службы Windows вызовы выглядят примерно так:

    1. Main
    2. Настроить хост
    3. Главный
    4. Настроить хост
    5. ConstructUsing
    6. КогдаНачало
    7. при запуске
    8. Выполнять работу

Так что Main определенно вызывается (на самом деле кажется, что он вызывается дважды!). Одна из возможных проблем заключается в том, что OnStart вызывается из другого потока, чем Main, но даже копирование вызова XmlConfigurator.Configure() в OnStart таким образом, что он вызывается из нового потока, приводит к тому, что ведение журнала не работает.

На данный момент мне интересно, работал ли кто-нибудь с Log4Net с TopShelf?

Примеры журналов

Вот пример файла журнала, который я создал при установке сервиса:

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:\github\log-issue\LogIssue\bin\Release\LogIssue.exe"
2018-06-12 11:55:20,643 [1] DEBUG Topshelf.Runtime.Windows.HostInstaller [(null)] - Image path: "D:\github\log-issue\LogIssue\bin\Release\LogIssue.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:\github\log-issue\LogIssue\bin\Release
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

person Jamie Twells    schedule 07.06.2018    source источник


Ответы (2)


Я решил проблему. Вернее, кто-то по имени Кварв год назад решил проблему здесь: https://github.com/Topshelf/Topshelf/issues/206#issuecomment-312581963

Проблема

По сути, при запуске path/to/exe start в окне командной строки TopShelf запустит два экземпляра приложения.

Первый экземпляр будет выполнять некоторую настройку и настройку, а второй экземпляр будет фактической службой Windows, которую мы хотим запустить и продолжать работать.

Поскольку оба выполняются одновременно, возникает условие гонки для тех, кто первым сможет получить доступ к файлу журнала и заблокировать его. Это означает, что либо TopShelf будет вести журнал, либо ваше приложение будет вести журнал, в зависимости от того, кто первым заблокирует файл.

Как это объясняет то, что мы видим

Если TopShelf сначала блокирует файл журнала, приложение не регистрируется.

Я понял, что могу исправить ведение журнала, если перед запуском службы задержу 1 секунду, но до сих пор не понимал, почему. Первый экземпляр уже завершил свою настройку, это делается с файлом журнала, и срок действия блокировки истекает, затем мое приложение может прийти и настроить свое ведение журнала и записать в файл.

Я также понял, что мы можем перезапустить службу, и она внезапно начнет работать и вести журнал. Я не знаю, что это так, но готов поспорить, что когда вызывается перезагрузка, TopShelf ведет себя по-другому и не запускает второй экземпляр программы, он просто вызывает OnStop, затем OnStart. Если у кого-то есть информация о перемещении TopShelf при перезапуске службы, мне было бы интересно узнать об этом.

Это также объясняет, почему проблема не воспроизводится для всех. Условия гонки дают разные результаты на разном оборудовании.

Решение

Есть несколько решений, которые решают проблему.

  1. В проблеме TopShelf, указанной выше, первое предложение — установить службу с помощью модуля PowerShell:

    Start-Service <serviceName>
    

    Мы также могли бы использовать sc start <serviceName> в командной строке вместо PowerShell.

    Похоже, что это не запускает несколько экземпляров и не блокирует файл, что приводит к более последовательному и предсказуемому опыту по сравнению с другим методом запуска службы, выполнив:

    path/to/exe start
    
  2. Мы могли бы убедиться, что журнал блокирует файл на как можно меньшее время, чтобы уменьшить вероятность взаимоблокировки. Это повлияет на производительность при использовании ведения журнала, но решит проблему. Мы могли бы просто добавить:

    <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
    

    в RollingFileAppender в файле App.config.

  3. Мы также можем добавить задержку в одну секунду к методу OnStart, чтобы дать первому экземпляру время для завершения.

  4. Мы также могли бы изменить настройку Log4Net, чтобы они не конфликтовали из-за файла. Это решение, которое я выбрал. В разделе log4net в файле App.config я добавил:

    <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" />
    

    приведет к тому, что файл журнала будет назван с использованием идентификатора процесса, который в данный момент выполняется. Таким образом, каждый экземпляр получает свой собственный файл журнала, и проблема блокировки больше не возникает.

Примечание

Похоже, что включение XmlConfigurator.Configure(); в качестве одной из первых строк в Main() как-то важно. Я до сих пор не совсем понимаю, почему это важно, но это может быть потому, что, насколько я могу судить, x.UseLog4Net(); не вызывает XmlConfigurator.Configure();, а HostLogger.Get(Name)) вызывает. Это можно увидеть в исходном коде TopShelf (функция CreateLogWriterFactory ).

person Jamie Twells    schedule 13.06.2018

Для ясности здесь идет весь код, указанный по имени файла:

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>

Программа.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;
    }
}

Рабочий.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. either of these ...
    • download this from github (https://github.com/jamietwells/log-issue)
    • create a new project and ...
      • add the references for topshelf and log4net
      • скопируйте код выше в соответствующие файлы
  2. F5 для сборки и запуска.
  3. Примечание 2 файла, созданные в корне диска D
  4. остановить запуск, удалить 2 файла
  5. открыть командную строку от имени администратора
  6. введите команды, как показано ниже, чтобы перейти в каталог и сказать ему установить службу
  7. перейдите к диспетчеру служб "services.msc" и обратите внимание на то, что в списке указана служба "LogIssue"
  8. нажмите, чтобы запустить службу
  9. файлы заметок были созданы заново, откройте оба для получения результатов ниже

Вот мои результаты (нажмите на картинку для увеличения)...

введите здесь описание изображения

На этом этапе стоит отметить, что вызов журнала, сделанный в 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();
    }
}
person War    schedule 11.06.2018
comment
Я только что проверил фиксацию, которую вы сделали в репозитории, и, к сожалению, я тоже не могу заставить ее работать. У ведения журнала та же проблема, что и у меня ранее, а именно то, что она не регистрируется при установке и запуске в качестве службы Windows. Это сработало для вас? Возможно, это проблема моей локальной установки? - person Jamie Twells; 12.06.2018
comment
@JamieTwells с коммитом War, вы скопировали его в свое фактическое решение или протестировали решение в репозитории? - person WhatsThePoint; 12.06.2018
comment
@JamieTwells проблема только тогда, когда служба развернута? или он тоже не создает два текстовых файла при запуске из VS? - person War; 12.06.2018
comment
@WhatsThePoint Я вытащил коммит и установил его. Я дал War доступ к моему репозиторию, и War совершил исправление. проблема только при развертывании службы? Да только когда устанавливал то запускал. он не создает два текстовых файла при запуске из VS. Он создает два текстовых файла, но строку. Почему эта строка не регистрируется? не регистрируется в файле log.txt. - person Jamie Twells; 12.06.2018
comment
@JamieTwells дайте мне знать, если это все еще не работает для вас, хотя я не знаю, что еще я могу добавить ... может быть что-то специфичное для машины на данный момент, если проблема не устранена. - person War; 12.06.2018
comment
@War Это может быть специфично для машины, да. Я проверю на своем ноутбуке, хотя я вижу то же самое при развертывании на сервере, поэтому я думаю, что мы делаем что-то немного другое. Можете ли вы удалить два файла и запустить службу через командную строку с помощью path/to/exe start? - person Jamie Twells; 12.06.2018
comment
@War Хорошо, да, теперь это работает для меня в этом последнем коммите. Конечно все логи TopShelf не пропали, но логи приложений работают. Может быть, у нас может быть только один. Либо журналы TopShelf, либо журналы приложений. Не оба? - person Jamie Twells; 12.06.2018
comment
Я не уверен, что ты имеешь в виду ?? Если Log4Net является вашей системой ведения журналов, используйте log4net, если нет, используйте что-то еще. выберите структуру ведения журнала и придерживайтесь ее, иначе вы окажетесь в мире боли. С Log4Net вполне возможно вести журнал практически во что угодно, поэтому я бы предложил работать с этим, поскольку теперь у вас есть рабочий пример. - person War; 12.06.2018
comment
Давайте продолжим обсуждение в чате. - person Jamie Twells; 12.06.2018