C# — c# windows service


Содержание

C#: создаем службу, отслеживающую состояние других служб

Показан пример, как на C# создать службу, которая будет следить за состоянием других служб

Использовалась Visual Studio 2020

Создаем новый проект — Служба Windows (.NET Framework)

Дальнейший алгоритм в комментариях к коду

Program.cs:

Service1.cs: (Тут нужно шаблон поиска изменить и получателей уведомления, так-же обратите внимание, где пишется лог)

SendMail.cs: (тут указываете параметры своей почты)

Вот и все, раз в минуту будет происходить проверка состояния служб

Если Вы хотите обменяться ссылками со мной между сайтами — пишите в контактах

Windows-служба — Создание настраиваемой Windows-службы FileSystemWatcher

Продукты и технологии:

Класс FileSystemWatcher, XML, Windows-службы

В статье рассматриваются:

  • структура настраиваемых параметров папок;
  • запуск процесса FileSystemWatcher для прослушивания изменений;
  • запуск и остановка экземпляров FileSystemWatcher в Windows-службе.

Класс FileSystemWatcher — очень мощное средство, которое является частью Microsoft .NET Framework с версии 1.1, и, согласно официальному определению (bit.ly/2b8iOvQ), он «слушает уведомления об изменениях в файловой системе и генерирует события, когда изменяется каталог или файл в каталоге».

Этот класс способен обнаруживать события в файловой системе, такие как создание, модификация или удаление файлов и папок; он полностью настраиваемый, и его конструктор принимает параметры вроде пути к папке и расширения файла, которое следует прослушивать, а также булев параметр, позволяющий задать, что процесс прослушивания должен работать рекурсивно по структуре папок. Однако включение этих параметров в ваш исходный код — плохой подход, поскольку они не помогут, если приложению понадобится наблюдать за новыми папками и расширениями имен файлов, что к тому же потребует дополнительного кодирования, повторной компиляции и развертывания. Если только вы не уверены, что приложению никогда не понадобится изменять эти параметры, гораздо лучше реализовать механизм, позволяющий изменять конфигурацию без модификации исходного кода.

В этой статье я исследую, как написать приложение, которое использует класс FileSystemWatcher только раз, а потом, применяя XML-сериализацию, позволяет вносить дальнейшие изменения в параметры приложения, такие как имена папок, расширения файлов и действия, выполняемые при генерации события. Тем самым все изменения можно легко вводить простым обновлением XML-файла и перезапуском Windows-службы.

Простоты ради я не буду вдаваться в детали того, как запускать это консольное приложение на C# в качестве Windows-службы, но в сети полно ресурсов по этому вопросу.

Структура настраиваемых параметров папок

Поскольку я планирую десериализовать XML-файл параметров в структурированный C#-класс, первым компонентом приложения должно быть определение параметров, которые FileSystemWatcher требует для работы. На рис. 1 показан код, который определяет этот класс.

Рис. 1. Определение класса CustomFolderSettings

Теперь рассмотрим, как XML-файл можно транслировать в этот C#-класс путем десериализации. Пожалуйста, заметьте, что единственного экземпляра класса CustomFolderSettings не будет — вместо этого будет список (List ), позволяющий Windows-службе прослушивать многие другие расположения папок и расширения файлов.

На рис. 2 приведен пример XML-файла параметров, который я могу предоставлять FileSystemWatcher со всеми аргументами, необходимыми ему для работы. К этому моменту важно понимать, что информация, содержащаяся в этом XML-файле (рис. 2), будет передаваться в C#-класс (рис. 1).

Рис. 2. Структура XML-файла параметров

Далее обратите внимание на то, что параметр равен true для первой папки, но false для второй. Это простой способ отключить один из FileSystemWatcher без его удаления из XML-файла, подразумевающий, что, несмотря на присутствие определенной конфигурации, класс пропустит ее при выполнении.

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

Параметр содержит приложение, которое будет запущено — в данном примере оболочка командной строки (CMD.EXE).

Параметр содержит параметры, которые будут передаваться исполняемому файлу как аргументы. Вот пример с рис. 2:

В период выполнения это транслируется в следующее:

Это приведет к записи строки в файл c:\temp\it_works_ZIP.txt, а значение <0>в XML будет заменено реальным именем файла, обнаруженным FileSystemWatcher. Если вы знакомы с C#-методом string.Format, у вас не возникнет никаких проблем с пониманием того, что здесь происходит.

К этому моменту у меня есть один XML-файл конфигурации и один C#-класс с соответствующими атрибутами, поэтому следующий шаг — десериализация XML-информации в список классов (List ). На рис. 3 показан метод, выполняющий этот ключевой этап.

Рис. 3. Десериализация XML-файла параметров

Как только этот метод выполнен, у вас появляется список, содержащий все требуемые экземпляры FileSystemWatcher, поэтому следующий шаг — запуск класса FileSystemWatcher, который начинает процесс прослушивания.

Разумеется, метод должен знать, где находится XML-файл параметров, и для определения его местонахождения я использую файл App.config. Вот содержимое App.config:

Важно помнить, что любые изменения в XML-файле параметров или в файле App.config потребуют перезапуска Windows-службы, чтобы эти изменения вступили в силу.

Запуск процесса FileSystemWatcher для прослушивания изменений


К этому моменту все параметры, необходимые для нескольких (или минимум одного) экземпляров FileSystemWatcher, доступны в списке, созданном в коде на рис. 3.

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

Рис. 4. Инициализация экземпляров FileSystemWatcher

В этом коде FileSystemWatcher прослушивает только событие создания; однако доступны и другие события, такие как Deleted и Renamed.

Я хочу особо выделить строку, где функция подписывается на событие Created в FileSystemWatcher. Здесь я использую лямбда-выражение по важной причине: поскольку у меня есть список экземпляров класса FileSystemWatcher, мне нужно сопоставить с каждым экземпляром конкретный исполняемый файл. Если бы я обрабатывал это иначе (т. е. не через лямбда-выражение, а напрямую присваивая функцию), сохранялся бы только последний исполняемый файл и все экземпляры FileSystemWatcher выполняли бы одно и то же действие.

