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-28 08:57:21 +03:00
parent da06b000c1
commit 9f690f69a4
75 changed files with 1375 additions and 1198 deletions

View File

@ -1,55 +0,0 @@
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

@ -1,152 +0,0 @@
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

@ -1,34 +0,0 @@
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

@ -1,28 +0,0 @@
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

@ -1,29 +0,0 @@
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

@ -1,18 +0,0 @@
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

@ -1,45 +0,0 @@
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

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

View File

@ -1,138 +0,0 @@
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

@ -1,23 +0,0 @@
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

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

View File

@ -1,68 +1,65 @@
// <auto-generated /> // <auto-generated />
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OnecMonitor.Agent; using OnecMonitor.Agent;
#nullable disable #nullable disable
namespace OnecMonitor.Agent.Migrations namespace OnecMonitor.Agent.Migrations
{ {
[DbContext(typeof(AppDbContext))] [DbContext(typeof(AppDbContext))]
[Migration("20230113112033_Init")] [Migration("20250227185511_Initial")]
partial class Init partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); modelBuilder.HasAnnotation("ProductVersion", "9.0.1");
modelBuilder.Entity("OnecMonitor.Agent.Models.TechLogSeance", b => modelBuilder.Entity("OnecMonitor.Agent.Models.AgentInstance", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("FinishDateTime") b.Property<string>("InstanceName")
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("StartDateTime") b.HasKey("Id");
.IsRequired()
.HasColumnType("TEXT"); b.ToTable("AgentInstance");
});
b.Property<int>("Status")
.HasColumnType("INTEGER"); modelBuilder.Entity("OnecMonitor.Agent.Models.TechLogSeance", b =>
{
b.Property<string>("Template") b.Property<string>("Id")
.IsRequired() .ValueGeneratedOnAdd()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.HasKey("Id"); b.Property<string>("FinishDateTime")
.IsRequired()
b.ToTable("TechLogSeances"); .HasColumnType("TEXT");
});
b.Property<string>("StartDateTime")
modelBuilder.Entity("OnecMonitor.Common.Models.AgentInstance", b => .IsRequired()
{ .HasColumnType("TEXT");
b.Property<string>("Id")
.ValueGeneratedOnAdd() b.Property<int>("Status")
.HasColumnType("TEXT"); .HasColumnType("INTEGER");
b.Property<string>("InstanceName") b.Property<string>("Template")
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<double>("UtcOffset") b.HasKey("Id");
.HasColumnType("REAL");
b.ToTable("TechLogSeances");
b.HasKey("Id"); });
#pragma warning restore 612, 618
b.ToTable("AgentInstance"); }
}); }
#pragma warning restore 612, 618 }
}
}
}

View File

@ -1,52 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable #nullable disable
namespace OnecMonitor.Agent.Migrations namespace OnecMonitor.Agent.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class Init : Migration public partial class Initial : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "AgentInstance", name: "AgentInstance",
columns: table => new columns: table => new
{ {
Id = table.Column<string>(type: "TEXT", nullable: false), Id = table.Column<string>(type: "TEXT", nullable: false),
InstanceName = table.Column<string>(type: "TEXT", nullable: false), InstanceName = table.Column<string>(type: "TEXT", nullable: false)
UtcOffset = table.Column<double>(type: "REAL", nullable: false) },
}, constraints: table =>
constraints: table => {
{ table.PrimaryKey("PK_AgentInstance", x => x.Id);
table.PrimaryKey("PK_AgentInstance", x => x.Id); });
});
migrationBuilder.CreateTable(
migrationBuilder.CreateTable( name: "TechLogSeances",
name: "TechLogSeances", columns: table => new
columns: table => new {
{ Id = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(type: "TEXT", nullable: false), StartDateTime = table.Column<string>(type: "TEXT", nullable: false),
StartDateTime = table.Column<string>(type: "TEXT", nullable: false), FinishDateTime = table.Column<string>(type: "TEXT", nullable: false),
FinishDateTime = table.Column<string>(type: "TEXT", nullable: false), Template = table.Column<string>(type: "TEXT", nullable: false),
Template = table.Column<string>(type: "TEXT", nullable: false), Status = table.Column<int>(type: "INTEGER", nullable: false)
Status = table.Column<int>(type: "INTEGER", nullable: false) },
}, constraints: table =>
constraints: table => {
{ table.PrimaryKey("PK_TechLogSeances", x => x.Id);
table.PrimaryKey("PK_TechLogSeances", x => x.Id); });
}); }
}
/// <inheritdoc />
/// <inheritdoc /> protected override void Down(MigrationBuilder migrationBuilder)
protected override void Down(MigrationBuilder migrationBuilder) {
{ migrationBuilder.DropTable(
migrationBuilder.DropTable( name: "AgentInstance");
name: "AgentInstance");
migrationBuilder.DropTable(
migrationBuilder.DropTable( name: "TechLogSeances");
name: "TechLogSeances"); }
} }
} }
}

View File

@ -1,65 +1,62 @@
// <auto-generated /> // <auto-generated />
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OnecMonitor.Agent; using OnecMonitor.Agent;
#nullable disable #nullable disable
namespace OnecMonitor.Agent.Migrations namespace OnecMonitor.Agent.Migrations
{ {
[DbContext(typeof(AppDbContext))] [DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot partial class AppDbContextModelSnapshot : ModelSnapshot
{ {
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); modelBuilder.HasAnnotation("ProductVersion", "9.0.1");
modelBuilder.Entity("OnecMonitor.Agent.Models.TechLogSeance", b => modelBuilder.Entity("OnecMonitor.Agent.Models.AgentInstance", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("FinishDateTime") b.Property<string>("InstanceName")
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("StartDateTime") b.HasKey("Id");
.IsRequired()
.HasColumnType("TEXT"); b.ToTable("AgentInstance");
});
b.Property<int>("Status")
.HasColumnType("INTEGER"); modelBuilder.Entity("OnecMonitor.Agent.Models.TechLogSeance", b =>
{
b.Property<string>("Template") b.Property<string>("Id")
.IsRequired() .ValueGeneratedOnAdd()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.HasKey("Id"); b.Property<string>("FinishDateTime")
.IsRequired()
b.ToTable("TechLogSeances"); .HasColumnType("TEXT");
});
b.Property<string>("StartDateTime")
modelBuilder.Entity("OnecMonitor.Common.Models.AgentInstance", b => .IsRequired()
{ .HasColumnType("TEXT");
b.Property<string>("Id")
.ValueGeneratedOnAdd() b.Property<int>("Status")
.HasColumnType("TEXT"); .HasColumnType("INTEGER");
b.Property<string>("InstanceName") b.Property<string>("Template")
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<double>("UtcOffset") b.HasKey("Id");
.HasColumnType("REAL");
b.ToTable("TechLogSeances");
b.HasKey("Id"); });
#pragma warning restore 612, 618
b.ToTable("AgentInstance"); }
}); }
#pragma warning restore 612, 618 }
}
}
}

View File

@ -0,0 +1,7 @@
namespace OnecMonitor.Agent.Models;
public class AgentInstance
{
public Guid Id { get; set; } = Guid.Empty;
public string InstanceName { get; set; } = string.Empty;
}

View File

@ -2,8 +2,10 @@ using System.Reflection;
using Grpc.Net.Client; using Grpc.Net.Client;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using OnecMonitor.Agent; using OnecMonitor.Agent;
using OnecMonitor.Agent.Models;
using OnecMonitor.Agent.Services; using OnecMonitor.Agent.Services;
using OnecMonitor.Agent.Services.InfoBases; using OnecMonitor.Agent.Services.InfoBases;
using OnecMonitor.Agent.Services.MaintenanceTasks;
using OnecMonitor.Agent.Services.TechLog; using OnecMonitor.Agent.Services.TechLog;
using OnecMonitor.Common.Storage; using OnecMonitor.Common.Storage;
using OnecMonitor.Common.TechLog; using OnecMonitor.Common.TechLog;
@ -19,30 +21,65 @@ var host = Host.CreateDefaultBuilder(args)
services.AddSingleton<RasHolder>(); services.AddSingleton<RasHolder>();
services.AddDbContext<AppDbContext>(); services.AddDbContext<AppDbContext>();
// Commands watcher connection services.AddTransient<OnecMonitorConnection>();
services.AddSingleton<OnecMonitorConnection>();
services.AddSingleton<TechLogFolderWatcher>(); services.AddSingleton<TechLogFolderWatcher>();
services.AddSingleton<TechLogExporter>(); services.AddSingleton<TechLogExporter>();
services.AddHostedService<TechLogSeancesWatcher>(); services.AddHostedService<TechLogSeancesWatcher>();
services.AddSingleton<MaintenanceTaskExecutorQueue>();
services.AddHostedService<MaintenanceTaskExecutor>();
services.AddSingleton<InfoBasesUpdateTasksQueue>(); services.AddSingleton<InfoBasesUpdateTasksQueue>();
services.AddHostedService<InfoBasesUpdater>(); services.AddHostedService<InfoBasesUpdater>();
services.AddHostedService<CommandsWatcher>(); services.AddSingleton<CommandsWatcher>();
}) })
.Build(); .Build();
var appLifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
await using var scope = host.Services.CreateAsyncScope(); await using var scope = host.Services.CreateAsyncScope();
await using var appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); await using var appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.Database.MigrateAsync(); await appDbContext.Database.MigrateAsync();
var appLifetime = host.Services.GetRequiredService<IHostApplicationLifetime>(); // update agent instance info
var techLogeExporter = host.Services.GetRequiredService<TechLogExporter>(); var configuration = host.Services.GetRequiredService<IConfiguration>();
var agentInstance = appDbContext.AgentInstance.FirstOrDefault();
var instanceName = configuration.GetValue("Agent:InstanceName", Environment.MachineName);
if (string.IsNullOrEmpty(instanceName))
instanceName = Environment.MachineName;
if (agentInstance == null)
{
agentInstance = new AgentInstance
{
Id = Guid.NewGuid(),
InstanceName = instanceName
};
appDbContext.AgentInstance.Add(agentInstance);
appDbContext.SaveChanges();
}
else if (agentInstance.InstanceName != instanceName)
{
agentInstance.InstanceName = instanceName;
appDbContext.AgentInstance.Update(agentInstance);
appDbContext.SaveChanges();
}
// another init actions
var techLogExporter = host.Services.GetRequiredService<TechLogExporter>();
appLifetime.ApplicationStopping.Register(() => appLifetime.ApplicationStopping.Register(() =>
{ {
techLogeExporter.Stop(); techLogExporter.Stop();
techLogeExporter.Dispose(); techLogExporter.Dispose();
}); });
await host.Services.GetRequiredService<CommandsWatcher>().Start(appLifetime.ApplicationStopping);
host.Run(); host.Run();

View File

