Почему моя служба Windows не регистрируется, пока служба не будет перезапущена - PullRequest
0 голосов
/ 07 июня 2018

tl; dr

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

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

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

Служба должна создать два файла, один из которых просто говорит "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?"); logs правильно .

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

  • Сборка проекта в режиме 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?" и, чтобы сделать его еще более странным - перезапускает службу (Службы> Щелкните правой кнопкой мыши LogIssue> Перезапустить) приводит к тому, что все журналы снова начинают работать отлично.

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

Что я делаю не так, что приводит к неправильной регистрации?

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

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

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

  • при установке Windows Service вызовы выполняются примерно так:

    1. Main
    2. ConfigureHost
  • при запуске Windows Service , вызовы идут примерно так:

    1. Main
    2. ConfigureHost
    3. Main
    4. ConfigureHost
    5. ConstructUsing
    6. WhenStarted
    7. OnStart
    8. DoWork

Так что 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

Ответы [ 2 ]

0 голосов
/ 13 июня 2018

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

Проблема

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

Первый экземпляр будет выполнять некоторую настройку и настройку, а вторым экземпляром будет фактическая Служба 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 ).

0 голосов
/ 11 июня 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>

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)
    • создайте новый проект и ...
      • добавьтессылки на верхнюю полку и log4net
      • скопируйте приведенный выше код в соответствующие файлы
  2. F5 для сборки и запуска.
  3. Примечание 2 файла, созданные в корневом каталоге диска D
  4. , перестают работать, удалите 2 файла
  5. , откройте командную строку с правами администратора
  6. , введите команды, как показано ниже, чтобы перейти в каталог и сообщитьэто вstall service
  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();
    }
}
...