На рис. 5 приведен код для функции, которая собственно и выполняет действие на основе индивидуальных критериев для каждого экземпляра FileSystemWatcher.

Рис. 5. Выполнение действия на основе критериев для каждого экземпляра

Наконец, на рис. 6 показана функция ExecuteCommandLineProcess, которая является стандартным способом выполнения инструкций в командной строке.

Рис. 6. Выполнение инструкций в командной строке

Запуск и остановка FileSystemWatcher в Windows-службе

Как я и говорил в самом начале, это приложение рассчитано на выполнение в качестве Windows-службы, поэтому мне нужен какой-то способ автоматического запуска и остановки экземпляров FileSystemWatcher при запуске, остановке и перезапуске Windows-службы. Хотя я не собираюсь углубляться здесь в определение Windows-службы, стоит упомянуть два основных метода в реализации Windows-службы: OnStart и OnStop. Изначально всякий раз, когда Windows-служба запускается, она должна выполнять два действия: заполнять список экземпляров FileSystemWatcher из XML-файла (рис. 3), а затем запускать эти экземпляры (рис. 4).

Вот код, необходимый для запуска процесса из Windows-службы:

И, наконец, метод на рис. 7 реализует логику для остановки FileSystemWatcher; он требует остановки или перезапуска Windows-службы.

Рис. 7. Остановка FileSystemWatcher

Заключение

FileSystemWatcher — мощный класс, который позволяет отслеживать (прослушивать) изменения, происходящие в файловой системе, такие как создание, удаление, модификация и переименование файлов и папок. Это приложение, предназначенное для выполнения в качестве Windows-службы, было разработано с целью облегчить изменение отслеживаемых файлов и папок, включая расширения файлов. В подходе, которому я следую, используется очень удобная концепция из .NET Framework — сериализация и десериализация, что позволяет передавать данные в класс FileSystemWatcher из XML-файла без каких-либо изменений в исходном коде. Вместо этого после любой модификации в XML-файле параметров достаточно перезапустить Windows-службу, и вуаля — изменения вступают в силу.

Диего Ордонес (Diego Ordonez) — инженер-конструктор с более чем 15-летним опытом в индустрии IT. В основном работал с технологиями ГИС (GIS) и САПР (CAD) в качестве аналитика, разработчика и архитектора. Является обладателем званий Microsoft Certified Professional Developer в C#, ASP.NET, ADO.NET, SQL Server и по-настоящему любит изучать и применять технологии, связанные с .NET Framework. Живет в Калгари (штат Альберта, Канада) с женой и двумя прелестными дочурками. Работает в Altus Geomatics руководителем группы GIS (bit.ly/2aWfi34).

Выражаю благодарность за рецензирование статьи эксперту Microsoft Джеймсу Маккафри (James McCaffrey).

Многопоточность в Windows Service

Добрый день.
Дали задание переписать простую Windows Form в Windows Service.
Windows Form была заточена на то чтобы все события обрабатывались в главном UI потоке.
Для этих целей использовалось «InvokeRequired/BeginInvoke/Invoke».

В Windows Service нету UI, но переписывать логику будет крайне накладно.
Какие есть методы чтобы события из второстепенных потоков обрабатывались в главным потоке?

20.06.2020, 12:15

Service для Windows
Доброго времени суток всем! У меня такой вопросик, вот решил написать для себя сервис для винды.

Windows service + WiX
Здравствуйте. Я создал проект Windows service с таким кодом: public partial class Service1 .

Windows Service C#
Господа помогите. мозг взорван курсовую завтра сдавать. Пытаюсь написать Windows Service на C#.

Сервер на Windows service
Мне нужно сделать сервер как службу windows,т.е. сделать его как проект windows service..может кто.

Windows Service — мониторинг папки
Ребят, нужна помощь в создании «Windows Service». Его задача: есть папка «folder1». Сервис должен.

20.06.2020, 13:13 2 20.06.2020, 13:30 [ТС] 3 20.06.2020, 13:36 4
20.06.2020, 13:36
20.06.2020, 13:52 5
20.06.2020, 14:36 6

Как-то странно, ведь инвокер на формах нужен исключительно для обращения к UI єлементам. Если вьі завязьівали на єто еще и логику синхронизации, то.

20.06.2020, 15:05 [ТС] 7

Wolfdp, aquaMakc, Usaga,
Сейчас попробую описать часть кода словами чтобы было ясно к чему всё это.
1) В отдельном потоке живёт TCPListener который принимает сетевые соединения. При приёме нового соединения генерирует событие которое передаётся главному (UI) потоку.
Главный потов обрабатывает новое соединение и создаёт TCPClient который запускается в новом потоке.
2) Когда TCPClient получает сообщение он генерирует событие которое передаётся главному потоку.

В результате, вся логика происходит в главном потоке и в результате чего мне не надо думать о целостности данных поскольку всё происходит в одном потоке. Форма принимала события от других потоков и выполняла все действия.

П.С. Почему я так делаю. На данный момент я знаю что когда событие на форме отработала (Скажем нажатие кнопки) то форма (Поток) не исчезает а ждёт других действий. В тоже время если создавать свой поток то после обработки поток закрывается. И как сделать так чтобы поток уходил в ожидание событие из другого потока я не знаю.


Добавлено через 9 минут

Мой сервер рассчитан на 10 соединений, и чтобы сделать оптимальнее, я все события из других потоков перенаправлю в главный поток. В итоге мне не надо работать с «потокобезопасные коллекции, средства синхронизации потоков, блокировка ресурсов. «. все эти методы я знаю но для 10 соединений всё слишком не практично.

Вопрос мой в том как в Windows service в втором потоке создать событие которое будет обрабатываться в главном потоке?

20.06.2020, 15:06 8
20.06.2020, 15:35 9
20.06.2020, 16:21 10

Вот примерно такой вот ерунды я и ожидал. Не обижайтесь.

Вам не нужен никакой «главный поток». Запускайте прослушку порта в отдельном потоке, при получении нового соединения запускайте его обработку в новом потоке (берите из пула). Ни какой завязки на «главный поток» тут не нужно вообще.

20.06.2020, 16:28 [ТС] 11

