Перейти к содержанию

Создание консольных команд tadmin

Создание консольных команд tadmin

Начиная со сборки 3.4.0 в системе добавлено открытое API для создания собственных консольных команд для утилиты tadmin. Исходный код всех типовых консольных команд доступен для модификации в проектных расширениях Tessa.Extensions.Default.Console. В проекте Tessa.Extensions.Console рекомендуется добавлять новые консольные команды, которые можно использовать в рамках проекта в любых целях интеграции и автоматизации взаимодействия с системой.

Команды, которые создаются в рамках API, разделяют на серверные и клиентские:

  1. Серверные команды не используют контейнер Unity и метаинформацию от веб-сервиса. Им доступно прямое соединение с базой данных в соответствии со строками подключения в app.json. Также они могут выполнять любые другие действия, связанные с файлами и другими локальными ресурсами, например, генерировать ключ токена безопасности (команда GetKey) или упаковывать папку с клиентским приложением в файл карточки .json для возможности последующего импорта карточки на сервер (команда PackageApp).

  2. Клиентские команды используют контейнер Unity с клиентскими зависимости (теми из них, которые не связаны с UI). Такие команды в первую очередь выполняют логин к веб-сервису с заданными параметрами подключения, получают метаинформацию с сервера (опционально), и далее работают по созданной сессии, вызывая методы веб-сервиса. Перед выходом вызывается закрытие сессии. Например, это команда удаления карточек DeleteCard или импорта локализации ImportLocalization. Клиентским командам доступны все те же возможности, что и серверным, т.е. они могут как подключаться к веб-сервису, так и взаимодействовать с базой данных напрямую, однако, это приведёт к большому количеству аргументов одной и той же команды для подключения как к веб-сервису, так и к базе данных, что несколько усложнит её использование.

Серверная команда для прямого взаимодействия с БД

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

Откройте решение с проектными расширениями Tessa.Extensions.sln в Visual Studio или другой подходящей IDE, раскройте проект Tessa.Extensions.Console и добавьте туда папку SearchUser. В этой папке будут расположены все необходимые классы для регистрации команды и описания её бизнес-логики.

Создайте класс Operation. В нём описываются действия, выполняемые командой. Метод ExecuteAsync получает управление сразу после проверки корректности переданных аргументов, но при этом соединение с базой данных или другие действия не выполняются автоматически. Соединение с базой данных открывается посредством метода DefaultConsoleHelper.CreateDbManagerAsync, код этого метода открыт в проекте расширений Tessa.Extensions.Default.Console.

Метод возвращает целое число - код возврата из консольной утилиты, которые можно будет проверить в командных файлах (if errorlevel для Windows, $? в bash для Linux). Всегда возвращайте число 0, если всё успешно, или различные отрицательные числа в случае разных ошибок.

using System; using System.Threading.Tasks; using Tessa.Extensions.Default.Console; using Tessa.Platform.ConsoleApps; using Tessa.Platform.Data;

namespace Tessa.Extensions.Console.SearchUser { public static class Operation { public static async Task<int> ExecuteAsync( IConsoleLogger logger, string userLogin, string configurationString, string databaseName) { using (DbManager db = await DefaultConsoleHelper.CreateDbManagerAsync( logger, configurationString, databaseName)) { IQueryBuilderFactory builderFactory = new QueryBuilderFactory(db.GetDbms());

Guid? userID = await db .SetCommand( builderFactory .Select().C("ID") .From("PersonalRoles") .Where().LowerC("Login").Equals().LowerP("Login") .Build(), db.Parameter("Login", userLogin)) .LogCommand() .ExecuteAsync<Guid?>();

if (!userID.HasValue) { logger.Error("Can't find user by login: \"{0}\"'", userLogin); return -1; }

logger.Write(userID.Value.ToString("D")); }

return 0; } } }

Создайте класс Command, в котором указывается название консольной команды [Verb], её аргументы [Argument], текстовое описание команды или аргументов [Description] для вывода в окне справки --help или локализованное описание [LocalizableDescription] (можно использовать только строки локализации, присутствующие в типовой поставке платформы, что применимо в основном для некоторых типовых аргументов).

