1
0
mirror of https://github.com/akpaevj/onecmonitor.git synced 2025-03-03 14:42:18 +02:00

промежуточная

This commit is contained in:
akpaev.e 2025-02-26 11:32:51 +03:00
parent aae7d51c31
commit da06b000c1
93 changed files with 2554 additions and 615 deletions

View File

@ -1,7 +1,8 @@
using System.Diagnostics;
using OneSTools.Common.Extensions;
using OneSTools.Common.Platform.Services;
namespace OneSTools.Common.Platform;
namespace OneSTools.Common.Platform.RemoteAdministration;
public class Rac(V8Platform platform, string host = "localhost", int port = 1545)
{
@ -14,7 +15,7 @@ public class Rac(V8Platform platform, string host = "localhost", int port = 1545
}
public List<V8Cluster> GetClusters()
=> GetOutputItems("cluster list")
=> GetOutputItems("cluster list", 10)
.Select(c => new V8Cluster
{
Id = c["cluster"],
@ -23,12 +24,9 @@ public class Rac(V8Platform platform, string host = "localhost", int port = 1545
Port = int.Parse(c["port"])
})
.ToList();
public List<V8InfoBaseSummary> GetInfoBasesSummaries(V8Cluster cluster)
=> GetInfoBasesSummaries(cluster.Id);
public List<V8InfoBaseSummary> GetInfoBasesSummaries(string clusterId)
=> GetOutputItems($"infobase --cluster={clusterId} summary list")
=> GetOutputItems($"infobase --cluster={clusterId} summary list", 10)
.Select(c => new V8InfoBaseSummary()
{
Id = c["infobase"],
@ -36,11 +34,8 @@ public class Rac(V8Platform platform, string host = "localhost", int port = 1545
})
.ToList();
public V8InfoBase GetInfoBase(V8Cluster cluster, V8InfoBaseSummary infoBaseSummary, string user, string password)
=> GetInfoBase(cluster.Id, infoBaseSummary.Name, user, password);
public V8InfoBase GetInfoBase(string clusterId, string infoBaseId, string user, string password)
=> GetOutputItems($"infobase --cluster={clusterId} info --infobase={infoBaseId} --infobase-user={user} --infobase-pwd={password}")
=> GetOutputItems($"infobase --cluster={clusterId} info --infobase={infoBaseId} --infobase-user={user} --infobase-pwd={password}", 20)
.Select(c => new V8InfoBase
{
Id = c["infobase"],
@ -54,16 +49,13 @@ public class Rac(V8Platform platform, string host = "localhost", int port = 1545
public void BlockConnections(string clusterId, string infoBaseId, string user, string password,
string permissionCode, string deniedMessage)
=> StartRacAndGetOutput($"infobase --cluster={clusterId} update --infobase={infoBaseId} --infobase-user={user} --infobase-pwd={password} --sessions-deny=on --scheduled-jobs-deny=on --permission-code={permissionCode} --denied-message=\"{deniedMessage}\"");
public List<V8Session> GetInfoBaseSessions(V8Cluster cluster, V8InfoBase infoBase)
=> GetInfoBaseSessions(cluster.Id, infoBase.Id);
=> StartRacAndGetOutput($"infobase --cluster={clusterId} update --infobase={infoBaseId} --infobase-user={user} --infobase-pwd={password} --sessions-deny=on --scheduled-jobs-deny=on --permission-code={permissionCode} --denied-message=\"{deniedMessage}\"", 10);
public List<V8Session> GetInfoBaseSessions(string clusterId, string infoBaseId)
{
var infoBases = GetInfoBasesSummaries(clusterId);
return GetOutputItems($"session --cluster={clusterId} list --infobase={infoBaseId}")
return GetOutputItems($"session --cluster={clusterId} list --infobase={infoBaseId}", 20)
.Select(c => new V8Session
{
Id = c["session"],
@ -76,21 +68,15 @@ public class Rac(V8Platform platform, string host = "localhost", int port = 1545
.ToList();
}
public void TerminateSession(V8Cluster cluster, V8Session session)
=> TerminateSession(cluster.Id, session.Id);
public void TerminateSession(string clusterId, string sessionId)
=> StartRacAndGetOutput($"session --cluster={clusterId} terminate --session={sessionId}");
public void UnblockConnections(V8Cluster cluster, V8InfoBase infoBase, string user, string password)
=> UnblockConnections(cluster.Id, infoBase.Id, user, password);
=> StartRacAndGetOutput($"session --cluster={clusterId} terminate --session={sessionId}", 10);
public void UnblockConnections(string clusterId, string infoBaseId, string user, string password)
=> StartRacAndGetOutput($"infobase --cluster={clusterId} update --infobase={infoBaseId} --infobase-user={user} --infobase-pwd={password} --sessions-deny=off --scheduled-jobs-deny=off");
=> StartRacAndGetOutput($"infobase --cluster={clusterId} update --infobase={infoBaseId} --infobase-user={user} --infobase-pwd={password} --sessions-deny=off --scheduled-jobs-deny=off", 10);
private List<Dictionary<string, string>> GetOutputItems(string command)
private List<Dictionary<string, string>> GetOutputItems(string command, int commandTimeout)
{
var output = StartRacAndGetOutput(command);
var output = StartRacAndGetOutput(command, commandTimeout);
var outputItems = output.Split($"{Environment.NewLine}{Environment.NewLine}", StringSplitOptions.RemoveEmptyEntries).ToList();
@ -100,7 +86,7 @@ public class Rac(V8Platform platform, string host = "localhost", int port = 1545
.ToList();
}
private string StartRacAndGetOutput(string command)
private string StartRacAndGetOutput(string command, int commandTimeout)
{
if (!platform.HasRac)
throw new Exception($"{platform.PlatformPath} doesn't contain 1cv8 executable");
@ -111,20 +97,33 @@ public class Rac(V8Platform platform, string host = "localhost", int port = 1545
RedirectStandardOutput = true,
RedirectStandardInput = true,
RedirectStandardError = true,
CreateNoWindow = true,
UseShellExecute = false,
Arguments = $"{host}:{port} {command}"
};
using var process = new Process();
process.StartInfo = psi;
if (!process.Start())
throw new Exception($"Failed to start {psi.FileName}");
process.WaitForExit();
if (!process.Start())
{
process.Close();
throw new Exception($"Ошибка запуска {psi.FileName}");
}
if (process.WaitForExit(TimeSpan.FromSeconds(commandTimeout)) && process.ExitCode != 0)
{
using var errorStream = process.StandardError;
var error = errorStream.ReadToEnd();
process.Close();
throw new Exception($"Ошибка выполнения команды RAC: {error}");
}
if (process.ExitCode != 0)
throw new Exception($"Failed to execute rac command {process.StandardError.ReadToEnd()}");
using var outputStream = process.StandardOutput;
var output = outputStream.ReadToEnd();
process.Close();
return process.StandardOutput.ReadToEnd();
return output;
}
}

View File

@ -1,6 +1,6 @@
using MessagePack;
namespace OneSTools.Common.Platform;
namespace OneSTools.Common.Platform.RemoteAdministration;
[MessagePackObject]
public class V8Cluster

View File

@ -1,6 +1,6 @@
using MessagePack;
namespace OneSTools.Common.Platform;
namespace OneSTools.Common.Platform.RemoteAdministration;
[MessagePackObject]
public class V8InfoBase : V8InfoBaseSummary

View File

@ -1,6 +1,6 @@
using MessagePack;
namespace OneSTools.Common.Platform;
namespace OneSTools.Common.Platform.RemoteAdministration;
[MessagePackObject]
public class V8InfoBaseSummary

View File

@ -1,4 +1,4 @@
namespace OneSTools.Common.Platform;
namespace OneSTools.Common.Platform.RemoteAdministration;
public class V8Session
{

View File

@ -1,6 +1,6 @@
using System.Text.RegularExpressions;
namespace OneSTools.Common.Platform;
namespace OneSTools.Common.Platform.Services;
public record ArgsKeyValue(string Key, string Value);

View File

@ -1,7 +1,8 @@
using System.ComponentModel;
using MessagePack;
using OneSTools.Common.Platform.Services;
namespace OneSTools.Common.Platform;
namespace OneSTools.Common.Platform.Services;
[DisplayName("Служба агента сервера 1С")]
[MessagePackObject]

View File

@ -1,7 +1,8 @@
using System.ComponentModel;
using MessagePack;
using OneSTools.Common.Platform.Services;
namespace OneSTools.Common.Platform;
namespace OneSTools.Common.Platform.Services;
[DisplayName("Служба сервера удаленного администрирования 1С")]
[MessagePackObject]

View File

@ -1,7 +1,7 @@
using System.ComponentModel;
using MessagePack;
namespace OneSTools.Common.Platform;
namespace OneSTools.Common.Platform.Services;
[DisplayName("Служба 1С")]
[MessagePackObject]

View File

@ -1,4 +1,4 @@
namespace OneSTools.Common.Platform;
namespace OneSTools.Common.Platform.Services;
public enum V8ServiceType
{

View File

@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
using Microsoft.Win32;
using OneSTools.Common.Extensions;
namespace OneSTools.Common.Platform;
namespace OneSTools.Common.Platform.Services;
public static class V8Services
{

View File

@ -0,0 +1,55 @@
namespace OneSTools.Common.Platform.Unpack;
public struct BlockHeader(
uint dataSize = 0,
uint pageSize = FileFormat.V8DefaultPageSize,
uint nextPageAddr = FileFormat.V8FfSignature)
{
public uint DataSize { get; } = dataSize;
public uint PageSize { get; } = pageSize;
public uint NextPageAddr { get; } = nextPageAddr;
private static void ReadExpectedByte(Stream reader, int expectedValue)
{
if (reader.ReadByte() != expectedValue)
throw new File8FormatException();
}
private static uint ReadHexData(Stream reader)
{
var hex = new byte[8];
if (reader.Read(hex, 0, 8) < 8)
{
throw new File8FormatException();
}
try
{
return Convert.ToUInt32(System.Text.Encoding.ASCII.GetString(hex), 16);
}
catch
{
throw new File8FormatException();
}
}
public static BlockHeader Read(Stream reader)
{
ReadExpectedByte(reader, 0x0D);
ReadExpectedByte(reader, 0x0A);
var dataSize = ReadHexData(reader);
ReadExpectedByte(reader, 0x20);
var pageSize = ReadHexData(reader);
ReadExpectedByte(reader, 0x20);
var nextPageAddr = ReadHexData(reader);
ReadExpectedByte(reader, 0x20);
ReadExpectedByte(reader, 0x0D);
ReadExpectedByte(reader, 0x0A);
return new BlockHeader(dataSize, pageSize, nextPageAddr);
}
}

View File

@ -0,0 +1,152 @@
using System.IO.Compression;
namespace OneSTools.Common.Platform.Unpack;
public class BlockReader : Stream
{
private BlockHeader _currentHeader;
private readonly Stream _reader;
private readonly int _dataSize;
private byte[] _currentPageData;
private int _currentPageOffset;
private bool _isPacked;
private bool _isContainer;
public BlockReader(Stream basicStream)
{
_reader = basicStream;
_currentHeader = BlockHeader.Read(_reader);
_dataSize = (int)_currentHeader.DataSize;
ReadPage();
AnalyzeState();
}
private void ReadPage()
{
var currentDataSize = Math.Min(_dataSize, (int)_currentHeader.PageSize);
_currentPageData = new byte[currentDataSize];
_reader.Read(_currentPageData, 0, currentDataSize);
_currentPageOffset = 0;
}
private void AnalyzeState()
{
var bufferToCheck = _currentPageData;
try
{
using var inputStream = new MemoryStream(bufferToCheck);
using var deflateStream = new DeflateStream(inputStream, CompressionMode.Decompress);
using var outputStream = new MemoryStream();
deflateStream.CopyTo(outputStream);
var tmp = outputStream.ToArray();
_isPacked = true;
bufferToCheck = tmp;
}
catch
{
_isPacked = false;
}
_isContainer = FileFormat.IsContainer(bufferToCheck);
}
private void MoveNextBlock()
{
if (_currentHeader.NextPageAddr == FileFormat.V8FfSignature)
{
_currentPageData = null;
return;
}
_reader.Seek(_currentHeader.NextPageAddr, SeekOrigin.Begin);
_currentHeader = BlockHeader.Read(_reader);
ReadPage();
}
public bool IsPacked => _isPacked;
public bool IsContainer => _isContainer;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => _dataSize;
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush()
{
throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
if (_currentPageData == null)
{
return 0;
}
var bytesRead = 0;
var countLeft = count;
while (countLeft > 0)
{
var leftInPage = _currentPageData.Length - _currentPageOffset;
if (leftInPage == 0)
{
MoveNextBlock();
if (_currentPageData == null)
{
break;
}
}
var readFromCurrentPage = Math.Min(leftInPage, countLeft);
Buffer.BlockCopy(_currentPageData, _currentPageOffset, buffer, offset, readFromCurrentPage);
_currentPageOffset += readFromCurrentPage;
offset += readFromCurrentPage;
bytesRead += readFromCurrentPage;
countLeft -= readFromCurrentPage;
}
return bytesRead;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public static byte[] ReadDataBlock(Stream reader)
{
var blockReader = new BlockReader(reader);
var buf = new byte[blockReader.Length];
blockReader.ReadExactly(buf, 0, buf.Length);
return buf;
}
}

View File

@ -0,0 +1,34 @@
namespace OneSTools.Common.Platform.Unpack;
public struct ContainerHeader
{
public readonly uint NextPageAddr;
public readonly uint PageSize;
public readonly uint StorageVer;
public readonly uint Reserved;
private ContainerHeader(uint nextPageAddr = FileFormat.V8FfSignature, uint pageSize = FileFormat.V8DefaultPageSize, uint storageVer = 0, uint reserved = 0)
{
NextPageAddr = nextPageAddr;
PageSize = pageSize;
StorageVer = 0;
Reserved = 0;
}
public static ContainerHeader Read(Stream reader)
{
const int headerSize = 16;
var buf = new byte[headerSize];
if (reader.Read(buf, 0, headerSize) < headerSize)
{
throw new File8FormatException();
}
return new ContainerHeader(
nextPageAddr: BitConverter.ToUInt32(buf, 0),
pageSize: BitConverter.ToUInt32(buf, 4),
storageVer: BitConverter.ToUInt32(buf, 8),
reserved: BitConverter.ToUInt32(buf, 12)
);
}
}

View File

@ -0,0 +1,28 @@
namespace OneSTools.Common.Platform.Unpack;
public readonly struct ElementAddress(uint headerAddress, uint dataAddress, uint signature = FileFormat.V8FfSignature)
{
public uint HeaderAddress { get; } = headerAddress;
public uint DataAddress { get; } = dataAddress;
public uint Signature { get; } = signature;
public static IList<ElementAddress> Parse(byte[] buf)
{
const int elementSize = 4 + 4 + 4;
var result = new List<ElementAddress>();
for (var offset = 0; offset + elementSize <= buf.Length; offset += elementSize)
{
var headerAddress = BitConverter.ToUInt32(buf, offset);
var dataAddress = BitConverter.ToUInt32(buf, offset + 4);
var signature = BitConverter.ToUInt32(buf, offset + 8);
result.Add(new ElementAddress(headerAddress, dataAddress, signature));
}
return result;
}
public override string ToString()
=> $"{HeaderAddress:x8}:{DataAddress:x8}:{Signature:x8}";
}

View File

@ -0,0 +1,29 @@
namespace OneSTools.Common.Platform.Unpack;
public struct ElementHeader(string name, DateTime creationDate, DateTime modificationDate)
{
public readonly DateTime CreationDate = creationDate;
public readonly DateTime ModificationDate = modificationDate;
public readonly string Name = name;
public static DateTime File8Date(ulong serializedDate)
{
return new DateTime((long) serializedDate * 1000);
}
public static ElementHeader Parse(byte[] buf)
{
var serializedCreationDate = BitConverter.ToUInt64(buf, 0);
var serializedModificationDate = BitConverter.ToUInt64(buf, 8);
// 4 байта на Reserved
var enc = new System.Text.UnicodeEncoding(bigEndian: false, byteOrderMark: false);
const int nameOffset = 8 + 8 + 4;
var name = enc.GetString(buf, nameOffset, buf.Length - nameOffset - 4).TrimEnd('\0');
var creationDate = File8Date(serializedCreationDate);
var modificationDate = File8Date(serializedModificationDate);
return new ElementHeader(name, creationDate, modificationDate);
}
}

View File

@ -0,0 +1,18 @@
namespace OneSTools.Common.Platform.Unpack;
public class File8
{
internal File8(ElementHeader header, uint dataOffset)
{
DataOffset = (int)dataOffset;
Name = header.Name;
ModificationTime = header.ModificationDate;
CreationTime = header.CreationDate;
}
public string Name { get; }
public DateTime ModificationTime { get; }
public DateTime CreationTime { get; }
public int DataOffset { get; }
}

View File

@ -0,0 +1,45 @@
using System.Collections;
namespace OneSTools.Common.Platform.Unpack;
public class File8Collection : IEnumerable<File8>
{
private readonly IReadOnlyList<File8?> _data;
public File8Collection(IEnumerable<File8?> data)
{
var fileList = new List<File8?>();
fileList.AddRange(data);
_data = fileList;
}
public int Count()
{
return _data.Count;
}
public File8? Get(int index)
{
return _data[index];
}
public File8? Get(string name)
{
return _data.First(f => f != null && f.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
}
public File8? Find(string name)
{
return _data.FirstOrDefault(f => f != null && f.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
}
public IEnumerator<File8> GetEnumerator()
{
return _data.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@ -0,0 +1,3 @@
namespace OneSTools.Common.Platform.Unpack;
public class File8FormatException : Exception;

View File

@ -0,0 +1,138 @@
using System.IO.Compression;
using System.Reflection.Metadata;
namespace OneSTools.Common.Platform.Unpack;
public class File8Reader : IDisposable
{
private readonly Stream _reader;
private readonly bool _dataPacked;
private int _storageVersion;
public decimal StorageVersion => _storageVersion;
public File8Collection Elements { get; }
public File8Reader(string filePath, bool dataPacked = true)
{
const int magicSize = 100 * 1024;
var fileStream = new FileStream(filePath, FileMode.Open);
if (fileStream.Length >= magicSize)
_reader = fileStream;
else
{
var memoryStream = new MemoryStream();
fileStream.CopyTo(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
_reader = memoryStream;
}
_dataPacked = dataPacked;
var fileList = ReadFileList();
Elements = new File8Collection(fileList);
}
public File8Reader(Stream stream, bool dataPacked = true)
{
_reader = stream;
_dataPacked = dataPacked;
var fileList = ReadFileList();
Elements = new File8Collection(fileList);
}
private List<File8> ReadFileList()
{
var containerHeader = ContainerHeader.Read(_reader);
_storageVersion = (int)containerHeader.StorageVer;
var elemsAddrBuf = BlockReader.ReadDataBlock(_reader);
var addresses = ElementAddress.Parse(elemsAddrBuf);
var fileList = new List<File8>();
foreach (var address in addresses)
{
if (address.HeaderAddress == FileFormat.V8FfSignature || address.Signature != FileFormat.V8FfSignature)
continue;
_reader.Seek(address.HeaderAddress, SeekOrigin.Begin);
var buf = BlockReader.ReadDataBlock(_reader);
var fileHeader = ElementHeader.Parse(buf);
fileList.Add(new File8(fileHeader, address.DataAddress));
}
return fileList;
}
public void Extract(File8 element, string destDir, bool recursiveUnpack = false)
{
if (!Directory.Exists(destDir))
{
Directory.CreateDirectory(destDir);
}
Stream fileExtractor;
if (element.DataOffset == FileFormat.V8FfSignature)
{
// Файл есть, но пуст
fileExtractor = new MemoryStream();
}
else
{
_reader.Seek(element.DataOffset, SeekOrigin.Begin);
var blockExtractor = new BlockReader(_reader);
if (blockExtractor.IsPacked && _dataPacked)
{
fileExtractor = new DeflateStream(blockExtractor, CompressionMode.Decompress);
}
else
{
fileExtractor = blockExtractor;
}
if (blockExtractor.IsContainer && recursiveUnpack)
{
var outputDirectory = Path.Combine(destDir, element.Name);
var tmpData = new MemoryStream(); // TODO: переделать MemoryStream --> FileStream
fileExtractor.CopyTo(tmpData);
tmpData.Seek(0, SeekOrigin.Begin);
var internalContainer = new File8Reader(tmpData, dataPacked: false);
internalContainer.ExtractAll(outputDirectory, recursiveUnpack);
return;
}
}
// Просто файл
var outputFileName = Path.Combine(destDir, element.Name);
using var outputFile = new FileStream(outputFileName, FileMode.Create);
fileExtractor.CopyTo(outputFile);
}
/// <summary>
/// Извлекает все файлы из контейнера.
/// </summary>
/// <param name="destDir">Каталог назначения.</param>
/// <param name="recursiveUnpack">Если установлен в Истина, то все найденные вложенные восьмофайлы
/// будут распакованы в отдельные подкаталоги. Необязательный.</param>
public void ExtractAll(string destDir, bool recursiveUnpack = false)
{
foreach (var element in Elements)
{
Extract(element, destDir, recursiveUnpack);
}
}
public void Dispose()
{
_reader.Close();
}
public void Close()
{
_reader.Close();
}
}

View File

@ -0,0 +1,23 @@
namespace OneSTools.Common.Platform.Unpack;
internal static partial class FileFormat
{
public const uint V8FfSignature = 0x7fffffff;
public const uint V8DefaultPageSize = 512;
public static bool IsContainer(byte[] data)
{
var reader = new MemoryStream(data);
try
{
ContainerHeader.Read(reader);
BlockHeader.Read(reader);
}
catch (File8FormatException)
{
return false;
}
return true;
}
}

View File

@ -15,13 +15,20 @@ var host = Host.CreateDefaultBuilder(args)
{
options.ServiceName = "OnecMonitorAgent";
});
services.AddSystemd();
services.AddSingleton<RasHolder>();
services.AddSingleton<InfoBasesUpdater>();
services.AddDbContext<AppDbContext>();
// Commands watcher connection
services.AddSingleton<OnecMonitorConnection>();
services.AddSingleton<TechLogFolderWatcher>();
services.AddSingleton<TechLogExporter>();
services.AddHostedService<TechLogSeancesWatcher>();
services.AddSingleton<InfoBasesUpdateTasksQueue>();
services.AddHostedService<InfoBasesUpdater>();
services.AddHostedService<CommandsWatcher>();
})
.Build();

View File

@ -4,6 +4,8 @@ using OnecMonitor.Agent.Services.InfoBases;
using OnecMonitor.Agent.Services.TechLog;
using OnecMonitor.Common.DTO;
using OneSTools.Common.Platform;
using OneSTools.Common.Platform.RemoteAdministration;
using OneSTools.Common.Platform.Services;
namespace OnecMonitor.Agent.Services
{
@ -11,14 +13,14 @@ namespace OnecMonitor.Agent.Services
{
private readonly OnecMonitorConnection _server;
private readonly AppDbContext _appDbContext;
private readonly InfoBasesUpdater _infoBasesUpdater;
private readonly InfoBasesUpdateTasksQueue _updateTasksQueue;
private readonly RasHolder _rasHolder;
private readonly TechLogExporter _techLogExporter;
private readonly ILogger<CommandsWatcher> _logger;
public CommandsWatcher(
IServiceProvider serviceProvider,
InfoBasesUpdater infoBasesUpdater,
InfoBasesUpdateTasksQueue updateTasksQueue,
TechLogExporter techLogExporter,
RasHolder rasHolder,
ILogger<CommandsWatcher> logger)
@ -28,7 +30,7 @@ namespace OnecMonitor.Agent.Services
_server = scope.ServiceProvider.GetRequiredService<OnecMonitorConnection>();
_appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
_techLogExporter = techLogExporter;
_infoBasesUpdater = infoBasesUpdater;
_updateTasksQueue = updateTasksQueue;
_logger = logger;
}
@ -92,22 +94,19 @@ namespace OnecMonitor.Agent.Services
private async Task SendInstalledPlatforms(Message message, CancellationToken cancellationToken)
{
_logger.LogTrace("Send installed platforms");
var platforms = V8Platforms.GetInstalledPlatforms();
await _server.Send(MessageType.InstalledPlatforms, platforms, message, cancellationToken);
}
private async Task HandleUpdateInfoBasesRequest(Message message, CancellationToken cancellationToken)
{
await _updateTasksQueue.QueueAsync(message, cancellationToken);
await _server.SendOk(message, cancellationToken);
_infoBasesUpdater.RequestInfoBasesUpdateTask();
}
private async Task HandleUpdateSettingsRequest(Message message, CancellationToken cancellationToken)
{
await _server.SendOk(message, cancellationToken);
await UpdateSettings(cancellationToken);
}
@ -127,8 +126,6 @@ namespace OnecMonitor.Agent.Services
private async Task SendV8Clusters(Message message, CancellationToken cancellationToken)
{
_logger.LogTrace("Send clusters");
var ragents = V8Services.GetActiveRagentServices();
var clusters = new List<V8Cluster>();
@ -140,8 +137,6 @@ namespace OnecMonitor.Agent.Services
private async Task SendV8InfoBases(Message message, CancellationToken cancellationToken)
{
_logger.LogTrace("Send infobases");
var request = MessagePackSerializer.Deserialize<InfoBasesRequestDto>(message.Data, cancellationToken: cancellationToken);
var ragent = V8Services.GetActiveRagentForClusterPort(request.Cluster.Port);
@ -156,23 +151,18 @@ namespace OnecMonitor.Agent.Services
private async Task SendRagentServices(Message message, CancellationToken cancellationToken)
{
_logger.LogTrace("Send ragent services");
var services = V8Services.GetRagentServices();
await _server.Send(MessageType.RagentServices, services, message, cancellationToken);
}
private async Task SendRasServices(Message message, CancellationToken cancellationToken)
{
_logger.LogTrace("Send ras services");
var services = _rasHolder.GetRasServices();
await _server.Send(MessageType.RasServices, services, message, cancellationToken);
}
private async Task UpdateTechLogSeancesByRequest(Message message, CancellationToken cancellationToken)
{
_logger.LogTrace("Updating tech log seances by server request");
await _server.SendOk(message, cancellationToken);
if (_techLogExporter.Enabled)
@ -181,14 +171,12 @@ namespace OnecMonitor.Agent.Services
private async Task UpdateTechLogSeances(CancellationToken cancellationToken)
{
_logger.LogTrace("Updating tech log seances");
try
{
var seances = await _server.Get<List<TechLogSeanceDto>>(
MessageType.TechLogSeancesRequest,
MessageType.TechLogSeances,
cancellationToken);;
cancellationToken);
await _appDbContext.Database.BeginTransactionAsync(cancellationToken);
@ -223,8 +211,6 @@ namespace OnecMonitor.Agent.Services
await _appDbContext.Database.CommitTransactionAsync(cancellationToken);
await _appDbContext.SaveChangesAsync(cancellationToken);
_logger.LogTrace("Tech log seances updated");
}
catch (Exception ex)
{

View File

@ -0,0 +1,15 @@
using System.Threading.Channels;
using OnecMonitor.Common.DTO;
namespace OnecMonitor.Agent.Services.InfoBases;
public class InfoBasesUpdateTasksQueue
{
private readonly Channel<Message> _updateRequestsChannel = Channel.CreateUnbounded<Message>();
public async Task QueueAsync(Message message, CancellationToken cancellationToken)
=> await _updateRequestsChannel.Writer.WriteAsync(message, cancellationToken);
public async Task<Message> DequeueAsync(CancellationToken cancellationToken)
=> await _updateRequestsChannel.Reader.ReadAsync(cancellationToken);
}

View File

@ -1,49 +1,73 @@
using System.Reflection;
using System.Text;
using OnecMonitor.Agent.Extensions;
using System.Threading.Channels;
using OnecMonitor.Common.DTO;
using OneSTools.Common.Designer.Batch;
using OneSTools.Common.Extensions;
using OneSTools.Common.Platform;
using Org.BouncyCastle.Asn1.X509;
using OneSTools.Common.Platform.RemoteAdministration;
using OneSTools.Common.Platform.Services;
namespace OnecMonitor.Agent.Services.InfoBases;
public sealed class InfoBasesUpdater : IDisposable
public sealed class InfoBasesUpdater : BackgroundService
{
private readonly InfoBasesUpdateTasksQueue _queue;
private readonly AsyncServiceScope _scope;
private readonly OnecMonitorConnection _server;
private readonly IHostApplicationLifetime _applicationLifetime;
private readonly RasHolder _rasHolder;
private readonly ILogger<InfoBasesUpdater> _logger;
private bool _disposed;
private readonly object _racLocker = new();
public InfoBasesUpdater(IServiceProvider serviceProvider, RasHolder rasHolder, IHostApplicationLifetime applicationLifetime, ILogger<InfoBasesUpdater> logger)
public InfoBasesUpdater(
IServiceProvider serviceProvider,
InfoBasesUpdateTasksQueue tasksQueue,
RasHolder rasHolder,
IHostApplicationLifetime applicationLifetime,
ILogger<InfoBasesUpdater> logger)
{
_scope = serviceProvider.CreateAsyncScope();
_queue = tasksQueue;
_server = _scope.ServiceProvider.GetRequiredService<OnecMonitorConnection>();
_applicationLifetime = applicationLifetime;
_rasHolder = rasHolder;
_logger = logger;
}
public void RequestInfoBasesUpdateTask()
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Task.Run(async () =>
while (!stoppingToken.IsCancellationRequested)
{
var accessCode = "12345";
var message = "Технические работы";
await _queue.DequeueAsync(stoppingToken);
try
{
await RequestInfoBasesUpdateTask(stoppingToken);
}
catch (Exception e)
{
_logger.LogError(e, $"Ошибка обработки задания обновления: {e.Message}");
}
}
}
private async Task RequestInfoBasesUpdateTask(CancellationToken cancellationToken)
{
try
{
const string accessCode = "12345";
const string message = "Технические работы";
var task = await _server.Get<UpdateInfoBaseTaskDto>(
MessageType.UpdateInfoBasesTaskRequest,
MessageType.UpdateInfoBasesTask,
_applicationLifetime.ApplicationStopping);
var config = task.Configurations.FirstOrDefault(c => c.IsUpdate || c.IsConfiguration);
var extensions = task.Configurations.Where(c => c.IsExtension).ToList();
var config = task.Files.FirstOrDefault(c => c.IsUpdate || c.IsConfiguration);
var extensions = task.Files.Where(c => c.IsExtension).ToList();
var configurationsPaths = new Dictionary<string, string>();
task.Configurations.ForEach(i =>
task.Files.ForEach(i =>
{
var path = Path.Join(Path.GetTempPath(), $"{i.Id}.cfu") ;
@ -53,131 +77,159 @@ public sealed class InfoBasesUpdater : IDisposable
file.Write(i.Data);
file.Close();
}
configurationsPaths.Add(i.Id.ToString(), path);
});
await Parallel.ForEachAsync(task.InfoBases, async (infoBase, cancellationToken) =>
{
var log = new List<UpdateInfoBaseTaskLogItemDto>();
try
var tasks = new List<Task>();
foreach (var infoBase in task.InfoBases)
{
var ibTask = Task.Factory.StartNew(async () =>
{
var ragent = V8Services.GetActiveRagentForClusterPort(infoBase.Cluster.Port);
var ras = _rasHolder.GetActiveRasForRagent(ragent);
var rac = Rac.GetRacForRasService(ras);
var platform = ragent.Platform;
if (!platform.HasOnecV8)
throw new Exception("Для платформы агента не установлен конфигуратор");
OnecV8BatchMode GetBatchDesigner()
=> new(platform, $"{infoBase.Cluster.Host}:{infoBase.Cluster.Port}", infoBase.InfoBaseName);
OnecV8BatchMode GetBatchEnterprise()
=> new(platform, $"{infoBase.Cluster.Host}:{infoBase.Cluster.Port}", infoBase.InfoBaseName, false);
await AddLogItemAndSend(task, infoBase, log, "Блокировка соединений и регламентных заданий", cancellationToken);
rac.BlockConnections(
infoBase.Cluster.Id,
infoBase.InfoBaseInternalId.ToString(),
infoBase.Credentials.User,
infoBase.Credentials.Password,
accessCode,
message);
await AddLogItemAndSend(task, infoBase, log, "Завершение сессий", cancellationToken);
var sessions = rac.GetInfoBaseSessions(infoBase.Cluster.Id, infoBase.InfoBaseInternalId.ToString());
sessions
.Where(c => c.AppId != "RAS")
.ToList()
.ForEach(s => rac.TerminateSession(infoBase.Cluster.Id, s.Id));
if (config != null)
var log = new List<UpdateInfoBaseTaskLogItemDto>();
try
{
if (config.IsConfiguration)
{
await AddLogItemAndSend(task, infoBase, log, "Загрузка файла конфигурации", cancellationToken);
var ragent = V8Services.GetActiveRagentForClusterPort(infoBase.Cluster.Port);
var ras = _rasHolder.GetActiveRasForRagent(ragent);
var rac = Rac.GetRacForRasService(ras);
var platform = ragent.Platform;
using var loadCfgBatch = GetBatchDesigner();
loadCfgBatch.LoadConfiguration(
configurationsPaths[config.Id.ToString()],
if (!platform.HasOnecV8)
throw new Exception("Для платформы агента не установлен конфигуратор");
OnecV8BatchMode GetBatchDesigner()
=> new(platform, $"{infoBase.Cluster.Host}:{infoBase.Cluster.Port}", infoBase.InfoBaseName);
OnecV8BatchMode GetBatchEnterprise()
=> new(platform, $"{infoBase.Cluster.Host}:{infoBase.Cluster.Port}", infoBase.InfoBaseName, false);
await AddLogItemAndSend(task, infoBase, log, "Блокировка соединений и регламентных заданий", cancellationToken);
lock (_racLocker)
rac.BlockConnections(
infoBase.Cluster.Id,
infoBase.InfoBaseInternalId,
infoBase.Credentials.User,
infoBase.Credentials.Password,
accessCode,
message);
await AddLogItemAndSend(task, infoBase, log, "Завершение сессий", cancellationToken);
lock (_racLocker)
{
var sessions = rac.GetInfoBaseSessions(infoBase.Cluster.Id, infoBase.InfoBaseInternalId);
sessions
.Where(c => !c.AppId.Contains("RAS", StringComparison.CurrentCultureIgnoreCase))
.ToList()
.ForEach(s =>
{
try
{
rac.TerminateSession(infoBase.Cluster.Id, s.Id);
}
catch
{
// Игнорируем, т.к. сеанс уже мог быть закрыт, мог быть повисшим и т.п.
}
});
}
if (config != null)
{
if (config.IsConfiguration)
{
await AddLogItemAndSend(task, infoBase, log, "Загрузка файла конфигурации", cancellationToken);
using var loadCfgBatch = GetBatchDesigner();
loadCfgBatch.LoadConfiguration(
configurationsPaths[config.Id.ToString()],
infoBase.Credentials.User,
infoBase.Credentials.Password,
accessCode,
true);
await AddLogItemAndSend(task, infoBase, log, loadCfgBatch.OutFileContent, cancellationToken);
}
else
{
await AddLogItemAndSend(task, infoBase, log, "Обновление ИБ файлом обновления конфигурации", cancellationToken);
using var updateCfgBatch = GetBatchDesigner();
updateCfgBatch.UpdateConfiguration(
configurationsPaths[config.Id.ToString()],
infoBase.Credentials.User,
infoBase.Credentials.Password,
accessCode,
true);
await AddLogItemAndSend(task, infoBase, log, updateCfgBatch.OutFileContent, cancellationToken);
}
}
if (extensions.Count > 0)
await AddLogItemAndSend(task, infoBase, log, "Загрузка расширений ИБ", cancellationToken);
foreach (var extension in extensions)
{
await AddLogItemAndSend(task, infoBase, log, $"Загрузка расширения {extension.Name}", cancellationToken);
using var loadExtBatch = GetBatchDesigner();
loadExtBatch.LoadExtension(
extension.Name,
configurationsPaths[extension.Id.ToString()],
infoBase.Credentials.User,
infoBase.Credentials.Password,
accessCode,
true);
await AddLogItemAndSend(task, infoBase, log, loadCfgBatch.OutFileContent, cancellationToken);
await AddLogItemAndSend(task, infoBase, log, loadExtBatch.OutFileContent, cancellationToken);
}
else
var needAcceptLegalUsing = config != null;
if (needAcceptLegalUsing)
{
await AddLogItemAndSend(task, infoBase, log, "Обновление ИБ файлом обновления конфигурации", cancellationToken);
using var updateCfgBatch = GetBatchDesigner();
updateCfgBatch.UpdateConfiguration(
configurationsPaths[config.Id.ToString()],
await AddLogItemAndSend(task, infoBase, log, "Подтверждение легальности получения и запуск обработчиков обновления", cancellationToken);
var acceptLegalBatch = GetBatchEnterprise();
var epfPath = GetExternalDataProcessorPath("ПодтверждениеЛегальности.epf");
acceptLegalBatch.ExecuteExternalDataProcessor(
epfPath,
infoBase.Credentials.User,
infoBase.Credentials.Password,
accessCode,
true);
await AddLogItemAndSend(task, infoBase, log, updateCfgBatch.OutFileContent, cancellationToken);
}
await AddLogItemAndSend(task, infoBase, log, "Разблокировка соединений и регламентных заданий", cancellationToken);
lock (_racLocker)
rac.UnblockConnections(
infoBase.Cluster.Id,
infoBase.InfoBaseInternalId,
infoBase.Credentials.User,
infoBase.Credentials.Password);
await AddLogItemAndSend(task, infoBase, log, "Обновление завершено", _applicationLifetime.ApplicationStopping, false, true);
}
if (extensions.Count > 0)
await AddLogItemAndSend(task, infoBase, log, "Загрузка расширений ИБ", cancellationToken);
foreach (var extension in extensions)
catch (Exception e)
{
await AddLogItemAndSend(task, infoBase, log, $"Загрузка расширения {extension.Name}", cancellationToken);
using var loadExtBatch = GetBatchDesigner();
loadExtBatch.LoadExtension(
extension.Name,
configurationsPaths[extension.Id.ToString()],
infoBase.Credentials.User,
infoBase.Credentials.Password,
accessCode,
true);
await AddLogItemAndSend(task, infoBase, log, loadExtBatch.OutFileContent, cancellationToken);
await AddLogItemAndSend(task, infoBase, log, e.ToString(), _applicationLifetime.ApplicationStopping, true, true);
}
}, _applicationLifetime.ApplicationStopping);
tasks.Add(ibTask);
}
var needAcceptLegalUsing = config != null;
if (needAcceptLegalUsing)
{
await AddLogItemAndSend(task, infoBase, log, "Подтверждение легальности получения и запуск обработчиков обновления", cancellationToken);
var acceptLegalBatch = GetBatchEnterprise();
var epfPath = GetExternalDataProcessorPath("ПодтверждениеЛегальности.epf");
acceptLegalBatch.ExecuteExternalDataProcessor(
epfPath,
infoBase.Credentials.User,
infoBase.Credentials.Password,
accessCode,
true);
}
await AddLogItemAndSend(task, infoBase, log, "Разблокировка соединений и регламентных заданий", cancellationToken);
rac.UnblockConnections(
infoBase.Cluster.Id,
infoBase.InfoBaseInternalId.ToString(),
infoBase.Credentials.User,
infoBase.Credentials.Password);
await AddLogItemAndSend(task, infoBase, log, "Обновление завершено", cancellationToken, false, true);
}
catch (Exception e)
{
await AddLogItemAndSend(task, infoBase, log, e.ToString(), cancellationToken, true, true);
}
});
});
Task.WaitAll(tasks.ToArray(), cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
}
private async Task AddLogItemAndSend(
@ -199,13 +251,14 @@ public sealed class InfoBasesUpdater : IDisposable
InfoBaseId = infoBase.Id,
TaskId = task.Id,
});
await SendLog(log, cancellationToken);
}
private async Task SendLog(List<UpdateInfoBaseTaskLogItemDto> log, CancellationToken cancellationToken)
=> await _server.Send(MessageType.UpdateInfoBaseTaskLog, log, cancellationToken);
private string GetExternalDataProcessorPath(string fileName)
private static string GetExternalDataProcessorPath(string fileName)
=> Path.Join(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Asserts", fileName);
private void Dispose(bool disposing)
@ -222,14 +275,11 @@ public sealed class InfoBasesUpdater : IDisposable
_disposed = true;
}
public void Dispose()
public override Task StopAsync(CancellationToken cancellationToken)
{
Dispose(true);
GC.SuppressFinalize(this);
}
~InfoBasesUpdater()
{
Dispose(false);
_scope.Dispose();
_server.Dispose();
return base.StopAsync(cancellationToken);
}
}

View File

@ -3,6 +3,7 @@ using System.Net;
using System.Net.Sockets;
using OneSTools.Common.Extensions;
using OneSTools.Common.Platform;
using OneSTools.Common.Platform.Services;
namespace OnecMonitor.Agent.Services.InfoBases;
@ -63,7 +64,7 @@ public class RasHolder : IDisposable
_processes.Remove(process);
};
_processes.Add(process!);
_processes.Add(process);
var serviceModel = new RasService
{

View File

@ -9,7 +9,8 @@ namespace OnecMonitor.Agent.Services
{
public class OnecMonitorConnection : ServerConnection
{
public OnecMonitorConnection(IServiceProvider serviceProvider, IHostApplicationLifetime hostApplicationLifetime)
public OnecMonitorConnection(IServiceProvider serviceProvider, IHostApplicationLifetime hostApplicationLifetime)
: base(serviceProvider.GetRequiredService<ILogger<OnecMonitorConnection>>())
{
var configuration = serviceProvider.GetRequiredService<IConfiguration>();

View File

@ -1,7 +1,7 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Default": "Trace",
"Microsoft.Hosting.Lifetime": "Information"
}
},

View File

@ -33,6 +33,7 @@
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.1" />
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="9.0.1" />
</ItemGroup>

View File

@ -9,6 +9,6 @@ public class UpdateInfoBaseTaskDto
public Guid Id { get; set; }
[Key(1)]
public List<InfoBaseDto> InfoBases { get; set; } = [];
[Key(4)]
public List<ConfigurationDto> Configurations { get; set; } = [];
[Key(2)]
public List<V8FileDto> Files { get; set; } = [];
}

View File

@ -3,7 +3,7 @@ using MessagePack;
namespace OnecMonitor.Common.DTO;
[MessagePackObject]
public class ConfigurationDto
public class V8FileDto
{
[Key(0)]
public Guid Id { get; set; }

View File

@ -6,13 +6,15 @@ using System.Diagnostics.SymbolStore;
using System.Net.Sockets;
using System.Reflection.PortableExecutable;
using System.Threading.Channels;
using Microsoft.Extensions.Logging;
namespace OnecMonitor.Common;
public abstract class FastConnection : IDisposable
public abstract class FastConnection(ILogger<FastConnection> logger) : IDisposable
{
protected Socket? Socket;
private CancellationToken _cancellationToken;
private readonly ConcurrentDictionary<Guid, TaskCompletionSource<Message>> _calls = new();
private readonly Channel<Message> _inputChannel = Channel.CreateBounded<Message>(1000);
private readonly Channel<Message> _outputChannel = Channel.CreateBounded<Message>(1000);
@ -33,8 +35,10 @@ public abstract class FastConnection : IDisposable
protected void RunStreamLoops(CancellationToken cancellationToken)
{
_ = StartWritingToStream(cancellationToken);
_ = StartReadingFromStream(cancellationToken);
_cancellationToken = cancellationToken;
Task.Factory.StartNew(StartWritingToStream, TaskCreationOptions.LongRunning);
Task.Factory.StartNew(StartReadingFromStream, TaskCreationOptions.LongRunning);
_disconnectingEventSemaphore.Release();
}
@ -113,11 +117,14 @@ public abstract class FastConnection : IDisposable
{
var cts = new TaskCompletionSource<Message>();
_calls.TryAdd(message.Header.CallId, cts);
logger.LogTrace($"Постановка сообщения в очередь отправки. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
await _outputChannel.Writer.WriteAsync(message, cancellationToken);
logger.LogTrace($"Ожидание подтверждения на сообщение. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
var result = await cts.Task.WaitAsync(cancellationToken);
logger.LogTrace($"Подтверждение сообщения получено. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
if (result == null)
throw new TimeoutException("Ошибка получения ответа на вызов");
@ -139,11 +146,14 @@ public abstract class FastConnection : IDisposable
{
var cts = new TaskCompletionSource<Message>();
_calls.TryAdd(message.Header.CallId, cts);
logger.LogTrace($"Постановка сообщения в очередь отправки. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
await _outputChannel.Writer.WriteAsync(message, cancellationToken);
logger.LogTrace($"Ожидание подтверждения на сообщение. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
var result = await cts.Task.WaitAsync(cancellationToken);
logger.LogTrace($"Подтверждение сообщения получено. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
if (result == null)
throw new TimeoutException("Ошибка получения ответа на вызов");
@ -167,6 +177,8 @@ public abstract class FastConnection : IDisposable
if (header.Length > 0)
await Socket!.SendAsync(data, cancellationToken);
logger.LogTrace($"Отправлено сообщение в поток. Тип: {header.Type}. Идентификатор: {header.CallId}");
}
catch
{
@ -174,18 +186,20 @@ public abstract class FastConnection : IDisposable
}
}
private async Task StartWritingToStream(CancellationToken cancellationToken)
private async Task StartWritingToStream()
{
try
{
while (!cancellationToken.IsCancellationRequested)
while (!_cancellationToken.IsCancellationRequested)
{
var item = await _outputChannel.Reader.ReadAsync(cancellationToken);
var message = await _outputChannel.Reader.ReadAsync(_cancellationToken);
await Socket!.SendAsync(item.Header.AsMemory(), cancellationToken);
await Socket!.SendAsync(message.Header.AsMemory(), _cancellationToken);
if (item.Data.Length > 0)
await Socket!.SendAsync(item.Data, cancellationToken);
if (message.Data.Length > 0)
await Socket!.SendAsync(message.Data, _cancellationToken);
logger.LogTrace($"Отправлено сообщение в поток. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
}
}
catch
@ -194,29 +208,31 @@ public abstract class FastConnection : IDisposable
}
}
private async Task StartReadingFromStream(CancellationToken cancellationToken)
private async Task StartReadingFromStream()
{
try
{
while (!cancellationToken.IsCancellationRequested)
while (!_cancellationToken.IsCancellationRequested)
{
var headerBuffer = await ReadBytesFromStream(MessageHeader.HeaderLength, cancellationToken);
var headerBuffer = await ReadBytesFromStream(MessageHeader.HeaderLength, _cancellationToken);
var header = MessageHeader.FromSpan(headerBuffer.Span);
Message message;
if (header.Length > 0)
{
var dataBuffer = await ReadBytesFromStream(header.Length, cancellationToken);
var dataBuffer = await ReadBytesFromStream(header.Length, _cancellationToken);
message = new Message(header, dataBuffer);
}
else
message = new Message(header);
logger.LogTrace($"Получено сообщение из потока. Тип: {header.Type}. Идентификатор: {header.CallId}");
if (_calls.TryGetValue(message.Header.CallId, out var cts))
cts.TrySetResult(message);
else
await _inputChannel.Writer.WriteAsync(message, cancellationToken);
await _inputChannel.Writer.WriteAsync(message, _cancellationToken);
}
}
catch

View File

@ -0,0 +1,34 @@
syntax = "proto3";
option csharp_namespace = "OnecMonitor.Common";
service OnecMonitorService {
}
message AgentInstanceDto {
string id = 1;
string instanceName = 2;
double UtcOffset = 3;
}
message ClusterDto {
string id = 1;
string host = 2;
int32 port = 3;
}
message CredentialsDto {
string user = 1;
string password = 2;
}
message InfoBaseDto {
string id = 1;
string internalId = 2;
string infoBaseName = 3;
CredentialsDto credentials = 4;
string publishAddress = 5;
ClusterDto cluster = 6;
}

View File

@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging;
namespace OnecMonitor.Common;
public class ServerConnection : FastConnection
public class ServerConnection(ILogger<ServerConnection> logger) : FastConnection(logger)
{
private ILogger<ServerConnection> _logger = null!;
private string _host = null!;

View File

@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OnecMonitor.Server.Converters.Sqlite;
using OnecMonitor.Server.Models;
using OnecMonitor.Common.DTO;
using OnecMonitor.Server.Models.MaintenanceTasks;
namespace OnecMonitor.Server
{
@ -15,13 +16,15 @@ namespace OnecMonitor.Server
public DbSet<LogTemplate> LogTemplates { get; set; }
public DbSet<TechLogSeance> TechLogSeances { get; set; }
public DbSet<TechLogFilter> TechLogFilters { get; set; }
public DbSet<V8Configuration> Configurations { get; set; }
public DbSet<V8File> V8Files { get; set; }
public DbSet<Credentials> Credentials { get; set; }
public DbSet<InfoBase> InfoBases { get; set; }
public DbSet<Cluster> Clusters { get; set; }
public DbSet<UpdateInfoBaseTask> UpdateInfoBaseTasks { get; set; }
public DbSet<UpdateInfoBaseTaskLogItem> UpdateInfoBaseTaskLogItems { get; set; }
public DbSet<TechLogSettings> TechLogSettings { get; set; }
public DbSet<MaintenanceTask> MaintenanceTasks { get; set; }
public DbSet<MaintenanceStepNode> MaintenanceStepNodes { get; set; }
public AppDbContext(IHostEnvironment hostEnvironment)
=> DbPath = Path.Join(hostEnvironment.ContentRootPath, "om-server.db");

View File

@ -1,10 +1,14 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AutoMapper;
using OnecMonitor.Server.Models;
using OnecMonitor.Server.Models.MaintenanceTasks;
using OnecMonitor.Server.ViewModels.Agents;
using OnecMonitor.Server.ViewModels.Clusters;
using OnecMonitor.Server.ViewModels.Configurations;
using OnecMonitor.Server.ViewModels.V8Files;
using OnecMonitor.Server.ViewModels.Credentials;
using OnecMonitor.Server.ViewModels.InfoBases;
using OnecMonitor.Server.ViewModels.MaintenanceTasks;
using OnecMonitor.Server.ViewModels.TechLogSeances;
using OnecMonitor.Server.ViewModels.TechLogSettings;
using OnecMonitor.Server.ViewModels.UpdateInfoBaseTasks;
@ -22,7 +26,7 @@ public class CommonProfile : Profile
CreateMap<Cluster, SelectableItemViewModel>().ReverseMap();
CreateMap<LogTemplate, SelectableItemViewModel>().ReverseMap();
CreateMap<Credentials, SelectableItemViewModel>().ReverseMap();
CreateMap<V8Configuration, SelectableItemViewModel>()
CreateMap<V8File, SelectableItemViewModel>()
.ForMember(c => c.Name, opt => opt.MapFrom(src => src.ToString()))
.ReverseMap();
@ -36,11 +40,11 @@ public class CommonProfile : Profile
.ForMember(c => c.InfoBases, i => i.Ignore())
.ForMember(c => c.Clusters, i => i.Ignore());
CreateMap<V8Configuration, ConfigurationListItemViewModel>()
CreateMap<V8File, V8FileListItemViewModel>()
.ReverseMap()
.ForMember(c => c.Id, i => i.Ignore());
CreateMap<V8Configuration, ConfigurationEditViewModel>()
CreateMap<V8File, V8FileEditViewModel>()
.ReverseMap()
.ForMember(c => c.Id, i => i.Ignore());
@ -67,7 +71,7 @@ public class CommonProfile : Profile
.ForMember(c => c.Id, i => i.Ignore())
.ForMember(c => c.Cluster, i => i.Ignore());
CreateMap<V8Configuration, ConfigurationEditViewModel>()
CreateMap<V8File, V8FileEditViewModel>()
.ReverseMap()
.ForMember(c => c.Id, i => i.Ignore());
@ -79,12 +83,32 @@ public class CommonProfile : Profile
CreateMap<UpdateInfoBaseTask, UpdateInfoBaseTaskEditViewModel>()
.ReverseMap()
.ForMember(c => c.Id, i => i.Ignore())
.ForMember(c => c.Configurations, i => i.Ignore())
.ForMember(c => c.Files, i => i.Ignore())
.ForMember(c => c.InfoBases, i => i.Ignore())
.ForMember(c => c.Log, i => i.Ignore());
CreateMap<TechLogSettings, TechLogSettingsEditViewModel>()
.ReverseMap()
.ForMember(c => c.Id, i => i.Ignore());
CreateMap<MaintenanceTask, MaintenanceTaskEditViewModel>()
.ReverseMap()
.ForMember(c => c.Id, i => i.Ignore())
.ForMember(c => c.InfoBases, i => i.Ignore());
CreateMap<MaintenanceTask, MaintenanceTaskListItemViewModel>().ReverseMap();
CreateMap<MaintenanceStep, MaintenanceStepViewModel>()
.ReverseMap()
.ForMember(c => c.Id, i => i.Ignore())
.ForMember(c => c.File, i => i.Ignore());
CreateMap<MaintenanceStepNode, MaintenanceStepNodeViewModel>()
.ForMember(c => c.StepId, i => i.MapFrom(src => src.Step.Id))
.ReverseMap()
.ForMember(c => c.Id, i => i.Ignore())
.ForMember(c => c.LeftNode, i => i.Ignore())
.ForMember(c => c.RightNode, i => i.Ignore())
.ForMember(c => c.Step, i => i.Ignore());
}
}

View File

@ -14,7 +14,7 @@ public class DtoProfile : Profile
.ForMember(c => c.Id, opt => opt.MapFrom(src => src.ClusterInternalId))
.ReverseMap();
CreateMap<V8Configuration, ConfigurationDto>()
CreateMap<V8File, V8FileDto>()
.ForMember(c => c.Data, opt => opt.MapFrom(src => File.ReadAllBytes(src.DataPath)))
.ReverseMap();

View File

@ -38,7 +38,7 @@ public class CredentialsController(AppDbContext appDbContext, IMapper mapper) :
if (!ModelState.IsValid)
return View("Edit", await PrepareViewModel(vm, cancellationToken));
var model = isNew ? new Credentials()
var model = isNew ? new Credentials
{
Id = Guid.NewGuid()
} : await appDbContext.Credentials

View File

@ -1,73 +0,0 @@
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using OnecMonitor.Server.Helpers;
using OnecMonitor.Server.Models;
using OnecMonitor.Server.ViewModels.Maintenance;
namespace OnecMonitor.Server.Controllers;
public class MaintenanceController(AppDbContext appDbContext, IMapper mapper) : Controller
{
public async Task<IActionResult> Index()
{
return View(new MaintenanceIndexViewModel());
}
public async Task<IActionResult> Edit(Guid id, CancellationToken cancellationToken)
{
return View(await PrepareViewModel(new MaintenanceEditViewModel(), cancellationToken));
}
public IActionResult Step(MaintenanceStepViewModel vm)
=> vm.Kind switch
{
MaintenanceStepKind.LockConnections
or MaintenanceStepKind.LoadExtension
or MaintenanceStepKind.UpdateConfiguration
or MaintenanceStepKind.LoadConfiguration
or MaintenanceStepKind.StartExternalDataProcessor => PartialView($"Steps/{vm.Kind}", vm),
MaintenanceStepKind.UnlockConnections or MaintenanceStepKind.CloseConnections
or MaintenanceStepKind.UpdateDatabase => Ok(),
_ => NotFound()
};
public IActionResult ValidateStep(MaintenanceStepViewModel vm)
{
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (vm.Kind)
{
case MaintenanceStepKind.LoadExtension:
case MaintenanceStepKind.UpdateConfiguration:
case MaintenanceStepKind.LoadConfiguration:
case MaintenanceStepKind.StartExternalDataProcessor:
if (vm.FileId == null || vm.FileId == Guid.Empty)
ModelState.AddModelError(nameof(vm.FileId), "Не указан файл");
break;
case MaintenanceStepKind.LockConnections:
if (string.IsNullOrEmpty(vm.AccessCode))
ModelState.AddModelError(nameof(vm.AccessCode), "Не указан код доступа");
if (string.IsNullOrEmpty(vm.Message))
ModelState.AddModelError(nameof(vm.Message), "Не указан тест сообщения");
break;
}
var view = PartialView($"Steps/{vm.Kind}", vm);
view.StatusCode = ModelState.IsValid ? 200 : 400;
return view;
}
private async Task<MaintenanceEditViewModel> PrepareViewModel(MaintenanceEditViewModel vm, CancellationToken cancellationToken)
{
vm.AvailableInfoBases = await UiHelper.SelectableItemsFrom(
appDbContext.InfoBases,
vm.InfoBases,
mapper,
cancellationToken);
vm.MaintenanceStepKinds = UiHelper.SelectListFromEnum<MaintenanceStepKind>();
return vm;
}
}

View File

@ -0,0 +1,182 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using OnecMonitor.Server.Helpers;
using OnecMonitor.Server.Models;
using OnecMonitor.Server.Models.MaintenanceTasks;
using OnecMonitor.Server.ViewModels;
using OnecMonitor.Server.ViewModels.MaintenanceTasks;
namespace OnecMonitor.Server.Controllers;
public class MaintenanceTasksController(AppDbContext appDbContext, IMapper mapper) : Controller
{
private readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
public async Task<IActionResult> Index(CancellationToken cancellationToken)
=> View(new MaintenanceTasksIndexViewModel
{
Items = await appDbContext.MaintenanceTasks
.AsNoTracking()
.ProjectTo<MaintenanceTaskListItemViewModel>(mapper.ConfigurationProvider)
.ToListAsync(cancellationToken)
});
public async Task<IActionResult> Edit(Guid id, CancellationToken cancellationToken)
{
if (id == Guid.Empty)
return View(await PrepareViewModel(new MaintenanceTaskEditViewModel(), cancellationToken));
var model = await appDbContext.MaintenanceTasks
.AsNoTracking()
.Include(c => c.RootNode)
.ThenInclude(c => c.Step)
.AsNoTracking()
.Include(c => c.InfoBases)
.FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
if (model == null)
return NotFound();
await LoadNodesRecursively(model.RootNode, cancellationToken);
var vm = mapper.Map<MaintenanceTaskEditViewModel>(model);
var rootNodeVm = mapper.Map<MaintenanceStepNodeViewModel>(model.RootNode);
vm.SerializedStepNode = JsonSerializer.Serialize(rootNodeVm, _jsonOptions);
return View(await PrepareViewModel(vm, cancellationToken));
}
[HttpPost]
public async Task<IActionResult> Save(MaintenanceTaskEditViewModel vm, CancellationToken cancellationToken)
{
var isNew = vm.Id == Guid.Empty;
if (!ModelState.IsValid)
return View("Edit", await PrepareViewModel(vm, cancellationToken));
var model = isNew ? new MaintenanceTask
{
Id = Guid.NewGuid()
} : await appDbContext.MaintenanceTasks
.Include(c => c.RootNode)
.FirstOrDefaultAsync(i => i.Id == vm.Id, cancellationToken);
if (model == null)
return NotFound();
mapper.Map(vm, model);
model.RootNode = JsonSerializer.Deserialize<MaintenanceStepNode>(vm.SerializedStepNode, _jsonOptions)!;
model.RootNodeId = model.RootNode.Id;
await UiHelper.UpdateModelItems(appDbContext.InfoBases, vm.InfoBases, model.InfoBases, cancellationToken);
if (isNew)
appDbContext.MaintenanceTasks.Add(model);
else
appDbContext.MaintenanceTasks.Update(model);
await appDbContext.SaveChangesAsync(cancellationToken);
return RedirectToAction("Index");
}
[HttpPost]
public async Task<IActionResult> EditStep(CancellationToken cancellationToken)
{
var vm = await HttpContext.Request.ReadFromJsonAsync<MaintenanceStepViewModel>(_jsonOptions, cancellationToken);
return await UpdateStep(vm!, cancellationToken);
}
[HttpPost]
public async Task<IActionResult> UpdateStep(MaintenanceStepViewModel vm, CancellationToken cancellationToken)
=> PartialView("EditStep", await PrepareViewModel(vm, cancellationToken));
[HttpPost]
public async Task<IActionResult> ValidateStep(MaintenanceStepViewModel vm, CancellationToken cancellationToken)
{
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (vm.Kind)
{
case MaintenanceStepKind.LoadExtension:
case MaintenanceStepKind.UpdateConfiguration:
case MaintenanceStepKind.LoadConfiguration:
case MaintenanceStepKind.StartExternalDataProcessor:
if (vm.FileId == null || vm.FileId == Guid.Empty)
ModelState.AddModelError(nameof(vm.FileId), "Не указан файл");
break;
case MaintenanceStepKind.LockConnections:
if (string.IsNullOrEmpty(vm.AccessCode))
ModelState.AddModelError(nameof(vm.AccessCode), "Не указан код доступа");
if (string.IsNullOrEmpty(vm.Message))
ModelState.AddModelError(nameof(vm.Message), "Не указан тест сообщения");
break;
}
if (ModelState.IsValid)
return Json(vm);
var view = PartialView("EditStep", await PrepareViewModel(vm, cancellationToken));
view.StatusCode = 400;
return view;
}
private async Task<MaintenanceTaskEditViewModel> PrepareViewModel(MaintenanceTaskEditViewModel vm, CancellationToken cancellationToken)
{
vm.AvailableInfoBases = await UiHelper.SelectableItemsFrom(
appDbContext.InfoBases,
vm.InfoBases,
mapper,
cancellationToken);
return vm;
}
private async Task<MaintenanceStepViewModel> PrepareViewModel(MaintenanceStepViewModel vm, CancellationToken cancellationToken)
{
vm.Kinds = UiHelper.SelectListFromEnum<MaintenanceStepKind>(vm.Kind);
var files = vm.Kind switch
{
MaintenanceStepKind.LoadConfiguration => appDbContext.V8Files.Where(c => c.IsConfiguration),
MaintenanceStepKind.LoadExtension => appDbContext.V8Files.Where(c => c.IsExtension),
MaintenanceStepKind.UpdateConfiguration => appDbContext.V8Files.Where(c => c.IsUpdate),
MaintenanceStepKind.StartExternalDataProcessor => appDbContext.V8Files.Where(c => c.IsExternalDataProcessor),
_ => null
};
if (files is not null)
vm.Files = await UiHelper.SelectListFrom(files, c => c.ToString(), vm.FileId, cancellationToken);
return vm;
}
private async Task LoadNodesRecursively(MaintenanceStepNode node, CancellationToken cancellationToken)
{
if (node.LeftNodeId != null && node.LeftNodeId != Guid.Empty)
node.LeftNode = await LoadNodeRecursively(node.LeftNodeId, cancellationToken);
if (node.RightNodeId != null && node.RightNodeId != Guid.Empty)
node.RightNode = await LoadNodeRecursively(node.RightNodeId, cancellationToken);
}
private async Task<MaintenanceStepNode> LoadNodeRecursively(Guid? id, CancellationToken cancellationToken)
{
var node = (await appDbContext.MaintenanceStepNodes
.AsNoTracking()
.Include(c => c.Step)
.FirstOrDefaultAsync(c => c.Id == id, cancellationToken))!;
await LoadNodesRecursively(node, cancellationToken);
return node;
}
}

View File

@ -27,7 +27,7 @@ public class UpdateInfoBaseTasksController(AppDbContext appDbContext, AgentsConn
var vm = id != Guid.Empty
? await appDbContext.UpdateInfoBaseTasks
.AsNoTracking()
.Include(c => c.Configurations)
.Include(c => c.Files)
.Include(c => c.InfoBases)
.ProjectTo<UpdateInfoBaseTaskEditViewModel>(mapper.ConfigurationProvider)
.FirstOrDefaultAsync(cancellationToken)
@ -43,8 +43,8 @@ public class UpdateInfoBaseTasksController(AppDbContext appDbContext, AgentsConn
{
var isNew = vm.Id == Guid.Empty;
var configIds = vm.Configurations.Select(c => c.Id).ToList();
var configs = appDbContext.Configurations
var configIds = vm.Files.Select(c => c.Id).ToList();
var configs = appDbContext.V8Files
.AsNoTracking()
.Where(c => configIds.Contains(c.Id.ToString()))
.ToList();
@ -52,7 +52,7 @@ public class UpdateInfoBaseTasksController(AppDbContext appDbContext, AgentsConn
var countOfUpdatesAndConfigs = configs.Count(c => c.IsUpdate || c.IsConfiguration);
if (countOfUpdatesAndConfigs > 1)
ModelState.AddModelError(
nameof(UpdateInfoBaseTaskEditViewModel.Configurations),
nameof(UpdateInfoBaseTaskEditViewModel.Files),
"Список конфигураций может содержать только одну конфигурацию или обновление конфигурации");
if (!ModelState.IsValid)
@ -63,7 +63,7 @@ public class UpdateInfoBaseTasksController(AppDbContext appDbContext, AgentsConn
Id = Guid.NewGuid()
} : await appDbContext.UpdateInfoBaseTasks
.Include(c => c.InfoBases)
.Include(c => c.Configurations)
.Include(c => c.Files)
.Include(c => c.Log)
.FirstOrDefaultAsync(i => i.Id == vm.Id, cancellationToken);
@ -76,7 +76,7 @@ public class UpdateInfoBaseTasksController(AppDbContext appDbContext, AgentsConn
mapper.Map(vm, model);
await UiHelper.UpdateModelItems(appDbContext.InfoBases, vm.InfoBases, model.InfoBases, cancellationToken);
await UiHelper.UpdateModelItems(appDbContext.Configurations, vm.Configurations, model.Configurations, cancellationToken);
await UiHelper.UpdateModelItems(appDbContext.V8Files, vm.Files, model.Files, cancellationToken);
await appDbContext.SaveChangesAsync(cancellationToken);
@ -190,9 +190,9 @@ public class UpdateInfoBaseTasksController(AppDbContext appDbContext, AgentsConn
private async Task<UpdateInfoBaseTaskEditViewModel> PrepareViewModel(UpdateInfoBaseTaskEditViewModel vm, CancellationToken cancellationToken)
{
vm.AvailableConfigurations = await UiHelper.SelectableItemsFrom(
appDbContext.Configurations,
vm.Configurations,
vm.AvailableFiles = await UiHelper.SelectableItemsFrom(
appDbContext.V8Files,
vm.Files,
mapper,
cancellationToken);

View File

@ -1,26 +1,29 @@
using System.Net.Http.Headers;
using System.Runtime.InteropServices.JavaScript;
using System.Text.Json;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using OnecMonitor.Server;
using OnecMonitor.Server.Models;
using OnecMonitor.Server.ViewModels;
using OnecMonitor.Server.ViewModels.Configurations;
using OnecMonitor.Server.ViewModels.V8Files;
using OneSTools.Common.Platform.Unpack;
namespace OnecMonitor.Server.Controllers;
public class ConfigurationsController(AppDbContext appDbContext, IMapper mapper, IWebHostEnvironment webHostEnvironment) : Controller
public class V8FilesController(AppDbContext appDbContext, IMapper mapper, IWebHostEnvironment webHostEnvironment) : Controller
{
public async Task<IActionResult> Index()
{
return View(new ConfigurationsIndexViewModel
return View(new V8FilesIndexViewModel
{
Items = await appDbContext.Configurations
.ProjectTo<ConfigurationListItemViewModel>(mapper.ConfigurationProvider)
Items = await appDbContext.V8Files
.ProjectTo<V8FileListItemViewModel>(mapper.ConfigurationProvider)
.ToListAsync()
});
}
@ -28,14 +31,14 @@ public class ConfigurationsController(AppDbContext appDbContext, IMapper mapper,
public async Task<IActionResult> Edit(Guid id)
{
if (id == Guid.Empty)
return View(new ConfigurationEditViewModel());
return View(new V8FileEditViewModel());
var item = await appDbContext.Configurations.FindAsync(id);
var item = await appDbContext.V8Files.FindAsync(id);
if (item == null)
return NotFound();
return View(new ConfigurationEditViewModel
return View(new V8FileEditViewModel
{
Id = item.Id,
Name = item.Name,
@ -48,31 +51,46 @@ public class ConfigurationsController(AppDbContext appDbContext, IMapper mapper,
MultipartBodyLengthLimit = int.MaxValue,
ValueLengthLimit = int.MaxValue)
]
public async Task<IActionResult> Save(Guid id, ConfigurationEditViewModel vm, CancellationToken cancellationToken)
public async Task<IActionResult> Save(Guid id, V8FileEditViewModel vm, CancellationToken cancellationToken)
{
var isNew = id == Guid.Empty;
if (!isNew)
ModelState.Remove(nameof(ConfigurationEditViewModel.File));
ModelState.Remove(nameof(V8FileEditViewModel.File));
if (!ModelState.IsValid)
return View("Edit", vm);
var model = isNew ? new V8Configuration
var model = isNew ? new V8File
{
Id = Guid.NewGuid()
} : await appDbContext.Configurations.FindAsync([id], cancellationToken);
} : await appDbContext.V8Files.FindAsync([id], cancellationToken);
if (model == null)
return NotFound();
if (isNew)
{
// save file
var extension = Path.GetExtension(vm.File.FileName);
if (extension.Equals(".CF", StringComparison.InvariantCultureIgnoreCase))
model.IsConfiguration = true;
else if (extension.Equals(".CFE", StringComparison.InvariantCultureIgnoreCase))
model.IsExtension = true;
else if (extension.Equals(".CFU", StringComparison.InvariantCultureIgnoreCase))
model.IsUpdate = true;
else if (extension.Equals(".EPF", StringComparison.InvariantCultureIgnoreCase))
model.IsExternalDataProcessor = true;
else
ModelState.AddModelError(nameof(V8FileEditViewModel.File), "Invalid file format");
if (!ModelState.IsValid)
return View("Edit", vm);
var fileName = $"{vm.Name}_{vm.Version}{extension}";
var path = Path.Combine(webHostEnvironment.ContentRootPath, "Data", fileName);
if (System.IO.File.Exists(path))
return View("Error", new ErrorViewModel($"File {path} already exists."));
@ -81,34 +99,27 @@ public class ConfigurationsController(AppDbContext appDbContext, IMapper mapper,
model.DataPath = path;
await appDbContext.Configurations.AddAsync(model, cancellationToken);
if (extension.Equals(".CF", StringComparison.InvariantCultureIgnoreCase))
model.IsConfiguration = true;
else if (extension.Equals(".CFE", StringComparison.InvariantCultureIgnoreCase))
model.IsExtension = true;
else if (extension.Equals(".CFU", StringComparison.InvariantCultureIgnoreCase))
model.IsUpdate = true;
await appDbContext.V8Files.AddAsync(model, cancellationToken);
}
model.Name = vm.Name;
model.Version = vm.Version;
await appDbContext.SaveChangesAsync(cancellationToken);
return RedirectToAction("Index");
}
public async Task<IActionResult> Delete(Guid id, CancellationToken cancellationToken)
{
var item = await appDbContext.Configurations.FindAsync(
var item = await appDbContext.V8Files.FindAsync(
[id],
cancellationToken: cancellationToken);
if (System.IO.File.Exists(item!.DataPath))
System.IO.File.Delete(item.DataPath);
appDbContext.Configurations.Remove(item!);
appDbContext.V8Files.Remove(item!);
await appDbContext.SaveChangesAsync(cancellationToken);
return RedirectToAction("Index");

View File

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace OnecMonitor.Server.Extensions;
public static class EnumExtension
{
public static string GetDisplay(this Enum value)
=> value.GetAttributeOfType<DisplayAttribute>()?.Name ?? value.ToString();
private static T? GetAttributeOfType<T>(this Enum enumVal) where T : Attribute
{
var type = enumVal.GetType();
var memInfo = type.GetMember(enumVal.ToString());
var attributes = memInfo[0].GetCustomAttributes(typeof(T), false);
return (attributes.Length > 0) ? (T)attributes[0] : null;
}
}

View File

@ -9,7 +9,7 @@ namespace OnecMonitor.Server.Helpers;
public static class UiHelper
{
public static SelectList SelectListFromEnum<T1>() where T1 : struct, Enum
public static SelectList SelectListFromEnum<T1>(T1? selectedValue = null) where T1 : struct, Enum
{
var values = Enum.GetValues<T1>().ToList();
var selectListItems = values.Select(i => new { Id = i.ToString(), Name = i.GetAttributeOfType<DisplayAttribute>()?.Name ?? i.ToString() }).ToList();
@ -18,7 +18,8 @@ public static class UiHelper
return new SelectList(
selectListItems,
"Id",
"Name");
"Name",
selectedValue == null ? "" : selectedValue);
}
public static async Task<SelectList> SelectListFrom<T1>(

View File

@ -10,7 +10,7 @@ using OnecMonitor.Server;
namespace OnecMonitor.Server.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250214151907_Initial")]
[Migration("20250225183414_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -170,6 +170,9 @@ namespace OnecMonitor.Server.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("MaintenanceTaskId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
@ -184,6 +187,8 @@ namespace OnecMonitor.Server.Migrations
b.HasIndex("CredentialsId");
b.HasIndex("MaintenanceTaskId");
b.ToTable("InfoBases");
});
@ -206,6 +211,87 @@ namespace OnecMonitor.Server.Migrations
b.ToTable("LogTemplates");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStep", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AccessCode")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<string>("FileId")
.HasColumnType("TEXT");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("FileId");
b.ToTable("MaintenanceStep");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<string>("LeftNodeId")
.HasColumnType("TEXT");
b.Property<string>("RightNodeId")
.HasColumnType("TEXT");
b.Property<string>("StepId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LeftNodeId");
b.HasIndex("RightNodeId");
b.HasIndex("StepId");
b.ToTable("MaintenanceStepNodes");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("RootNodeId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RootNodeId");
b.ToTable("MaintenanceTasks");
});
modelBuilder.Entity("OnecMonitor.Server.Models.TechLogFilter", b =>
{
b.Property<string>("Id")
@ -344,7 +430,7 @@ namespace OnecMonitor.Server.Migrations
b.ToTable("UpdateInfoBaseTaskLogItems");
});
modelBuilder.Entity("OnecMonitor.Server.Models.V8Configuration", b =>
modelBuilder.Entity("OnecMonitor.Server.Models.V8File", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
@ -360,6 +446,9 @@ namespace OnecMonitor.Server.Migrations
b.Property<bool>("IsExtension")
.HasColumnType("INTEGER");
b.Property<bool>("IsExternalDataProcessor")
.HasColumnType("INTEGER");
b.Property<bool>("IsUpdate")
.HasColumnType("INTEGER");
@ -373,22 +462,22 @@ namespace OnecMonitor.Server.Migrations
b.HasKey("Id");
b.ToTable("Configurations");
b.ToTable("V8Files");
});
modelBuilder.Entity("UpdateInfoBaseTaskV8Configuration", b =>
modelBuilder.Entity("UpdateInfoBaseTaskV8File", b =>
{
b.Property<string>("ConfigurationsId")
b.Property<string>("FilesId")
.HasColumnType("TEXT");
b.Property<string>("UpdateTasksId")
.HasColumnType("TEXT");
b.HasKey("ConfigurationsId", "UpdateTasksId");
b.HasKey("FilesId", "UpdateTasksId");
b.HasIndex("UpdateTasksId");
b.ToTable("UpdateInfoBaseTaskV8Configuration");
b.ToTable("UpdateInfoBaseTaskV8File");
});
modelBuilder.Entity("AgentTechLogSeance", b =>
@ -467,11 +556,58 @@ namespace OnecMonitor.Server.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", null)
.WithMany("InfoBases")
.HasForeignKey("MaintenanceTaskId");
b.Navigation("Cluster");
b.Navigation("Credentials");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStep", b =>
{
b.HasOne("OnecMonitor.Server.Models.V8File", "File")
.WithMany()
.HasForeignKey("FileId");
b.Navigation("File");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", b =>
{
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", "LeftNode")
.WithMany()
.HasForeignKey("LeftNodeId");
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", "RightNode")
.WithMany()
.HasForeignKey("RightNodeId");
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStep", "Step")
.WithMany()
.HasForeignKey("StepId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("LeftNode");
b.Navigation("RightNode");
b.Navigation("Step");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b =>
{
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", "RootNode")
.WithMany()
.HasForeignKey("RootNodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("RootNode");
});
modelBuilder.Entity("OnecMonitor.Server.Models.UpdateInfoBaseTaskLogItem", b =>
{
b.HasOne("OnecMonitor.Server.Models.InfoBase", "InfoBase")
@ -491,11 +627,11 @@ namespace OnecMonitor.Server.Migrations
b.Navigation("Task");
});
modelBuilder.Entity("UpdateInfoBaseTaskV8Configuration", b =>
modelBuilder.Entity("UpdateInfoBaseTaskV8File", b =>
{
b.HasOne("OnecMonitor.Server.Models.V8Configuration", null)
b.HasOne("OnecMonitor.Server.Models.V8File", null)
.WithMany()
.HasForeignKey("ConfigurationsId")
.HasForeignKey("FilesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -523,6 +659,11 @@ namespace OnecMonitor.Server.Migrations
b.Navigation("InfoBases");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b =>
{
b.Navigation("InfoBases");
});
modelBuilder.Entity("OnecMonitor.Server.Models.UpdateInfoBaseTask", b =>
{
b.Navigation("Log");

View File

@ -22,23 +22,6 @@ namespace OnecMonitor.Server.Migrations
table.PrimaryKey("PK_Agents", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Configurations",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Version = table.Column<string>(type: "TEXT", nullable: false),
DataPath = table.Column<string>(type: "TEXT", nullable: false),
IsUpdate = table.Column<bool>(type: "INTEGER", nullable: false),
IsExtension = table.Column<bool>(type: "INTEGER", nullable: false),
IsConfiguration = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Configurations", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Credentials",
columns: table => new
@ -127,6 +110,24 @@ namespace OnecMonitor.Server.Migrations
table.PrimaryKey("PK_UpdateInfoBaseTasks", x => x.Id);
});
migrationBuilder.CreateTable(
name: "V8Files",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Version = table.Column<string>(type: "TEXT", nullable: false),
DataPath = table.Column<string>(type: "TEXT", nullable: false),
IsUpdate = table.Column<bool>(type: "INTEGER", nullable: false),
IsExtension = table.Column<bool>(type: "INTEGER", nullable: false),
IsConfiguration = table.Column<bool>(type: "INTEGER", nullable: false),
IsExternalDataProcessor = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_V8Files", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Clusters",
columns: table => new
@ -204,25 +205,95 @@ namespace OnecMonitor.Server.Migrations
});
migrationBuilder.CreateTable(
name: "UpdateInfoBaseTaskV8Configuration",
name: "MaintenanceStep",
columns: table => new
{
ConfigurationsId = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(type: "TEXT", nullable: false),
Kind = table.Column<int>(type: "INTEGER", nullable: false),
AccessCode = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
Message = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
FileId = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MaintenanceStep", x => x.Id);
table.ForeignKey(
name: "FK_MaintenanceStep_V8Files_FileId",
column: x => x.FileId,
principalTable: "V8Files",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "UpdateInfoBaseTaskV8File",
columns: table => new
{
FilesId = table.Column<string>(type: "TEXT", nullable: false),
UpdateTasksId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UpdateInfoBaseTaskV8Configuration", x => new { x.ConfigurationsId, x.UpdateTasksId });
table.PrimaryKey("PK_UpdateInfoBaseTaskV8File", x => new { x.FilesId, x.UpdateTasksId });
table.ForeignKey(
name: "FK_UpdateInfoBaseTaskV8Configuration_Configurations_ConfigurationsId",
column: x => x.ConfigurationsId,
principalTable: "Configurations",
name: "FK_UpdateInfoBaseTaskV8File_UpdateInfoBaseTasks_UpdateTasksId",
column: x => x.UpdateTasksId,
principalTable: "UpdateInfoBaseTasks",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UpdateInfoBaseTaskV8Configuration_UpdateInfoBaseTasks_UpdateTasksId",
column: x => x.UpdateTasksId,
principalTable: "UpdateInfoBaseTasks",
name: "FK_UpdateInfoBaseTaskV8File_V8Files_FilesId",
column: x => x.FilesId,
principalTable: "V8Files",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MaintenanceStepNodes",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Kind = table.Column<int>(type: "INTEGER", nullable: false),
LeftNodeId = table.Column<string>(type: "TEXT", nullable: true),
RightNodeId = table.Column<string>(type: "TEXT", nullable: true),
StepId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MaintenanceStepNodes", x => x.Id);
table.ForeignKey(
name: "FK_MaintenanceStepNodes_MaintenanceStepNodes_LeftNodeId",
column: x => x.LeftNodeId,
principalTable: "MaintenanceStepNodes",
principalColumn: "Id");
table.ForeignKey(
name: "FK_MaintenanceStepNodes_MaintenanceStepNodes_RightNodeId",
column: x => x.RightNodeId,
principalTable: "MaintenanceStepNodes",
principalColumn: "Id");
table.ForeignKey(
name: "FK_MaintenanceStepNodes_MaintenanceStep_StepId",
column: x => x.StepId,
principalTable: "MaintenanceStep",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MaintenanceTasks",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
RootNodeId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MaintenanceTasks", x => x.Id);
table.ForeignKey(
name: "FK_MaintenanceTasks_MaintenanceStepNodes_RootNodeId",
column: x => x.RootNodeId,
principalTable: "MaintenanceStepNodes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
@ -237,7 +308,8 @@ namespace OnecMonitor.Server.Migrations
InfoBaseName = table.Column<string>(type: "TEXT", nullable: false),
PublishAddress = table.Column<string>(type: "TEXT", nullable: false),
CredentialsId = table.Column<string>(type: "TEXT", nullable: false),
ClusterId = table.Column<string>(type: "TEXT", nullable: false)
ClusterId = table.Column<string>(type: "TEXT", nullable: false),
MaintenanceTaskId = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
@ -254,6 +326,11 @@ namespace OnecMonitor.Server.Migrations
principalTable: "Credentials",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_InfoBases_MaintenanceTasks_MaintenanceTaskId",
column: x => x.MaintenanceTaskId,
principalTable: "MaintenanceTasks",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
@ -334,6 +411,11 @@ namespace OnecMonitor.Server.Migrations
table: "InfoBases",
column: "CredentialsId");
migrationBuilder.CreateIndex(
name: "IX_InfoBases_MaintenanceTaskId",
table: "InfoBases",
column: "MaintenanceTaskId");
migrationBuilder.CreateIndex(
name: "IX_InfoBaseUpdateInfoBaseTask_UpdateTasksId",
table: "InfoBaseUpdateInfoBaseTask",
@ -344,6 +426,31 @@ namespace OnecMonitor.Server.Migrations
table: "LogTemplateTechLogSeance",
column: "TemplatesId");
migrationBuilder.CreateIndex(
name: "IX_MaintenanceStep_FileId",
table: "MaintenanceStep",
column: "FileId");
migrationBuilder.CreateIndex(
name: "IX_MaintenanceStepNodes_LeftNodeId",
table: "MaintenanceStepNodes",
column: "LeftNodeId");
migrationBuilder.CreateIndex(
name: "IX_MaintenanceStepNodes_RightNodeId",
table: "MaintenanceStepNodes",
column: "RightNodeId");
migrationBuilder.CreateIndex(
name: "IX_MaintenanceStepNodes_StepId",
table: "MaintenanceStepNodes",
column: "StepId");
migrationBuilder.CreateIndex(
name: "IX_MaintenanceTasks_RootNodeId",
table: "MaintenanceTasks",
column: "RootNodeId");
migrationBuilder.CreateIndex(
name: "IX_UpdateInfoBaseTaskLogItems_InfoBaseId",
table: "UpdateInfoBaseTaskLogItems",
@ -355,8 +462,8 @@ namespace OnecMonitor.Server.Migrations
column: "TaskId");
migrationBuilder.CreateIndex(
name: "IX_UpdateInfoBaseTaskV8Configuration_UpdateTasksId",
table: "UpdateInfoBaseTaskV8Configuration",
name: "IX_UpdateInfoBaseTaskV8File_UpdateTasksId",
table: "UpdateInfoBaseTaskV8File",
column: "UpdateTasksId");
}
@ -382,7 +489,7 @@ namespace OnecMonitor.Server.Migrations
name: "UpdateInfoBaseTaskLogItems");
migrationBuilder.DropTable(
name: "UpdateInfoBaseTaskV8Configuration");
name: "UpdateInfoBaseTaskV8File");
migrationBuilder.DropTable(
name: "LogTemplates");
@ -393,20 +500,29 @@ namespace OnecMonitor.Server.Migrations
migrationBuilder.DropTable(
name: "InfoBases");
migrationBuilder.DropTable(
name: "Configurations");
migrationBuilder.DropTable(
name: "UpdateInfoBaseTasks");
migrationBuilder.DropTable(
name: "Clusters");
migrationBuilder.DropTable(
name: "MaintenanceTasks");
migrationBuilder.DropTable(
name: "Agents");
migrationBuilder.DropTable(
name: "Credentials");
migrationBuilder.DropTable(
name: "MaintenanceStepNodes");
migrationBuilder.DropTable(
name: "MaintenanceStep");
migrationBuilder.DropTable(
name: "V8Files");
}
}
}

View File

@ -167,6 +167,9 @@ namespace OnecMonitor.Server.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("MaintenanceTaskId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
@ -181,6 +184,8 @@ namespace OnecMonitor.Server.Migrations
b.HasIndex("CredentialsId");
b.HasIndex("MaintenanceTaskId");
b.ToTable("InfoBases");
});
@ -203,6 +208,87 @@ namespace OnecMonitor.Server.Migrations
b.ToTable("LogTemplates");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStep", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AccessCode")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<string>("FileId")
.HasColumnType("TEXT");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("FileId");
b.ToTable("MaintenanceStep");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<string>("LeftNodeId")
.HasColumnType("TEXT");
b.Property<string>("RightNodeId")
.HasColumnType("TEXT");
b.Property<string>("StepId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LeftNodeId");
b.HasIndex("RightNodeId");
b.HasIndex("StepId");
b.ToTable("MaintenanceStepNodes");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("RootNodeId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RootNodeId");
b.ToTable("MaintenanceTasks");
});
modelBuilder.Entity("OnecMonitor.Server.Models.TechLogFilter", b =>
{
b.Property<string>("Id")
@ -341,7 +427,7 @@ namespace OnecMonitor.Server.Migrations
b.ToTable("UpdateInfoBaseTaskLogItems");
});
modelBuilder.Entity("OnecMonitor.Server.Models.V8Configuration", b =>
modelBuilder.Entity("OnecMonitor.Server.Models.V8File", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
@ -357,6 +443,9 @@ namespace OnecMonitor.Server.Migrations
b.Property<bool>("IsExtension")
.HasColumnType("INTEGER");
b.Property<bool>("IsExternalDataProcessor")
.HasColumnType("INTEGER");
b.Property<bool>("IsUpdate")
.HasColumnType("INTEGER");
@ -370,22 +459,22 @@ namespace OnecMonitor.Server.Migrations
b.HasKey("Id");
b.ToTable("Configurations");
b.ToTable("V8Files");
});
modelBuilder.Entity("UpdateInfoBaseTaskV8Configuration", b =>
modelBuilder.Entity("UpdateInfoBaseTaskV8File", b =>
{
b.Property<string>("ConfigurationsId")
b.Property<string>("FilesId")
.HasColumnType("TEXT");
b.Property<string>("UpdateTasksId")
.HasColumnType("TEXT");
b.HasKey("ConfigurationsId", "UpdateTasksId");
b.HasKey("FilesId", "UpdateTasksId");
b.HasIndex("UpdateTasksId");
b.ToTable("UpdateInfoBaseTaskV8Configuration");
b.ToTable("UpdateInfoBaseTaskV8File");
});
modelBuilder.Entity("AgentTechLogSeance", b =>
@ -464,11 +553,58 @@ namespace OnecMonitor.Server.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", null)
.WithMany("InfoBases")
.HasForeignKey("MaintenanceTaskId");
b.Navigation("Cluster");
b.Navigation("Credentials");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStep", b =>
{
b.HasOne("OnecMonitor.Server.Models.V8File", "File")
.WithMany()
.HasForeignKey("FileId");
b.Navigation("File");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", b =>
{
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", "LeftNode")
.WithMany()
.HasForeignKey("LeftNodeId");
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", "RightNode")
.WithMany()
.HasForeignKey("RightNodeId");
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStep", "Step")
.WithMany()
.HasForeignKey("StepId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("LeftNode");
b.Navigation("RightNode");
b.Navigation("Step");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b =>
{
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", "RootNode")
.WithMany()
.HasForeignKey("RootNodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("RootNode");
});
modelBuilder.Entity("OnecMonitor.Server.Models.UpdateInfoBaseTaskLogItem", b =>
{
b.HasOne("OnecMonitor.Server.Models.InfoBase", "InfoBase")
@ -488,11 +624,11 @@ namespace OnecMonitor.Server.Migrations
b.Navigation("Task");
});
modelBuilder.Entity("UpdateInfoBaseTaskV8Configuration", b =>
modelBuilder.Entity("UpdateInfoBaseTaskV8File", b =>
{
b.HasOne("OnecMonitor.Server.Models.V8Configuration", null)
b.HasOne("OnecMonitor.Server.Models.V8File", null)
.WithMany()
.HasForeignKey("ConfigurationsId")
.HasForeignKey("FilesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -520,6 +656,11 @@ namespace OnecMonitor.Server.Migrations
b.Navigation("InfoBases");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b =>
{
b.Navigation("InfoBases");
});
modelBuilder.Entity("OnecMonitor.Server.Models.UpdateInfoBaseTask", b =>
{
b.Navigation("Log");

View File

@ -1,11 +0,0 @@
using System.Configuration;
namespace OnecMonitor.Server.Models;
public abstract class MaintenanceStep
{
public MaintenanceStepKind Kind { get; set; }
public Configuration? Configuration { get; set; }
public string AccessCode { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}

View File

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace OnecMonitor.Server.Models.MaintenanceTasks;
public class MaintenanceStep : DatabaseObject
{
public MaintenanceStepKind Kind { get; set; }
[MaxLength(20)]
public string AccessCode { get; set; } = string.Empty;
[MaxLength(200)]
public string Message { get; set; } = string.Empty;
public Guid? FileId { get; set; }
public V8File? File { get; set; } = null!;
}

View File

@ -1,7 +1,7 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace OnecMonitor.Server.Models;
namespace OnecMonitor.Server.Models.MaintenanceTasks;
public enum MaintenanceStepKind
{

View File

@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace OnecMonitor.Server.Models.MaintenanceTasks;
public class MaintenanceStepNode : DatabaseObject
{
public MaintenanceStepNodeKind Kind { get; set; }
public Guid? LeftNodeId { get; set; }
public Guid? RightNodeId { get; set; }
public Guid StepId { get; set; }
[ForeignKey(nameof(LeftNodeId))]
public MaintenanceStepNode LeftNode { get; set; } = null!;
[ForeignKey(nameof(RightNodeId))]
public MaintenanceStepNode RightNode { get; set; } = null!;
[ForeignKey(nameof(StepId))]
public MaintenanceStep Step { get; set; } = null!;
}

View File

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace OnecMonitor.Server.Models.MaintenanceTasks;
public enum MaintenanceStepNodeKind
{
[Display(Name = "Простой")]
Simple,
[Display(Name = "Попытка/Исключение")]
TryCatch
}

View File

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace OnecMonitor.Server.Models.MaintenanceTasks;
public class MaintenanceTask : DatabaseObject
{
[MaxLength(200)]
public string Description { get; set; } = string.Empty;
public Guid RootNodeId { get; set; }
[ForeignKey(nameof(RootNodeId))]
public MaintenanceStepNode RootNode { get; set; } = null!;
public virtual List<InfoBase> InfoBases { get; set; } = [];
}

View File

@ -12,7 +12,7 @@ public class UpdateInfoBaseTask : DatabaseObject
public string Description { get; set; } = string.Empty;
public DateTime StartDateTime { get; set; } = DateTime.MinValue;
public virtual List<V8Configuration> Configurations { get; set; } = [];
public virtual List<V8File> Files { get; set; } = [];
public virtual List<InfoBase> InfoBases { get; set; } = [];
public virtual List<UpdateInfoBaseTaskLogItem> Log { get; set; } = [];
}

View File

@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore;
namespace OnecMonitor.Server.Models;
public class V8Configuration : DatabaseObject
public class V8File : DatabaseObject
{
public string Name { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
@ -10,6 +10,7 @@ public class V8Configuration : DatabaseObject
public bool IsUpdate { get; set; } = false;
public bool IsExtension { get; set; } = false;
public bool IsConfiguration { get; set; } = false;
public bool IsExternalDataProcessor { get; set; } = false;
public virtual List<UpdateInfoBaseTask> UpdateTasks { get; set; } = [];
@ -23,6 +24,8 @@ public class V8Configuration : DatabaseObject
postfix = "расширение";
else if (IsConfiguration)
postfix = "конфигурация";
else if (IsExternalDataProcessor)
postfix = "внешняя обработка";
return $"{Name} ({Version}, {postfix})";
}

View File

@ -14,10 +14,13 @@ using OnecMonitor.Server.AutoMapper;
using OnecMonitor.Server.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "OnecMonitor";
});
builder.Services.AddSystemd();
builder.WebHost.ConfigureKestrel((context, options) =>
{
options.Limits.MaxRequestBodySize = 2000 * 1024 * 1024;

View File

@ -0,0 +1,122 @@
import * as bootstrap from "bootstrap";
import {Modal} from "bootstrap";
import * as uuid from "uuid";
import { StepValidationResult } from './stepValidationResult'
export class EditStepDialog {
private dialogElement: HTMLDivElement;
private dialogBody: HTMLDivElement;
private modal: Modal;
private onSaveCallback: (step: any) => void = undefined;
constructor(element: HTMLDivElement) {
this.dialogElement = element;
this.modal = new bootstrap.Modal(element);
this.dialogBody = this.dialogElement.querySelector('.modal-body');
this.dialogElement.querySelector<HTMLButtonElement>('#save-btn').onclick = async () => {
try {
const result = await this.validateStepForm();
if (result.isValid) {
this.close();
this.onSaveCallback(result.payload);
} else
this.dialogBody.innerHTML = result.payload;
} catch (e) {
alert(e);
}
};
}
public open(onSave: (step: any) => void, step: any | undefined = undefined) {
this.onSaveCallback = onSave;
if (step == undefined)
step = {
Id: uuid.v4()
};
this.getEditStepForm(step).then(data => {
this.dialogBody.innerHTML = data;
const select = this.dialogBody.querySelector<HTMLSelectElement>("select[name='Kind']");
select.onchange = async () => {
await this.updateStepForm();
};
this.modal.show();
});
}
public close(): void {
this.modal.hide();
}
private async updateStepForm() {
const formData = new FormData(this.dialogBody.querySelector<HTMLFormElement>('form'));
const response = await fetch('/MaintenanceTasks/UpdateStep', {
method: 'POST',
body: formData
});
if (response.ok) {
this.dialogBody.innerHTML = await response.text();
const select = this.dialogBody.querySelector<HTMLSelectElement>("select[name='Kind']");
select.onchange = async () => {
await this.updateStepForm();
};
} else
alert(`Failed to update step form: ${response.statusText}`);
}
private async getEditStepForm(step: any | undefined = undefined): Promise<string> {
return new Promise<string | undefined>(async (resolve, reject) => {
const body = step != undefined ? JSON.stringify(step) : "";
const response = await fetch("/MaintenanceTasks/EditStep", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: body,
});
if (response.ok)
resolve(await response.text());
else
reject(`Failed to get step kind view: ${response.statusText}`);
});
}
private async validateStepForm(): Promise<StepValidationResult> {
return new Promise<StepValidationResult>(async (resolve, reject) => {
const formData = new FormData(this.dialogBody.querySelector<HTMLFormElement>('form'));
const validateResponse = await fetch('/MaintenanceTasks/ValidateStep', {
method: 'POST',
body: formData
});
if (validateResponse.status == 200) {
//editStepModal.hide()
const result = new StepValidationResult();
result.isValid = true;
result.payload = await validateResponse.json();
resolve(result);
} else if (validateResponse.status == 400) {
const result = new StepValidationResult();
result.isValid = false;
result.payload = await validateResponse.text();
resolve(result);
} else
reject(`Failed to validate step: ${validateResponse.statusText}`);
});
}
}

View File

@ -0,0 +1,4 @@
export enum MaintenanceStepNodeKind {
Simple,
TryCatch
}

View File

@ -0,0 +1,32 @@
import {EditStepDialog} from "./editStepDialog";
import {NodesGraphOptions} from "./nodesGraphOptions";
import {NodesGraphActionsDialog} from './nodesGraphActionsDialog'
import {StepsEditorHelper} from "./stepsEditorHelper";
export class NodesGraph {
private nodesGraphActionsDialog: NodesGraphActionsDialog
private editStepDialog: EditStepDialog
private readonly stepsEditorHelper: StepsEditorHelper
constructor(options: NodesGraphOptions) {
this.stepsEditorHelper = new StepsEditorHelper(options.nodesInputElement);
this.stepsEditorHelper.init().then(() => {
this.editStepDialog = new EditStepDialog(options.editStepModalElement);
this.nodesGraphActionsDialog = new NodesGraphActionsDialog(
this.editStepDialog,
this.stepsEditorHelper,
options.nodeGraphActionsElement
);
options.bodyElement.addEventListener('click', async e => {
if (options.nodesInputElement.value == undefined || options.nodesInputElement.value == "")
this.nodesGraphActionsDialog.show();
});
});
}
public handleNodeClick(nodeId: string) {
this.nodesGraphActionsDialog.show(nodeId);
}
}

View File

@ -0,0 +1,104 @@
import {EditStepDialog} from "./editStepDialog";
import * as uuid from "uuid";
import {MaintenanceStepNodeKind} from "./maintenanceStepNodeKind";
import {StepsEditorHelper} from './stepsEditorHelper'
export class NodesGraphActionsDialog {
private editStepDialog: EditStepDialog;
private stepsEditorHelper: StepsEditorHelper;
private dialogElement: HTMLDivElement;
constructor(editStepDialog: EditStepDialog, stepsEditorHelper: StepsEditorHelper, popupElement: HTMLDivElement) {
this.editStepDialog = editStepDialog;
this.stepsEditorHelper = stepsEditorHelper;
this.dialogElement = popupElement;
}
public show(nodeId: string | undefined = undefined): void {
const root = this.stepsEditorHelper.getRootNode();
const node = nodeId == undefined ? undefined : this.stepsEditorHelper.findNode(nodeId, root);
const editBtn = this.dialogElement.querySelector<HTMLButtonElement>('#edit-step-btn');
editBtn.hidden = nodeId == undefined;
editBtn.onclick = async () => {
this.dialogElement.hidePopover();
this.editStepDialog.open(step => {
node.Step = step;
node.StepId = step.id;
}, node.Step);
}
const deleteStepBtn = this.dialogElement.querySelector<HTMLButtonElement>('#delete-step-btn');
deleteStepBtn.hidden = nodeId == undefined;
deleteStepBtn.onclick = async () => {
this.dialogElement.hidePopover();
if (root.Id == node.Id)
this.stepsEditorHelper.saveNode(undefined);
else {
this.stepsEditorHelper.deleteNode(root, node);
this.stepsEditorHelper.saveNode(root);
}
await this.stepsEditorHelper.redrawNodes();
}
this.dialogElement.querySelector<HTMLButtonElement>('#add-step-btn').onclick = () => {
this.dialogElement.hidePopover();
this.addStep(MaintenanceStepNodeKind.Simple, nodeId);
}
this.dialogElement.querySelector<HTMLButtonElement>('#add-binary-step-btn').onclick = () => {
this.dialogElement.hidePopover();
this.addStep(MaintenanceStepNodeKind.TryCatch, nodeId);
}
const addErrorStepBtn = this.dialogElement.querySelector<HTMLButtonElement>('#add-error-step-btn');
addErrorStepBtn.hidden = nodeId == undefined || node.Kind == MaintenanceStepNodeKind.Simple;
addErrorStepBtn.onclick = async () => {
this.dialogElement.hidePopover();
this.addCatchNode(root, node, MaintenanceStepNodeKind.Simple);
}
const addErrorBinaryStepBtn = this.dialogElement.querySelector<HTMLButtonElement>('#add-error-binary-step-btn');
addErrorBinaryStepBtn.hidden = nodeId == undefined || node.Kind == MaintenanceStepNodeKind.Simple;
addErrorBinaryStepBtn.onclick = async () => {
this.dialogElement.hidePopover();
this.addCatchNode(root, node, MaintenanceStepNodeKind.TryCatch);
}
this.dialogElement.showPopover()
}
private addStep(kind: MaintenanceStepNodeKind, nodeId: string | undefined = undefined) {
if (nodeId == undefined)
this.editStepDialog.open(async step => {
await this.stepsEditorHelper.addRootNode({
Id: uuid.v4(),
Kind: kind,
Step: step,
StepId: step.id
});
});
else
this.editStepDialog.open(async step => {
await this.stepsEditorHelper.addLeftNode(nodeId, {
Id: uuid.v4(),
Kind: kind,
Step: step,
StepId: step.id
});
});
}
private addCatchNode(root: any, parent: any, kind: MaintenanceStepNodeKind) {
this.editStepDialog.open(async step => {
await this.stepsEditorHelper.addRightNode(root, parent, {
Id: uuid.v4(),
Kind: kind,
Step: step,
StepId: step.id
});
});
}
}

View File

@ -0,0 +1,6 @@
export class NodesGraphOptions {
bodyElement: HTMLDivElement
nodeGraphActionsElement: HTMLDivElement
editStepModalElement: HTMLDivElement
nodesInputElement: HTMLInputElement
}

View File

@ -0,0 +1,4 @@
export class StepValidationResult {
isValid: boolean;
payload: any;
}

View File

@ -0,0 +1,165 @@
import Mermaid from "mermaid";
export class StepsEditorHelper {
private schemaInput: HTMLInputElement;
constructor(input: HTMLInputElement) {
this.schemaInput = input;
}
public async init() {
Mermaid.initialize({
theme: 'dark',
securityLevel: 'loose',
startOnLoad: false,
darkMode: true
})
await this.redrawNodes()
}
findNode(id: string, node: any): any | undefined {
if (node.Id == id)
return node;
const currentNode = node;
let foundNode = undefined;
if (currentNode.hasOwnProperty('RightNode') && currentNode.RightNode != undefined)
foundNode = this.findNode(id, currentNode.RightNode);
if (foundNode == undefined && currentNode.hasOwnProperty('LeftNode') && currentNode.LeftNode != undefined)
foundNode = this.findNode(id, currentNode.LeftNode);
return foundNode;
}
deleteNode(node: any, removingNode: any) {
if (node.hasOwnProperty('RightNode') && node.RightNode != undefined)
{
if (node.RightNode.Id == removingNode.Id)
{
delete node['RightNode'];
delete node['RightNodeId'];
return;
}
else
this.deleteNode(node.RightNode, removingNode);
}
if (node.hasOwnProperty('LeftNode') && node.LeftNode != undefined)
{
if (node.LeftNode.Id == removingNode.Id)
{
delete node['LeftNode'];
delete node['LeftNodeId'];
return;
}
else
this.deleteNode(node.LeftNode, removingNode);
}
}
async addRootNode(node: any) {
this.saveNode(node);
await this.redrawNodes();
}
async addLeftNode(parentId: string, node: any) {
const root = this.getRootNode();
const parent = this.findNode(parentId, root);
parent.LeftNodeId = node.Id;
parent.LeftNode = node;
this.saveNode(root);
await this.redrawNodes();
}
async addRightNode(root: any, parent: any, node: any) {
parent.RightNodeId = node.Id;
parent.RightNode = node;
this.saveNode(root);
await this.redrawNodes();
}
saveNode(node: any | undefined) {
if (node == undefined)
this.schemaInput.value = '';
else
this.schemaInput.value = JSON.stringify(node);
}
getRootNode(): any | undefined {
if (this.schemaInput.value == "")
return undefined;
else
return JSON.parse(this.schemaInput.value);
}
private nodeToGraphDefinition(currentNode: any): string {
let currentNodeText = "";
let currentNodeLeft = currentNode.Id;
// is try/catch node
if (currentNode.Kind == 1) {
const node = currentNode;
currentNodeLeft = `${currentNodeLeft}{${currentNode.Step.title}}`
let needSetCurrentNodeText = true;
if (node.hasOwnProperty('LeftNode') && node.LeftNode != undefined) {
currentNodeText += currentNodeLeft + " -->|Успешно| " + this.nodeToGraphDefinition(node.LeftNode) + '\r';
needSetCurrentNodeText = false;
}
if (node.hasOwnProperty('RightNode') && node.RightNode != undefined) {
currentNodeText += currentNodeLeft + " -->|Ошибка| " + this.nodeToGraphDefinition(node.RightNode) + '\r';
needSetCurrentNodeText = false;
}
if (needSetCurrentNodeText)
currentNodeText = currentNodeLeft;
} else {
const node = currentNode;
currentNodeText = `${currentNodeLeft}[${currentNode.Step.title}]`
if (node.hasOwnProperty('LeftNode') && node.LeftNode != undefined)
currentNodeText += " --> " + this.nodeToGraphDefinition(node.LeftNode) + '\r';
}
if (currentNodeText[currentNodeText.length - 1] != '\r')
currentNodeText += '\r';
currentNodeText += `click ${currentNode.Id} OM.clickNode\r`;
return currentNodeText;
}
async redrawNodes() {
const root = this.getRootNode();
const nodesDefinition = root == undefined ? "" : this.nodeToGraphDefinition(root);
let graphDefinition = "flowchart TD\n" + nodesDefinition;
await this.redrawGraph(graphDefinition);
}
private async redrawGraph(definition: string) {
document.querySelector('.mermaid')?.remove()
const graphBody = document.querySelector('#graphBody');
const mermaidContainer = document.createElement('div');
mermaidContainer.classList.add('mermaid');
mermaidContainer.classList.add('text-center');
mermaidContainer.textContent = definition;
graphBody.appendChild(mermaidContainer);
await Mermaid.run()
}
}

View File

@ -6,14 +6,21 @@ import '../node_modules/bootstrap-icons/font/bootstrap-icons.css';
//modules
import '../node_modules/bootstrap/dist/js/bootstrap.bundle.min'
require('../Scripts/itemSelectionDialog')
require('../Scripts/deleteDialog')
require('../Scripts/techLog')
require('../Scripts/infoBasesUpdatingTaskLog')
require('./maintenanceTasks')
require('./itemSelectionDialog')
require('./deleteDialog')
require('./techLog')
require('./infoBasesUpdatingTaskLog')
require('./infoBasesUpdateTask')
export * from '../Scripts/itemSelectionDialog'
export * from '../Scripts/deleteDialog'
export * from '../Scripts/techLog'
export * from '../Scripts/infoBasesUpdatingTaskLog'
export * from './maintenanceTasks'
export * from './itemSelectionDialog'
export * from './deleteDialog'
export * from './techLog'
export * from './infoBasesUpdatingTaskLog'
export * from './infoBasesUpdateTask'
export {EditStepDialog} from "./MaintenanceTask/editStepDialog";
export {StepValidationResult} from "./MaintenanceTask/stepValidationResult";
export {MaintenanceStepNodeKind} from "./MaintenanceTask/maintenanceStepNodeKind";
export {NodesGraphOptions} from "./MaintenanceTask/nodesGraphOptions";
export {NodesGraph} from "./MaintenanceTask/nodesGraph";
export {NodesGraphActionsDialog} from "./MaintenanceTask/nodesGraphActionsDialog";
export {StepsEditorHelper} from "./MaintenanceTask/stepsEditorHelper";

View File

@ -0,0 +1,32 @@
import {NodesGraph} from "./MaintenanceTask/nodesGraph";
let nodesGraph: NodesGraph = undefined;
export async function initStepsEditor() {
nodesGraph = new NodesGraph({
nodesInputElement: getSchemaNodesElement(),
editStepModalElement: getStepEditDialogElement(),
nodeGraphActionsElement: getNodeActionsElement(),
bodyElement: getGraphBodyElement()
});
}
export function clickNode(nodeId: string) {
nodesGraph.handleNodeClick(nodeId);
}
function getNodeActionsElement(): HTMLDivElement {
return document.getElementById('node-actions') as HTMLDivElement;
}
function getGraphBodyElement(): HTMLDivElement {
return document.getElementById('graphBody') as HTMLDivElement;
}
function getSchemaNodesElement(): HTMLInputElement {
return document.getElementById('schema-nodes') as HTMLInputElement;
}
function getStepEditDialogElement(): HTMLDivElement {
return document.getElementById('edit-step-dialog') as HTMLDivElement;
}

View File

@ -15,6 +15,8 @@ using AutoMapper;
using AutoMapper.QueryableExtensions;
using OnecMonitor.Server.Helpers;
using OneSTools.Common.Platform;
using OneSTools.Common.Platform.RemoteAdministration;
using OneSTools.Common.Platform.Services;
namespace OnecMonitor.Server.Services
{
@ -40,6 +42,7 @@ namespace OnecMonitor.Server.Services
public event AgentDisconnectedHandler? AgentDisconnected;
public AgentConnection(Socket socket, TechLogProcessor techLogProcessor, IServiceProvider serviceProvider)
: base(serviceProvider.GetRequiredService<ILogger<AgentConnection>>())
{
Socket = socket;
@ -315,7 +318,7 @@ namespace OnecMonitor.Server.Services
{
var task = await _appDbContext.UpdateInfoBaseTasks
.Where(c => c.InfoBases.Any(i => i.Cluster.Agent.Id == AgentInstance!.Id))
.Include(c => c.Configurations)
.Include(c => c.Files)
.Include(c => c.InfoBases)
.ThenInclude(c => c.Credentials)
.Include(c => c.InfoBases)

View File

@ -2,6 +2,7 @@ using System.ComponentModel;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using OnecMonitor.Server.Models;
using OneSTools.Common.Platform;
using OneSTools.Common.Platform.Services;
namespace OnecMonitor.Server.ViewModels.Agents;

View File

@ -1,6 +0,0 @@
namespace OnecMonitor.Server.ViewModels.Configurations;
public class ConfigurationsIndexViewModel
{
public List<ConfigurationListItemViewModel> Items { get; init; } = [];
}

View File

@ -1,32 +0,0 @@
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
using OnecMonitor.Server.Models;
namespace OnecMonitor.Server.ViewModels.Maintenance;
public class MaintenanceEditViewModel
{
public Guid Id { get; set; }
public string Description { get; set; } = string.Empty;
public List<SelectableItemViewModel> InfoBases { get; set; } = [];
[ValidateNever]
public List<SelectableItemViewModel> AvailableInfoBases { get; set; } = [];
public List<MaintenanceStepViewModel> MaintenanceSteps { get; set; } = [];
[ValidateNever]
public SelectList MaintenanceStepKinds { get; set; } = null!;
[ValidateNever]
public SelectList ExternalDataProcessors { get; set; } = null!;
[ValidateNever]
public SelectList Extensions { get; set; } = null!;
[ValidateNever]
public SelectList ConfigUpdates { get; set; } = null!;
[ValidateNever]
public SelectList Configs { get; set; } = null!;
}

View File

@ -1,6 +0,0 @@
namespace OnecMonitor.Server.ViewModels.Maintenance;
public class MaintenanceIndexViewModel
{
public List<MaintenanceListItemViewModel> Items { get; set; } = [];
}

View File

@ -1,7 +0,0 @@
namespace OnecMonitor.Server.ViewModels.Maintenance;
public class MaintenanceListItemViewModel
{
public Guid Id { get; set; }
public string Description { get; set; } = string.Empty;
}

View File

@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using OnecMonitor.Server.Models.MaintenanceTasks;
namespace OnecMonitor.Server.ViewModels.MaintenanceTasks;
public class MaintenanceStepNodeViewModel
{
public Guid Id { get; set; }
public MaintenanceStepNodeKind Kind { get; set; }
[ValidateNever]
public Guid? LeftNodeId { get; set; }
[ValidateNever]
public Guid? RightNodeId { get; set; }
public Guid StepId { get; set; }
public MaintenanceStepNodeViewModel? LeftNode { get; set; } = null!;
public MaintenanceStepNodeViewModel? RightNode { get; set; } = null!;
public MaintenanceStepViewModel Step { get; set; } = null!;
}

View File

@ -1,21 +1,35 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
using OnecMonitor.Server.Extensions;
using OnecMonitor.Server.Helpers;
using OnecMonitor.Server.Models;
using OnecMonitor.Server.Models.MaintenanceTasks;
namespace OnecMonitor.Server.ViewModels.Maintenance;
namespace OnecMonitor.Server.ViewModels.MaintenanceTasks;
public class MaintenanceStepViewModel
{
[JsonPropertyName("id")]
public Guid Id { get; set; }
[JsonPropertyName("kind")]
public MaintenanceStepKind Kind { get; set; }
[ValidateNever]
public SelectList Kinds { get; set; } = null!;
[ValidateNever]
[JsonPropertyName("accessCode")]
public string AccessCode { get; set; } = string.Empty;
[ValidateNever]
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
[ValidateNever]
[JsonPropertyName("fileId")]
public Guid? FileId { get; set; }
[ValidateNever]
public SelectList Files { get; set; } = null!;
[ValidateNever]
public string AccessCode { get; set; } = string.Empty;
[ValidateNever]
public string Message { get; set; } = string.Empty;
[JsonPropertyName("title")]
public string Title => Kind.GetDisplay();
}

View File

@ -0,0 +1,21 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
using Newtonsoft.Json;
using OnecMonitor.Server.Models;
namespace OnecMonitor.Server.ViewModels.MaintenanceTasks;
public class MaintenanceTaskEditViewModel
{
public Guid Id { get; set; }
[DisplayName("Описание")]
public string Description { get; set; } = string.Empty;
[DisplayName("Информационные базы")]
public List<SelectableItemViewModel> InfoBases { get; set; } = [];
[ValidateNever]
public List<SelectableItemViewModel> AvailableInfoBases { get; set; } = [];
public string SerializedStepNode { get; set; } = string.Empty;
}

View File

@ -0,0 +1,7 @@
namespace OnecMonitor.Server.ViewModels.MaintenanceTasks;
public class MaintenanceTaskListItemViewModel
{
public Guid Id { get; set; }
public string Description { get; set; } = string.Empty;
}

View File

@ -0,0 +1,6 @@
namespace OnecMonitor.Server.ViewModels.MaintenanceTasks;
public class MaintenanceTasksIndexViewModel
{
public List<MaintenanceTaskListItemViewModel> Items { get; set; } = [];
}

View File

@ -12,9 +12,9 @@ public class UpdateInfoBaseTaskEditViewModel
[ValidateNever]
[DisplayName("Конфигурации")]
public List<SelectableItemViewModel> Configurations { get; set; } = [];
public List<SelectableItemViewModel> Files { get; set; } = [];
[ValidateNever]
public List<SelectableItemViewModel> AvailableConfigurations { get; set; } = [];
public List<SelectableItemViewModel> AvailableFiles { get; set; } = [];
[DisplayName("Информационные базы")]
public List<SelectableItemViewModel> InfoBases { get; set; } = [];

View File

@ -1,8 +1,8 @@
using Microsoft.AspNetCore.Mvc;
namespace OnecMonitor.Server.ViewModels.Configurations;
namespace OnecMonitor.Server.ViewModels.V8Files;
public class ConfigurationEditViewModel
public class V8FileEditViewModel
{
public Guid Id { get; init; }
public string Name { get; set; } = string.Empty;

View File

@ -1,6 +1,6 @@
namespace OnecMonitor.Server.ViewModels.Configurations;
namespace OnecMonitor.Server.ViewModels.V8Files;
public class ConfigurationListItemViewModel
public class V8FileListItemViewModel
{
public Guid Id { get; init; }
public string Name { get; set; } = string.Empty;
@ -17,8 +17,10 @@ public class ConfigurationListItemViewModel
return "Обновление";
else if (IsConfiguration)
return "Конфигурация";
else
else if (IsExtension)
return "Расширение";
else
return "Внешняя обработка";
}
}
}

View File

@ -0,0 +1,6 @@
namespace OnecMonitor.Server.ViewModels.V8Files;
public class V8FilesIndexViewModel
{
public List<V8FileListItemViewModel> Items { get; init; } = [];
}

View File

@ -0,0 +1,101 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using NuGet.Protocol
@using OnecMonitor.Server.Extensions
@using OnecMonitor.Server.Helpers
@using OnecMonitor.Server.Models.MaintenanceTasks
@using OnecMonitor.Server.ViewModels
@model OnecMonitor.Server.ViewModels.MaintenanceTasks.MaintenanceTaskEditViewModel
@{
ViewData["Title"] = "Задача обслуживания";
}
<h2 class="mb-3">Задача обслуживания</h2>
<form method="post" asp-action="Save" style="height: 100%">
<div class="mb-3">
<button class="btn btn-primary" type="submit">Сохранить</button>
<button class="btn btn-secondary" onclick="history.back()" type="button">Назад</button>
</div>
<input type="text" hidden class="form-control" asp-for="Id" value="@Model.Id">
<div class="mb-3">
<label class="form-label" asp-for="Description">Описание</label>
<input type="text" class="form-control" asp-for="Description" value="@Model.Description">
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="mb-3">
@await Html.PartialAsync("SelectItemDialog", new SelectItemDialogViewModel(
Html.DisplayNameFor(c => c.InfoBases),
nameof(Model.InfoBases),
Model.InfoBases,
Model.AvailableInfoBases))
</div>
<div class="mb-3">
<div class="card">
<div class="card-header">Схема</div>
<input id="schema-nodes" type="text" hidden class="form-control" name="SerializedStepNode" value="@Model.SerializedStepNode">
<div id="graphBody" class="card-body" style="min-height: 200px"></div>
</div>
</div>
</form>
<div class="modal" id="select-step-kind-dialog" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Выберите тип шага</h5>
</div>
<div class="modal-body">
<div class="list-group">
@foreach (var item in Enum.GetValues<MaintenanceStepKind>())
{
<button type="button" data-kind="@((int)item)" class="list-group-item list-group-item-action">@item.GetDisplay()</button>
}
</div>
</div>
</div>
</div>
</div>
<div class="modal" id="edit-step-dialog" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Изменение шага обслуживания</h5>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<button id="save-btn" type="button" class="btn btn-primary">Сохранить</button>
</div>
</div>
</div>
</div>
<div id="node-actions" popover class="border-0 bg-transparent position-fixed">
<div class="card">
<div class="card-header">Выберите действие</div>
<div class="card-body">
<div class="list-group">
<button id="delete-step-btn" type="button" class="list-group-item list-group-item-action">Удалить</button>
<button id="edit-step-btn" type="button" class="list-group-item list-group-item-action">Изменить</button>
<button id="add-step-btn" type="button" class="list-group-item list-group-item-action">Добавить шаг</button>
<button id="add-error-step-btn" type="button" class="list-group-item list-group-item-action">Добавить шаг исключения</button>
<button id="add-binary-step-btn" type="button" class="list-group-item list-group-item-action">Добавить шаг c обработкой исключения</button>
<button id="add-error-binary-step-btn" type="button" class="list-group-item list-group-item-action">Добавить шаг исключения c обработкой исключения</button>
</div>
</div>
</div>
</div>
@section Scripts
{
<script type="text/javascript">
OM.initStepsEditor();
</script>
}

View File

@ -0,0 +1,48 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using OnecMonitor.Server.Extensions
@using OnecMonitor.Server.Models.MaintenanceTasks
@using OnecMonitor.Server.ViewModels
@model OnecMonitor.Server.ViewModels.MaintenanceTasks.MaintenanceStepViewModel
<form method="post" style="height: 100%">
<input type="text" hidden class="form-control" asp-for="Id" value="@Model.Id">
<div class="mb-3">
<label class="form-label" asp-for="Kind">Тип шага</label>
<select asp-for="Kind" class="form-control" asp-items="Model.Kinds"></select>
<span asp-validation-for="Kind" class="text-danger"></span>
</div>
@if (Model.Kind == MaintenanceStepKind.LockConnections)
{
<div class="mb-3">
<label class="form-label" asp-for="AccessCode">Код доступа</label>
<input type="text" class="form-control" asp-for="AccessCode" value="@Model.AccessCode">
<span asp-validation-for="AccessCode" class="text-danger"></span>
</div>
<div class="mb-3">
<label class="form-label" asp-for="Message">Сообщение</label>
<input type="text" class="form-control" asp-for="Message" value="@Model.Message">
<span asp-validation-for="Message" class="text-danger"></span>
</div>
}
@if (Model is
{
Kind: MaintenanceStepKind.LoadConfiguration or
MaintenanceStepKind.UpdateConfiguration or
MaintenanceStepKind.LoadExtension or
MaintenanceStepKind.StartExternalDataProcessor
})
{
<div class="mb-3">
<label class="form-label" asp-for="FileId">Файл</label>
<select asp-for="FileId" class="form-control" asp-items="Model.Files"></select>
<span asp-validation-for="FileId" class="text-danger"></span>
</div>
}
</form>

View File

@ -0,0 +1,43 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using OnecMonitor.Server.ViewModels
@using OnecMonitor.Server.ViewModels.Agents;
@model OnecMonitor.Server.ViewModels.MaintenanceTasks.MaintenanceTasksIndexViewModel
@{
ViewData["Title"] = "Задачи обслуживания";
}
<h2 class="mb-3">Задачи обслуживания</h2>
<div class="mb-3">
<a class="btn btn-primary" asp-action="Edit" role="button">Добавить</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="col">Описание</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Description</td>
<td class="text-center">
<a class="btn btn-outline-warning btn-sm" asp-action="Edit" asp-route-id="@item.Id">Изменить</a>
<a class="btn btn-outline-danger btn-sm" onclick="OM.openDeleteDialog('@item.Id', '@item.Description')">Удалить</a>
</td>
</tr>
}
</tbody>
</table>
</div>
@await Html.PartialAsync("DeleteDialog", new DeleteDialogViewModel
{
Controller = "MaintenanceTasks",
Action = "Delete"
})

View File

@ -51,8 +51,13 @@
</a>
</li>
<li class="w-100">
<a asp-controller="Configurations" asp-action="Index" class="nav-link text-white px-0">
<small class="d-none d-sm-inline">Конфигурации</small>
<a asp-controller="V8Files" asp-action="Index" class="nav-link text-white px-0">
<small class="d-none d-sm-inline">Файлы 1С</small>
</a>
</li>
<li class="w-100">
<a asp-controller="MaintenanceTasks" asp-action="Index" class="nav-link text-white px-0">
<small class="d-none d-sm-inline">Задачи обслуживания</small>
</a>
</li>
<li class="w-100">

View File

@ -1,4 +1,7 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using OnecMonitor.Server.Extensions
@using OnecMonitor.Server.Helpers
@using OnecMonitor.Server.Models.MaintenanceTasks
@using OnecMonitor.Server.ViewModels
@model OnecMonitor.Server.ViewModels.UpdateInfoBaseTasks.UpdateInfoBaseTaskEditViewModel
@ -24,10 +27,10 @@
<div class="mb-3">
@await Html.PartialAsync("SelectItemDialog", new SelectItemDialogViewModel(
Html.DisplayNameFor(c => c.Configurations),
nameof(Model.Configurations),
Model.Configurations,
Model.AvailableConfigurations))
Html.DisplayNameFor(c => c.Files),
nameof(Model.Files),
Model.Files,
Model.AvailableFiles))
</div>
<div class="mb-3">

View File

@ -1,38 +1,38 @@
@using OnecMonitor.Server.ViewModels.Configurations;
@model ConfigurationEditViewModel
@{
ViewData["Title"] = "Конфигурация";
}
<h2 class="mb-3">Конфигурация</h2>
<form method="post" asp-action="Save" enctype="multipart/form-data" style="height: 100%">
<div class="mb-3">
<button class="btn btn-primary" type="submit">Сохранить</button>
<button class="btn btn-secondary" onclick="history.back()" type="button">Назад</button>
</div>
<input type="text" hidden class="form-control" asp-for="Id" value="@Model.Id">
<div class="mb-3">
<label class="form-label" asp-for="Name">Имя</label>
<input type="text" class="form-control" asp-for="Name" value="@Model.Name">
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="mb-3">
<label class="form-label" asp-for="Version">Версия</label>
<input type="text" class="form-control" asp-for="Version" value="@Model.Version">
<span asp-validation-for="Version" class="text-danger"></span>
</div>
@if (Model.Id == Guid.Empty)
{
<div>
<input class="form-control" accept=".cf,.cfu,.cfe" type="file" asp-for="File">
<span asp-validation-for="File" class="text-danger" disabled></span>
</div>
}
@using OnecMonitor.Server.ViewModels.V8Files;
@model V8FileEditViewModel
@{
ViewData["Title"] = "Файл 1С";
}
<h2 class="mb-3">Файл 1С</h2>
<form method="post" asp-action="Save" enctype="multipart/form-data" style="height: 100%">
<div class="mb-3">
<button class="btn btn-primary" type="submit">Сохранить</button>
<button class="btn btn-secondary" onclick="history.back()" type="button">Назад</button>
</div>
<input type="text" hidden class="form-control" asp-for="Id" value="@Model.Id">
<div class="mb-3">
<label class="form-label" asp-for="Name">Имя</label>
<input type="text" class="form-control" asp-for="Name" value="@Model.Name">
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="mb-3">
<label class="form-label" asp-for="Version">Версия</label>
<input type="text" class="form-control" asp-for="Version" value="@Model.Version">
<span asp-validation-for="Version" class="text-danger"></span>
</div>
@if (Model.Id == Guid.Empty)
{
<div>
<input class="form-control" accept=".cf,.cfu,.cfe,.epf" type="file" asp-for="File">
<span asp-validation-for="File" class="text-danger" disabled></span>
</div>
}
</form>

View File

@ -1,45 +1,45 @@
@using OnecMonitor.Server.ViewModels
@model OnecMonitor.Server.ViewModels.Configurations.ConfigurationsIndexViewModel
@{
ViewData["Title"] = "Конфигурации";
}
<h2 class="mb-3">Конфигурации</h2>
<div class="mb-3">
<a class="btn btn-primary" asp-action="Edit" role="button">Добавить</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Имя</th>
<th>Версия</th>
<th>Тип</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Name</td>
<td>@item.Version</td>
<td>@item.Type</td>
<td class="text-center">
<a class="btn btn-outline-warning btn-sm" asp-action="Edit" asp-route-id="@item.Id">Изменить</a>
<a class="btn btn-outline-danger btn-sm" onclick="OM.openDeleteDialog('@item.Id', '@item.Name')">Удалить</a>
</td>
</tr>
}
</tbody>
</table>
</div>
@await Html.PartialAsync("DeleteDialog", new DeleteDialogViewModel
{
Controller = "Configurations",
Action = "Delete"
})
@using OnecMonitor.Server.ViewModels
@model OnecMonitor.Server.ViewModels.V8Files.V8FilesIndexViewModel
@{
ViewData["Title"] = "Файлы 1С";
}
<h2 class="mb-3">Файлы 1С</h2>
<div class="mb-3">
<a class="btn btn-primary" asp-action="Edit" role="button">Добавить</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Имя</th>
<th>Версия</th>
<th>Тип</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Name</td>
<td>@item.Version</td>
<td>@item.Type</td>
<td class="text-center">
<a class="btn btn-outline-warning btn-sm" asp-action="Edit" asp-route-id="@item.Id">Изменить</a>
<a class="btn btn-outline-danger btn-sm" onclick="OM.openDeleteDialog('@item.Id', '@item.Name')">Удалить</a>
</td>
</tr>
}
</tbody>
</table>
</div>
@await Html.PartialAsync("DeleteDialog", new DeleteDialogViewModel
{
Controller = "V8Files",
Action = "Delete"
})

View File

@ -1,7 +1,7 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Default": "Trace",
"Microsoft.Hosting.Lifetime": "Information"
}
},

View File

@ -50,6 +50,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
@ -78,6 +79,14 @@
<_ContentIncludedByDefault Remove="Views\MaintenanceTasks\Steps\LoadExtension.cshtml" />
<_ContentIncludedByDefault Remove="Views\MaintenanceTasks\Steps\LockConnections.cshtml" />
<_ContentIncludedByDefault Remove="Views\MaintenanceTasks\Steps\UpdateConfiguration.cshtml" />
<_ContentIncludedByDefault Remove="Views\UpdateInfoBaseTasks\Steps\LockConnections.cshtml" />
<_ContentIncludedByDefault Remove="Views\UpdateInfoBaseTasks\Steps\UpdateConfiguration.cshtml" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Models\MaintenanceTasks\LoadConfigurationStep.cs" />
<Compile Remove="Migrations\20250225154747_Initial.cs" />
<Compile Remove="Migrations\20250225154747_Initial.Designer.cs" />
</ItemGroup>
<ProjectExtensions>

View File

@ -13,7 +13,9 @@
"chart.js": "4.4.1",
"signalr": "^2.4.3",
"vis-timeline": "7.7.3",
"visjs-network": "4.25.0"
"visjs-network": "4.25.0",
"mermaid": "^11.4.1",
"uuid": "^11.0.5"
},
"devDependencies": {
"@types/ace": "^0.0.52",

View File

@ -7,6 +7,11 @@
"allowJs": true,
"skipLibCheck": true,
"moduleResolution": "node",
"sourceRoot": "Scripts",
"mapRoot": "wwwroot",
"typeRoots": [
"node_modules/@types"
]
},
"exclude": [
"node_modules",

View File

@ -1,41 +0,0 @@
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
script: [
'./Scripts/index.ts'
]
},
devtool: 'inline-source-map',
optimization: {
minimize: false,
usedExports: false
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
transpileOnly: true
}
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader']
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
path: path.resolve(__dirname, 'wwwroot'),
filename: 'dist/app.js',
library: {
}
},
};