Не просто верю а на своём опыте уже почувствовал что надо делать сразу всё правильно. Но эта тема для другого разговора.

А теперь по проблеме, вот пример кода. В скобках указанны ID потоков в которых они выполняются.
ID 10 — Главный поток.
ID 11 — Второстепенный поток.

Вопрос в том что как метод «private void XXX_Mehod(object sender, EventArgs e)» запустить не с 11 потока (именно в 11 потоке был вызвано событие) а чтобы обработка события вызванная в 11 потоке обработалось в 10 потоке?

20.06.2020, 16:36 12

Где и как обрабатывается это событие?

20.06.2020, 16:37 [ТС] 13

Не обижаюсь, я сам прекрасно понимаю что пишу через зад но что есть то есть (почему так вышло тема для другого топика, но пока задача стоит такая и с вашей помощи пытаюсь её решить).

Пока вопрос в том как мне из других потоков (их может быть 10) генерировать события так чтобы обработка всех событий происходила исключительно в одном и том же потоке?

20.06.2020, 16:48 14
20.06.2020, 17:16 [ТС] 15

Также как и прошлая ссылка на тему «как не надо делать» поставил на заметку, буду читать (в том топике сходу нашел то что я не так делаю но это уже из другой темы).

Klient это ошибка которой уже 2 года. Но пока не исправляю

Вот полный код с проекта.

Запрос может обрабатываться до 10 секунд. И чтобы не было проблем с целостностью данных я сделал так что все запросы уходят на один поток а этот самый поток по очереди обрабатывает все запросы.
П.С. я знаю что делаю не правильно, но это мой первый большой проект. И в нём я сделал большое количество ошибок. Не судите строго. Просто помогите мне.

Сейчас спустя полтора года я уже понял что такой потокобезопасные коллекции/Lock/семафоры. Но тогда когда всё начинал изучать я про события не чего не знал и про отдельные потоки. И про унаследование классов тоже не знал. Но нельзя сесть за C# и начать с ходу писать всё как надо. Всё изучал на коленках, всё познавал при помощи проб и ошибок. И то что вышло (Криво, боко, страшно с точки зрения программирование) работает и работает хорошо. Просто надо переделать на Windows Server а не переписывать всё с нуля по всем догмам программирования чтобы потом опять понять что всё что я писал опять не правильною.

20.06.2020, 17:26 16

Ставим курсор, ПКМ -> Переименовать. или 2 раза подряд CTRL+R. Переименование происходит сразу по всему пространству имён.

Вот так делать не надо.

20.06.2020, 17:26 17

fufel, а про то, что в асинхронном коде нельзя использовать Mutex/Monitor/lock, уже знаете?

За lock уже сам компилятор бьёт по шаловливым ручкам, а с Mutex и Monitor уже возможны интересные приключения ))

20.06.2020, 18:41 [ТС] 18

Ладно ребята, всем спасибо за уделённое ваше внимание. И так помогли )))
Буду читать те ссылки которые вы дали, и буду дальше решать свою задачу.

Не в обиду но в основном так и говорят что «Вот так делать не над/Так не надо», но как делать надо (пример код) не дают.
Если посмотриш на картинку то там выходит такое дело.
Есть центр, он центра надо сделать 4 шага чтобы попасть в обведённую кругом точку.
Черный цвет это твой решение, оно идеально, оно превосходно. Я сделал так как я смог (Зелёный цвет). Затратил больше шагов, с самого начала сделал первый шаг совсем в другую сторону но я смог дойти до нужной точки, и хоть через зад но всё прекрасно работает. Но показав своё решение я получаю выше стоящий ответ.
И что мне теперь делать? всё откатывать назад и пытатся найти другой (более вернее путь)? так я могу сделать три попытки (Желтый цвет) и все попытки уйдут в молоко. И что мне остаётся делать? Так как я сделал (работает) но это очень плохо, а как надо негде взять.
Это относится и к второму комментарию, я прекрасно понимаю что делаю через з*д но нету у меня во круге тех кто может мне сказать как надо делать. Есть google, вы и моя задача.

amr-now, намотал на ус, почитаю и попробую сделать правильные выводы (не шагнув на желтую линию), и буду дальше решать свою задачу.

20.06.2020, 19:12 19

fufel, ну тут уж насколько позволит здоровье.

Тему concurrency не бросайте. Она сейчас новая и модная. Даже устоявшегося перевода не имеет.

Concurrency — одновременное или псевдоодновременное выполнение различных частей программного кода; или одного кода для разных данных. Является обобщенным понятием для параллельного, многопоточного, асинхронного программирования.


Как теория действует и для C#, и для Java, и для JavaScript. Так что в любом случае тему надо знать.

Вьётся вокруг страшных слов Rx, Promise и тд.

——
Так что на одних Invoke() свет клином не сошелся ))

20.06.2020, 19:41 20

fufel, хороший совет «как надо» начинается с полного описания задачи и мотивации, которая привела к вопросу. Вникать и расписывать решение вопроса, который уже изначально было понятно, что решает кривую задачу (привязки к «главному потоку» в службе, где это не играет роли) — дело бесполезное. Поэтому всё и свелось к фразам «так вообще не делается» и «это фигня и через зад».

Собственно, вам озвучили, что вы ищете решение несуществующей проблемы. XY problem. Так, что диаграмма выше как-то тут не вяжется. Уж извините.

20.06.2020, 19:41
20.06.2020, 19:41

Использование SerialPort в Windows Service
Всем доброго времени суток. При использование в Windows Form делаю вот так: private void.

Windows Service. Timer не срабатывает
День добрый. Извините, если не в тот раздел пишу. Проблема в следующем: Решил написать.

C# windows service рабочий стол
Добрый день ! Вопросик такой возник , помогите разобраться. Пишу сервис под вин хп и выше.

How can a Windows Service determine its ServiceName?

I’ve looked and couldn’t find what should be a simple question:

How can a Windows Service determine the ServiceName for which it was started?

I know the installation can hack at the registry and add a command line argument, but logically that seems like it should be unnecessary, hence this question.

I’m hoping to run multiple copies of a single binary more cleanly than the registry hack.