@ -1,28 +1,34 @@
using MessagePack; using MessagePack;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using OnecMonitor.Agent.Services.InfoBases; using OnecMonitor.Agent.Services.InfoBases;
using OnecMonitor.Agent.Services.MaintenanceTasks;
using OnecMonitor.Agent.Services.TechLog; using OnecMonitor.Agent.Services.TechLog;
using OnecMonitor.Common.DTO; using OnecMonitor.Common.DTO;
using OnecMonitor.Common.DTO.MaintenanceTasks;
using OneSTools.Common.Platform; using OneSTools.Common.Platform;
using OneSTools.Common.Platform.RemoteAdministration; using OneSTools.Common.Platform.RemoteAdministration;
using OneSTools.Common.Platform.Services; using OneSTools.Common.Platform.Services;
namespace OnecMonitor.Agent.Services namespace OnecMonitor.Agent.Services
{ {
internal class CommandsWatcher : BackgroundService internal class CommandsWatcher
{ {
private readonly OnecMonitorConnection _server; private readonly OnecMonitorConnection _server;
private readonly AppDbContext _appDbContext; private readonly AppDbContext _appDbContext;
private readonly InfoBasesUpdateTasksQueue _updateTasksQueue; private readonly InfoBasesUpdateTasksQueue _updateTasksQueue;
private readonly MaintenanceTaskExecutorQueue _maintenanceTasksQueue;
private readonly RasHolder _rasHolder; private readonly RasHolder _rasHolder;
private readonly TechLogExporter _techLogExporter; private readonly TechLogExporter _techLogExporter;
private readonly IHostApplicationLifetime _applicationLifetime;
private readonly ILogger<CommandsWatcher> _logger; private readonly ILogger<CommandsWatcher> _logger;
public CommandsWatcher( public CommandsWatcher(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
InfoBasesUpdateTasksQueue updateTasksQueue, InfoBasesUpdateTasksQueue updateTasksQueue,
MaintenanceTaskExecutorQueue maintenanceTasksQueue,
TechLogExporter techLogExporter, TechLogExporter techLogExporter,
RasHolder rasHolder, RasHolder rasHolder,
IHostApplicationLifetime appLifetime,
ILogger<CommandsWatcher> logger) ILogger<CommandsWatcher> logger)
{ {
var scope = serviceProvider.CreateAsyncScope(); var scope = serviceProvider.CreateAsyncScope();
@ -31,65 +37,74 @@ namespace OnecMonitor.Agent.Services
_appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); _appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
_techLogExporter = techLogExporter; _techLogExporter = techLogExporter;
_updateTasksQueue = updateTasksQueue; _updateTasksQueue = updateTasksQueue;
_maintenanceTasksQueue = maintenanceTasksQueue;
_applicationLifetime = appLifetime;
_logger = logger; _logger = logger;
_server.MessageReceived += MessageReceived;
_applicationLifetime.ApplicationStopping.Register(() =>
{
_server.MessageReceived -= MessageReceived;
});
} }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) private async void MessageReceived(object? sender, Message message)
{ {
_logger.LogTrace("Start watching commands"); try
await _server.Send(MessageType.SubscribingForCommands, stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{ {
try // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (message.Header.Type)
{ {
var message = await _server.ReadMessage(stoppingToken); case MessageType.UpdateTechLogSeancesRequest:
await UpdateTechLogSeancesByRequest(message, _applicationLifetime.ApplicationStopping);
await UpdateSettings(stoppingToken); break;
case MessageType.InstalledPlatformsRequest:
try await SendInstalledPlatforms(message, _applicationLifetime.ApplicationStopping);
{ break;
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault case MessageType.ClustersRequest:
switch (message.Header.Type) await SendV8Clusters(message, _applicationLifetime.ApplicationStopping);
{ break;
case MessageType.UpdateTechLogSeancesRequest: case MessageType.InfoBasesRequest:
await UpdateTechLogSeancesByRequest(message, stoppingToken); await SendV8InfoBases(message, _applicationLifetime.ApplicationStopping);
break; break;
case MessageType.InstalledPlatformsRequest: case MessageType.RagentServicesRequest:
await SendInstalledPlatforms(message, stoppingToken); await SendRagentServices(message, _applicationLifetime.ApplicationStopping);
break; break;
case MessageType.ClustersRequest: case MessageType.RasServicesRequest:
await SendV8Clusters(message, stoppingToken); await SendRasServices(message, _applicationLifetime.ApplicationStopping);
break; break;
case MessageType.InfoBasesRequest: case MessageType.UpdateInfoBasesRequest:
await SendV8InfoBases(message, stoppingToken); await HandleUpdateInfoBasesRequest(message, _applicationLifetime.ApplicationStopping);
break; break;
case MessageType.RagentServicesRequest: case MessageType.UpdateSettingsRequest:
await SendRagentServices(message, stoppingToken); await HandleUpdateSettingsRequest(message, _applicationLifetime.ApplicationStopping);
break; break;
case MessageType.RasServicesRequest: case MessageType.MaintenanceTask:
await SendRasServices(message, stoppingToken); await HandleMaintenanceTask(message, _applicationLifetime.ApplicationStopping);
break; break;
case MessageType.UpdateInfoBasesRequest: default:
await HandleUpdateInfoBasesRequest(message, stoppingToken); throw new Exception($"Получено неожиданное сообщение: {message.Header.Type}");
break;
case MessageType.UpdateSettingsRequest:
await HandleUpdateSettingsRequest(message, stoppingToken);
break;
default:
throw new Exception($"Получено неожиданное сообщение: {message.Header.Type}");
}
}
catch (OperationCanceledException) {}
catch (Exception ex)
{
_logger.LogTrace(ex, "Ошибка обработки сообщения");
await _server.SendError(message, ex.Message, stoppingToken);
}
} }
catch (OperationCanceledException) {}
} }
catch (OperationCanceledException) {}
catch (Exception ex)
{
_logger.LogTrace(ex, "Ошибка обработки сообщения");
await _server.SendError(message, ex.Message, _applicationLifetime.ApplicationStopping);
}
}
private async Task HandleMaintenanceTask(Message message, CancellationToken cancellationToken)
{
var task = MessagePackSerializer.Deserialize<MaintenanceTaskDto>(message.Data, cancellationToken: cancellationToken);
await _maintenanceTasksQueue.QueueAsync(task, cancellationToken);
await _server.SendOk(message, cancellationToken);
}
public async Task Start(CancellationToken token)
{
await _server.Start(true);
} }
private async Task SendInstalledPlatforms(Message message, CancellationToken cancellationToken) private async Task SendInstalledPlatforms(Message message, CancellationToken cancellationToken)
@ -106,8 +121,9 @@ namespace OnecMonitor.Agent.Services
private async Task HandleUpdateSettingsRequest(Message message, CancellationToken cancellationToken) private async Task HandleUpdateSettingsRequest(Message message, CancellationToken cancellationToken)
{ {
await _server.SendOk(message, cancellationToken);
await UpdateSettings(cancellationToken); await UpdateSettings(cancellationToken);
await _server.SendOk(message, cancellationToken);
} }
private async Task UpdateSettings(CancellationToken cancellationToken) private async Task UpdateSettings(CancellationToken cancellationToken)

View File

@ -12,12 +12,10 @@ public sealed class InfoBasesUpdater : BackgroundService
private readonly InfoBasesUpdateTasksQueue _queue; private readonly InfoBasesUpdateTasksQueue _queue;
private readonly AsyncServiceScope _scope; private readonly AsyncServiceScope _scope;
private readonly OnecMonitorConnection _server; private readonly OnecMonitorConnection _serverConnection;
private readonly IHostApplicationLifetime _applicationLifetime; private readonly IHostApplicationLifetime _applicationLifetime;
private readonly RasHolder _rasHolder; private readonly RasHolder _rasHolder;
private readonly ILogger<InfoBasesUpdater> _logger; private readonly ILogger<InfoBasesUpdater> _logger;
private bool _disposed;
private readonly object _racLocker = new();
public InfoBasesUpdater( public InfoBasesUpdater(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
@ -28,7 +26,7 @@ public sealed class InfoBasesUpdater : BackgroundService
{ {
_scope = serviceProvider.CreateAsyncScope(); _scope = serviceProvider.CreateAsyncScope();
_queue = tasksQueue; _queue = tasksQueue;
_server = _scope.ServiceProvider.GetRequiredService<OnecMonitorConnection>(); _serverConnection = _scope.ServiceProvider.GetRequiredService<OnecMonitorConnection>();
_applicationLifetime = applicationLifetime; _applicationLifetime = applicationLifetime;
_rasHolder = rasHolder; _rasHolder = rasHolder;
_logger = logger; _logger = logger;
@ -36,6 +34,8 @@ public sealed class InfoBasesUpdater : BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
await _serverConnection.Start();
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
{ {
await _queue.DequeueAsync(stoppingToken); await _queue.DequeueAsync(stoppingToken);
@ -58,7 +58,7 @@ public sealed class InfoBasesUpdater : BackgroundService
const string accessCode = "12345"; const string accessCode = "12345";
const string message = "Технические работы"; const string message = "Технические работы";
var task = await _server.Get<UpdateInfoBaseTaskDto>( var task = await _serverConnection.Get<UpdateInfoBaseTaskDto>(
MessageType.UpdateInfoBasesTaskRequest, MessageType.UpdateInfoBasesTaskRequest,
MessageType.UpdateInfoBasesTask, MessageType.UpdateInfoBasesTask,
_applicationLifetime.ApplicationStopping); _applicationLifetime.ApplicationStopping);
@ -69,7 +69,7 @@ public sealed class InfoBasesUpdater : BackgroundService
var configurationsPaths = new Dictionary<string, string>(); var configurationsPaths = new Dictionary<string, string>();
task.Files.ForEach(i => task.Files.ForEach(i =>
{ {
var path = Path.Join(Path.GetTempPath(), $"{i.Id}.cfu") ; var path = Path.Join(Path.GetTempPath(), $"{i.Id}{i.FileExtension}") ;
if (!File.Exists(path)) if (!File.Exists(path))
{ {
@ -108,35 +108,31 @@ public sealed class InfoBasesUpdater : BackgroundService
await AddLogItemAndSend(task, infoBase, log, "Блокировка соединений и регламентных заданий", cancellationToken); await AddLogItemAndSend(task, infoBase, log, "Блокировка соединений и регламентных заданий", cancellationToken);
lock (_racLocker) rac.BlockConnections(
rac.BlockConnections( infoBase.Cluster.Id,
infoBase.Cluster.Id, infoBase.InfoBaseInternalId,
infoBase.InfoBaseInternalId, infoBase.Credentials.User,
infoBase.Credentials.User, infoBase.Credentials.Password,
infoBase.Credentials.Password, accessCode,
accessCode, message);
message);
await AddLogItemAndSend(task, infoBase, log, "Завершение сессий", cancellationToken); await AddLogItemAndSend(task, infoBase, log, "Завершение сессий", cancellationToken);
lock (_racLocker) var sessions = rac.GetInfoBaseSessions(infoBase.Cluster.Id, infoBase.InfoBaseInternalId);
{ sessions
var sessions = rac.GetInfoBaseSessions(infoBase.Cluster.Id, infoBase.InfoBaseInternalId); .Where(c => !c.AppId.Contains("RAS", StringComparison.CurrentCultureIgnoreCase))
sessions .ToList()
.Where(c => !c.AppId.Contains("RAS", StringComparison.CurrentCultureIgnoreCase)) .ForEach(s =>
.ToList() {
.ForEach(s => try
{ {
try rac.TerminateSession(infoBase.Cluster.Id, s.Id);
{ }
rac.TerminateSession(infoBase.Cluster.Id, s.Id); catch
} {
catch // Игнорируем, т.к. сеанс уже мог быть закрыт, мог быть повисшим и т.п.
{ }
// Игнорируем, т.к. сеанс уже мог быть закрыт, мог быть повисшим и т.п. });
}
});
}
if (config != null) if (config != null)
{ {
@ -189,7 +185,7 @@ public sealed class InfoBasesUpdater : BackgroundService
await AddLogItemAndSend(task, infoBase, log, loadExtBatch.OutFileContent, cancellationToken); await AddLogItemAndSend(task, infoBase, log, loadExtBatch.OutFileContent, cancellationToken);
} }
var needAcceptLegalUsing = config != null; var needAcceptLegalUsing = config is { IsConfiguration: true };
if (needAcceptLegalUsing) if (needAcceptLegalUsing)
{ {
await AddLogItemAndSend(task, infoBase, log, "Подтверждение легальности получения и запуск обработчиков обновления", cancellationToken); await AddLogItemAndSend(task, infoBase, log, "Подтверждение легальности получения и запуск обработчиков обновления", cancellationToken);
@ -206,12 +202,11 @@ public sealed class InfoBasesUpdater : BackgroundService
await AddLogItemAndSend(task, infoBase, log, "Разблокировка соединений и регламентных заданий", cancellationToken); await AddLogItemAndSend(task, infoBase, log, "Разблокировка соединений и регламентных заданий", cancellationToken);
lock (_racLocker) rac.UnblockConnections(
rac.UnblockConnections( infoBase.Cluster.Id,
infoBase.Cluster.Id, infoBase.InfoBaseInternalId,
infoBase.InfoBaseInternalId, infoBase.Credentials.User,
infoBase.Credentials.User, infoBase.Credentials.Password);
infoBase.Credentials.Password);
await AddLogItemAndSend(task, infoBase, log, "Обновление завершено", _applicationLifetime.ApplicationStopping, false, true); await AddLogItemAndSend(task, infoBase, log, "Обновление завершено", _applicationLifetime.ApplicationStopping, false, true);
} }
@ -256,30 +251,14 @@ public sealed class InfoBasesUpdater : BackgroundService
} }
private async Task SendLog(List<UpdateInfoBaseTaskLogItemDto> log, CancellationToken cancellationToken) private async Task SendLog(List<UpdateInfoBaseTaskLogItemDto> log, CancellationToken cancellationToken)
=> await _server.Send(MessageType.UpdateInfoBaseTaskLog, log, cancellationToken); => await _serverConnection.Send(MessageType.UpdateInfoBaseTaskLog, log, cancellationToken);
private static string GetExternalDataProcessorPath(string fileName) private static string GetExternalDataProcessorPath(string fileName)
=> Path.Join(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Asserts", fileName); => Path.Join(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Asserts", fileName);
private void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_scope.Dispose();
_server.Dispose();
}
_disposed = true;
}
public override Task StopAsync(CancellationToken cancellationToken) public override Task StopAsync(CancellationToken cancellationToken)
{ {
_scope.Dispose(); _scope.Dispose();
_server.Dispose();
return base.StopAsync(cancellationToken); return base.StopAsync(cancellationToken);
} }
} }

View File

@ -0,0 +1,25 @@
using System.Collections.Concurrent;
using OnecMonitor.Common.DTO;
using OnecMonitor.Common.DTO.MaintenanceTasks;
using OneSTools.Common.Designer.Batch;
using OneSTools.Common.Platform;
using OneSTools.Common.Platform.RemoteAdministration;
namespace OnecMonitor.Agent.Services.MaintenanceTasks;
public class MaintenanceStepNodeContext
{
public InfoBaseDto InfoBase { get; set; } = null!;
public MaintenanceStepNodeDto Node { get; set; } = null!;
public string AccessCode { get; set; } = string.Empty;
public ConcurrentDictionary<Guid, string> V8Files { get; set; } = [];
public List<MaintenanceStepNodeLogItemDto> Log { get; set; } = [];
public Rac Rac { get; set; } = null!;
public V8Platform Platform { get; set; } = null!;
public OnecV8BatchMode GetBatchDesigner()
=> new(Platform, $"{InfoBase.Cluster.Host}:{InfoBase.Cluster.Port}", InfoBase.InfoBaseName);
public OnecV8BatchMode GetBatchEnterprise()
=> new(Platform, $"{InfoBase.Cluster.Host}:{InfoBase.Cluster.Port}", InfoBase.InfoBaseName, false);
}

View File

@ -0,0 +1,296 @@
using System.Collections.Concurrent;
using OnecMonitor.Agent.Services.InfoBases;
using OnecMonitor.Common.DTO;
using OnecMonitor.Common.DTO.MaintenanceTasks;
using OnecMonitor.Common.Extensions;
using OnecMonitor.Common.Models.MaintenanceTasks;
using OneSTools.Common.Designer.Batch;
using OneSTools.Common.Platform.RemoteAdministration;
using OneSTools.Common.Platform.Services;
namespace OnecMonitor.Agent.Services.MaintenanceTasks;
public class MaintenanceTaskExecutor : BackgroundService
{
private readonly MaintenanceTaskExecutorQueue _queue;
private readonly AsyncServiceScope _scope;
private readonly OnecMonitorConnection _serverConnection;
private readonly IHostApplicationLifetime _applicationLifetime;
private readonly RasHolder _rasHolder;
private readonly ILogger<MaintenanceTaskExecutor> _logger;
public MaintenanceTaskExecutor(
IServiceProvider serviceProvider,
MaintenanceTaskExecutorQueue tasksQueue,
RasHolder rasHolder,
IHostApplicationLifetime applicationLifetime,
ILogger<MaintenanceTaskExecutor> logger)
{
_scope = serviceProvider.CreateAsyncScope();
_queue = tasksQueue;
_serverConnection = _scope.ServiceProvider.GetRequiredService<OnecMonitorConnection>();
_applicationLifetime = applicationLifetime;
_rasHolder = rasHolder;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await _serverConnection.Start();
while (!stoppingToken.IsCancellationRequested)
{
var task = await _queue.DequeueAsync(stoppingToken);
try
{
await StartMaintenanceTask(task, stoppingToken);
}
catch (Exception e)
{
_logger.LogError(e, $"Ошибка обработки задания обслуживания: {e.Message}");
}
}
}
private async Task StartMaintenanceTask(MaintenanceTaskDto task, CancellationToken cancellationToken)
{
var v8Files = await SaveTaskV8Files(task, cancellationToken);
await Parallel.ForEachAsync(task.InfoBases, cancellationToken, async (infoBase, stoppingToken) =>
{
var log = new List<MaintenanceStepNodeLogItemDto>();
var context = new MaintenanceStepNodeContext
{
InfoBase = infoBase,
Log = log,
V8Files = v8Files,
Node = task.RootNode
};
try
{
var ragent = V8Services.GetActiveRagentForClusterPort(infoBase.Cluster.Port);
var ras = _rasHolder.GetActiveRasForRagent(ragent);
context.Rac = Rac.GetRacForRasService(ras);
context.Platform = ragent.Platform;
if (!context.Platform.HasOnecV8)
throw new Exception("Для платформы агента не установлен конфигуратор");
var currentNode = task.RootNode;
while (!stoppingToken.IsCancellationRequested && currentNode != null)
{
context.Node = currentNode;
if (currentNode.Kind == MaintenanceStepNodeKind.TryCatch)
{
try
{
HandleTaskStepNode(context);
currentNode = currentNode.LeftNode;
}
catch (Exception e)
{
AddLogItem(context, e.ToString(), true);
currentNode = currentNode!.RightNode;
}
}
else
{
HandleTaskStepNode(context);
currentNode = currentNode.LeftNode;
}
}
AddLogItem(context, "Завершено", false, true);
await SendLog(log, stoppingToken);
}
catch (Exception e)
{
AddLogItem(context, e.ToString(), true, true);
await SendLog(log, stoppingToken);
}
});
}
private static void HandleTaskStepNode(MaintenanceStepNodeContext context)
{
AddLogItem(context, $"Обработка шага \"{context.Node.Step.Kind.GetDisplay()}\"");
switch (context.Node.Step.Kind)
{
case MaintenanceStepKind.LockConnections:
LockConnections(context);
break;
case MaintenanceStepKind.CloseConnections:
CloseConnections(context);
break;
case MaintenanceStepKind.UnlockConnections:
UnlockConnections(context);
break;
case MaintenanceStepKind.LoadExtension:
LoadExtension(context);
break;
case MaintenanceStepKind.UpdateConfiguration:
UpdateConfiguration(context);
break;
case MaintenanceStepKind.LoadConfiguration:
LoadConfiguration(context);
break;
case MaintenanceStepKind.StartExternalDataProcessor:
StartExternalDataProcessor(context);
break;
default:
throw new Exception($"Неизвестный тип шага \"{context.Node.Step.Kind.GetDisplay()}\"");
}
}
private static void AddLogItem(MaintenanceStepNodeContext context, string message, bool isError = false, bool isFinish = false)
{
context.Log.Add(new MaintenanceStepNodeLogItemDto
{
Id = Guid.NewGuid(),
Message = message,
IsError = isError,
IsFinish = isFinish,
TimeStamp = DateTime.Now,
InfoBaseId = context.InfoBase.Id,
StepNodeId = context.Node.Id
});
}
private async Task SendLog(List<MaintenanceStepNodeLogItemDto> log, CancellationToken cancellationToken)
=> await _serverConnection.Send(MessageType.MaintenanceStepNodeLog, log, cancellationToken);
private static async Task<ConcurrentDictionary<Guid, string>> SaveTaskV8Files(MaintenanceTaskDto task,
CancellationToken cancellationToken)
{
var files = new ConcurrentDictionary<Guid, string>();
await SaveStepNodeV8File(task.RootNode, files, cancellationToken);
return files;
}
private static async Task SaveStepNodeV8File(MaintenanceStepNodeDto node, ConcurrentDictionary<Guid, string> files, CancellationToken cancellationToken)
{
if (node.Step.File != null && !files.ContainsKey(node.Step.File.Id))
{
var path = Path.Join(Path.GetTempPath(), $"{node.Step.File.Id}{node.Step.File.FileExtension}") ;
if (!File.Exists(path))
{
await using var file = File.Create(path);
file.Write(node.Step.File.Data);
file.Close();
}
files.TryAdd(node.Step.File.Id, path);
node.Step.File.Data = null!;
}
if (node.LeftNode != null)
await SaveStepNodeV8File(node.LeftNode, files, cancellationToken);
if (node.RightNode != null)
await SaveStepNodeV8File(node.RightNode, files, cancellationToken);
}
private static void LockConnections(MaintenanceStepNodeContext context)
{
context.Rac.BlockConnections(
context.InfoBase.Cluster.Id,
context.InfoBase.InfoBaseInternalId,
context.InfoBase.Credentials.User,
context.InfoBase.Credentials.Password,
context.Node.Step.AccessCode,
context.Node.Step.Message);
context.AccessCode = context.Node.Step.AccessCode;
}
private static void CloseConnections(MaintenanceStepNodeContext context)
{
var sessions = context.Rac.GetInfoBaseSessions(context.InfoBase.Cluster.Id, context.InfoBase.InfoBaseInternalId);
sessions
.Where(c => !c.AppId.Contains("RAS", StringComparison.CurrentCultureIgnoreCase))
.ToList()
.ForEach(s =>
{
try
{
context.Rac.TerminateSession(context.InfoBase.Cluster.Id, s.Id);
}
catch
{
// Игнорируем, т.к. сеанс уже мог быть закрыт, мог быть повисшим и т.п.
}
});
}
private static void UnlockConnections(MaintenanceStepNodeContext context)
{
context.Rac.UnblockConnections(
context.InfoBase.Cluster.Id,
context.InfoBase.InfoBaseInternalId,
context.InfoBase.Credentials.User,
context.InfoBase.Credentials.Password);
}
private static void LoadExtension(MaintenanceStepNodeContext context)
{
var filePath = context.V8Files[context.Node.Step.File!.Id];
using var batch = context.GetBatchDesigner();
batch.LoadExtension(
context.Node.Step.File.Name,
filePath,
context.InfoBase.Credentials.User,
context.InfoBase.Credentials.Password,
context.AccessCode,
true);
}
private static void LoadConfiguration(MaintenanceStepNodeContext context)
{
var filePath = context.V8Files[context.Node.Step.File!.Id];
using var batch = context.GetBatchDesigner();
batch.LoadConfiguration(
filePath,
context.InfoBase.Credentials.User,
context.InfoBase.Credentials.Password,
context.AccessCode,
true);
}
private static void UpdateConfiguration(MaintenanceStepNodeContext context)
{
var filePath = context.V8Files[context.Node.Step.File!.Id];
using var batch = context.GetBatchDesigner();
batch.UpdateConfiguration(
filePath,
context.InfoBase.Credentials.User,
context.InfoBase.Credentials.Password,
context.AccessCode,
true);
}
private static void StartExternalDataProcessor(MaintenanceStepNodeContext context)
{
var filePath = context.V8Files[context.Node.Step.File!.Id];
var batch = context.GetBatchEnterprise();
batch.ExecuteExternalDataProcessor(
filePath,
context.InfoBase.Credentials.User,
context.InfoBase.Credentials.Password,
context.AccessCode,
true);
}
}

View File

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

View File

@ -3,56 +3,48 @@ using OnecMonitor.Common.DTO;
using MessagePack; using MessagePack;
using System.Net.Sockets; using System.Net.Sockets;
using System.Net; using System.Net;
using OnecMonitor.Agent.Models;
using OneSTools.Common.Platform; using OneSTools.Common.Platform;
namespace OnecMonitor.Agent.Services namespace OnecMonitor.Agent.Services
{ {
public class OnecMonitorConnection : ServerConnection public class OnecMonitorConnection : ServerConnection
{ {
public OnecMonitorConnection(IServiceProvider serviceProvider, IHostApplicationLifetime hostApplicationLifetime) private readonly string _host;
: base(serviceProvider.GetRequiredService<ILogger<OnecMonitorConnection>>()) private readonly int _port;
private readonly AgentInstance _agent;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
public OnecMonitorConnection(
IServiceProvider serviceProvider,
IConfiguration configuration,
IHostApplicationLifetime hostApplicationLifetime,
ILogger<OnecMonitorConnection> logger) : base(logger)
{ {
var configuration = serviceProvider.GetRequiredService<IConfiguration>(); _hostApplicationLifetime = hostApplicationLifetime;
using var scope = serviceProvider.CreateAsyncScope(); using var scope = serviceProvider.CreateAsyncScope();
using var appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); using var appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var agentInstance = appDbContext.AgentInstance.FirstOrDefault(); _agent = appDbContext.AgentInstance.FirstOrDefault()!;
var instanceName = configuration.GetValue("Agent:InstanceName", Environment.MachineName); _host = configuration.GetValue("OnecMonitor:Host", "0.0.0.0");
if (string.IsNullOrEmpty(instanceName)) _port = configuration.GetValue("OnecMonitor:Port", 7001);
instanceName = Environment.MachineName; }
if (agentInstance == null) public async Task Start(bool mainConnection = false)
{
await Start(_host, _port, async () =>
{ {
agentInstance = new AgentInstance await WriteMessageToStream(MessageType.AgentInfo,
{ new AgentInstanceDto
Id = Guid.NewGuid(), {
InstanceName = instanceName, Id = _agent.Id,
UtcOffset = TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow).TotalSeconds InstanceName = _agent.InstanceName,
}; MainConnection = mainConnection,
UtcOffset = TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow).TotalSeconds
appDbContext.AgentInstance.Add(agentInstance); },
appDbContext.SaveChanges(); _hostApplicationLifetime.ApplicationStopping);
} }, _hostApplicationLifetime.ApplicationStopping);
else if (agentInstance.InstanceName != instanceName)
{
agentInstance.InstanceName = instanceName;
appDbContext.AgentInstance.Update(agentInstance);
appDbContext.SaveChanges();
}
Connected += async (_, _) =>
{
await WriteMessageToStream(MessageType.AgentInfo, agentInstance, hostApplicationLifetime.ApplicationStopping);
};
var host = configuration.GetValue("OnecMonitor:Host", "0.0.0.0");
var port = configuration.GetValue("OnecMonitor:Port", 7001);
var logger = serviceProvider.GetRequiredService<ILogger<OnecMonitorConnection>>();
Start(host, port, logger, hostApplicationLifetime.ApplicationStopping);
} }
} }
} }

View File

@ -50,6 +50,8 @@ namespace OnecMonitor.Agent.Services.TechLog
{ {
_techLogWatcher.Stop(); _techLogWatcher.Stop();
}); });
_onecMonitorConnection.Start();
try try
{ {

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -20,11 +20,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ConfigureAwait.Fody" Version="3.3.2" PrivateAssets="All" />
<PackageReference Include="Fody" Version="6.9.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MessagePack" Version="3.1.2" /> <PackageReference Include="MessagePack" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.1" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
@ -45,6 +40,7 @@
<ItemGroup> <ItemGroup>
<Folder Include="Asserts\" /> <Folder Include="Asserts\" />
<Folder Include="Migrations\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -8,12 +8,14 @@ using System.Threading.Tasks;
namespace OnecMonitor.Common.DTO; namespace OnecMonitor.Common.DTO;
[MessagePackObject] [MessagePackObject]
public class AgentInstance public class AgentInstanceDto
{ {
[Key(0)] [Key(0)]
public Guid Id { get; set; } = Guid.Empty; public Guid Id { get; set; } = Guid.Empty;
[Key(1)] [Key(1)]
public string InstanceName { get; set; } = string.Empty; public string InstanceName { get; set; } = string.Empty;
[Key(2)] [Key(2)]
public bool MainConnection { get; set; }
[Key(3)]
public double UtcOffset { get; set; } = 0; public double UtcOffset { get; set; } = 0;
} }

View File

@ -0,0 +1,19 @@
using MessagePack;
using OnecMonitor.Common.Models.MaintenanceTasks;
namespace OnecMonitor.Common.DTO.MaintenanceTasks;
[MessagePackObject]
public class MaintenanceStepDto
{
[Key(0)]
public Guid Id { get; set; } = Guid.Empty;
[Key(1)]
public MaintenanceStepKind Kind { get; set; }
[Key(2)]
public string AccessCode { get; set; } = string.Empty;
[Key(3)]
public string Message { get; set; } = string.Empty;
[Key(4)]
public V8FileDto? File { get; set; }
}

View File

@ -0,0 +1,19 @@
using MessagePack;
using OnecMonitor.Common.Models.MaintenanceTasks;
namespace OnecMonitor.Common.DTO.MaintenanceTasks;
[MessagePackObject]
public class MaintenanceStepNodeDto
{
[Key(0)]
public Guid Id { get; set; } = Guid.Empty;
[Key(1)]
public MaintenanceStepNodeKind Kind { get; set; }
[Key(2)]
public MaintenanceStepNodeDto? LeftNode { get; set; }
[Key(3)]
public MaintenanceStepNodeDto? RightNode { get; set; }
[Key(4)]
public MaintenanceStepDto Step { get; set; } = null!;
}

View File

@ -0,0 +1,21 @@
using MessagePack;
namespace OnecMonitor.Common.DTO.MaintenanceTasks;
public class MaintenanceStepNodeLogItemDto
{
[Key(0)]
public Guid Id { get; set; }
[Key(1)]
public DateTime TimeStamp { get; set; }
[Key(2)]
public bool IsError { get; set; }
[Key(3)]
public bool IsFinish { get; set; }
[Key(4)]
public string Message { get; set; } = string.Empty;
[Key(5)]
public Guid InfoBaseId { get; set; }
[Key(6)]
public Guid StepNodeId { get; set; }
}

View File

@ -0,0 +1,14 @@
using MessagePack;
namespace OnecMonitor.Common.DTO.MaintenanceTasks;
[MessagePackObject]
public class MaintenanceTaskDto
{
[Key(0)]
public Guid Id { get; set; } = Guid.Empty;
[Key(1)]
public MaintenanceStepNodeDto RootNode { get; set; } = null!;
[Key(2)]
public List<InfoBaseDto> InfoBases { get; set; } = [];
}

View File

@ -8,7 +8,6 @@
// Client messages // Client messages
AgentInfo, AgentInfo,
SubscribingForCommands,
TechLogSeancesRequest, TechLogSeancesRequest,
LastFilePositionRequest, LastFilePositionRequest,
TechLogEventContent, TechLogEventContent,
@ -21,6 +20,8 @@
UpdateInfoBasesTaskRequest, UpdateInfoBasesTaskRequest,
SettingsRequest, SettingsRequest,
MaintenanceStepNodeLog,
// Server messages // Server messages
LastFilePosition, LastFilePosition,
TechLogSeances, TechLogSeances,
@ -33,6 +34,8 @@
UpdateInfoBasesRequest, UpdateInfoBasesRequest,
UpdateInfoBasesTask, UpdateInfoBasesTask,
UpdateSettingsRequest, UpdateSettingsRequest,
Settings Settings,
MaintenanceTask,
} }
} }

View File

@ -10,13 +10,15 @@ public class V8FileDto
[Key(1)] [Key(1)]
public string Name { get; set; } public string Name { get; set; }
[Key(2)] [Key(2)]
public string FileExtension { get; set; }
[Key(3)]
public string Version { get; set; } public string Version { get; set; }
[Key(3)]
public bool IsUpdate { get; set; } = false;
[Key(4)] [Key(4)]
public bool IsExtension { get; set; } = false; public bool IsUpdate { get; set; } = false;
[Key(5)] [Key(5)]
public bool IsConfiguration { get; set; } = false; public bool IsExtension { get; set; } = false;
[Key(6)] [Key(6)]
public bool IsConfiguration { get; set; } = false;
[Key(7)]
public byte[] Data { get; set; } = []; public byte[] Data { get; set; } = [];
} }

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace OnecMonitor.Server.Extensions; namespace OnecMonitor.Common.Extensions;
public static class EnumExtension public static class EnumExtension
{ {

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Diagnostics.SymbolStore; using System.Diagnostics.SymbolStore;
using System.Net.Sockets; using System.Net.Sockets;
using System.Reflection.PortableExecutable; using System.Reflection.PortableExecutable;
using System.Runtime.CompilerServices;
using System.Threading.Channels; using System.Threading.Channels;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -16,11 +17,11 @@ public abstract class FastConnection(ILogger<FastConnection> logger) : IDisposab
private CancellationToken _cancellationToken; private CancellationToken _cancellationToken;
private readonly ConcurrentDictionary<Guid, TaskCompletionSource<Message>> _calls = new(); private readonly ConcurrentDictionary<Guid, TaskCompletionSource<Message>> _calls = new();
private readonly Channel<Message> _inputChannel = Channel.CreateBounded<Message>(1000); private readonly Channel<Message> _messagesChannel = Channel.CreateUnbounded<Message>();
private readonly Channel<Message> _outputChannel = Channel.CreateBounded<Message>(1000);
private readonly SemaphoreSlim _disconnectingEventSemaphore = new(0); private readonly SemaphoreSlim _disconnectingEventSemaphore = new(0);
private bool _disposedValue; private bool _disposedValue;
public event EventHandler<Message>? MessageReceived;
protected internal event EventHandler? Disconnected; protected internal event EventHandler? Disconnected;
private void RaiseDisconnected() private void RaiseDisconnected()
@ -43,9 +44,6 @@ public abstract class FastConnection(ILogger<FastConnection> logger) : IDisposab
_disconnectingEventSemaphore.Release(); _disconnectingEventSemaphore.Release();
} }
public async Task<Message> ReadMessage(CancellationToken cancellationToken)
=> await _inputChannel.Reader.ReadAsync(cancellationToken);
public async Task SendOk(Message callMessage, CancellationToken cancellationToken) public async Task SendOk(Message callMessage, CancellationToken cancellationToken)
{ {
var header = new MessageHeader(MessageType.Ok, 0, callMessage.Header.CallId); var header = new MessageHeader(MessageType.Ok, 0, callMessage.Header.CallId);
@ -115,16 +113,16 @@ public abstract class FastConnection(ILogger<FastConnection> logger) : IDisposab
private async Task<TResult> WriteMessageAndWaitResult<TResult>(Message message, MessageType responseMessageType, CancellationToken cancellationToken) private async Task<TResult> WriteMessageAndWaitResult<TResult>(Message message, MessageType responseMessageType, CancellationToken cancellationToken)
{ {
var cts = new TaskCompletionSource<Message>(); var cts = new TaskCompletionSource<Message>(TaskCreationOptions.RunContinuationsAsynchronously);
_calls.TryAdd(message.Header.CallId, cts); _calls.TryAdd(message.Header.CallId, cts);
logger.LogTrace($"Постановка сообщения в очередь отправки. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}"); logger.LogTrace($"Постановка сообщения в очередь отправки. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
await _outputChannel.Writer.WriteAsync(message, cancellationToken); await _messagesChannel.Writer.WriteAsync(message, cancellationToken);
logger.LogTrace($"Ожидание подтверждения на сообщение. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}"); logger.LogTrace($"Ожидание подтверждения получения сообщения. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
var result = await cts.Task.WaitAsync(cancellationToken); var result = await cts.Task.WaitAsync(cancellationToken);
logger.LogTrace($"Подтверждение сообщения получено. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}"); logger.LogTrace($"Подтверждение получения сообщения получено. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
if (result == null) if (result == null)
throw new TimeoutException("Ошибка получения ответа на вызов"); throw new TimeoutException("Ошибка получения ответа на вызов");
@ -141,14 +139,14 @@ public abstract class FastConnection(ILogger<FastConnection> logger) : IDisposab
private async Task WriteMessage(Message message, bool needWait, CancellationToken cancellationToken) private async Task WriteMessage(Message message, bool needWait, CancellationToken cancellationToken)
{ {
if (!needWait) if (!needWait)
await _outputChannel.Writer.WriteAsync(message, cancellationToken); await _messagesChannel.Writer.WriteAsync(message, cancellationToken);
else else
{ {
var cts = new TaskCompletionSource<Message>(); var cts = new TaskCompletionSource<Message>(TaskCreationOptions.RunContinuationsAsynchronously);
_calls.TryAdd(message.Header.CallId, cts); _calls.TryAdd(message.Header.CallId, cts);
logger.LogTrace($"Постановка сообщения в очередь отправки. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}"); logger.LogTrace($"Постановка сообщения в очередь отправки. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
await _outputChannel.Writer.WriteAsync(message, cancellationToken); await _messagesChannel.Writer.WriteAsync(message, cancellationToken);
logger.LogTrace($"Ожидание подтверждения на сообщение. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}"); logger.LogTrace($"Ожидание подтверждения на сообщение. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
var result = await cts.Task.WaitAsync(cancellationToken); var result = await cts.Task.WaitAsync(cancellationToken);
@ -165,7 +163,29 @@ public abstract class FastConnection(ILogger<FastConnection> logger) : IDisposab
throw new Exception($"Получено неожиданное сообщение. Ожидаемый тип: {MessageType.Ok}"); throw new Exception($"Получено неожиданное сообщение. Ожидаемый тип: {MessageType.Ok}");
} }
} }
private async Task StartWritingToStream()
{
try
{
while (!_cancellationToken.IsCancellationRequested)
{
var message = await _messagesChannel.Reader.ReadAsync(_cancellationToken);
await Socket!.SendAsync(message.Header.AsMemory(), _cancellationToken);
if (message.Data.Length > 0)
await Socket!.SendAsync(message.Data, _cancellationToken);
logger.LogTrace($"Отправлено сообщение. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
}
}
catch
{
RaiseDisconnected();
}
}
protected async Task WriteMessageToStream<T>(MessageType messageType, T item, CancellationToken cancellationToken) protected async Task WriteMessageToStream<T>(MessageType messageType, T item, CancellationToken cancellationToken)
{ {
var data = MessagePackSerializer.Serialize(item, cancellationToken: cancellationToken).AsMemory(); var data = MessagePackSerializer.Serialize(item, cancellationToken: cancellationToken).AsMemory();
@ -178,29 +198,7 @@ public abstract class FastConnection(ILogger<FastConnection> logger) : IDisposab
if (header.Length > 0) if (header.Length > 0)
await Socket!.SendAsync(data, cancellationToken); await Socket!.SendAsync(data, cancellationToken);
logger.LogTrace($"Отправлено сообщение в поток. Тип: {header.Type}. Идентификатор: {header.CallId}"); logger.LogTrace($"Отправлено сообщение. Тип: {header.Type}. Идентификатор: {header.CallId}");
}
catch
{
RaiseDisconnected();
}
}
private async Task StartWritingToStream()
{
try
{
while (!_cancellationToken.IsCancellationRequested)
{
var message = await _outputChannel.Reader.ReadAsync(_cancellationToken);
await Socket!.SendAsync(message.Header.AsMemory(), _cancellationToken);
if (message.Data.Length > 0)
await Socket!.SendAsync(message.Data, _cancellationToken);
logger.LogTrace($"Отправлено сообщение в поток. Тип: {message.Header.Type}. Идентификатор: {message.Header.CallId}");
}
} }
catch catch
{ {
@ -231,8 +229,10 @@ public abstract class FastConnection(ILogger<FastConnection> logger) : IDisposab
if (_calls.TryGetValue(message.Header.CallId, out var cts)) if (_calls.TryGetValue(message.Header.CallId, out var cts))
cts.TrySetResult(message); cts.TrySetResult(message);
else if (MessageReceived is not null)
MessageReceived.Invoke(this, message);
else else
await _inputChannel.Writer.WriteAsync(message, _cancellationToken); await SendError(message, "Не найдено подключенных обработчиков сообщений", _cancellationToken);
} }
} }
catch catch

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

View File

@ -1,15 +1,15 @@
using System.ComponentModel; using System.ComponentModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace OnecMonitor.Server.Models.MaintenanceTasks; namespace OnecMonitor.Common.Models.MaintenanceTasks;
public enum MaintenanceStepKind public enum MaintenanceStepKind
{ {
[Display(Name = "Блокировка сеансов")] [Display(Name = "Блокировка соединений")]
LockConnections, LockConnections,
[Display(Name = "Закрытие сеансов")] [Display(Name = "Закрытие сеансов")]
CloseConnections, CloseConnections,
[Display(Name = "Разблокировка сеансов")] [Display(Name = "Разблокировка соединений")]
UnlockConnections, UnlockConnections,
[Display(Name = "Загрузка расширения")] [Display(Name = "Загрузка расширения")]
LoadExtension, LoadExtension,
@ -17,8 +17,6 @@ public enum MaintenanceStepKind
UpdateConfiguration, UpdateConfiguration,
[Display(Name = "Загрузка конфигурации")] [Display(Name = "Загрузка конфигурации")]
LoadConfiguration, LoadConfiguration,
[Display(Name = "Обновление информационной базы")]
UpdateDatabase,
[Display(Name = "Запуск внешней обработки")] [Display(Name = "Запуск внешней обработки")]
StartExternalDataProcessor StartExternalDataProcessor
} }

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace OnecMonitor.Server.Models.MaintenanceTasks; namespace OnecMonitor.Common.Models.MaintenanceTasks;
public enum MaintenanceStepNodeKind public enum MaintenanceStepNodeKind
{ {

View File

@ -1,34 +0,0 @@
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

@ -1,20 +1,17 @@
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OnecMonitor.Common.DTO;
namespace OnecMonitor.Common; namespace OnecMonitor.Common;
public class ServerConnection(ILogger<ServerConnection> logger) : FastConnection(logger) public class ServerConnection(ILogger<ServerConnection> logger) : FastConnection(logger)
{ {
private ILogger<ServerConnection> _logger = null!;
private string _host = null!; private string _host = null!;
private int _port; private int _port;
public event EventHandler? Connected;
protected void Start(string host, int port, ILogger<ServerConnection> logger, CancellationToken token) protected async Task Start(string host, int port, Func<Task> afterConnectCallback, CancellationToken token)
{ {
_logger = logger;
_host = host; _host = host;
_port = port; _port = port;
@ -23,22 +20,22 @@ public class ServerConnection(ILogger<ServerConnection> logger) : FastConnection
logger.LogWarning("Отключен от сервера"); logger.LogWarning("Отключен от сервера");
if (!token.IsCancellationRequested) if (!token.IsCancellationRequested)
_ = TryConnectInLoop(token); _ = TryConnectInLoop(afterConnectCallback, token);
}; };
_ = TryConnectInLoop(token); await TryConnectInLoop(afterConnectCallback, token);
} }
private async Task TryConnectInLoop(CancellationToken cancellationToken) private async Task TryConnectInLoop(Func<Task> afterConnectCallback, CancellationToken cancellationToken)
{ {
await ConnectInLoop(cancellationToken); await ConnectInLoop(afterConnectCallback, cancellationToken);
RunStreamLoops(cancellationToken); RunStreamLoops(cancellationToken);
_logger.LogTrace("Циклы потоков чтения/записи запущены"); logger.LogTrace("Циклы потоков чтения/записи запущены");
} }
private async Task ConnectInLoop(CancellationToken cancellationToken) private async Task ConnectInLoop(Func<Task> afterConnectCallback, CancellationToken cancellationToken)
{ {
do do
{ {
@ -46,11 +43,13 @@ public class ServerConnection(ILogger<ServerConnection> logger) : FastConnection
{ {
await Reconnect(cancellationToken); await Reconnect(cancellationToken);
_logger.LogTrace("Установлено соединение с сервером"); logger.LogTrace("Установлено соединение с сервером");
await afterConnectCallback();
} }
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.NotConnected) catch (SocketException ex) when (ex.SocketErrorCode == SocketError.NotConnected)
{ {
_logger.LogTrace("Ошибка установки соединения с сервером"); logger.LogTrace("Ошибка установки соединения с сервером");
} }
if (Socket?.Connected == true) if (Socket?.Connected == true)
@ -61,16 +60,17 @@ public class ServerConnection(ILogger<ServerConnection> logger) : FastConnection
private async Task Reconnect(CancellationToken cancellationToken) private async Task Reconnect(CancellationToken cancellationToken)
{ {
_logger.LogTrace($"Попытка подключения к {_host}:{_port}"); logger.LogTrace($"Попытка подключения к {_host}:{_port}");
Socket?.Dispose(); Socket?.Dispose();
Socket = null;
var addresses = await Dns.GetHostAddressesAsync(_host, AddressFamily.InterNetwork, cancellationToken); var addresses = await Dns.GetHostAddressesAsync(_host, AddressFamily.InterNetwork, cancellationToken);
if (addresses.Length == 0) if (addresses.Length == 0)
throw new Exception("Не удалось определить адрес сервера"); throw new Exception("Не удалось определить адрес сервера");
var endPoint = new IPEndPoint(addresses[0], _port); var endPoint = new IPEndPoint(addresses[0], _port);
_logger.LogTrace($"Адрес сервера: {endPoint.Address}"); logger.LogTrace($"Адрес сервера: {endPoint.Address}");
Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
{ {
@ -91,9 +91,5 @@ public class ServerConnection(ILogger<ServerConnection> logger) : FastConnection
if (!Socket.Connected) if (!Socket.Connected)
throw new SocketException((int)SocketError.NotConnected); throw new SocketException((int)SocketError.NotConnected);
_logger.LogInformation("Подключен к серверу");
Connected?.Invoke(this, EventArgs.Empty);
} }
} }

View File

@ -5,13 +5,13 @@ namespace OnecMonitor.Common.TechLog
{ {
public static class TechLogParser public static class TechLogParser
{ {
public static bool TryParse(AgentInstance agentInstance, TechLogEventContentDto item, out TjEvent tjEvent) public static bool TryParse(AgentInstanceDto agentInstanceDto, TechLogEventContentDto item, out TjEvent tjEvent)
{ {
var content = item.Content.AsSpan(); var content = item.Content.AsSpan();
tjEvent = new TjEvent() tjEvent = new TjEvent()
{ {
AgentId = agentInstance.Id, AgentId = agentInstanceDto.Id,
SeanceId = item.SeanceId, SeanceId = item.SeanceId,
Folder = item.Folder, Folder = item.Folder,
File = item.File, File = item.File,
@ -19,7 +19,7 @@ namespace OnecMonitor.Common.TechLog
}; };
int offset; int offset;
if (TryParseDateTime(content[..26], agentInstance, out var dateTime)) if (TryParseDateTime(content[..26], agentInstanceDto, out var dateTime))
{ {
offset = 27; offset = 27;
tjEvent.DateTime = dateTime; tjEvent.DateTime = dateTime;
@ -57,11 +57,11 @@ namespace OnecMonitor.Common.TechLog
return true; return true;
} }
private static bool TryParseDateTime(ReadOnlySpan<char> content, AgentInstance agentInstance, out DateTime dateTime) private static bool TryParseDateTime(ReadOnlySpan<char> content, AgentInstanceDto agentInstanceDto, out DateTime dateTime)
{ {
if (DateTime.TryParse(content, out var eventDateTime)) if (DateTime.TryParse(content, out var eventDateTime))
{ {
dateTime = DateTime.SpecifyKind(eventDateTime.AddSeconds(-agentInstance.UtcOffset), DateTimeKind.Utc); dateTime = DateTime.SpecifyKind(eventDateTime.AddSeconds(-agentInstanceDto.UtcOffset), DateTimeKind.Utc);
return true; return true;
} }
else else

View File

@ -14,7 +14,7 @@ namespace OnecMonitor.Common.TechLog
{ {
private readonly ILogger<TechLogProcessor> _logger; private readonly ILogger<TechLogProcessor> _logger;
private readonly ActionBlock<(AgentInstance, TechLogEventContentDto)> _parseblock; private readonly ActionBlock<(AgentInstanceDto, TechLogEventContentDto)> _parseblock;
private readonly BatchBlock<TjEvent> _batchBlock; private readonly BatchBlock<TjEvent> _batchBlock;
private readonly ActionBlock<TjEvent[]> _sendBlock; private readonly ActionBlock<TjEvent[]> _sendBlock;
@ -53,7 +53,7 @@ namespace OnecMonitor.Common.TechLog
MaxDegreeOfParallelism = Environment.ProcessorCount, MaxDegreeOfParallelism = Environment.ProcessorCount,
BoundedCapacity = 10000 BoundedCapacity = 10000
}; };
_parseblock = new ActionBlock<(AgentInstance AgentInstance, TechLogEventContentDto Item)>(async i => _parseblock = new ActionBlock<(AgentInstanceDto AgentInstance, TechLogEventContentDto Item)>(async i =>
{ {
try try
{ {
@ -83,9 +83,9 @@ namespace OnecMonitor.Common.TechLog
} }
} }
public async Task ProcessTjEventContent(AgentInstance agentInstance, TechLogEventContentDto tjEventContent, CancellationToken cancellationToken = default) public async Task ProcessTjEventContent(AgentInstanceDto agentInstanceDto, TechLogEventContentDto tjEventContent, CancellationToken cancellationToken = default)
{ {
await _parseblock.SendAsync((agentInstance, tjEventContent), cancellationToken); await _parseblock.SendAsync((agentInstance: agentInstanceDto, tjEventContent), cancellationToken);
_logger.LogTrace("Tj event content has been sent to the parsing block"); _logger.LogTrace("Tj event content has been sent to the parsing block");
} }

View File

@ -10,12 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="ClickHouse.Client" Version="7.10.0" /> <PackageReference Include="ClickHouse.Client" Version="7.10.0" />
<PackageReference Include="ConfigureAwait.Fody" Version="3.3.2" PrivateAssets="All" />
<PackageReference Include="Dapper" Version="2.1.35" /> <PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Fody" Version="6.9.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" /> <PackageReference Include="Grpc.Net.Client" Version="2.67.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.67.0" /> <PackageReference Include="Grpc.Net.ClientFactory" Version="2.67.0" />
<PackageReference Include="Grpc.Tools" Version="2.69.0"> <PackageReference Include="Grpc.Tools" Version="2.69.0">

View File

@ -25,6 +25,8 @@ namespace OnecMonitor.Server
public DbSet<TechLogSettings> TechLogSettings { get; set; } public DbSet<TechLogSettings> TechLogSettings { get; set; }
public DbSet<MaintenanceTask> MaintenanceTasks { get; set; } public DbSet<MaintenanceTask> MaintenanceTasks { get; set; }
public DbSet<MaintenanceStepNode> MaintenanceStepNodes { get; set; } public DbSet<MaintenanceStepNode> MaintenanceStepNodes { get; set; }
public DbSet<MaintenanceStep> MaintenanceSteps { get; set; }
public DbSet<MaintenanceStepNodeLogItem> MaintenanceStepNodeLogs { get; set; }
public AppDbContext(IHostEnvironment hostEnvironment) public AppDbContext(IHostEnvironment hostEnvironment)
=> DbPath = Path.Join(hostEnvironment.ContentRootPath, "om-server.db"); => DbPath = Path.Join(hostEnvironment.ContentRootPath, "om-server.db");

View File

@ -1,9 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AutoMapper; using AutoMapper;
using OnecMonitor.Server.Models; using OnecMonitor.Server.Models;
using OnecMonitor.Server.Models.MaintenanceTasks; using OnecMonitor.Server.Models.MaintenanceTasks;
using OnecMonitor.Server.ViewModels.Agents;
using OnecMonitor.Server.ViewModels.Clusters; using OnecMonitor.Server.ViewModels.Clusters;
using OnecMonitor.Server.ViewModels.V8Files; using OnecMonitor.Server.ViewModels.V8Files;
using OnecMonitor.Server.ViewModels.Credentials; using OnecMonitor.Server.ViewModels.Credentials;
@ -96,7 +93,7 @@ public class CommonProfile : Profile
.ForMember(c => c.Id, i => i.Ignore()) .ForMember(c => c.Id, i => i.Ignore())
.ForMember(c => c.InfoBases, i => i.Ignore()); .ForMember(c => c.InfoBases, i => i.Ignore());
CreateMap<MaintenanceTask, MaintenanceTaskListItemViewModel>().ReverseMap(); CreateMap<MaintenanceTask, MaintenanceTaskListItemViewModel>();
CreateMap<MaintenanceStep, MaintenanceStepViewModel>() CreateMap<MaintenanceStep, MaintenanceStepViewModel>()
.ReverseMap() .ReverseMap()

View File

@ -16,6 +16,7 @@ public class DtoProfile : Profile
CreateMap<V8File, V8FileDto>() CreateMap<V8File, V8FileDto>()
.ForMember(c => c.Data, opt => opt.MapFrom(src => File.ReadAllBytes(src.DataPath))) .ForMember(c => c.Data, opt => opt.MapFrom(src => File.ReadAllBytes(src.DataPath)))
.ForMember(c => c.FileExtension, opt => opt.MapFrom(src => Path.GetExtension(src.DataPath)))
.ReverseMap(); .ReverseMap();
CreateMap<InfoBase, InfoBaseDto>().ReverseMap(); CreateMap<InfoBase, InfoBaseDto>().ReverseMap();

View File

@ -46,7 +46,7 @@ namespace OnecMonitor.Server.Controllers
if (agent == null) if (agent == null)
return NotFound(); return NotFound();
var agentConnection = connectionsManager.GetCommandsSubscriberConnection(agent.Id); var agentConnection = connectionsManager.GetAgentConnection(agent.Id);
var installedPlatforms = agentConnection == null ? [] : await agentConnection.GetInstalledPlatforms(cancellationToken); var installedPlatforms = agentConnection == null ? [] : await agentConnection.GetInstalledPlatforms(cancellationToken);
var ragents = agentConnection == null ? [] : await agentConnection.GetRagentServices(cancellationToken); var ragents = agentConnection == null ? [] : await agentConnection.GetRagentServices(cancellationToken);

View File

@ -109,7 +109,7 @@ public class ClustersController(AppDbContext appDbContext, AgentsConnectionsMana
var connectedAgents = connectionsManager.GetConnectedAgents(appDbContext.Agents.ToList()); var connectedAgents = connectionsManager.GetConnectedAgents(appDbContext.Agents.ToList());
foreach (var agent in connectedAgents) foreach (var agent in connectedAgents)
{ {
var commandsConnection = connectionsManager.GetCommandsSubscriberConnection(agent.Id); var commandsConnection = connectionsManager.GetAgentConnection(agent.Id);
if (commandsConnection == null) if (commandsConnection == null)
continue; continue;

View File

@ -110,7 +110,7 @@ public class InfoBasesController(AppDbContext appDbContext, AgentsConnectionsMan
var connectedAgents = connectionsManager.GetConnectedAgents(appDbContext.Agents.ToList()); var connectedAgents = connectionsManager.GetConnectedAgents(appDbContext.Agents.ToList());
foreach (var agent in connectedAgents) foreach (var agent in connectedAgents)
{ {
var commandsConnection = connectionsManager.GetCommandsSubscriberConnection(agent.Id); var commandsConnection = connectionsManager.GetAgentConnection(agent.Id);
if (commandsConnection == null) if (commandsConnection == null)
continue; continue;

View File

@ -4,30 +4,39 @@ using AutoMapper;
using AutoMapper.QueryableExtensions; using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using OnecMonitor.Common.Models.MaintenanceTasks;
using OnecMonitor.Server.Helpers; using OnecMonitor.Server.Helpers;
using OnecMonitor.Server.Models; using OnecMonitor.Server.Models;
using OnecMonitor.Server.Models.MaintenanceTasks; using OnecMonitor.Server.Models.MaintenanceTasks;
using OnecMonitor.Server.Services;
using OnecMonitor.Server.ViewModels; using OnecMonitor.Server.ViewModels;
using OnecMonitor.Server.ViewModels.MaintenanceTasks; using OnecMonitor.Server.ViewModels.MaintenanceTasks;
namespace OnecMonitor.Server.Controllers; namespace OnecMonitor.Server.Controllers;
public class MaintenanceTasksController(AppDbContext appDbContext, IMapper mapper) : Controller public class MaintenanceTasksController(AppDbContext appDbContext, AgentsConnectionsManager connectionsManager, IMapper mapper) : Controller
{ {
private readonly JsonSerializerOptions _jsonOptions = new() private readonly JsonSerializerOptions _jsonOptions = new()
{ {
PropertyNameCaseInsensitive = true, PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
}; };
public async Task<IActionResult> Index(CancellationToken cancellationToken) public async Task<IActionResult> Index(CancellationToken cancellationToken)
=> View(new MaintenanceTasksIndexViewModel {
var items = await appDbContext.MaintenanceTasks
.AsNoTracking()
.Include(c => c.RootNode)
.ToListAsync(cancellationToken);
foreach (var item in items)
await LoadNodesRecursively(item.RootNode, false, cancellationToken);
return View(new MaintenanceTasksIndexViewModel
{ {
Items = await appDbContext.MaintenanceTasks Items = mapper.Map<List<MaintenanceTaskListItemViewModel>>(items)
.AsNoTracking()
.ProjectTo<MaintenanceTaskListItemViewModel>(mapper.ConfigurationProvider)
.ToListAsync(cancellationToken)
}); });
}
public async Task<IActionResult> Edit(Guid id, CancellationToken cancellationToken) public async Task<IActionResult> Edit(Guid id, CancellationToken cancellationToken)
{ {
@ -45,7 +54,7 @@ public class MaintenanceTasksController(AppDbContext appDbContext, IMapper mappe
if (model == null) if (model == null)
return NotFound(); return NotFound();
await LoadNodesRecursively(model.RootNode, cancellationToken); await LoadNodesRecursively(model.RootNode, false, cancellationToken);
var vm = mapper.Map<MaintenanceTaskEditViewModel>(model); var vm = mapper.Map<MaintenanceTaskEditViewModel>(model);
var rootNodeVm = mapper.Map<MaintenanceStepNodeViewModel>(model.RootNode); var rootNodeVm = mapper.Map<MaintenanceStepNodeViewModel>(model.RootNode);
@ -67,21 +76,30 @@ public class MaintenanceTasksController(AppDbContext appDbContext, IMapper mappe
Id = Guid.NewGuid() Id = Guid.NewGuid()
} : await appDbContext.MaintenanceTasks } : await appDbContext.MaintenanceTasks
.Include(c => c.RootNode) .Include(c => c.RootNode)
.AsNoTracking()
.FirstOrDefaultAsync(i => i.Id == vm.Id, cancellationToken); .FirstOrDefaultAsync(i => i.Id == vm.Id, cancellationToken);
if (model == null) if (model == null)
return NotFound(); return NotFound();
mapper.Map(vm, model); if (!isNew)
model.RootNode = JsonSerializer.Deserialize<MaintenanceStepNode>(vm.SerializedStepNode, _jsonOptions)!; await LoadNodesRecursively(model.RootNode, false, cancellationToken);
model.RootNodeId = model.RootNode.Id;
var rootNode = JsonSerializer.Deserialize<MaintenanceStepNode>(vm.SerializedStepNode, _jsonOptions)!;
mapper.Map(vm, model);
model.RootNodeId = rootNode.Id;
var newNodes = GetNodesList(rootNode);
var oldNodes = GetNodesList(model.RootNode);
await UiHelper.UpdateModelItems(appDbContext.MaintenanceStepNodes, newNodes, oldNodes, cancellationToken);
await UiHelper.UpdateModelItems(appDbContext.InfoBases, vm.InfoBases, model.InfoBases, cancellationToken); await UiHelper.UpdateModelItems(appDbContext.InfoBases, vm.InfoBases, model.InfoBases, cancellationToken);
model.RootNode = null!;
if (isNew) if (isNew)
appDbContext.MaintenanceTasks.Add(model); appDbContext.MaintenanceTasks.Add(model);
else
appDbContext.MaintenanceTasks.Update(model);
await appDbContext.SaveChangesAsync(cancellationToken); await appDbContext.SaveChangesAsync(cancellationToken);
@ -128,6 +146,60 @@ public class MaintenanceTasksController(AppDbContext appDbContext, IMapper mappe
return view; return view;
} }
public async Task<IActionResult> Start(Guid id, CancellationToken cancellationToken)
{
try
{
var task = await appDbContext.MaintenanceTasks
.AsNoTracking()
.Include(c => c.RootNode)
.ThenInclude(c => c.Step)
.AsNoTracking()
.Include(c => c.InfoBases)
.ThenInclude(infoBase => infoBase.Cluster)
.ThenInclude(cluster => cluster.Agent)
.FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
if (task == null)
return NotFound();
await LoadNodesRecursively(task.RootNode, true, cancellationToken);
var affectedAgents = task.InfoBases.Select(c => c.Cluster.Agent).Distinct().ToList();
var connectedAgents = connectionsManager.GetAgentsConnections(affectedAgents);
foreach (var connection in connectedAgents)
await connection.StartMaintenanceTask(task, cancellationToken);
var taskToUpdate = await appDbContext.MaintenanceTasks.FindAsync([id], cancellationToken);
taskToUpdate!.StartDateTime = DateTime.Now;
await appDbContext.SaveChangesAsync(cancellationToken);
return RedirectToAction("Index");
}
catch (Exception e)
{
return View("Error", new ErrorViewModel(e.Message));
}
}
public async Task<IActionResult> Delete(Guid id, CancellationToken cancellationToken)
{
try
{
var item = await appDbContext.MaintenanceTasks.FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
appDbContext.MaintenanceTasks.Remove(item!);
await appDbContext.SaveChangesAsync(cancellationToken);
return RedirectToAction("Index");
}
catch (Exception ex)
{
return View("Error", new ErrorViewModel(ex.ToString()));
}
}
private async Task<MaintenanceTaskEditViewModel> PrepareViewModel(MaintenanceTaskEditViewModel vm, CancellationToken cancellationToken) private async Task<MaintenanceTaskEditViewModel> PrepareViewModel(MaintenanceTaskEditViewModel vm, CancellationToken cancellationToken)
{ {
@ -159,24 +231,56 @@ public class MaintenanceTasksController(AppDbContext appDbContext, IMapper mappe
return vm; return vm;
} }
private async Task LoadNodesRecursively(MaintenanceStepNode node, CancellationToken cancellationToken) private async Task LoadNodesRecursively(MaintenanceStepNode node, bool includeFully, CancellationToken cancellationToken)
{ {
if (node.LeftNodeId != null && node.LeftNodeId != Guid.Empty) if (node.LeftNodeId != null && node.LeftNodeId != Guid.Empty)
node.LeftNode = await LoadNodeRecursively(node.LeftNodeId, cancellationToken); node.LeftNode = await LoadNodeRecursively(node.LeftNodeId, includeFully, cancellationToken);
if (node.RightNodeId != null && node.RightNodeId != Guid.Empty) if (node.RightNodeId != null && node.RightNodeId != Guid.Empty)
node.RightNode = await LoadNodeRecursively(node.RightNodeId, cancellationToken); node.RightNode = await LoadNodeRecursively(node.RightNodeId, includeFully, cancellationToken);
} }
private async Task<MaintenanceStepNode> LoadNodeRecursively(Guid? id, CancellationToken cancellationToken) private async Task<MaintenanceStepNode> LoadNodeRecursively(Guid? id, bool includeFully, CancellationToken cancellationToken)
{ {
var node = (await appDbContext.MaintenanceStepNodes var query = appDbContext.MaintenanceStepNodes
.AsNoTracking() .AsNoTracking()
.Include(c => c.Step) .Include(c => c.Step);
.FirstOrDefaultAsync(c => c.Id == id, cancellationToken))!;
await LoadNodesRecursively(node, cancellationToken);
return node; var fullQuery = includeFully switch
{
true => query
.ThenInclude(c => c.File)
.FirstOrDefaultAsync(c => c.Id == id, cancellationToken),
_ => query.FirstOrDefaultAsync(c => c.Id == id, cancellationToken),
};
var node = await fullQuery;
await LoadNodesRecursively(node!, includeFully, cancellationToken);
return node!;
}
private static List<MaintenanceStepNode> GetNodesList(MaintenanceStepNode node)
{
var list = new List<MaintenanceStepNode>();
FillNodesList(node, list);
return list;
}
private static void FillNodesList(MaintenanceStepNode node, List<MaintenanceStepNode> list)
{
list.Add(node);
if (node.LeftNodeId != null && node.LeftNodeId != Guid.Empty)
{
FillNodesList(node.LeftNode, list);
}
if (node.RightNodeId != null && node.RightNodeId != Guid.Empty)
{
FillNodesList(node.RightNode, list);
}
} }
} }

View File

@ -140,7 +140,8 @@ namespace OnecMonitor.Server.Controllers
model.Agents.Where(c => !newConnectedAgents.Contains(c)).ToList().ForEach(c => model.Agents.Remove(c)); model.Agents.Where(c => !newConnectedAgents.Contains(c)).ToList().ForEach(c => model.Agents.Remove(c));
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
await connectionsManager.UpdateTechLogSeances(affectedAgents, cancellationToken);
await RequestTechLogSeancesUpdating(affectedAgents, cancellationToken);
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
@ -163,7 +164,7 @@ namespace OnecMonitor.Server.Controllers
await dbContext.Database.CommitTransactionAsync(cancellationToken); await dbContext.Database.CommitTransactionAsync(cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
await connectionsManager.UpdateTechLogSeances(item.Agents, cancellationToken); await RequestTechLogSeancesUpdating(item.Agents, cancellationToken);
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
@ -173,6 +174,21 @@ namespace OnecMonitor.Server.Controllers
throw; throw;
} }
} }
private async Task RequestTechLogSeancesUpdating(List<Agent> agents, CancellationToken cancellationToken)
{
foreach (var connection in connectionsManager.GetAgentsConnections(agents))
{
try
{
await connection.RequestTechLogSeancesUpdating(cancellationToken);
}
catch
{
// ignored
}
}
}
public async Task<IActionResult> TechLog(Guid id, int pageNumber = 1, [FromQuery] string filter = "", CancellationToken cancellationToken = default) public async Task<IActionResult> TechLog(Guid id, int pageNumber = 1, [FromQuery] string filter = "", CancellationToken cancellationToken = default)
{ {

View File

@ -66,7 +66,7 @@ public class TechLogSettingsController(AppDbContext appDbContext, ITechLogStorag
await appDbContext.Database.CommitTransactionAsync(cancellationToken); await appDbContext.Database.CommitTransactionAsync(cancellationToken);
var agents = connectionsManager.GetConnectedAgents(await appDbContext.Agents.ToListAsync(cancellationToken)); var agents = connectionsManager.GetConnectedAgents(await appDbContext.Agents.ToListAsync(cancellationToken));
var subscribers = connectionsManager.GetCommandSubscribers(agents); var subscribers = connectionsManager.GetAgentsConnections(agents);
foreach (var subscriber in subscribers) foreach (var subscriber in subscribers)
await subscriber.SendUpdateSettingsRequest(cancellationToken); await subscriber.SendUpdateSettingsRequest(cancellationToken);

View File

@ -102,7 +102,7 @@ public class UpdateInfoBaseTasksController(AppDbContext appDbContext, AgentsConn
foreach (var agent in connectedAgents) foreach (var agent in connectedAgents)
{ {
var commandsConnection = connectionsManager.GetCommandsSubscriberConnection(agent.Id)!; var commandsConnection = connectionsManager.GetAgentConnection(agent.Id)!;
await commandsConnection.RequestInfoBasesUpdating(cancellationToken); await commandsConnection.RequestInfoBasesUpdating(cancellationToken);
} }

View File

@ -1,18 +1,10 @@
using System.Net.Http.Headers;
using System.Runtime.InteropServices.JavaScript;
using System.Text.Json;
using AutoMapper; using AutoMapper;
using AutoMapper.QueryableExtensions; using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using OnecMonitor.Server;
using OnecMonitor.Server.Models; using OnecMonitor.Server.Models;
using OnecMonitor.Server.ViewModels; using OnecMonitor.Server.ViewModels;
using OnecMonitor.Server.ViewModels.V8Files; using OnecMonitor.Server.ViewModels.V8Files;
using OneSTools.Common.Platform.Unpack;
namespace OnecMonitor.Server.Controllers; namespace OnecMonitor.Server.Controllers;

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

View File

@ -76,6 +76,31 @@ public static class UiHelper
.ToList() .ToList()
.ForEach(c => modelItems.Remove(c)); .ForEach(c => modelItems.Remove(c));
} }
public static async Task UpdateModelItems<T>(
IQueryable<T> queryable,
List<T> newItems,
List<T> oldItems,
CancellationToken cancellationToken) where T : DatabaseObject
{
var ids = newItems.Select(c => c.Id);
var addedItems = await queryable
.Where(c => ids.Contains(c.Id))
.ToListAsync(cancellationToken);
// add new
addedItems
.Where(c => !oldItems.Contains(c))
.ToList()
.ForEach(oldItems.Add);
// remove deleted
oldItems
.Where(c => !addedItems.Contains(c))
.ToList()
.ForEach(c => oldItems.Remove(c));
}
private static T? GetAttributeOfType<T>(this Enum enumVal) where T : Attribute private static T? GetAttributeOfType<T>(this Enum enumVal) where T : Attribute
{ {

View File

@ -10,7 +10,7 @@ using OnecMonitor.Server;
namespace OnecMonitor.Server.Migrations namespace OnecMonitor.Server.Migrations
{ {
[DbContext(typeof(AppDbContext))] [DbContext(typeof(AppDbContext))]
[Migration("20250225183414_Initial")] [Migration("20250227193339_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -237,7 +237,7 @@ namespace OnecMonitor.Server.Migrations
b.HasIndex("FileId"); b.HasIndex("FileId");
b.ToTable("MaintenanceStep"); b.ToTable("MaintenanceSteps");
}); });
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", b => modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", b =>
@ -270,6 +270,42 @@ namespace OnecMonitor.Server.Migrations
b.ToTable("MaintenanceStepNodes"); b.ToTable("MaintenanceStepNodes");
}); });
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNodeLogItem", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("InfoBaseId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsError")
.HasColumnType("INTEGER");
b.Property<bool>("IsFinish")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("StepNodeId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<long>("TimeStamp")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("InfoBaseId");
b.HasIndex("StepNodeId");
b.ToTable("MaintenanceStepNodeLogs");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b => modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@ -281,10 +317,19 @@ namespace OnecMonitor.Server.Migrations
.HasMaxLength(200) .HasMaxLength(200)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<long>("FinishDateTime")
.HasColumnType("INTEGER");
b.Property<bool>("IsFaulted")
.HasColumnType("INTEGER");
b.Property<string>("RootNodeId") b.Property<string>("RootNodeId")
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<long>("StartDateTime")
.HasColumnType("INTEGER");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("RootNodeId"); b.HasIndex("RootNodeId");
@ -597,6 +642,25 @@ namespace OnecMonitor.Server.Migrations
b.Navigation("Step"); b.Navigation("Step");
}); });
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNodeLogItem", b =>
{
b.HasOne("OnecMonitor.Server.Models.InfoBase", "InfoBase")
.WithMany()
.HasForeignKey("InfoBaseId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", "StepNode")
.WithMany("Logs")
.HasForeignKey("StepNodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("InfoBase");
b.Navigation("StepNode");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b => modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b =>
{ {
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", "RootNode") b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", "RootNode")
@ -659,6 +723,11 @@ namespace OnecMonitor.Server.Migrations
b.Navigation("InfoBases"); b.Navigation("InfoBases");
}); });
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", b =>
{
b.Navigation("Logs");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b => modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b =>
{ {
b.Navigation("InfoBases"); b.Navigation("InfoBases");

View File

@ -205,7 +205,7 @@ namespace OnecMonitor.Server.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "MaintenanceStep", name: "MaintenanceSteps",
columns: table => new columns: table => new
{ {
Id = table.Column<string>(type: "TEXT", nullable: false), Id = table.Column<string>(type: "TEXT", nullable: false),
@ -216,9 +216,9 @@ namespace OnecMonitor.Server.Migrations
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_MaintenanceStep", x => x.Id); table.PrimaryKey("PK_MaintenanceSteps", x => x.Id);
table.ForeignKey( table.ForeignKey(
name: "FK_MaintenanceStep_V8Files_FileId", name: "FK_MaintenanceSteps_V8Files_FileId",
column: x => x.FileId, column: x => x.FileId,
principalTable: "V8Files", principalTable: "V8Files",
principalColumn: "Id"); principalColumn: "Id");
@ -272,9 +272,9 @@ namespace OnecMonitor.Server.Migrations
principalTable: "MaintenanceStepNodes", principalTable: "MaintenanceStepNodes",
principalColumn: "Id"); principalColumn: "Id");
table.ForeignKey( table.ForeignKey(
name: "FK_MaintenanceStepNodes_MaintenanceStep_StepId", name: "FK_MaintenanceStepNodes_MaintenanceSteps_StepId",
column: x => x.StepId, column: x => x.StepId,
principalTable: "MaintenanceStep", principalTable: "MaintenanceSteps",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@ -284,6 +284,9 @@ namespace OnecMonitor.Server.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<string>(type: "TEXT", nullable: false), Id = table.Column<string>(type: "TEXT", nullable: false),
StartDateTime = table.Column<long>(type: "INTEGER", nullable: false),
IsFaulted = table.Column<bool>(type: "INTEGER", nullable: false),
FinishDateTime = table.Column<long>(type: "INTEGER", nullable: false),
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false), Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
RootNodeId = table.Column<string>(type: "TEXT", nullable: false) RootNodeId = table.Column<string>(type: "TEXT", nullable: false)
}, },
@ -357,6 +360,35 @@ namespace OnecMonitor.Server.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "MaintenanceStepNodeLogs",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
TimeStamp = table.Column<long>(type: "INTEGER", nullable: false),
IsError = table.Column<bool>(type: "INTEGER", nullable: false),
IsFinish = table.Column<bool>(type: "INTEGER", nullable: false),
Message = table.Column<string>(type: "TEXT", nullable: false),
InfoBaseId = table.Column<string>(type: "TEXT", nullable: false),
StepNodeId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MaintenanceStepNodeLogs", x => x.Id);
table.ForeignKey(
name: "FK_MaintenanceStepNodeLogs_InfoBases_InfoBaseId",
column: x => x.InfoBaseId,
principalTable: "InfoBases",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MaintenanceStepNodeLogs_MaintenanceStepNodes_StepNodeId",
column: x => x.StepNodeId,
principalTable: "MaintenanceStepNodes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "UpdateInfoBaseTaskLogItems", name: "UpdateInfoBaseTaskLogItems",
columns: table => new columns: table => new
@ -427,9 +459,14 @@ namespace OnecMonitor.Server.Migrations
column: "TemplatesId"); column: "TemplatesId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MaintenanceStep_FileId", name: "IX_MaintenanceStepNodeLogs_InfoBaseId",
table: "MaintenanceStep", table: "MaintenanceStepNodeLogs",
column: "FileId"); column: "InfoBaseId");
migrationBuilder.CreateIndex(
name: "IX_MaintenanceStepNodeLogs_StepNodeId",
table: "MaintenanceStepNodeLogs",
column: "StepNodeId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MaintenanceStepNodes_LeftNodeId", name: "IX_MaintenanceStepNodes_LeftNodeId",
@ -446,6 +483,11 @@ namespace OnecMonitor.Server.Migrations
table: "MaintenanceStepNodes", table: "MaintenanceStepNodes",
column: "StepId"); column: "StepId");
migrationBuilder.CreateIndex(
name: "IX_MaintenanceSteps_FileId",
table: "MaintenanceSteps",
column: "FileId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MaintenanceTasks_RootNodeId", name: "IX_MaintenanceTasks_RootNodeId",
table: "MaintenanceTasks", table: "MaintenanceTasks",
@ -479,6 +521,9 @@ namespace OnecMonitor.Server.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "LogTemplateTechLogSeance"); name: "LogTemplateTechLogSeance");
migrationBuilder.DropTable(
name: "MaintenanceStepNodeLogs");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "TechLogFilters"); name: "TechLogFilters");
@ -519,7 +564,7 @@ namespace OnecMonitor.Server.Migrations
name: "MaintenanceStepNodes"); name: "MaintenanceStepNodes");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "MaintenanceStep"); name: "MaintenanceSteps");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "V8Files"); name: "V8Files");

View File

@ -234,7 +234,7 @@ namespace OnecMonitor.Server.Migrations
b.HasIndex("FileId"); b.HasIndex("FileId");
b.ToTable("MaintenanceStep"); b.ToTable("MaintenanceSteps");
}); });
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", b => modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", b =>
@ -267,6 +267,42 @@ namespace OnecMonitor.Server.Migrations
b.ToTable("MaintenanceStepNodes"); b.ToTable("MaintenanceStepNodes");
}); });
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNodeLogItem", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("InfoBaseId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsError")
.HasColumnType("INTEGER");
b.Property<bool>("IsFinish")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("StepNodeId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<long>("TimeStamp")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("InfoBaseId");
b.HasIndex("StepNodeId");
b.ToTable("MaintenanceStepNodeLogs");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b => modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@ -278,10 +314,19 @@ namespace OnecMonitor.Server.Migrations
.HasMaxLength(200) .HasMaxLength(200)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<long>("FinishDateTime")
.HasColumnType("INTEGER");
b.Property<bool>("IsFaulted")
.HasColumnType("INTEGER");
b.Property<string>("RootNodeId") b.Property<string>("RootNodeId")
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<long>("StartDateTime")
.HasColumnType("INTEGER");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("RootNodeId"); b.HasIndex("RootNodeId");
@ -594,6 +639,25 @@ namespace OnecMonitor.Server.Migrations
b.Navigation("Step"); b.Navigation("Step");
}); });
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNodeLogItem", b =>
{
b.HasOne("OnecMonitor.Server.Models.InfoBase", "InfoBase")
.WithMany()
.HasForeignKey("InfoBaseId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", "StepNode")
.WithMany("Logs")
.HasForeignKey("StepNodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("InfoBase");
b.Navigation("StepNode");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b => modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b =>
{ {
b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", "RootNode") b.HasOne("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", "RootNode")
@ -656,6 +720,11 @@ namespace OnecMonitor.Server.Migrations
b.Navigation("InfoBases"); b.Navigation("InfoBases");
}); });
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceStepNode", b =>
{
b.Navigation("Logs");
});
modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b => modelBuilder.Entity("OnecMonitor.Server.Models.MaintenanceTasks.MaintenanceTask", b =>
{ {
b.Navigation("InfoBases"); b.Navigation("InfoBases");

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using OnecMonitor.Common.Models.MaintenanceTasks;
namespace OnecMonitor.Server.Models.MaintenanceTasks; namespace OnecMonitor.Server.Models.MaintenanceTasks;
@ -12,5 +13,5 @@ public class MaintenanceStep : DatabaseObject
public string Message { get; set; } = string.Empty; public string Message { get; set; } = string.Empty;
public Guid? FileId { get; set; } public Guid? FileId { get; set; }
public V8File? File { get; set; } = null!; public V8File? File { get; set; }
} }

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using OnecMonitor.Common.Models.MaintenanceTasks;
namespace OnecMonitor.Server.Models.MaintenanceTasks; namespace OnecMonitor.Server.Models.MaintenanceTasks;
@ -16,4 +17,6 @@ public class MaintenanceStepNode : DatabaseObject
public MaintenanceStepNode RightNode { get; set; } = null!; public MaintenanceStepNode RightNode { get; set; } = null!;
[ForeignKey(nameof(StepId))] [ForeignKey(nameof(StepId))]
public MaintenanceStep Step { get; set; } = null!; public MaintenanceStep Step { get; set; } = null!;
public virtual List<MaintenanceStepNodeLogItem> Logs { get; set; } = [];
} }

View File

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using OnecMonitor.Server.Models.MaintenanceTasks;
namespace OnecMonitor.Server.Models.MaintenanceTasks;
public class MaintenanceStepNodeLogItem : DatabaseObject
{
[DataType(DataType.Date)]
public DateTime TimeStamp { get; set; }
public bool IsError { get; set; }
public bool IsFinish { get; set; }
public string Message { get; set; } = string.Empty;
public Guid InfoBaseId { get; set; }
public Guid StepNodeId { get; set; }
[ForeignKey(nameof(InfoBaseId))]
public virtual InfoBase InfoBase { get; set; } = null!;
[ForeignKey(nameof(StepNodeId))]
public virtual MaintenanceStepNode StepNode { get; set; } = null!;
}

View File

@ -5,6 +5,9 @@ namespace OnecMonitor.Server.Models.MaintenanceTasks;
public class MaintenanceTask : DatabaseObject public class MaintenanceTask : DatabaseObject
{ {
public DateTime StartDateTime { get; set; } = DateTime.MinValue;
public bool IsFaulted { get; set; } = false;
public DateTime FinishDateTime { get; set; } = DateTime.MinValue;
[MaxLength(200)] [MaxLength(200)]
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public Guid RootNodeId { get; set; } public Guid RootNodeId { get; set; }

View File

@ -29,9 +29,9 @@ builder.WebHost.ConfigureKestrel((context, options) =>
var host = context.Configuration.GetValue("OnecMonitor:Http:Host", "0.0.0.0")!; var host = context.Configuration.GetValue("OnecMonitor:Http:Host", "0.0.0.0")!;
var port = context.Configuration.GetValue("OnecMonitor:Http:Port", 7002); var port = context.Configuration.GetValue("OnecMonitor:Http:Port", 7002);
options.Listen(IPAddress.Parse(host), port, options => options.Listen(IPAddress.Parse(host), port, configure =>
{ {
options.Protocols = HttpProtocols.Http1; configure.Protocols = HttpProtocols.Http1;
}); });
}); });

View File

@ -13,7 +13,9 @@ using System.Runtime.InteropServices;
using System.Text; using System.Text;
using AutoMapper; using AutoMapper;
using AutoMapper.QueryableExtensions; using AutoMapper.QueryableExtensions;
using OnecMonitor.Common.DTO.MaintenanceTasks;
using OnecMonitor.Server.Helpers; using OnecMonitor.Server.Helpers;
using OnecMonitor.Server.Models.MaintenanceTasks;
using OneSTools.Common.Platform; using OneSTools.Common.Platform;
using OneSTools.Common.Platform.RemoteAdministration; using OneSTools.Common.Platform.RemoteAdministration;
using OneSTools.Common.Platform.Services; using OneSTools.Common.Platform.Services;
@ -30,14 +32,11 @@ namespace OnecMonitor.Server.Services
private readonly ILogger<AgentConnection> _logger; private readonly ILogger<AgentConnection> _logger;
public Guid ConnectionId { get; } public Guid ConnectionId { get; }
public AgentInstance? AgentInstance { get; private set; } public AgentInstanceDto? AgentInstance { get; private set; }
public delegate void AgentConnectedHandler(AgentConnection agentConnection); public delegate void AgentConnectedHandler(AgentConnection agentConnection);
public event AgentConnectedHandler? AgentConnected; public event AgentConnectedHandler? AgentConnected;
public delegate void AgentSubscribedForCommandsHandler(AgentConnection agentConnection);
public event AgentSubscribedForCommandsHandler? SubscribedForCommands;
public delegate void AgentDisconnectedHandler(AgentConnection agentConnection); public delegate void AgentDisconnectedHandler(AgentConnection agentConnection);
public event AgentDisconnectedHandler? AgentDisconnected; public event AgentDisconnectedHandler? AgentDisconnected;
@ -60,69 +59,52 @@ namespace OnecMonitor.Server.Services
_logger = _agentScope.ServiceProvider.GetRequiredService<ILogger<AgentConnection>>(); _logger = _agentScope.ServiceProvider.GetRequiredService<ILogger<AgentConnection>>();
} }
public async Task Listen(CancellationToken cancellationToken) public void Listen(CancellationToken cancellationToken)
{ {
RunStreamLoops(cancellationToken); MessageReceived += async (_, message) =>
// first message must be an init message
var firstMessage = await ReadMessage(cancellationToken);
if (firstMessage.Header.Type != MessageType.AgentInfo)
{
Socket?.Close();
throw new Exception("First message must be \"Agent info\", connection closed");
}
await HandleInitMessage(firstMessage.Data, cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{ {
try try
{ {
var message = await ReadMessage(cancellationToken); // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (message.Header.Type)
try
{ {
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault case MessageType.AgentInfo:
switch (message.Header.Type) await HandleInitMessage(message, cancellationToken);
{ break;
case MessageType.TechLogEventContent: case MessageType.TechLogEventContent:
await HandleTechLogEventContent(message.Data, cancellationToken); await HandleTechLogEventContent(message.Data, cancellationToken);
break; break;
case MessageType.LastFilePositionRequest: case MessageType.LastFilePositionRequest:
await HandleLastFilePositionRequest(message, cancellationToken); await HandleLastFilePositionRequest(message, cancellationToken);
break; break;
case MessageType.TechLogSeancesRequest: case MessageType.TechLogSeancesRequest:
await UpdateTechLogSeances(message, cancellationToken); await UpdateTechLogSeances(message, cancellationToken);
break; break;
case MessageType.SubscribingForCommands: case MessageType.UpdateInfoBasesTaskRequest:
await HandleSubscribingForCommands(message, cancellationToken); await HandleUpdateInfoBasesTaskRequest(message, cancellationToken);
break; break;
case MessageType.UpdateInfoBasesTaskRequest: case MessageType.UpdateInfoBaseTaskLog:
await HandleUpdateInfoBasesTaskRequest(message, cancellationToken); await HandleUpdateInfoBasesTaskLog(message, cancellationToken);
break; break;
case MessageType.UpdateInfoBaseTaskLog: case MessageType.SettingsRequest:
await HandleUpdateInfoBasesTaskLog(message, cancellationToken); await HandleSettingsRequest(message, cancellationToken);
break; break;
case MessageType.SettingsRequest: default:
await HandleSettingsRequest(message, cancellationToken); throw new Exception($"Получено неожиданное сообщение: {message.Header.Type}");
break;
default:
throw new Exception($"Получено неожиданное сообщение: {message.Header.Type}");
}
}
catch (OperationCanceledException) {}
catch (Exception ex)
{
_logger.LogTrace(ex, "Ошибка обработки сообщения");
await SendError(message, ex.ToString(), cancellationToken);
} }
} }
catch (OperationCanceledException) {} catch (OperationCanceledException) {}
} catch (Exception ex)
{
_logger.LogTrace(ex, "Ошибка обработки сообщения");
await SendError(message, ex.ToString(), cancellationToken);
}
};
RunStreamLoops(cancellationToken);
} }
public async Task HandleSettingsRequest(Message message, CancellationToken cancellationToken) private async Task HandleSettingsRequest(Message message, CancellationToken cancellationToken)
{ {
var tjSettings = await _appDbContext.TechLogSettings.FirstOrDefaultAsync(cancellationToken); var tjSettings = await _appDbContext.TechLogSettings.FirstOrDefaultAsync(cancellationToken);
@ -190,6 +172,9 @@ namespace OnecMonitor.Server.Services
public async Task RequestInfoBasesUpdating(CancellationToken cancellationToken) public async Task RequestInfoBasesUpdating(CancellationToken cancellationToken)
=> await Send(MessageType.UpdateInfoBasesRequest, cancellationToken); => await Send(MessageType.UpdateInfoBasesRequest, cancellationToken);
public async Task StartMaintenanceTask(MaintenanceTask task, CancellationToken cancellationToken)
=> await Send(MessageType.MaintenanceTask, _mapper.Map<MaintenanceTaskDto>(task), cancellationToken);
private async Task UpdateTechLogSeances(Message callMessage, CancellationToken cancellationToken) private async Task UpdateTechLogSeances(Message callMessage, CancellationToken cancellationToken)
{ {
@ -204,21 +189,21 @@ namespace OnecMonitor.Server.Services
var seances = new List<TechLogSeanceDto>(); var seances = new List<TechLogSeanceDto>();
agentSeances.ForEach(c => agentSeances.ForEach(seance =>
{ {
StringBuilder templateBuilder = new(); StringBuilder templateBuilder = new();
c.Templates.ForEach(c => seance.Templates.ForEach(template =>
{ {
// add template id and combine templates // add template id and combine templates
templateBuilder.AppendLine(c.Content.Replace("{LOG_PATH}", $"{{LOG_PATH}}{c.Id}")); templateBuilder.AppendLine(template.Content.Replace("{LOG_PATH}", $"{{LOG_PATH}}{template.Id}"));
}); });
seances.Add(new TechLogSeanceDto() seances.Add(new TechLogSeanceDto()
{ {
Id = c.Id, Id = seance.Id,
StartDateTime = c.StartDateTime, StartDateTime = seance.StartDateTime,
FinishDateTime = c.FinishDateTime, FinishDateTime = seance.FinishDateTime,
Template = templateBuilder.ToString() Template = templateBuilder.ToString()
}); });
}); });
@ -226,9 +211,9 @@ namespace OnecMonitor.Server.Services
await Send(MessageType.TechLogSeances, seances, callMessage, cancellationToken); await Send(MessageType.TechLogSeances, seances, callMessage, cancellationToken);
} }
private async Task HandleInitMessage(ReadOnlyMemory<byte> messageData, CancellationToken cancellationToken) private async Task HandleInitMessage(Message message, CancellationToken cancellationToken)
{ {
AgentInstance = ParseMessageData<AgentInstance>(messageData, cancellationToken); AgentInstance = ParseMessageData<AgentInstanceDto>(message.Data, cancellationToken);
await _appDbContext.Database.BeginTransactionAsync(cancellationToken); await _appDbContext.Database.BeginTransactionAsync(cancellationToken);
@ -260,20 +245,12 @@ namespace OnecMonitor.Server.Services
AgentConnected?.Invoke(this); AgentConnected?.Invoke(this);
} }
catch (Exception ex) catch
{ {
await _appDbContext.Database.RollbackTransactionAsync(cancellationToken); await _appDbContext.Database.RollbackTransactionAsync(cancellationToken);
throw new RpcException(Status.DefaultCancelled, ex.Message);
} }
} }
private async Task HandleSubscribingForCommands(Message requestMessage, CancellationToken cancellationToken)
{
SubscribedForCommands?.Invoke(this);
await SendOk(requestMessage, cancellationToken);
}
private async Task HandleLastFilePositionRequest(Message requestMessage, CancellationToken cancellationToken) private async Task HandleLastFilePositionRequest(Message requestMessage, CancellationToken cancellationToken)
{ {
var request = ParseMessageData<LastFilePositionRequestDto>(requestMessage.Data, cancellationToken); var request = ParseMessageData<LastFilePositionRequestDto>(requestMessage.Data, cancellationToken);

View File

@ -4,6 +4,7 @@ using System.Collections.Concurrent;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using OnecMonitor.Common.DTO;
namespace OnecMonitor.Server.Services namespace OnecMonitor.Server.Services
{ {
@ -11,17 +12,13 @@ namespace OnecMonitor.Server.Services
IConfiguration configuration, IConfiguration configuration,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
TechLogProcessor techLogProcessor, TechLogProcessor techLogProcessor,
ILogger<AgentsConnectionsManager> logger) ILogger<AgentsConnectionsManager> logger) : BackgroundService
: BackgroundService
{ {
private readonly string _host = configuration.GetValue("OnecMonitor:Tcp:Host", "0.0.0.0"); private readonly string _host = configuration.GetValue("OnecMonitor:Tcp:Host", "0.0.0.0");
private readonly int _port = configuration.GetValue("OnecMonitor:Tcp:Port", 7001); private readonly int _port = configuration.GetValue("OnecMonitor:Tcp:Port", 7001);
private readonly Socket _socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); private readonly Socket _socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// k - agent id, v - connection id private readonly HashSet<AgentConnection> _connections = [];
private readonly ConcurrentDictionary<Guid, Guid> _commandsSubscribers = new();
private ConcurrentDictionary<Guid, AgentConnection> Connections { get; } = new();
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
@ -38,9 +35,8 @@ namespace OnecMonitor.Server.Services
var agentConnection = new AgentConnection(client, techLogProcessor, serviceProvider); var agentConnection = new AgentConnection(client, techLogProcessor, serviceProvider);
agentConnection.AgentConnected += AgentConnection_Connected; agentConnection.AgentConnected += AgentConnection_Connected;
agentConnection.AgentDisconnected += AgentConnection_Disconnected; agentConnection.AgentDisconnected += AgentConnection_Disconnected;
agentConnection.SubscribedForCommands += AgentConnection_SubscribedForCommands;
_ = agentConnection.Listen(stoppingToken); agentConnection.Listen(stoppingToken);
} }
_socket.Close(); _socket.Close();
@ -48,62 +44,40 @@ namespace OnecMonitor.Server.Services
private void AgentConnection_Connected(AgentConnection agentConnection) private void AgentConnection_Connected(AgentConnection agentConnection)
{ {
Connections.TryAdd(agentConnection.ConnectionId, agentConnection); lock (_connections)
_connections.Add(agentConnection);
logger.LogInformation($"Агент подключился: {agentConnection.AgentInstance!.InstanceName}");
logger.LogInformation($"Агент подключился: {agentConnection.AgentInstance!.InstanceName}. Идентификатор соединения: {agentConnection.ConnectionId}");
} }
private void AgentConnection_Disconnected(AgentConnection agentConnection) private void AgentConnection_Disconnected(AgentConnection agentConnection)
{ {
Connections.TryRemove(agentConnection.ConnectionId, out _); lock (_connections)
_connections.Remove(agentConnection);
agentConnection.AgentConnected -= AgentConnection_Connected; agentConnection.AgentConnected -= AgentConnection_Connected;
agentConnection.AgentDisconnected -= AgentConnection_Disconnected; agentConnection.AgentDisconnected -= AgentConnection_Disconnected;
agentConnection.SubscribedForCommands -= AgentConnection_SubscribedForCommands;
var commandsWatcher = _commandsSubscribers.FirstOrDefault(c => c.Value == agentConnection.ConnectionId); logger.LogInformation($"Агент отключился: {agentConnection.AgentInstance!.InstanceName}. Идентификатор соединения: {agentConnection.ConnectionId}");
if (commandsWatcher.Key != Guid.Empty)
_commandsSubscribers.TryRemove(commandsWatcher.Key, out _);
logger.LogInformation($"Агент отключиться: {agentConnection.AgentInstance!.InstanceName}");
}
private void AgentConnection_SubscribedForCommands(AgentConnection agentConnection)
{
_commandsSubscribers.TryAdd(agentConnection.AgentInstance!.Id, agentConnection.ConnectionId);
} }
public bool IsConnected(Guid agentId) public bool IsConnected(Guid agentId)
=> _commandsSubscribers.ContainsKey(agentId); => GetAgentConnection(agentId) != null;
public AgentConnection? GetCommandsSubscriberConnection(Guid id) public AgentConnection? GetAgentConnection(Guid agentId)
{ {
if (_commandsSubscribers.TryGetValue(id, out var connectionId) && AgentConnection? agentConnection;
Connections.TryGetValue(connectionId, out var agentConnection))
return agentConnection; lock (_connections)
agentConnection = _connections.FirstOrDefault(c => c.AgentInstance!.MainConnection && c.AgentInstance.Id == agentId);
return null; return agentConnection;
} }
public List<AgentConnection> GetCommandSubscribers(List<Agent> agents) public List<AgentConnection> GetAgentsConnections(List<Agent> agents)
=> agents.Select(c => GetCommandsSubscriberConnection(c.Id)).Where(c => c != null).ToList()!; => agents.Select(c => GetAgentConnection(c.Id)).Where(c => c != null).ToList()!;
public List<Agent> GetConnectedAgents(List<Agent> agents) public List<Agent> GetConnectedAgents(List<Agent> agents)
=> agents.Where(c => _commandsSubscribers.ContainsKey(c.Id)).ToList(); => agents.Where(c => IsConnected(c.Id)).ToList();
public async Task UpdateTechLogSeances(List<Agent> agents, CancellationToken cancellationToken)
{
foreach (var connection in agents.Select(agent => GetCommandsSubscriberConnection(agent.Id)).OfType<AgentConnection>())
{
try
{
await connection.RequestTechLogSeancesUpdating(cancellationToken);
}
catch
{
// ignored
}
}
}
} }
} }

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using OnecMonitor.Common.Models.MaintenanceTasks;
using OnecMonitor.Server.Models.MaintenanceTasks; using OnecMonitor.Server.Models.MaintenanceTasks;
namespace OnecMonitor.Server.ViewModels.MaintenanceTasks; namespace OnecMonitor.Server.ViewModels.MaintenanceTasks;