Здесь мы указываем логин пользователя userLogin, по которому будет выполняться поиск идентификатора. Остальные аргументы являются типовыми и используются при подключении к базе данных.

using System.ComponentModel; using System.IO; using System.Threading.Tasks; using NLog; using Tessa.Localization; using Tessa.Platform; using Tessa.Platform.CommandLine; using Tessa.Platform.ConsoleApps;

namespace Tessa.Extensions.Console.SearchUser { public static class Command { [Verb("SearchUser")] [Description("Write user id to standard output searching by login")] public static async Task SearchUser( [Output] TextWriter stdOut, [Error] TextWriter stdErr, [Argument, Description("User login to search.")] string userLogin, [Argument("cs"), LocalizableDescription("Common_CLI_ConfigurationString")] string configurationString = null, [Argument("db"), LocalizableDescription("Common_CLI_DatabaseName")] string databaseName = null, [Argument("q"), LocalizableDescription("Common_CLI_Quiet")] bool quiet = false, [Argument("nologo")] [LocalizableDescription("CLI_NoLogo")] bool nologo = false) { Check.ArgumentNotNullOrEmpty(userLogin, nameof(userLogin));

if (!nologo && !quiet) { ConsoleAppHelper.WriteLogo(stdOut); }

IConsoleLogger logger = new ConsoleLogger( LogManager.GetLogger(nameof(SearchUser)), stdOut, stdErr, quiet);

int result = await Operation.ExecuteAsync( logger, userLogin, configurationString, databaseName);

ConsoleAppHelper.EnvironmentExit(result); } } }

Создайте класс CommandRegistrator для регистрации метода команды, приведённого в предыдущем классе. Обратите внимание, что требуется указать типы всех параметров метода Command.SearchUser в порядке их следования.

using System.IO; using Tessa.Platform.CommandLine; using Tessa.Platform.ConsoleApps;

namespace Tessa.Extensions.Console.SearchUser { [ConsoleRegistrator] public sealed class CommandRegistrator : ConsoleRegistratorBase { public override void RegisterCommands() { this.CommandContext .AddCommand<TextWriter, TextWriter, string, string, string, bool, bool>(Command.SearchUser) ; } } }

Соберите проект расширений Tessa.Extensions.Console. В выходной папке проекта bin\Release\netcoreapp3.1 (для конфигурации Release) будет доступна собранная сборка с расширением Tessa.Extensions.Console.dll, которую скопируйте с заменой в подпапку extensions в папке с консольной утилитой tadmin (в папке со сборкой это папки Tools для Windows и linux/tools для Linux).

Теперь вы можете выполнить следующие команды, чтобы убедиться, что для консольной команды SearchUser доступна справка, и при поиске сотрудника с логином admin выводится его идентификатор (параметр -q позволяет отключить информационные и служебные сообщения).

tadmin SearchUser --help

tadmin SearchUser admin -q

Клиентская команда для подключения к веб-сервису

Рассмотрим создание команды с именем SelectFromView, которая будет подключаться к веб-сервису с указанными параметрами, выполнять представление с заданным алиасом, а результаты представления вместе с заголовками колонок будут выводится на окно консоли (стандартный вывод, который также можно перенаправить в файл или в стандартный ввод другой команды).

Откройте решение с проектными расширениями Tessa.Extensions.sln в Visual Studio или другой подходящей IDE, раскройте проект Tessa.Extensions.Console и добавьте туда папку SelectFromView. В этой папке будут расположены все необходимые классы для регистрации команды и описания её бизнес-логики.

Создайте класс OperationContext, который будет содержать параметры настройки этой команды, не связанные с подключением к веб-сервису. Здесь это ViewAlias - алиас выполняемого представления, и PageLimit - количество отображаемых строк с первой страницы представления, если представление поддерживает пейджинг.

