Windows service. Поиск системных ошибок и отображение их в WinForm C#

В этой статье мы разберем как с нуля создать приложение, которое будет работать со службами windows и отображать системные ошибки в WinForm (C#).

План этой статьи:

  • Создание службы
  • Event Viewer
  • Код службы
  • Проверка работы службы(Запуск службы вручную)
  • Отображение WinForm

Создание службы


Открываем Visual Studio. Дальше File → New → Project → (Windows Desktop) → Windows Service (.Net Framework) → Ok.

Дальше нужно создать установщик. В открывшемся окне щелкаем ПКМ и выбираем «Add Installer». У вас создастся «ProjectInstaller.cs[Design]» после чего нужно будет перейти к коду «F7» или ПКМ «View Code». Нужно найти строку «InitializeComponent();», поставить на нее курсор и нажать «F12», дальше нужно добавить следующие строки:

this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem; //Чтоб не запрашивало имя пользователя и пароль
this.serviceInstaller1.Description = "Show me system error."; //Описание которое будет видно в списке служб
this.serviceInstaller1.DisplayName = "GetSystemError"; //Название службы которое будет видно в списке служб

Но добавлять эти строки нужно только в следующей последовательности и месте. Иначе будет ошибка при установке сервиса.



Event Viewer


Это нужно для проверки правильной работы нашей программы.

Event Viewer — программа для просмотра журнала событий которая есть на каждом компьютере с windows. Каждая программа, которая запускается на компьютере, публикует уведомление в журнале событий до того, как останавливается. Любой доступ к системе, изменение безопасности, подстройка операционной системы, аппаратный сбой и сбой драйвера — все это попадает в журнал событий. Event Viewer сканирует файлы текстового журнала, объединяет их и помещает в интерфейс.

Как его открыть? — Пуск → Event Viewer(в поиске) → «Просмотр журналов событий».

Дальше «Настраиваемые представления (Custom Views)» → «События управления (Administrative Events)». Тут мы можем увидеть все ошибки, предупреждения и информацию о них.

Есть 3 типа журнала: приложение(Application), системные(System) и безопасность(Security). Нам нужен только системный(System).

Код службы


Находим файл .cs с названием службы, у меня это «Service1.cs», открываем. В файле должно быть заготовлено 2 переопределенных метода:

  • OnStart(string[] args) — выполняется при запуске службы,
  • OnStop() — выполняется при остановке службы.

Есть так же еще несколько методов но они нам сейчас не потребуются. Найти их можно самостоятельно.

Данные которые мы получаем будем хранить в обновляемом текстовом файле, поэтому добавляем

using System.IO;

Добавляем код в метод OnStart(string[] args):

 
EventLog myLog; //Можно читать и записывать элементы журнала событий
string filepath = AppDomain.CurrentDomain.BaseDirectory +  @"\ServiceLog.txt";; //Хранится путь к файлу
List<string> list; //Список всех найденных ошибок
protected override void OnStart(string[] args)
{
        myLog = new EventLog();
        myLog.Log = "System"; //По умолчанию выставлен тип ошибок - приложение(Application), а нам нужны системные(System).
        myLog.Source = "System Error";

        for (int index = myLog.Entries.Count - 1; index > 0; index--) //Перебираем все системные ошибки от новых к более старым
        {
           var errEntry = myLog.Entries[index]; \\Чтение записи из журнала событий
           if (errEntry.EntryType == EventLogEntryType.Error) \\Проверка ошибка ли это
           {
                //Получаем нужные данные из записи
                var appName = errEntry.Source;
                list = new List<string>();
                list.Add("Entry Type: " + Convert.ToString(errEntry.EntryType));
                list.Add("Event log: " + (string)myLog.Log);
                list.Add("Machine Name: " + (string)errEntry.MachineName);
                list.Add("App Name: " + (string)errEntry.Source);
                list.Add("Message: " + (string)errEntry.Message);
                list.Add("Time Written: " + errEntry.TimeWritten.ToString());
                list.Add("-*-");

                WriteToFile(list); //Записываем данные в файл
            }
        }
}
public void WriteToFile(List<string> list) //Запись в файл
{
        using (StreamWriter sw = File.AppendText(filepath))
        {
            for (int i = 0; i < list.Count; i++)
                 sw.WriteLine(list[i]);
        }
}

Дальше нужно собрать решение «Solution» -> «Rebuild Solution». После успешной сборки можно проверять работу.

Проверка работы службы(Запуск службы вручную)


Службу windows нельзя запускать как обычное приложение. Можно запустить только через командную строку от имени администратора.

Запускаем командную строку от имени администратора. Ввести следующие команды:

cd C:\Windows\Microsoft.NET\Framework\v4.0.30319
InstallUtil.exe  Путь\Имя вашей службы.exe
(InstallUtil.exe C:\Users\Анжелика\source\repos\WindowsService1\WindowsService1 \bin\Debug\WindowsService1.exe)
Команда для удаления сервиса: InstallUtil.exe -u Путь\Имя вашей службы.exe 

Дальше нажимаем клавишу Win+R. Вводим «Services.msc». Находим в списке свой сервис, нажимаем на него, и нажимаем «Запустить(Start)». После успешного запуска сформируется файл по указанному в коде пути в котором будет находиться список системных ошибок.
Не забываем удалить службу после проверки.

Отображение WinForm


Для отображения в консоли если постараться то можно найти статьи, но для отображения в WinForm я так и не нашла, так что вот. По умолчанию проект службы создается типа «Application». Для отображения через консоль этот параметр в настройках нужно поменять, для WinForm оставить как есть. Дальше нужно добавить форму в проект. «WindowsService1» → ПКМ → Add → Windows Form → Add. И делаем примерно следующий дизайн. Дальше меняем файл «Program.cs».

Добавляем using:

using System.Windows.Forms;
using System.Security.Principal;
using System.ComponentModel;
using System.Diagnostics;

И меняем метод Main:

static void Main(string[] args)
        {
            WindowsPrincipal windowsPricipal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
            bool hasAdministrativeRight = windowsPricipal.IsInRole(WindowsBuiltInRole.Administrator);

            if (hasAdministrativeRight == false) //проверка на права администратора
            {
                ProcessStartInfo processInfo = new ProcessStartInfo(); //создаем новый процесс
                processInfo.Verb = "runas"; //процесс должен быть запущен с правами администратора
                processInfo.FileName = Application.ExecutablePath; 
                try
                {
                    Process.Start(processInfo); //пытаемся запустить процесс
                }
                catch (Win32Exception){}
                Application.Exit();
            }
            else
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new Form1(args)); //Запускаем форму
            }
        }

