Создание консольных команд tadmin
Создание консольных команд tadmin¶
Начиная со сборки 3.4.0
в системе добавлено открытое API для создания собственных консольных команд для утилиты tadmin. Исходный код всех типовых консольных команд доступен для модификации в проектных расширениях Tessa.Extensions.Default.Console
. В проекте Tessa.Extensions.Console
рекомендуется добавлять новые консольные команды, которые можно использовать в рамках проекта в любых целях интеграции и автоматизации взаимодействия с системой.
Команды, которые создаются в рамках API, разделяют на серверные и клиентские:
-
Серверные команды не используют контейнер Unity и метаинформацию от веб-сервиса. Им доступно прямое соединение с базой данных в соответствии со строками подключения в
app.json
. Также они могут выполнять любые другие действия, связанные с файлами и другими локальными ресурсами, например, генерировать ключ токена безопасности (командаGetKey
) или упаковывать папку с клиентским приложением в файл карточки .json для возможности последующего импорта карточки на сервер (командаPackageApp
). -
Клиентские команды используют контейнер 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\net5.0
(для конфигурации 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\net5.0
(для конфигурации 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