View File

@ -1,7 +1,8 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using OnecMonitor.Server.Extensions; using OnecMonitor.Common.Extensions;
using OnecMonitor.Common.Models.MaintenanceTasks;
using OnecMonitor.Server.Helpers; using OnecMonitor.Server.Helpers;
using OnecMonitor.Server.Models; using OnecMonitor.Server.Models;
using OnecMonitor.Server.Models.MaintenanceTasks; using OnecMonitor.Server.Models.MaintenanceTasks;

View File

@ -3,5 +3,8 @@ namespace OnecMonitor.Server.ViewModels.MaintenanceTasks;
public class MaintenanceTaskListItemViewModel public class MaintenanceTaskListItemViewModel
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public DateTime StartDateTime { get; set; }
public DateTime FinishDateTime { get; set; }
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public bool IsFaulted { get; set; } = false;
} }

View File

@ -1,8 +1,6 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers @using Microsoft.AspNetCore.Mvc.TagHelpers
@using NuGet.Protocol @using OnecMonitor.Common.Extensions
@using OnecMonitor.Server.Extensions @using OnecMonitor.Common.Models.MaintenanceTasks
@using OnecMonitor.Server.Helpers
@using OnecMonitor.Server.Models.MaintenanceTasks
@using OnecMonitor.Server.ViewModels @using OnecMonitor.Server.ViewModels
@model OnecMonitor.Server.ViewModels.MaintenanceTasks.MaintenanceTaskEditViewModel @model OnecMonitor.Server.ViewModels.MaintenanceTasks.MaintenanceTaskEditViewModel