namespace Tessa.Extensions.Console.SelectFromView { public class OperationContext { public string ViewAlias { get; set; }

public int PageLimit { get; set; } } }

Создайте класс Operation. В нём описываются действия, выполняемые командой. Через конструктор можно получить любые зависимости из клиентского Unity-контейнера, в т.ч. IViewService для взаимодействия с представлениями и ISession для описания открытой сессии текущего пользователя.

Метод ExecuteAsync получает управление уже после того, как был успешно выполнен логин к веб-сервису, и создана сессия. Сессия будет закрыта после выхода из метода.

Метод возвращает целое число - код возврата из консольной утилиты, которые можно будет проверить в командных файлах (if errorlevel для Windows, $? в bash для Linux). Всегда возвращайте число 0, если всё успешно, или различные отрицательные числа в случае разных ошибок.

using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Tessa.Platform; using Tessa.Platform.Collections; using Tessa.Platform.ConsoleApps; using Tessa.Platform.Runtime; using Tessa.Views; using Tessa.Views.Metadata;

namespace Tessa.Extensions.Console.SelectFromView { public sealed class Operation : ConsoleOperation<OperationContext> { public Operation( IViewService viewService, ISession session, IConsoleLogger logger, ConsoleSessionManager sessionManager) : base(logger, sessionManager, extendedInitialization: true) { // параметр extendedInitialization определяет, будет ли выполнены получение метаинформации от сервера // и её инициализация на клиенте; здесь мы используем метаинформацию по представлению this.viewService = viewService; this.session = session; }

private readonly IViewService viewService;

private readonly ISession session;

public override async Task<int> ExecuteAsync( OperationContext context, CancellationToken cancellationToken = default) { if (!this.SessionManager.IsOpened) { return -1; }

this.Logger.Info("Getting metadata for view \"{0}\"", context.ViewAlias);

ITessaView view = this.viewService.GetByName(context.ViewAlias); if (view is null) { this.Logger.Error("Can't acquire metadata for view \"{0}\"", context.ViewAlias); return -1; }

IViewMetadata viewMetadata = view.Metadata; ITessaViewRequest viewRequest = new TessaViewRequest(viewMetadata) { CalculateRowCounting = false };

var viewSpecialParameters = new ViewSpecialParameters( new ViewCurrentUserParameters(this.session), new ViewPagingParameters(), new ViewCardParameters());

var parameters = new List<RequestParameter>(); viewSpecialParameters.ProvideCurrentUserIdParameter(parameters);

if (viewMetadata.Paging != Paging.No && context.PageLimit > 0) { viewSpecialParameters.ProvidePageLimitParameter( parameters, Paging.Always, context.PageLimit, false);

viewSpecialParameters.ProvidePageOffsetParameter( parameters, Paging.Always, 1, context.PageLimit, false); }

viewRequest.Values = parameters;

// получаем данные представления ITessaViewResult viewResult = await view.GetDataAsync(viewRequest, cancellationToken);

string[] columns = (viewResult.Columns ?? EmptyHolder<string>.Array).Cast<string>().ToArray(); this.Logger.WriteLine(string.Join('\t', columns));

IList<object> viewResultRows = viewResult.Rows; if (viewResultRows != null) { foreach (IList<object> row in viewResultRows) { this.Logger.WriteLine( string.Join('\t', row.Select(x => FormattingHelper.FormatToString(x, true)))); } }

return 0; } } }

Создайте класс Command, в котором указывается название консольной команды [Verb], её аргументы [Argument], текстовое описание команды или аргументов [Description] для вывода в окне справки --help или локализованное описание [LocalizableDescription] (можно использовать только строки локализации, присутствующие в типовой поставке платформы, что применимо в основном для некоторых типовых аргументов).

Здесь мы указываем аргумент без имени viewAlias с алиасом выполняемого представления и аргумент -page:XX с ограничением на количество строк, выводимых на первой странице представления, если оно поддерживает пейджинг. Остальные аргументы являются типовыми и используются при подключении к веб-сервису.

using System.ComponentModel; using System.IO; using System.Threading.Tasks; using Tessa.Localization; using Tessa.Platform; using Tessa.Platform.CommandLine; using Tessa.Platform.ConsoleApps; using Unity;

namespace Tessa.Extensions.Console.SelectFromView { public static class Command { [Verb("SelectFromView")] [Description("Write data from view to standard output.")] public static async Task SelectFromView( [Output] TextWriter stdOut, [Error] TextWriter stdErr, [Argument] [Description("View alias to select from.")] string viewAlias, [Argument("a")] [LocalizableDescription("Common_CLI_Address")] string address = null, [Argument("i")] [LocalizableDescription("Common_CLI_Instance")] string instanceName = null, [Argument("u")] [LocalizableDescription("Common_CLI_UserName")] string userName = null, [Argument("p")] [LocalizableDescription("Common_CLI_Password")] string password = null, [Argument("page")] [Description("Page limit for the first page if view allows paging.")] int pageLimit = 20, [Argument("q"), LocalizableDescription("Common_CLI_Quiet")] bool quiet = false, [Argument("nologo")] [LocalizableDescription("CLI_NoLogo")] bool nologo = false) { Check.ArgumentNotNullOrEmpty(viewAlias, nameof(viewAlias));

if (!nologo && !quiet) { ConsoleAppHelper.WriteLogo(stdOut); }

IUnityContainer container = new UnityContainer() .ConfigureConsoleForClient(stdOut, stdErr, quiet, instanceName, address);

int result; using (var operation = container.Resolve<Operation>()) { var context = new OperationContext { ViewAlias = viewAlias, PageLimit = pageLimit, };

if (!await operation.LoginAsync(userName, password)) { ConsoleAppHelper.EnvironmentExit(ConsoleAppHelper.FailedLoginExitCode); }

result = await operation.ExecuteAsync(context); await operation.CloseAsync(); }

ConsoleAppHelper.EnvironmentExit(result); } } }

Создайте класс CommandRegistrator для регистрации метода команды, приведённого в предыдущем классе. Обратите внимание, что требуется указать типы всех параметров метода Command.SelectFromView в порядке их следования.

using System.IO; using Tessa.Platform.CommandLine; using Tessa.Platform.ConsoleApps;

namespace Tessa.Extensions.Console.SelectFromView { [ConsoleRegistrator] public sealed class CommandRegistrator : ConsoleRegistratorBase { public override void RegisterCommands() { this.CommandContext .AddCommand<TextWriter, TextWriter, string, string, string, string, string, int, bool, bool>( Command.SelectFromView) ; } } }

Соберите проект расширений Tessa.Extensions.Console. В выходной папке проекта bin\Release\netcoreapp3.1 (для конфигурации Release) будет доступна собранная сборка с расширением Tessa.Extensions.Console.dll, которую скопируйте с заменой в подпапку extensions в папке с консольной утилитой tadmin (в папке со сборкой это папки Tools для Windows и linux/tools для Linux).

Теперь вы можете выполнить следующие команды, чтобы убедиться, что для консольной команды SelectFromView доступна справка, и при выполнении представления с алиасом KrDocStates выводятся заголовки колонок и строки представления, в данном случае это список состояний документов (параметр -q позволяет отключить информационные и служебные сообщения). Другой пример для представления с алиасом MyTasks отображает только первые две строки, выводимые представлением, за счёт использования аргумента -page:2.

tadmin SelectFromView --help

tadmin SelectFromView KrDocStates -a:https://localhost/tessa -u:admin -p:admin -q

tadmin SelectFromView MyTasks -page:2 -a:https://localhost/tessa -u:admin -p:admin -q

За примерами исходных кодов для других консольных команд обратитесь к проекту расширений Tessa.Extensions.Default.Console.

Команда tadmin для вычисления хеш-суммы файла

Ниже приведён пример простой команды, не выполняющей соединения с базой данных или веб-сервисов, а выполняющей вычисление хеш-суммы файла средствами API TESSA. Хеш-сумма вычисляется по тому же алгоритму, по которому платформа вычисляет её для файлов по умолчанию (свойство HashSignatureProvider.Files), в текущей версии системы это алгоритм SHA256.

Добавьте папку с именем GetFileHash в проект Tessa.Extensions.Console, папка будет содержать все файлы для одноимённой команды.

Создайте класс Operation, содержащий действия, выполняемые при вызове команды.

using System; using System.Buffers; using System.Diagnostics; using System.IO; using System.Security.Cryptography; using System.Threading.Tasks; using Tessa.Platform; using Tessa.Platform.ConsoleApps; using Tessa.Platform.IO;

namespace Tessa.Extensions.Console.GetFileHash { public static class Operation { public static async Task<int> ExecuteAsync( IConsoleLogger logger, string filePath) { if (!File.Exists(filePath)) { await logger.ErrorAsync("Can't find file: {0}", filePath); return -1; }

await logger.InfoAsync("Calculating hash for file: {0}", filePath);

var stopwatch = new Stopwatch(); stopwatch.Start();

using HashAlgorithm hashAlgorithm = HashSignatureProvider.Files.CreateAlgorithm(); { const int bufferSize = 1 * 1024 * 1024; // 1 Мб

await using FileStream fileStream = FileHelper.OpenRead(filePath, bufferSize: bufferSize); byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);

int read; while ((read = await fileStream.ReadAsync(buffer, 0, bufferSize)) > 0) { hashAlgorithm.TransformBlock(buffer, 0, read, null, 0); }

hashAlgorithm.TransformFinalBlock(buffer, 0, 0); ArrayPool<byte>.Shared.Return(buffer); }

stopwatch.Stop();

await logger.InfoAsync("Completed in {0:g}", stopwatch.Elapsed); await logger.WriteLineAsync(Convert.ToBase64String(hashAlgorithm.Hash));

return 0; } } }