Edit:

This is written in C#. My apps Main() entry point does different things, depending on command line arguments:

  • Install or Uninstall the service. The command line can provide a non-default ServiceName and can change the number of worker threads.
  • Run as a command-line executable (for debugging),
  • Run as a «Windows Service». Here, it creates an instance of my ServiceBase-derived class, then calls System.ServiceProcess.ServiceBase.Run(instance);

Currently, the installation step appends the service name and thread count to the ImagePath in the registry so the app can determine it’s ServiceName.

Create Windows Services in C#

IN this post, we go over how to get started using C# and .NET by creating a basic Windows service with these technologies.

Join the DZone community and get the full member experience.

Here I’m going to explain Windows Services in C# .NET.

  • Introduction of Windows Services.
  • How to create Windows Services in C# .NET.

Introduction

Windows Services normally start when the OS boots and runs an application in the background. Windows Services executes applications in its own session. It either starts automatically or we can manually pause, stop, and restart it.

You can find services in the following ways

    Go to Control Panel select “Services” ins >Open Visual Studio, go to File > New and select Project. Now select a new project from the Dialog box and select “Window Service” and click on the OK button.

Step 2

Go to Visual C# -> ”Windows Desktop” -> ”Windows Service,” give your project an appropriate name and then click OK

Once you click OK, the below screen will appear, which is your service

Step 3

Right-click on the blank area and select “Add Installer.”

Adding Installers to the Service

Before you can run a Windows Service, you need to install the Installer, which registers it with the Service Control Manager.


After Adding Installer, ProjectInstaller will add in your project and ProjectInstakker.cs file will be open. Don’t forget to save everything (by pressing ctrl + shift + s key)

The Solution Explorer looks like this:

Step 4

Right click on blank area and select “View Code”

Step 5

Its has a Constructor, which contains the InitializeComponent method.

The InitializeComponent method contains the logic which creates and initializes the user interface objects dragged on the form’s surface and provides the Property Grid of Form Designer.

Very important: Don’t ever try to call any method before the call of InitializeComponent method.

Step 6

Select the InitializeComponent method and press the F12 key to go the definition.

Step 7

Now add the below line while installing the service:

You also can add a description and display the service name (optionally).

Step 8

In this step, we will implement a timer, and code to call a service at a given time. We will create a simple write in a text file.

Service1.cs >Code explanation — the above code will call a service every 5 seconds and create a folder if none exist and write our message.

Step 9: Rebuild Your Application

Right-click on your project or solution and select Rebuild.

Step 10

Search “command Prompt” and run the program as an administrator:

Step 11

Fire the below command in the command prompt and press Enter.

Step 12

Now Go to your project source folder > bin > Debug and copy the full path of of the Windows Service.exe file

Step 13

Open the command prompt and fire the below command and press enter.

Syntax

InstallUtil.exe + Your copied path + \your service name + .exe

Our Path

Step 14

Open services by following the below steps:

  1. Press Window key + R.
  2. Type services.msc
  3. Find your Service.

Service Output

A log folder will be created in your bin folder.

If you want to uninstall the service, fire the below command.

  1. Syntax InstallUtil.exe -u + Your copied path + \your service name + .exe
  2. Our path InstallUtil.exe -u C:\Users\Faisal-Pathan\source\repos\MyFirstService\MyFirstService\bin\Debug\MyFirstService.exe


Summary

In this article, we learned how to create a Windows Service and install/uninstall it using the InstallUtil.exe from a command prompt.

I hope that I have explained each step clearly and that it can be easily understood by all developers. You can leave feedback/comments/questions to this article. Please let me know if you like and understand this article and how I could improve it.

I also uploaded this project on GitHub, here.

If you enjoyed this post, Faisal has also written a great article on C# Best Practices.

Windows Service in C#

Aug 22 ’18 Originally published at rendered-obsolete.github.io on Aug 21, 2020 ・1 min read

Part of our client runs as a Windows service for a few reasons:

  • It automatically starts when Windows 10 boots
  • OS can restart it if it fails
  • It will be running even when no user is logged in
  • When required, it has elevated privileges

Other than operations requiring elevated privileges, all those reasons only exist in a production environment. During development we want the convenience of launching/debugging from Visual Studio and easy viewing of stdout/stderr, so we also want it to function as a console application.

In our codebase and documentation this program is referred to as “layer0”.

This and other Windows programming makes heavy use of PInvoke. http://pinvoke.net/ is indispensible.

The Service

The key to creating a Windows service in C# is inheriting from System.ServiceProcess.ServiceBase.

StartLayer0() and StopLayer0() are routines that take care of startup and shutdown and are shared by the service and console application.

The Main()

Our program entry point:

The executable has 3 modes:

  • layer0.exe —install installs the service
  • layer0.exe —service executes as the service
  • layer0.exe executes as a normal console application

Service Installation

The —install option is used to install the service:

We first get the path and name of the current executable (layer0.exe).

CreateService() is the main call to create a service. SERVICE_AUTO_START means the service will start automatically. The application specifies itself along with the —service command line argument.

This places it in services.msc where Start executes layer0.exe —service :

setPermissions()

Our platform being non-critical to the system, ordinary users should be able to start/stop it.

Reference these Stack Overflow issues:

Failure Actions

Failure actions specify what happens when the service fails. This can be accessed from services.msc by right-clicking the service then Properties->Recovery :

This is a particularly nasty bit of pinvoke. Blame falls squarely on the function to change service configuration parameters, ChangeServiceConfig2(), because its second parameter specifies what type the third parameter is a pointer to an array of.

We heavily consulted and munged together the following sources:

The majority of this is setting up a SERVICE_FAILURE_ACTIONS for the call to ChangeServiceConfig2() . As mentioned in its documentation, if the service controller handles SC_ACTION_TYPE.RebootComputer the caller must have SE_SHUTDOWN_NAME privilege. This is fulfilled by GrantShutdownPrivilege() .

Using this function we can set failure actions programmatically:

GrantShutdownPrivilege() is pretty much taken verbatim from MSDN code:

As an alternative to the pinvoke nightmare, this can also be done with sc.exe:


We’ve got our Windows service running. Now we need to have it do something useful.

Работа со службами Windows в C#

Как включить, отключить, приостановить службу Windows?

Для работы со службами в интерфейсе Windows имеется хороший инструмент, который так и называется — Службы. Открыть его можно если выполнить команду меню Выполнить

Данный инструмент предоставит пользователю всю свободу действий для управления службами Windows. Для этого Вам только необходимо выбрать интересующую Вас службу и дважды нажать на неё. После этого перед Вами откроется окно Свойства этой службы. Тут можно вычитать подробное описание службы, познакомится с исполняемым файлом службы Windows, настроить тип запуска службы, остановить, включить, приостановить или продолжить работу службы(все эти настройки находится в первой вкладки для свойства любой службы):

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

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

Но всё это делается с помощью клавиатуры и мышки. Перед Вами на скриншотах показан основной функционал данного инструмента. В данной статье мы рассмотрим основные моменты работы со службами Windows не с помощью интерфейса операционной системы, а реализуем это программно на языке программирования C#.

Управление службами WIndows в C#

Для того чтобы начать общаться со службами Windows на языке C#, нам нужно подключить директиву

Если попытаться сделать это сразу, у Вас ничего не получиться. Всё потому, что для начала нужно добавить ссылку на компонент, который носит то же имя. А для этого нам необходимо выполнить:

  1. Правый клик мышкой по названию проекта.
  2. Добавить ссылку.
  3. В открывшемся окне переходим во вкладку .Net и ищем компонент System.ServiceProcess и выделяем его.
  4. Жмем ОК.

После чего добавляем вышеописанную директиву в проект:

Отключение, включение, приостановка и возобновление работы служб Windows

Для манипуляции с определенной службой Windows нам понадобится специальный класс(ServiceController), который предназначен для данных целей. Кроме этого нам понадобиться и имя службы, чтобы хоть как-то обращаться к службе. Имя службы Вы можете узнать в свойствах службы, используя для этого вышеописанное окно Службы. В приведенном ниже примере я используя имя службы Центра безопасности Windows — wscsvc.

[csharp]System.ServiceProcess.ServiceController ser = new System.ServiceProcess.ServiceController(«wscsvc»);// создаем объект для обращения к службе
ser.Start();//запуск службы Windows
ser.Pause();//приостановка службы Windows
ser.Continue();//возобновление работы службы
ser.Stop();//остановка службы
ser.Close();//закрываем созданный объект[/csharp]

Думаю объяснять значения команд не нужно, они интуитивно понятны, если знать английский. Но всё же я привел комментарии, которые доступно объясняют смысл соответствующей строчки кода.

Изменение типа запуска службы Windows в C#

К сожалению, я не нашел информацию как можно напрямую подключиться к службе и изменить тип её запуска(если кто-то обладает данной информацией, прошу поделиться в комментариях). Если это и возможно, то это не так просто, как в случае с включением/выключением службы. Ответ не лежит на поверхности. Ну, а так как программисты народ ленивый, пойдем простым путем — будем редактировать реестр Windows, в котором продублированы почти что все настройки операционной системы, в том числе тип запуск служб. В разделе

реестра хранятся различные параметры всех служб, среди которых есть параметр, регулирующий тип запуска службы. Носит данный параметр простое и запоминающееся имя — Start. Данный параметр принимает значения от 1 до 4, которые соответствуют следующим параметрам запуска:

  1. Автоматически(отложенный запуск).
  2. Автоматически.
  3. Вручную.
  4. Отключена.

И чтобы изменить тип запуска службы на тот, который нужен нам, необходимо всего лишь изменить один параметр. Тут нужно знать, что в разделе

находится множество других разделов, как правило, по одному на каждую службу. Данные разделы имеют те же названия, что и сами службы, поэтому Вы не должны потеряться. А для этого необходимо к указанному пути через слэш добавить название службы Windows:

[csharp]RegistryKey regkey;
string keyValueInt = «4»;//указываем тип запуска службы
string subKey = @»SYSTEM\CurrentControlSet\Services\wscsvc»;//месторасположения службы
try
<
regkey = Registry.LocalMachine.CreateSubKey(subKey); //указываем путь
regkey.SetValue(«Start», keyValueInt, RegistryValueKind.DWord); //создаем по заданному пути параметр типа DWord со значение запуска
regkey. Close();
>
catch (Exception ex)
<
MessageBox. Show(ex. ToString());
>[/csharp]

Данный отрезок кода изменяет запуск службы Центра обеспечения безопасности Windows на «отключено». Чтобы данный код не был выделен красным цветом, необходимо подключить следующую директиву:

[csharp]using Microsoft. Win32;[/csharp]

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

[csharp]System.Diagnostics.ProcessStartInfo processInfo = new System.Diagnostics.ProcessStartInfo();
processInfo.FileName = «sc»;
processInfo.Arguments = «config ServiceName start= auto»; // auto|demand|disabled|delayed-auto — перечисленные аргументы соответствуют типу запуска службы
processInfo.UseShellExecute = true;
processInfo.Verb = «runas»; // от имени администратора
processInfo.WindowStyle = ProcessWindowStyle.Hidden; // скрыть окно
System.Diagnostics.Process pr = new System.Diagnostics.Process();
try
<
pr = Process.Start(processInfo);
>
catch (Win32Exception)
<
//Ничего не делаем, потому что пользователь, возможно, нажал кнопку «Нет»
// в ответ на вопрос о запуске программы в окне предупреждения UAC (для Windows 7)
>
pr.WaitForExit();[/csharp]

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

Как проверить, установлена ли служба windows В C#

Я написал службу Windows, которая предоставляет службу WCF для GUI, установленного на том же компьютере. Когда я запускаю GUI, если я не могу подключиться к службе, мне нужно знать, потому ли это, что приложение службы еще не установлено, или потому, что служба не запущена. Если первый, я хочу установить его (как описано здесь); если последнее, я хочу запустить его.

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

6 ответов

вы также можете использовать следующее..

на самом деле цикл, как это:

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


но доступ к его свойствам, если служба не существует, приведет к InvalidOperationException. Итак, вот безопасный способ проверить, установлена ли служба:

для non-linq вы можете просто перебирать массив следующим образом:

Создание C# сервисов Шаг-За-Шагом. Часть 1

Автор: Terry Denham 10 Apr 2003. Перевод: MC707 29 Jan 2005 для Realcoding.NET

  • Скачать исходники – 8.79 Кб ( http://www. realcoding.net/downloads/creat_csharp_service/csharpsvclesson1_demo.zip)
  • Скачать демо-проект – 2.72 Кб ( http://www. realcoding.net/downloads/creat_csharp_service/csharpsvclesson1_src.zip)

Эта мульти-статья была поделена мной на следующие части:

  1. Создание проекта, инициализирующего сервиса и инсталляция.
  2. Добавление дополнительных сервисов в приложение.
  3. Добавление поддержки записи событий.

Начнем с создания нового C# проекта, выбрав console application. Visual Studio .NET предлагает мастер создания Windows Service используя C#, но я его не использовал, т.к. не все имеют VS.NET. Рисунок 1 показывает экран создания проекта.

Теперь, когда мы создали пустое приложение console application, нам нужно изменить некоторые его свойства, по умолчанию оставленные мастером. Откроем файл Class1.cs. Он содержит точку входа приложения. Переименум класс Class, чтобы лучше отразить его роль в приложении. Щелкнем правой клавишей мыши по узлу Class1.cs в Solution Explorer и выберем Rename. Переименуем файл Application.cs. Затем щелкнем правой клавишей мыши и выберем View Code. Изменим в коде класс Class на Application.

Также нам надо изменить пространство имен, чтобы показать, где существует наш класс. Я имею домен, где я использую мой пример, названный CodeBlooded и я собираюсь использовать его в качестве имени. Вы можете использовать любое имя какое пожелаете. Т.к. этот сервис – часть вымышленной игры Spades (которую я скоро закончу писать), мы будем использовать пространство имен CodeBlooded.Spades.Services. Отредактируем файл Application.cs и изменим пространство имен SpadesServer на CodeBlooded.Spades.Services.
Чтобы избежать изменений в каждом классе, добавляемом в проект, откроем диалог свойств проекта, щелкнув правой клавишей мыши по проекту SpadesServer и выберем Properties. В этом диалоге выберем Common Properties | General tab и сменим поле Application | Default Namespace на CodeBlooded.Spades.Services. Теперь любому классу будет автоматически назначено это пространство имен. Рисунок 2 показывает это.

Пока мы еще находимся в диалоге свойств проекта, перейдем на закладку Configuration Properties | Build и изменим Outputs | XML Documentation File на SpadesServer.xml. Это XML файл, который создаст компилятор для помощи в документировании проекта. Посмотрите в .NET framework SDK и посмотрите, какие тэги он поддерживает. Рисунок 3 показывает этот диалог.

Теперь позаботимся о некоторых конфигурационных деталях прежде чем заняться кодом. .NET framework имеет базовый класс ServiceBase, обеспечивающий интерфейс сервиса. Этот класс должен быть основан на коде, который мы добавим и иметь логику сервиса. Мы должны отменить стандартные методы OnStart и OnStop. Эти методы вызываются службой Service Control Manager, фактически управляющей сервисами.

Проблема в том, что методы OnStart и OnStop должны возвратить управление обратно в Service Control Manager в течение 1 минуты, чтобы Service Control Manager знал, запущен или остановлен сервис. Как же мы добьемся того, чтобы возвратить в течение 1 минуты главному управлению? Мы должны создать фоновый поток, берущий всю работу на себя.

Чтобы не переписывать заново код каждый раз при добавлении класса, я написал базовый класс, ответственный за создание потока и коммуникации с SCM. Создадим новый класс и назовем его SpadesServiceBase. Он выглядит примерно так.

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;

namespace CodeBlooded.Spades.Services
<
public class SpadesServiceBase : System.ServiceProcess.ServiceBase
<
public SpadesServiceBase()
<
// TODO: Add any initialization code here
>

///
/// Set things in motion so your service can do its work.
///
protected override void OnStart(string[] args)
<
// TODO: Add code here to start your service.
>

///
/// Stop this service.
///
protected override void OnStop()
<
// TODO: Add code here to perform any tear-down
// necessary to stop your service.
>
>
>

В конечном итоге получим 3 сервиса, управляющие состоянием игры со службой администрирования, которая будет управлять и/или обслуживать другие 3 сервиса. Чтобы это реализовать, служба администрирования должна происходить от класса SpadesServiceBase, но мы создадим другой класс, который происходит от класса SpadesServiceBase, от которого будут происходить 3 класса-потомка. Это поможет нам лучше изолировать общедоступный код, при наличии базового класса низшего уровня, содержащего код, применяемого ко всем службам, и классы-потомки базового класса будут иметь общий код, доступный всем порожденным службам.

Итак, создадим другой класс и назовем его SpadesChildServiceBase. Пусть он наследуется от SpadesServiceBase. Создавайте файл любым способом, но код должен выглядеть примрерно так:

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;

namespace CodeBlooded.Spades.Services
<
public class SpadesChildServiceBase : SpadesServiceBase
<
public SpadesChildServiceBase()
<
// TODO: Add any initialization code here
>

///
/// Set things in motion so your service can do its work.
///
protected override void OnStart(string[] args)
<
// TODO: Add code here to start your service.
base.OnStart( args );
>

///
/// Stop this service.
///
protected override void OnStop()
<
// TODO: Add code here to perform any
// tear-down necessary to stop your service.

Теперь мы собираемся написать код фонового потока и синхронизации объектов, используемых SCM, чтобы стартовать и останавливать фоновый поток. Фоновый поток будет периодически возобновляться и проделывать некоторые действия. Если мы хотим написать службу, которая зависит от сообщений, исходящих от клиента, то можно использовать любой механизм IPC для управления синхронизации. В следующих статьях я опишу другой способ, используя асинхронный вызов сокетов, но сейчас я просто создам опорный механизм, управляющий работой сервисов.

Чтобы облегчить основные классы, связанные с другими производными классами, базовый класс предоставляет виртуальный метод, называемый Execute, который должен быть реализован сервисом. Метод Execute вызывается периодически базовыми классами, поскольку это запрограммировано нашим таймером. Также, метод Execute должен содержать некоторую логику, отвечающую за периодическое действие каждого сервиса.

Мы должны добавить ссылку на System.Threading к нашему классу SpadesServiceBase и добавить переменную m_thread типа Thread. Также, мы должны использовать ManualResetEvent из пространства имен Threading, чтобы соединиться или остановить запрос от SCM. В методе OnStart класса SpadesServiceBase, мы создадим объект Thread, так же, как и ManualResetEvent. Чтобы запустить поток, нам нужно определить метод старта потока, который будет являться точкой входа потока. В нашем случае мы назовем метод ServiceMain и используем класс ThreadStart для определения нашего метода ServiceMain. Изменим код класса SpadesServiceBase на такой:

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
using System.Threading;

namespace CodeBlooded.Spades.Services
<
public class SpadesServiceBase : System.ServiceProcess.ServiceBase
<
public SpadesServiceBase() <
// создаем новый объект timespan
// с задержкой по умолчанию 10 сек.
m_delay = new TimeSpan(0, 0, 0, 10, 0 );
>

///
/// Set things in motion so your service can do its work.
///
protected override void OnStart(string[] args) <
// создаем объект threadstart для ServiceMain
ThreadStart ts = new ThreadStart( this.ServiceMain );

// создаем ручное событие ресета и
// инициализируем его
m_shutdownEvent = new ManualResetEvent(false);

// создаем рабочий поток
m_thread = new Thread( ts );

// вызываем базовый класс
base.OnStart( args );
>

///
/// Stop this service.
///
protected override void OnStop() <
// сигнал событию — завершить
m_shutdownEvent.Set();


// ждать поток для завершения в течение 10 сек.
m_thread.Join(10000);

// вызываем базовый класс
base.OnStop();
>

///
///
///
protected void ServiceMain() <
bool bSignaled = false;
int nReturnCode = 0;

while( true ) <
// ждем сигнального события или
// истечения задержки
bSignaled = m_shutdownEvent.WaitOne( m_delay, true );

// если пришел сигнал о завершении, выходим из цикла
if( bSignaled == true )
break;

protected Thread m_thread;
protected ManualResetEvent m_shutdownEvent;
protected TimeSpan m_delay;
>
>

Теперь, когда мы позаботились о деталях, приступим к созданию административного сервиса. Хоть и сервис не будет ответственен за многое, в следующих нескольких уроках мы добавим в него функциональности. Сейчас создадим новый класс SpadesAdminService, производный от SpadesServiceBase. Следующий код – шаблонный для административного сервиса.

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;

namespace CodeBlooded.Spades.Services
<
public class SpadesAdminService : SpadesServiceBase
<
public SpadesAdminService()
<
this.ServiceName = «SpadesAdminSvc»;
>

///
/// Set things in motion so your service can do its work.
///
protected override void OnStart(string[] args)
<
base.OnStart( args );
>

///
/// Stop this service.
///
protected override void OnStop()
<
base.OnStop();
>

// Запишем сообщение в Event Log, чтобы знать,
// что наш сервис работает
System.Diagnostics.EventLog.WriteEntry(«SpadesAdminSvc»,
ServiceName + «::Execute()»);

Теперь, когда сервис написан, нужно сделать некоторую рутинную работу, чтобы иметь именно сервис. Все что нам нужно сделать для этого – это создать совокупность объектов ServiceBase и просто вызвать метод Run базового класса ServiceBase. Добавьте следующий код к методу Main объекта Application. Следующий фрагмент кода показывает все необходимое для запуска сервиса.

static void Main(string[] args)
<
// создадим массив сервисов
ServiceBase[] servicesToRun;

// чтобы создать новый образец нового сервиса,
// просто добавьте его к списку сервисов,
// указанных в конструкторе массива ServiceBase
servicesToRun = new ServiceBase[] < new SpadesAdminService() >;

// запустим все созданные сервисы.
// Вообще-то, это не запустит сервисы, но
// зарегистрирует их в Service Control Manager
ServiceBase.Run( servicesToRun );
>

Теперь мы близко подобрались к раскрытию темы статьи. Все что нам осталось – добавить наши Installers. Они могут быть вызваны программой установщиком, который я опишу в следующей статье, либо вызовом программы InstallUtil из framework SDK.

Добавим новый инсталлер к нашему проекту и назовем его SpadesInstaller. После запуска мастера Add Class, добавим код к конструктору класса. Инсталлер использует атрибуты, принадлежащие классу, в котором инсталлер создает экземпляр каждого класса, которые имеют атрибут RunInstaller.

Все, что нам нужно – экземпляр ServiceProcessInstaller, чтобы управлять запуском сервиса, и создать объект ServiceInstaller для каждого устанавливаемого сервиса. После установки нескольких свойств этих объектов, добавим их к коллекции Installers. Следующий фрагмент кода показывает все необходимое для установки сервиса.

using System;
using System.Collections;
using System.ComponentModel;
using System.Configuration.Install;
using System.ServiceProcess;
namespace CodeBlooded.Spades.Services
<
///
/// Summary description for SpadesInstaller.
///
[RunInstaller(true)]
public class SpadesInstaller : System.Configuration.Install.Installer
<
public SpadesInstaller()
<
ServiceProcessInstaller process = new ServiceProcessInstaller();

ServiceInstaller serviceAdmin = new ServiceInstaller();

serviceAdmin.StartType = ServiceStartMode.Manual;
serviceAdmin.ServiceName = «SpadesAdminSvc»;
serviceAdmin.DisplayName = «Spades Administration Service»;

// добавим созданные инсталлеры
// к нашему контейнеру
Installers.Add( process );
Installers.Add( serviceAdmin );
>
>
>
После полного тестирования кода, запустим командную строку и перейдем в папку, где создается проект. Используем папку binDebug и выполним следующую команду для установки сервисов:

Если вы хотите удалить сервисы, выполните следующую команду:

installutil SpadesServer.exe -U

После этого мы можем запускать и останавливать сервисы из Service Control Manager.

В следующих статьях мы добавим большую функциональность framework, которую создали здесь. Добавим несколько дочерних сервисов, которые будут запускать незапущенный главный сервис. Главный сервис будет останавливать дочерние, если будет остановлен.

Windows Communication Foundation (WCF) служба

Опубликовано shwan в 13.01.2020 13.01.2020

Windows Communication Foundation (WCF) — программный фреймворк, используемый для обмена данными между приложениями, входящий в состав .NET Framework. Другим словами, WCF – это программная платформа от Microsoft для создания, настройки и развертывания распределенных сетевых сервисов.

Давайте рассмотрим процесс создания и вызова службы WCF.

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

Для начала необходимо создать новый проект WCF. Пусть наша Windows Communication Foundation служба будет возвращать количество оставшихся дней до нового года.

Visual studio создаст интерфейс и класс службы по умолчанию с именем IService1.cs и Service1.svc.

Нам необходимо переименовать их в соответствии с нашей предметной областью.

Давайте рассмотрим интерфейс INewYearService. Для начала нам необходимо в теле интерфейса объявить метод, который будет предоставлять служба для вызова. Для этого его необходимо пометить атрибутом [OperationContract].

Как вы видите данный метод возвращает экземпляр класса TimeToNewYear. Это вспомогательный класс, содержащий значения времени до нового года. Ниже приведена его структура. Для того, чтобы данный класс можно было использовать в качестве возвращаемого аргумента, его необходимо пометить атрибутом [DataContract], а свойства, доступные для чтения клиенту в возвращаемом значении помечаются атрибутом [DataMember].

Теперь нам остается реализовать интерфейс Windows Communication Foundation службы в классе NewYearService.svc.cs следующим образом:


Давайте проверим работу нашей службы wcf. Для этого нажмем кнопку Начать отладку. Обратите внимание, что возможные два варианта поведения системы. Если мы начнем отладку находясь в NewYearService.svc, от откроется отладчик службы. Во всех остальных случаях откроется окно браузера. Давайте рассмотрим каждый из вариантов подробнее.

Браузер

После запуска отладки отобразится браузер с файловой структурой нашей службы wcf.

Нам необходимо нажать на ссылку с именем нашей службы NewYearService.svc. Если все работает корректно, то мы увидим следующее окно, иначе будет показано сообщение с ошибкой.

Тестовый клиент WCF

В левой верхней части отладчика можно увидеть структура нашей службы wcf. Для проверки нашего метода выполним двойной щелчок левой кнопкой мыши по его имени. В правой части отладчика откроется форма запроса. Мы можем указать значение, которое будет передано в метод.

После установки передаваемых значений необходимо нажать кнопку Вызвать. Появится предупредительное сообщение. Можно смело ставить галочку Не выводить это сообщение в дальнейшем и нажимать кнопку ОК.

После этого в нижней правой части отладчика будет отображены значения возвращаемые нашей службой wcf.

Консольный клиент для WCF

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

В созданном консольном приложении нам необходимо добавить ссылку на службу wcf.

В открывшимся окне службы необходимо указать имя службы wcf и ввести ее адрес.

Для простоты можно нажать кнопку Найти, тогда адрес службы wcf будет определен автоматически.

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

Если настройка прошла корректно, то в обозревателе решения в консольном приложении отобразится ссылка на нашу службу wcf.

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

Перед началом отладки не забудьте установить консольное приложение автозагружаемым проектом. Получаем следующий результат.

Web клиент для WCF

Теперь рассмотрим, как нам обратиться к службе wcf из веб-приложения. Процесс подключения службы не отличается от подключения в консольном приложении. Давайте рассмотрим как можно настроить авторизацию с помощью Windows. Это потребует дополнительной настройки приложения. Для начала создадим проект нового MVC приложения.

Нажимаем кнопку ОК, и попадаем в меню настройки создания веб-приложения. Выберем MVC шаблон и изменим способ авторизации. Для этого нажмем на кнопку Изменить способ проверки подлинности.

Выбираем авторизацию с помощью Windows и нажимаем ОК в обоих окнах.

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

Для того, чтобы IISExpres перестала ругаться на нас за попытку создания windows-аутентификации нужно сделать ряд действий в наше службе.

Нужно дополнить наш web.config

Добавляем в любую точку раздела , если отсутствует раздел webServer или дополняем уже существующий.

Находим раздел и в нем вставляем следующее.

Далее нужно указать разделы биндинг и сервис

Раздел дополняем такой вот строчкой. В ней мы говорим, что именно такая схема аутентификации будет использоваться у нас.

На этом настройка web.config заканчивается. В итоге у нас должен получиться файл примерно следующего содержания.

Настройка applicationhost.config

Далее идем в папку vs нашего проекта (она скрыта по умолчанию). В ней ищем папку config, а уже в ней находим файл applicationhost.config, его то нам нужно будет поправить.

Находим вот такой раздел. Все Deny меняем на Allow, разрешая изменение установленного по умолчанию режима аутентификации.

Далее находим данную настройку. В ней false меняем на true, разрешая механизму работать.

И под конец находим вот эту настройку. Тут мы true меняем на false. Говоря нашему IISExpres, чтобы он не блокировал службу windows-аутентификации.

После этого сохраняем все конфигурационные файлы и пробуем сделать ссылку на службу wcf аналогично как при добавлении службы в консольное приложение.

Изменим контроллер главной страницы web-приложения, чтобы взывать нашу службу wcf.

Теперь нам осталось только изменить представление, чтобы вывести результат работы Windows Communication Foundation службы на экран пользователя.

Получаем следующий результат работы веб-приложения.

Итоги WCF

Исходный код приложения можно скачать из репозитория https://github.com/shwanoff/wcf.

Мы подробно рассмотрели процесс создания и настройки Windows Communication Foundation службы, а также продемонстрировали как можно подключится к wcf через консольное и веб-приложение. Также рекомендую прочитать статью Принципы SOLID C#. И не забудьте подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.

Цукерберг рекомендует:  Как перевести зарубежные сайты на русский язык
Понравилась статья? Поделиться с друзьями:
Все языки программирования для начинающих