View File

@ -1,7 +1,5 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers @using Microsoft.AspNetCore.Mvc.TagHelpers
@using OnecMonitor.Server.Extensions @using OnecMonitor.Common.Models.MaintenanceTasks
@using OnecMonitor.Server.Models.MaintenanceTasks
@using OnecMonitor.Server.ViewModels
@model OnecMonitor.Server.ViewModels.MaintenanceTasks.MaintenanceStepViewModel @model OnecMonitor.Server.ViewModels.MaintenanceTasks.MaintenanceStepViewModel
<form method="post" style="height: 100%"> <form method="post" style="height: 100%">

View File

@ -1,3 +1,4 @@
@using System.Globalization
@using Microsoft.AspNetCore.Mvc.TagHelpers @using Microsoft.AspNetCore.Mvc.TagHelpers
@using OnecMonitor.Server.ViewModels @using OnecMonitor.Server.ViewModels
@using OnecMonitor.Server.ViewModels.Agents; @using OnecMonitor.Server.ViewModels.Agents;
@ -18,17 +19,44 @@
<thead> <thead>
<tr> <tr>
<th class="col">Описание</th> <th class="col">Описание</th>
<th class="col">Запущена</th>
<th class="col">Есть ошибки</th>
<th class="col">Завершена</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var item in Model.Items) @foreach (var item in Model.Items)
{ {
var started = item.StartDateTime != DateTime.MinValue;
var finished = item.FinishDateTime != DateTime.MinValue;
<tr> <tr>
<td>@item.Description</td> <td>@item.Description</td>
<td>@(started ? item.StartDateTime.ToString(CultureInfo.InvariantCulture) : "")</td>
@if (item.IsFaulted)
{
<td class="text-danger">@item.IsFaulted</td>
}
else
{
<td class="text-success">@item.IsFaulted</td>
}
<td>@(finished ? item.FinishDateTime.ToString(CultureInfo.InvariantCulture) : "")</td>
<td class="text-center"> <td class="text-center">
<a class="btn btn-outline-warning btn-sm" asp-action="Edit" asp-route-id="@item.Id">Изменить</a> @if (started)
<a class="btn btn-outline-danger btn-sm" onclick="OM.openDeleteDialog('@item.Id', '@item.Description')">Удалить</a> {
<a class="btn btn-outline-info btn-sm" asp-action="Log" asp-route-id="@item.Id">Лог</a>
}
else
{
<a class="btn btn-outline-primary btn-sm" asp-action="Start" asp-route-id="@item.Id">Запустить</a>
<a class="btn btn-outline-warning btn-sm" asp-action="Edit" asp-route-id="@item.Id">Изменить</a>
}
@if (!started || finished)
{
<a class="btn btn-outline-danger btn-sm" onclick="OM.openDeleteDialog('@item.Id', '@item.Description')">Удалить</a>
}
</td> </td>
</tr> </tr>
} }

View File

@ -1,7 +1,4 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers @using Microsoft.AspNetCore.Mvc.TagHelpers
@using OnecMonitor.Server.Extensions
@using OnecMonitor.Server.Helpers
@using OnecMonitor.Server.Models.MaintenanceTasks
@using OnecMonitor.Server.ViewModels @using OnecMonitor.Server.ViewModels
@model OnecMonitor.Server.ViewModels.UpdateInfoBaseTasks.UpdateInfoBaseTaskEditViewModel @model OnecMonitor.Server.ViewModels.UpdateInfoBaseTasks.UpdateInfoBaseTaskEditViewModel

View File

@ -24,11 +24,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" /> <PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="ConfigureAwait.Fody" Version="3.3.2" PrivateAssets="All" />
<PackageReference Include="Fody" Version="6.9.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Grpc.AspNetCore" Version="2.67.0" /> <PackageReference Include="Grpc.AspNetCore" Version="2.67.0" />
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.67.0" /> <PackageReference Include="Grpc.AspNetCore.Server" Version="2.67.0" />
<PackageReference Include="Grpc.AspNetCore.Web" Version="2.67.0" /> <PackageReference Include="Grpc.AspNetCore.Web" Version="2.67.0" />