Создайте класс Command с указанием названия команды, её параметров и их описание для справки.

using System; using System.ComponentModel; using System.IO; using System.Threading.Tasks; using NLog; using Tessa.Localization; using Tessa.Platform.CommandLine; using Tessa.Platform.ConsoleApps;

namespace Tessa.Extensions.Console.GetFileHash { public static class Command { [Verb("GetFileHash")] [Description("Calculates file hash and prints it to console output.")] public static async Task GetFileHash( [Input] TextReader input, [Output] TextWriter stdOut, [Error] TextWriter stdErr, [Argument, Description("Path to a file.")] string filePath, [Argument("q"), LocalizableDescription("Common_CLI_Quiet")] bool quiet = false, [Argument("nologo")] [LocalizableDescription("CLI_NoLogo")] bool nologo = false) { if (string.IsNullOrWhiteSpace(filePath)) { throw new ArgumentException("Please, specify path to a file."); }

if (!nologo && !quiet) { ConsoleAppHelper.WriteLogo(stdOut); }

IConsoleLogger logger = new ConsoleLogger( LogManager.GetLogger(nameof(GetFileHash)), stdOut, stdErr, quiet);

int result = await Operation.ExecuteAsync(logger, filePath); ConsoleAppHelper.EnvironmentExit(result); } } }

Создайте класс CommandRegistrator для регистрации метода команды, приведённого в предыдущем классе. Обратите внимание, что требуется указать типы всех параметров метода Command.GetFileHash в порядке их следования.

using System.IO; using Tessa.Platform.CommandLine; using Tessa.Platform.ConsoleApps;

namespace Tessa.Extensions.Console.GetFileHash { [ConsoleRegistrator] public sealed class CommandRegistrator : ConsoleRegistratorBase { public override void RegisterCommands() { this.CommandContext .AddCommand<TextReader, TextWriter, TextWriter, string, bool, bool>(Command.GetFileHash) ; } } }

Примеры использования команды:

tadmin GetFileHash Tessa.dll

tadmin GetFileHash C:\Files\test.txt -q

Back to top