Сервис Chronos¶
Chronos – это приложение, позволяющее периодически запускать фоновые сервисы (плагины) как сервис Windows/Linux или как приложение в окне консоли (для отладки). Chronos позволяет очень просто реализовать любую фоновую работу или работу по расписанию, которая обычно выполняется написанием и установкой отдельных сервисов Windows/Linux. Например, плагин Chronos может пересчитывать динамические роли или удалять карточки из корзины, которые лежат там более 6 месяцев.
Хост Chronos¶
Запускается как консольное приложение или устанавливается как сервис Windows или Linux.
-
Установка и запуск сервиса (запустите в командной строке от имени администратора):
install-and-start.bat
-
Установка без запуска сервиса:
install.bat
-
Остановка и удаление сервиса без удаления:
uninstall.bat
Хост выполняет планирование и запуск найденных плагинов в соответствии с расписанием, которое задают сами плагины через атрибуты или конфиги.
Хост ищет плагины внутри задаваемой в конфиге хоста папки Plugins или в её подпапках на 1 уровень вниз. Подпапка плагина должна содержать файл plugins.xml (см. ниже), перечисленные в нём файлы .DLL для плагинов и все их зависимости.
Если хост запущен как сервис, то он пишет информацию по найденным плагинам в лог. При запуске в окне консоли список найденных плагинов выводится на консоль.
В оснастке ‘Службы’ сервис называется Syntellect Chronos
Содержимое дерева с плагинами (папки в квадратных скобках)
|
|_ app.json
|_ app-db.json
|_ app-ext.json
|_ app-plugins.json
|_ Chronos.exe
|_ Chronos.dll
|_ Chronos.Platform.dll
|_ Chronos.Plugins.dll
|_ NLog.dll
|_ NLog.config
|_ ...
|
|_ [ extensions ]
| |
| |_ extensions.xml
| |_ Tessa.Extensions.Default.Imaging.dll
| |_ Tessa.Extensions.Default.Server.dll
| |_ Tessa.Extensions.Default.Shared.dll
| |_ Tessa.Extensions.Imaging.dll
| |_ Tessa.Extensions.Server.dll
| |_ Tessa.Extensions.Shared.dll
|
|_ [ Plugins ]
|
|_ [ MyPlugin1Folder ]
| |
| |_ app.json
| |_ Chronos.Plugins.dll
| |_ MyPlugin1Assembly.dll
| |_ MyPlugin1Config.xml
| |_ plugins.xml
| |_ extensions.xml
|
|_ [ MyPlugin2Folder ]
| |
| |_ app.json
| |_ Chronos.Plugins.dll
| |_ MyPlugin2Assembly.dll
| |_ NLog.dll
| |_ Tessa.dll
| |_ extensions.xml
|
|_ Chronos.Plugins.dll
|_ MyPlugin3Assembly.dll
|_ MyPlugin4Assembly.dll
|_ Some.Useful.Shared.Lib.dll
|_ plugins.xml
|_ extensions.xml
Хост запускает каждый плагин в отдельном процессе-обёртке и логирует необработанные исключения.
При вежливой остановке хоста процессы всех плагинов будут гарантированно завершены. При аварийной остановке хоста (при закрытии окна консоли или при завершении процесса хоста через диспетчер задач) все процессы плагинов завершаются с вероятностью 99,9%. Если процессы плагинов не удалось завершить, то они будут гарантированно завершены при повторном запуске хоста.
В каждой из папок с плагинами может присутствовать файл plugins.xml, в котором указываются файлы сборок, сканируемые на наличие классов плагинов. Если файл отсутствует, содержит синтаксические ошибки или не содержит указаний по файлам сборок (элементов include), то на наличие плагинов сканируются все сборки в папке (это может быть медленно, поэтому такой вариант не рекомендуется).
Пример файла plugins.xml
<?xml version="1.0" encoding="utf-8" ?>
<plugins>
<include file="Tessa.Chronos.dll" />
</plugins>
Если плагин должен вызываться периодически, то скорее всего первый раз он будет запущен сразу же после запуска хоста (если параметр launchImmediately
в файле plugins.xml равен "true"
). После этого он будет вызываться через интервалы времени, отсчитываемые от первого запуска. Однако, полностью рассчитывать на это нельзя, первый запуск может произойти позже.
Хост использует планировщик Quartz.NET. Он поддерживает Cron, пул потоков, довольно гибок в настройке, масштабируется и тестировался под нагрузкой в Enterprise-решениях. Лицензия Apache 2.0.
Файл app.json
в каждой папке позволяет задать конфигурационные параметры плагинов.
При использовании API с расширениями требуется указать файл extensions.xml, в котором задан список сборок с расширениями (extensions.xml могут ссылаться друг на друга, как пример extensions.xml в папке Tessa типовой сборки ссылается на файл extensions.xml в папке extensions).
Пример файла extensions.xml
<?xml version="1.0" encoding="utf-8" ?>
<extensions>
<include file="Tessa.Extensions.Default.Imaging.dll" serverOnly="true" />
<include file="Tessa.Extensions.Default.Server.dll" serverOnly="true" />
<include file="Tessa.Extensions.Default.Shared.dll" />
<include file="Tessa.Extensions.Imaging.dll" serverOnly="true" />
<include file="Tessa.Extensions.Server.dll" serverOnly="true" />
<include file="Tessa.Extensions.Shared.dll" />
</extensions>
Дополнительные параметры командной строки¶
-
Справка по параметрам командной строки с указанием версии Chronos:
Chronos.exe --help
-
Запуск в режиме сервиса Windows (используется в командных файлах, не требуется вызывать вручную):
Chronos.exe --service
Класс плагина¶
Это класс, наследуемый от базового класса Plugin и имеющий атрибут [Plugin].
using Chronos.Plugins;
using Tessa.Platform;
namespace MyPluginNamespace
{
[Plugin]
public sealed class MyPlugin : Plugin
{
public override async Task EntryPointAsync(CancellationToken cancellationToken = default)
{
await using var companion = new UnityContainerCompanion { UseConfiguration = true };
await companion.ProcessAsync(
static (c, ct) => c.Container.RegisterServerForPluginAsync(cancellationToken: ct),
static async (c, ct) =>
{
// бизнес-логика плагина
},
cancellationToken);
}
}
}
С помощью свойств Name, Description и Version можно опционально указать имя, описание и версию плагина, которые будут использоваться хостом в информационных целях (для вывода в логи или в окно консоли).
[Plugin(Name = "My plugin #2", Description = "This plugin can do a lot of things.", Version = 3)]
public sealed class MyPlugin : Plugin
{
public override async Task EntryPointAsync(CancellationToken cancellationToken = default)
{
// код плагина
}
}
При использовании наследования атрибуты плагина не наследуются.
public abstract class MyPluginBase : Plugin
{
// этот класс не считается плагином, т.к. у него отсутствует атрибут [Plugin]
// если бы он присутствовал, то хост не смог бы его запустить, т.к. класс абстрактный
// если бы класс не был абстрактным, то дочерние классы не являлись бы автоматически плагинами
// и при наличии у них атрибута [Plugin] могли бы работать по совершенно другому расписанию
protected abstract void DoWork(string s);
public override async Task EntryPointAsync(CancellationToken cancellationToken = default)
{
// некоторая начальная инициализация
this.DoWork("some string");
}
}
[Plugin]
public sealed class MyPlugin1 : MyPluginBase
{
// этот плагин будет считаться самым обыкновенным
public override void DoWork(string s)
{
// код плагина
}
}
[Plugin]
public sealed class MyPlugin2 : MyPluginBase
{
// хост будет считать, что MyPlugin2 - это ещё один плагин и он никак не связан с MyPlugin1
public override void DoWork(string s)
{
// код плагина
}
}
Особенности плагина и его сборки¶
-
Сборка .DLL плагина:
-
Должна ссылаться на Chronos.Plugins.dll.
-
Может содержать несколько плагинов.
-
Должна располагаться в определённой папке хоста.
-
-
Плагин запускается хостом в отдельном процессе.
-
Плагин не содержит интерфейс обратного вызова хоста.
-
Плагин может почти что угодно: например, использовать API, предоставляемое Tessa. Оно позволяет:
-
Выполнить некоторую операцию над БД.
-
Учесть специальные блокировки (например, большинство таблиц справочника ролей не может одновременно изменяться несколькими плагинами). Блокировка реализуется через специальное поле Locked в некоторой таблице в БД.
-
Хост может завершить процесс плагина при своём завершении или перезапуске. Если плагин не успевает сделать какие-то ломающие изменения (например, в базе), то это его задача: восстановить систему в нормальное состояние при повторном запуске.
Желательно, чтобы всё взаимодействие legacy плагина с базой происходило в транзакциях.
Пример: если процесс установки блокировки, изменения БД и удаления блокировки прервать, то плагин должен начать транзакцию, сделать SELECT WITH ( HOLDLOCK )
или WITH ( XLOCK )
на отдельную таблицу с блокировками, наполнить 3 временные таблицы для INSERT
, UPDATE
, DELETE
, далее вызовом трёх команд модифицировать таблицу и завершить транзакцию.
Примечание: блокировка статического справочника ролей не имеет смысла.
Плагин может писать в лог при помощи NLog, но при этом будут использоваться правила логирования, определённые в NLog.config хоста.
Расписание запуска плагина¶
Плагин содержит метаданные, через которые он сообщает хосту, когда его запускать. Доступны следующие варианты настройки расписания запуска плагина.
Единовременный запуск плагина¶
Если не указать в аттрибуте [Plugin]
никаких настроек, определяющих логику вызова плагина, то он вызывается один раз при запуске хоста. Такой плагин может работать в вечном цикле, делать какую-то работу и периодически засыпать. Плагин также может использовать любой планировщик, например из библиотеки Quartz.NET. Однако, нельзя создавать хост плагинов внутри плагина.
[Plugin]
public sealed class OneTimePlugin : Plugin
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
public override async Task EntryPointAsync(CancellationToken cancellationToken = default)
{
// здесь начинается код плагина: он может содержать собственный механизм планирования
// для кода этого метода гарантируется, что он не будет вызван хостом одновременно в разных процессах
// при завершении хоста процесс, в котором выполняется плагин, принудительно завершается;
// поэтому не следует выполнять в плагине ломающие БД изменения, не заключив их в транзакцию
for (int i = 0; i < 10; i++)
{
logger.Trace("Plugin is running...");
await Task.Delay(1000, cancellationToken);
}
logger.Trace("Plugin has been stopped and will not run again until host will be restarted.");
}
}
Запуск плагина с заданным интервалом¶
С помощью свойства RepeatSecondInterval
можно задать интервал в секундах, через который плагин будет запускаться. Например, указанный плагин вызывается каждые 10 минут.
[Plugin(RepeatSecondInterval = 10 * 60, DisallowConcurrency = true)]
public sealed class PeriodicPlugin : Plugin
{
public override async Task EntryPointAsync(CancellationToken cancellationToken = default)
{
// для кода этого метода гарантируется, что он не будет вызван хостом одновременно в разных процессах,
// только если установлено свойство DisallowConcurrency = true атрибута [Plugin]
// иначе плагин может выполняться слишком долго и за прошедшее время будет вызван ещё один процесс плагина
// кроме того, ОС может перевести системные часы (например, при автоматической синхронизации времени),
// и плагин тут же будет вызван повторно, что приведёт к побочным эффектам,
// если плагин использует некоторый внешний ресурс (например, таблицу в БД)
}
}
Запуск плагина по выражению Cron¶
С помощью свойства Cron
плагину можно задать выражение Cron, используемое планировщиком Quartz для определения интервала запуска плагина. Например, запускать плагин каждую среду в 12:00 и каждую пятницу в 0:00. Если условие вызова нельзя построить одним выражением Cron, то дополнительные выражения могут задаваться с помощью атрибутов [PluginTrigger]
.
[Plugin(Cron = "0 0 12 ? * WED", DisallowConcurrency = true)]
[PluginTrigger(Cron = "0 0 0 ? * FRI")]
public sealed class CronPlugin : Plugin
{
public override async Task EntryPointAsync(CancellationToken cancellationToken = default)
{
// код плагина
}
}
Загрузка расписания плагина из конфигурационного файла xml¶
С помощью свойства ConfigFile
можно задать относительный путь к файлу, из которого будут загружаться настройки плагина. В данном примере файл pluginConfig.xml должен располагаться в одной папке со сборкой плагина.
[Plugin(ConfigFile = "pluginConfig.xml"))]
public sealed class ConfigFilePlugin : Plugin
{
public override async Task EntryPointAsync(CancellationToken cancellationToken = default)
{
// код плагина
}
}
Пример содержимого файла pluginConfig.xml:
<?xml version="1.0" encoding="UTF-8"?>
<plugin name="Plugin from config"
description="Info for this plugin is loaded from config file."
version="5"
disallowConcurrency="true"
disabled="true">
<!-- disabled="true" определяет, что плагин не будет использоваться (можно временно его отключить через конфиг) -->
<!-- Запускать каждую среду в 12:00 -->
<trigger cron="0 0 12 ? * WED" />
<!--
Дополнительно запускать с интервалом в 36 часов.
Поскольку начало отсчёта не определено, лучше вместо этого использовать выражение Cron.
-->
<trigger repeatSeconds="129600" />
</plugin>
Пример файла pluginConfig.xml, не содержащего информацию о плагине (при отсутствии другой информации из атрибутов Plugin и PluginTrigger плагин будет вызван один раз, как будто бы конфигурационного файла не было):
<?xml version="1.0" encoding="UTF-8"?>
<plugin />
Загрузка расписания плагина из конфигурационного файла app.json¶
Tip
Рекомендуется использовать данный вариант для настройки периодического запуска плагина.
С помощью свойства JsonName
можно задать имя плагина, настройки которого загружаются из конфигурационного файла app.json
по пути Settings->Plugins
. Данное свойство также может использоваться и для загрузки настроек отдельных триггеров плагина из конфигурационного файла app.json
пу пути Settings->Triggers
.
Пример кода плагина с указанием загрузки настроек из app.json
:
[Plugin(JsonName = "JsonNamePlugin"))]
[PluginTrigger(JsonName = "JsonNamePluginTrigger"))]
public sealed class JsonNamePlugin : Plugin
{
public override async Task EntryPointAsync(CancellationToken cancellationToken = default)
{
// код плагина
}
}
Настройки для плагинов в app.json
состоят из следующих групп:
-
Настройки триггеров плагинов. Данные настройки располагаются по пути
Settings->Triggers
. Здесь указываются именованные триггеры, которые можно использовать в настройках плагина/группы плагинов в свойствеTrigger
или в свойствеJsonName
для атрибута[PluginTrigger]
.Значение триггера можно задать одним из перечисленных ниже способов:
- указать в качестве значения число, определяющее интервал в секундах;
- указать в качестве значения строку, содержащую
cron
-выражение; - указать в качестве значения массив значений, в котором каждое значение должно быть или числом, или строкой, содержащей
cron
-выражение.
Пример содержимого файла
app.json
с настройками триггеров:{ "Settings": { "Triggers": { "IntervalTrigger": 60, // Триггер для запуска плагина с интервалом в 60 секунд
"CronTrigger": "0 0 0 * * ?", // Триггер для запуска плагина каждый день в 00:00
// Триггер, состояющий из двух простых триггеров "ComplexTrigger": [
// Запускать каждую среду в 12:00 "0 0 12 ? * WED"",
// Дополнительно запускать с интервалом в 36 часов. // Поскольку начало отсчёта не определено, лучше вместо этого использовать выражение Cron. 129600 ],
} } }
-
Настройки групп плагинов. Данные настройки раполагаются по пути
Settings->Groups
. Группа плагинов определяет настройки всех плагинов, для которых указана данная группа в настройках.Значение триггера для группы плагинов можно задать одним из перечисленных ниже способов:
- указать в качестве значения число, определяющее интервал в секундах;
- указать в качестве значения строку, содержащую
cron
-выражение; - указать в качестве значения строку, содержащую имя триггера из именованных триггеров;
- указать в качестве значения массив значений, в котором каждое значение должно быть или числом, или строкой, содержащей
cron
-выражение.
Пример содержимого файла
app.json
с настройками группы плагинов:{ "Settings": { "Groups": { "SimplePluginGroup": { "Enabled": true, "LaunchImmediately": false, "DisallowConcurrency": true, "Trigger": 30, "SequentialExecution": true } } } }
-
Настройки плагинов. Данные настройки располагаются по пути
Settings->Plugins
. Здесь указываются настройки конкретных плагинов, которые можно использовать в свойствеJsonName
для атрибута[Plugin]
. Настройки плагина можно задать напрямую или же указать группу плагинов, которая будет использоваться для определения настроек.Note
Если настройки
Enabled
,LaunchImmediately
иDisallowConcurrency
заданы и в настройках самого плагина и в настройках указанной группы плагинов, то будут применяться настройки, указанные в настройках плагина. Если настройкаTrigger
задана и в настройках самого плагина и в настройках указанной группы плагинов, то будет применяться настройка, указанная в настройках группы.Значение триггера для плагина можно задать одним из перечисленных ниже способов:
- указать в качестве значения число, определяющее интервал в секундах;
- указать в качестве значения строку, содержащую
cron
-выражение; - указать в качестве значения строку, содержащую имя триггера из именованных триггеров;
- указать в качестве значения массив значений, в котором каждое значение должно быть или числом, или строкой, содержащей
cron
-выражение.
Пример содержимого файла
app.json
с настройками плагина:{ "Settings": { "Plugins": { "SimpleJsonNamePlugin": { "Description": "Info for this plugin is loaded from json file.", "Version": 5, "Enabled": true, "LaunchImmediately": false, "DisallowConcurrency": true, "Trigger": 30 },
// Триггер плагина определяется из настроек триггеров "TriggerJsonNamePlugin": { "Description": "Trigger for this plugin is loaded from Triggers.", "Version": 5, "Enabled": true, "LaunchImmediately": false, "DisallowConcurrency": true, "Trigger": "CronTrigger" },
// Настройки плагина определяются по настройкам группы плагинов "GroupJsonNamePlugin": { "Description": "Info for this plugin is loaded from group.", "Version": 3, "Group": "SimplePluginGroup" } } } }
Загрузка расписания плагина с помощью кастомного обработчика¶
С помощью свойства ConfigResolver
можно указать тип кастомного обработчика, реализующего интерфейс IPluginConfigResolver
. Это позволяет сформировать триггеры плагина полностью из кода.
Данный способ также позволяет определить для запускаемого плагина дополнительный параметр в виде строки. Этот параметр передаётся в конструктор плагина в качестве параметра с типом string
.
Пример кода плагина с дополнительным параметром, получаемым через конструктор:
[Plugin(ConfigResolver = typeof(CustomConfigResolver)))]
public sealed class ConfigResolverPlugin : Plugin
{
private readonly string pluginParameter;
public ConfigResolverPlugin(string pluginParameter)
{
// Сохраняем значение параметра плагина, чтобы использовать при вызове плагина
this.pluginParameter = pluginParameter;
}
public override async Task EntryPointAsync(CancellationToken cancellationToken = default)
{
// код плагина
}
}
Пример класса для получения настроек плагина:
public sealed class CustomConfigResolver : IPluginConfigResolver
{
public ValueTask<IEnumerable<IPluginInstanceMetadata>?> TryResolveConfigsAsync(
IPluginInstanceMetadata item,
CancellationToken cancellationToken = default)
{
// Определяем настройки для двух экземпляров одного и того же плагина:
// Первый запускается раз в день в 00:00 с параметром "Daily"
// Второй запускается каждое воскресенье в 00:00 с параметром "Weekly"
var instances = new IPluginInstanceMetadata[]
{
new PluginInstanceMetadata(
new PluginMetadata
{
Name = "CustomConfigPlugin with parameter Daily",
Disabled = false,
DisallowConcurrency = true,
LaunchImmediately = false,
Cron = "0 0 0 * * ?"
},
pluginParameter: "Daily"),
new PluginInstanceMetadata(
new PluginMetadata
{
Name = "CustomConfigPlugin with parameter Weekly",
Disabled = false,
DisallowConcurrency = true,
LaunchImmediately = false,
Cron = "0 0 0 ? * SUN"
},
pluginParameter: "Weekly"),
};
return new ValueTask<IEnumerable<IPluginInstanceMetadata>?>(instances);
}
}
Important
В каждом из атрибутов [Plugin]
и [PluginTrigger]
можно указывать не более одного из свойств RepeatSecondPeriod
, Cron
, ConfigFile
, JsonName
, ConfigResolver
. Все другие комбинации атрибутов и их свойств допустимы. Например, частично информацию о плагине можно задать через атрибуты, а другую часть получить из одного или нескольких конфигурационных файлов.
Note
Описание синтаксиса CRON, который используется в расписании плагинов, а также в динамических ролях и генераторах метаролей, можно найти здесь: http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html
Примеры плагинов¶
-
Пересчёт таблиц в справочнике для динамических ролей и генератора ролей. Плагин вызывается один раз, хостит планировщик и сам вызывает различные задания по пересчёту ролей с различной периодичностью.
-
Пересчёт таблиц замещений в статических ролях. Плагин вызывается периодически.
-
ADSync: синхронизация сотрудников и ролей с Active Directory.
-
Плагин вызывается периодически по выражениям Cron (для синхронизации по требованию должен напрямую вызываться соответствующий метод API).
-
Каждый раз работа плагина по синхронизации может выполняться очень долго и может упасть, не закончив синхронизацию.
-
Пример класса плагина:
using Chronos.Plugins;
[Plugin]
class MyPlugin : Plugin
{
public override async Task EntryPointAsync(CancellationToken cancellationToken = default)
{
while (!this.StopRequested)
{
/* do work */
await Task.Delay(1000, cancellationToken);
}
}
public override async Task StopAsync(IPluginStopToken token)
{
// это поведение по умолчанию в базовом классе, поэтому переопределять
// таким образом метод не требуется. но можно его дополнить
this.StopRequested = true;
return await token.WaitUntilEntryPointFinishedAsync(token.CancellationTokenSource.Token);
}
}
-
StopAsync - метод, вызываемый хостом внутри процесса плагина при вежливой остановке плагина.
-
StopRequested - флаг, устанавливаемый в методе
StopAsync
, его может периодически проверятьEntryPointAsync
. -
Параметр
IPluginStopToken token
- токен, позволяющий определить состояние плагина из метода его вежливой остановки. Его методWaitUntilEntryPointFinishedAsync
дожидается завершения работы методаEntryPointAsync
плагина. СвойствоEntryPointFinished
позволяет проверить, завершён ли уже методEntryPoint
. СвойствоCancellationTokenSource
содержит объект, позволяющий отменить токенcancellationToken
, передаваемый в методEntryPointAsync
. Токен будет отменён автоматически по настройке"Chronos.PluginCancellationDelta"
вapp.json
за несколько секунд до того, как процесс плагина будет закрыт хост-процессом по таймауту, поэтому вызыватьCancellationTokenSource.Cancel()
необязательно.
Метод StopAsync
должен максимально быстро завершить выполнение плагина, но не завершать свою работу до тех пор, пока потоки, с которыми работает плагин, не будут завершены.
Хост вызовет метод StopAsync
в потоке, отличном от потока, в котором выполняется метод EntryPointAsync
. Если метод StopAsync
неожиданно прервётся, то исключение будет добавлено в лог.
Вежливая остановка может происходить при остановке хоста, запущенного как сервис Windows или Linux, или при вводе команды остановки stop в хосте, запущенном в консоли. При этом все работающие плагины имеют некоторое время, определяемое настройкой "Chronos.HostStopTimeout"
в app.json
Chronos (по умолчанию 30 секунд), для того, чтобы корректно завершить свою работу. Вежливой остановки не производится при закрытии окна консоли или при завершении процесса хоста через диспетчер.
Сборка плагинов для проектного решения¶
В расширениях проектного решения (папка Source в сборке) присутствует проект Tessa.Extensions.Chronos
, в котором рекомендуется располагать любые плагины (фоновые сервиса), используемые в вашем проекте.
Для того, чтобы собрать и запустить проект с плагинами в процессе Chronos, выполните следующие действия:
-
Убедитесь, что сервис Chronos остановлен.
-
Убедитесь, что в файле
app-extension-plugins.json
, расположенном в папкеExtensions\Tessa.Extensions.Chronos
, требуемые плагины включены, т.е. у них указано"Enabled": true
. В плагине-примереExamplePlugin
по умолчанию указано"Enabled": false
, замените его на"Enabled": true
. -
Соберите проект
Tessa.Extensions.Chronos
в Visual Studio. -
После сборки появится папка
Bin\Tessa.Extensions.Chronos
(относительно папки с файлом .sln), скопируйте её содержимое с заменой вPlugins\Tessa.Extensions.Chronos
. -
Запустите сервис Chronos в окне консоли. Названия ваших плагинов в сборке
Tessa.Extensions.Chronos
должны быть выведены на экране.
По умолчанию это ExamplePlugin
, который также выполняет запись в log.txt на уровне Trace
. Вы можете включить этот уровень логирования в NLog.config в папке с Chronos, заменив строку: <logger name="*" minlevel="Trace" writeTo="file" />
В дальнейшем для обновления плагинов достаточно повторить все выше описанные шаги.
Копирование можно автоматизировать при сборке, записав скрипт копирования в Extensions\Tessa.Extensions.Chronos\post-build.bat
.
Инструкция также доступна в файле readme.txt в папке проекта Tessa.Extensions.Chronos
.
Разработка плагинов, обрабатываемых веб-сервисом¶
Система поддерживает запуск плагинов на веб-сервисе (см. Настройка обработки фоновых операций).
Основное отличие от плагинов, создаваемых для сервиса Chronos в том, что плагин работает в том же контейнере зависимостей, что и сам веб-сервис. Поэтому использование классов-наследников Plugin
в том же виде, что и для сервиса Chronos, невозможно.
Для написания плагина, который будет выполняться на веб-сервисе, его нужно реализовать в виде класса, реализующего интерфейс IPluginHandler
, в сборке с серверными расширениями (например, Tessa.Extensions.Server
).
Интерфейс IPluginHandler
имеет следующие методы:
-
ResolveSettings
- метод для резолва настроек плагина. Данный метод получает информацию о плагине из конфигурационного файлаapp.json
и превращает их в объект с настройками. Если дополнительных настроек для плагина не предполагается, то достаточно возвращать объектPluginSettings
. Система поддерживает возможность создания своего класса с настройками. Рекомендуется наследовать класс с настройками плагина от классаPluginSettings
. -
ExecuteAsync
- метод для запуска плагина. Данный метод получает контекст выполнения плагина, который содержит настройки плагина.
Пример класса обработчика плагина:
using Tessa.Platform.Plugins;
namespace MyPluginHandlerNamespace
{
class MyPluginHandler : IPluginHandler
{
// Имя плагина. Можно вынести в отдельный класс с именами плагинов
public const string PluginName = "MyPlugin";
public MyPluginHandler()
{
// получение зависимостей из контейнера зависимостей
}
public IPluginSettings ResolveSettings(Dictionary<string, object?>? info = null)
{
// Пример создания объекта с настройками по умолчанию
var settings = new PluginSettings(PluginName);
if (info is not null)
{
settings.Deserialize(info);
}
return settings;
}
public override async ValueTask ExecuteAsync(IPluginExecutingContext context)
{
// код плагина
}
}
}
Созданный класс нужно зарегистрировать в IPluginHandlerResolver
по имени плагина. По этому имени плагина система будет заагружать настройки плагина из конфигурационного файла app.json
.
Пример регистратора обработчика плагина:
using Tessa.Platform;
using Tessa.Platform.Plugins;
using Unity;
namespace MyPluginHandlerNamespace
{
[Registrator]
public sealed class Registrator : RegistratorBase
{
public override void RegisterUnity()
{
this.UnityContainer
.RegisterSingleton<MyPluginHandler>()
;
}
public override void FinalizeRegistration()
{
this.UnityContainer
.TryResolve<IPluginHandlerResolver>()?
.Register<MyPluginHandler>(MyPluginHandler.PluginName)
;
}
}
}
Для того, чтобы запустить описанный выше плагин на сервисе Chronos, достаточно сделать следующий класс плагина:
using Chronos.Plugins;
using Tessa.Platform;
using Tessa.Platform.Plugins;
using MyPluginHandlerNamespace;
namespace MyPluginNamespace
{
[Plugin(JsonName = MyPluginHandler.PluginName]
public sealed class MyPlugin : Plugin
{
public override async Task EntryPointAsync(CancellationToken cancellationToken = default)
{
await using var companion = new UnityContainerCompanion { UseConfiguration = true };
await companion.ProcessAsync(
static (c, ct) => c.Container.RegisterServerForPluginAsync(cancellationToken: ct),
static async (c, ct) =>
{
// Получаем из контейнера объекты для резолва настроек плагина и резолва обработчика плагина
var pluginSettingsProvider = c.Container.Resolve<IPluginSettingsProvider>();
var pluginHandlerResolver = c.Container.Resolve<IPluginHandlerResolver>();
// Пытаемся получить обработчик плагина из зависимостей контейнера.
if (pluginHandlerResolver.TryResolve(MyPluginHandler.PluginName) is not { } handler)
{
return;
}
// Загружаем настройки плагина из app.json или получаем настройки по умолчанию из обработчика,
// если в app.json не заданы настройки нашего плагина.
var settings = pluginSettingsProvider.TryGetPluginSettings(MyPluginHandler.PluginName)
?? handler.ResolveSettings();
var context = new PluginExecutingContext(settings)
{
StopRequestedToken = this.StopRequestedToken,
CancellationToken = ct
};
await handler.ExecuteAsync(context);
},
cancellationToken);
}
}
}
Использование конфигурации из файла app.json¶
Chronos позволяет хранить настройки для плагинов в файле app.json
.
Например, нам необходимы несколько настроек для плагина TestPlugin
: TestString
- строковое значение, TestInt
- числовое значение.
Для этого необходимо добавить в блок Settings
файла app.json
:
Warning
Внимательно следите за синтаксисом! Json требует экранирования ‘': вместо ‘' необходимо указывать ‘\‘. Так же требуются запятые при перечислении элементов настроек, если синтаксис будет некорректным, то это приведет к ошибке при запуске любых плагинов Chronos.
{
"Settings": {
"TestPlugin.TestString": "test",
"TestPlugin.TestInt": 1
}
}
Получить эти настройки изнутри плагина можно так:
logger.Info("TestString = '{0}'", ConfigurationManager.Settings.TryGet("TestPlugin.TestString", string.Empty));
logger.Info("TestInt = '{0}'", ConfigurationManager.Settings.TryGet("TestPlugin.TestInt", 0));
Параметры логирования NLog для плагина¶
Note
Информация из этого раздела актуальна только для Chronos версии 1.3 или более поздней.
Если плагину требуются отличные от хоста параметры логирования, и он использует NLog, то определить уникальные для сборки с плагином параметры можно в её файле NLog.config, который следует расположить в одной папке со сборкой плагина. В качестве {basedir} будет использоваться папка с Chronos.exe, а не папка, в которой лежит NLog.config.
Пример файла NLog.config с конфигурацией NLog:
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<targets async="true">
<target name="file" xsi:type="File" encoding="utf-8" writeBom="true" fileName="${basedir}\Plugins\MyPlugins\log.txt" />
<target name="queries" xsi:type="File" encoding="utf-8" writeBom="true" fileName="${basedir}/queries.txt" layout="--${longdate}${newline}${message}${newline}GO${newline}" />
<target name="process" xsi:type="File" encoding="utf-8" writeBom="true" fileName="${basedir}/process.txt" layout="${longdate}${newline}${message}${newline}" />
<target name="null" xsi:type="Null" formatMessage="false" />
</targets>
<rules>
<logger name="SqlQueries" minlevel="Error" writeTo="queries" final="true" />
<logger name="SqlQueries" minlevel="Trace" writeTo="null" final="true" />
<logger name="Chronos.Process" minlevel="Error" writeTo="process" final="true" />
<logger name="Chronos.Process" minlevel="Trace" writeTo="null" final="true" />
<logger name="Configuration" minlevel="Info" writeTo="file" final="true" />
<logger name="Configuration" minlevel="Trace" writeTo="null" final="true" />
<logger name="Quartz.*" minlevel="Error" writeTo="file" final="true" />
<logger name="Quartz.*" minlevel="Trace" writeTo="null" final="true" />
<logger name="*" minlevel="Info" writeTo="file" />
</rules>
</nlog>