Добавляем новый класс «SystemError». («WindowsService1» -> ПКМ -> Add -> Class -> Add). Тут мы будем хранить данные об ошибках. Меняем его:

public string EntryType{ get; set; }
public string EventLog{ get; set; }
public string MachineName { get; set; }
public string AppName { get; set; }
public string Message { get; set; }
public string TimeWritten { get; set; }

Дальше в «Service1.cs» добавляем метод «RunFromForm(string[] args)» который запускает службу.

public void RunFromForm(string[] args)
        {
            OnStart(args);
            OnStop();
        }

Добавляем новый класс «GetListErrors». («WindowsService1» -> ПКМ -> Add -> Class -> Add). Тут мы будем доставать данные из файла. Добавляем using:

using System.IO;

Меняем его:

string filepath = AppDomain.CurrentDomain.BaseDirectory + @"\ServiceLog.txt";

        SystemError systemError;
        public List<SystemError> listSysErrs;
        public void ReadFile() //Метод чтения из файла и запись в список системных ошибок
        {
            systemError = new SystemError();
            listSysErrs = new List<SystemError>();

            using (StreamReader sr = new StreamReader(filepath))
            {
                string line;
                while ((line = sr.ReadLine()) != null)
                {
                    if (line.Contains("-*-"))
                    {
                        listSysErrs.Add(systemError);
                        systemError = new SystemError(); //Создается новый элемент системной ошибки в списке
                    } 
                    if (line.Contains("Entry Type"))
                    {
                        systemError.EntryType = line.Substring(12);
                    }
                    else if (line.Contains("Event log"))
                    {
                        systemError.EventLog = line.Substring(11);
                    }
                    else if (line.Contains("Machine Name"))
                    {
                        systemError.MachineName = line.Substring(14);
                    }
                    else if (line.Contains("App Name"))
                    {
                        systemError.AppName = line.Substring(10);
                    }
                    else if (line.Contains("Message"))
                    {
                        systemError.Message = line.Substring(9);
                    }
                    else if (line.Contains("Time Written"))
                    {
                        systemError.TimeWritten = line.Substring(14);
                    } 
                }
            }
        }

Дальше меняем код формы «Form1.cs». Добавляем using:

using System.ServiceProcess;
using System.Diagnostics;

Меняем его:

Service1 service = new Service1();
        List<SystemError> listSysErrs;
        string[] args;
        public Form1(string[] args)
        {
            InitializeComponent();

            this.args = args;
            if (Environment.UserInteractive)//Проверяет выполняется ли текущий процесс в режиме взаимодействия с пользователем
            {
                service.RunFromForm(args); //Запускаем службу
                GetListErrors getListErrors = new GetListErrors();
                getListErrors.ReadFile(); //получаем данные из файла результата службы
                listSysErrs = getListErrors.listSysErrs;
                FillDataGridView(); //Отображаем список в таблице
            }
            else
            {
                    ServiceBase[] ServicesToRun;
                    ServicesToRun = new ServiceBase[] { service };
                    ServiceBase.Run(ServicesToRun); 
               
            }
        }
        public void FillDataGridView() //Заполняет таблицу данными из списка
        {
            foreach (SystemError item in listSysErrs)
            {
                dataGridView1.Rows.Add(item.AppName, item.Message);
            }
        }

        private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e) //При нажатии на любую ячейку на панели будут отображаться больше данных именно по ошибке которую нажали
        {
            SystemError sys = listSysErrs[dataGridView1.CurrentRow.Index];

            label1.Text = "Entry Type: " + sys.EntryType +
                "\nEvent log: " + sys.EventLog +
                "\nMachine Name: " + sys.MachineName +
                "\nApp Name: " + sys.AppName +
                "\n\nMessage: " + FormatMessage(sys.Message) +
                "\n\nTime Written: " + sys.TimeWritten;
        }
        private string FormatMessage(string msg) //Так как весь текст не помещается на панель, то нужно подкорректировать строку так чтоб она была видна полностью а не уходила бы в закат
        {
            string retMsg = "";
            int count = 75;

            if (msg.Length > count - 9)
            {
                retMsg += msg.Substring(0, count - 9) + "\n";
                msg = msg.Substring(count - 9);
            }

            while (msg.Length > count)
            {
                retMsg += msg.Substring(0, count) + "\n";
                msg = msg.Substring(count);
            }
            retMsg += msg + "\n";
            return retMsg;
        }
        private void button1_Click(object sender, EventArgs e) //По нажатию на кнопку мы перезапускаем службу как и ранее
        {
                if (Environment.UserInteractive)
                {
                    while (dataGridView1.Rows.Count != 0)
                    {
                        dataGridView1.Rows.Remove(dataGridView1.Rows[dataGridView1.Rows.Count - 1]);
                    }
                    service.RunFromForm(args);

                    GetListErrors getListErrors = new GetListErrors();
                    getListErrors.ReedFile();
                    listSysErrs = getListErrors.listSysErrs;

                    FillDataGridView();
                }
                else
                {
                    ServiceBase[] ServicesToRun;
                    ServicesToRun = new ServiceBase[] { service };
                    ServiceBase.Run(ServicesToRun);
                }
        }
        private void button2_Click(object sender, EventArgs e)
        {
                Process.Start(AppDomain.CurrentDomain.BaseDirectory); //Выбираем файл
        }

Теперь можно запускать службу как обычное приложение. Выглядит результат следующим образом:

Поделиться публикацией

Комментарии 15

    +2

    … и зачем?

      0

      Особенно интересно зачем нужна служба.… Зачем?

        0
        Был такой заказ, значит кому-то нужно. Пришлось быстро разбираться, а инфы в нэте не так уж и много оказалось.
          +4
          Вы угораете? Какой инфы? Как работать с логами, сервисами и как рисовать формы из сишарпа?
        0
        Я делал похожий сценарий.

        На разных серверах и станциях крутятся службы, фиксирующих время последнего сообщения и отсылающего его (и сами сообщения по запросу) клиенту по tcp. Это позволяет оперативно увидеть сам факт появления новых сообщений и просматривать их, игнорируя ранее просмотренные.
          0

          Гм-гм, а что бы готовое решение-то не взять?

            0
            Например?
              +1

              Winlogbeat, из того, что первым пришло в голову, но мониторинг-тулзов дофига на рынке.

                0
                Посмотрел, выглядит совсем не так, как хотелось. Да и задача довольно простая для того, чтобы позволить себе роскошь самостоятельной реализации.
                  +2

                  Я когда-то тоже думал, что мониторинг — простая задача.

        +2
        Омг, Form1, Service1… Что это было?
          0
          Смысл заморачиваться с одной утилитой? Форма будет одна, сервис тоже. Если копать глубже, то можно ко многому прикопаться (например локализация, UX...)
            +2
            Если решили публиковать свой код, то можно хотя бы элементарно его оформить. Когда видишь такое, сразу пропадает доверие и желание читать.
          0
          Любой доступ к системе, изменение безопасности, подстройка операционной системы, аппаратный сбой и сбой драйвера — все это попадает в журнал событий.

          Не любой и не все, а согласно настройкам ведения логов

          Есть 3 типа журнала: приложение(Application), системные(System) и безопасность(Security). Нам нужен только системный(System).

          Есть ещё журналы приложений и служб (Applications and Services Logs)
            +1

            Посмотрите Topshelf. Он упростит вашу жизнь до одного стартап-класса. Серия полезностей в установке и возможность консольного запуска прилагаются.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое