Files
kompas3d-mcp/src/Kompas.Mcp.Core/KompasSession.cs
T
mikhail 579b191d7c refactor(core): унификация COM-хелперов и классификаторов топологии
Вынесены общие ComHelper (Release + Safe-геттеры) и GeomClassifiers
(FaceType/EdgeType), устранены дубли в 5+ сервисах. Освобождение doc3d RCW
в GetTopPart, void-возвраты вместо фиктивных bool, HashSet для OpKinds,
DocumentTypeName в паспорте модели, FileExtension без аллокаций.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:15:43 +03:00

163 lines
7.1 KiB
C#

using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Interop;
using Kompas.Mcp.Core.Threading;
using Kompas6API5;
using KompasAPI7;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Kompas.Mcp.Core;
/// <summary>
/// Подключение к КОМПАС и владение корневыми COM-объектами на время жизни сервера.
/// <para>
/// Подключается через API5 (<see cref="KompasObject"/>), так как он нужен для построения 3D
/// (ksPart) и снимков (ksDocument3D), а из него получает современный API7 (<see cref="IApplication"/>).
/// Все обращения к COM выполняются на STA-потоке <see cref="KompasDispatcher"/>.
/// </para>
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class KompasSession : IDisposable
{
// Порядок попыток. Нужен KompasObject (API5), поэтому .7 не используем как точку входа.
// Проверено на v24 Home: зарегистрирован KOMPAS.Application.5 (не LT).
private static readonly string[] ProgIds =
{
"KOMPAS.Application.5",
"KOMPASLT.Application.5",
};
private readonly KompasDispatcher _dispatcher;
private readonly ILogger<KompasSession> _log;
private KompasObject? _kompas; // API5 root
private IApplication? _app; // API7 application
private bool _launchedByUs;
public KompasSession(KompasDispatcher dispatcher, ILogger<KompasSession>? log = null)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_log = log ?? NullLogger<KompasSession>.Instance;
}
public bool IsConnected => _app is not null;
/// <summary>Сведения о последнем успешном подключении (null, пока не подключались).</summary>
public KompasConnectionInfo? LastInfo { get; private set; }
/// <summary>Показать/скрыть окно КОМПАС.</summary>
public Task SetVisibleAsync(bool visible, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => Application.Visible = visible, ct);
/// <summary>Корневой объект API5. Доступен только на STA-потоке после подключения.</summary>
public KompasObject Kompas =>
_kompas ?? throw new InvalidOperationException("Нет подключения к КОМПАС. Сначала вызовите ConnectAsync.");
/// <summary>Приложение API7. Доступно только на STA-потоке после подключения.</summary>
public IApplication Application =>
_app ?? throw new InvalidOperationException("Нет подключения к КОМПАС. Сначала вызовите ConnectAsync.");
/// <summary>Подключиться к КОМПАС: attach к запущенному или launch нового экземпляра.</summary>
public Task<KompasConnectionInfo> ConnectAsync(bool visible = true, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => ConnectCore(visible), ct);
private KompasConnectionInfo ConnectCore(bool visible)
{
if (_app is not null)
return LastInfo ??= BuildInfo(_lastProgId);
// 1) attach к уже запущенному экземпляру
foreach (var id in ProgIds)
{
if (NativeMethods.GetActiveObject(id) is KompasObject ko)
{
_kompas = ko;
_lastProgId = id;
_launchedByUs = false;
_log.LogInformation("Присоединились к запущенному КОМПАС ({ProgId})", id);
break;
}
}
// 2) запустить новый экземпляр
if (_kompas is null)
{
foreach (var id in ProgIds)
{
var type = Type.GetTypeFromProgID(id, throwOnError: false);
if (type is null) continue;
try
{
if (Activator.CreateInstance(type) is KompasObject ko)
{
_kompas = ko;
_lastProgId = id;
_launchedByUs = true;
_log.LogInformation("Запустили новый КОМПАС ({ProgId})", id);
break;
}
}
catch (Exception ex)
{
_log.LogWarning(ex, "Не удалось создать экземпляр {ProgId}", id);
}
}
}
if (_kompas is null)
throw new InvalidOperationException(
"Не удалось подключиться к КОМПАС. Убедитесь, что КОМПАС-3D установлен и запускается.");
_app = (IApplication?)_kompas.ksGetApplication7()
?? throw new InvalidOperationException("ksGetApplication7() вернул null — API7 недоступен.");
_app.Visible = visible;
return LastInfo = BuildInfo(_lastProgId);
}
private string _lastProgId = string.Empty;
private KompasConnectionInfo BuildInfo(string progId)
{
int major = 0, minor = 0, release = 0, build = 0;
try { _app!.GetSystemVersion(out major, out minor, out release, out build); }
catch (Exception ex) { _log.LogWarning(ex, "GetSystemVersion недоступен"); }
int buildNumber = 0;
try { buildNumber = _app!.BuildNumber; }
catch (Exception ex) { _log.LogWarning(ex, "BuildNumber недоступен"); }
return new KompasConnectionInfo
{
ProgId = progId,
WasLaunched = _launchedByUs,
Version = $"{major}.{minor}.{release}",
BuildNumber = buildNumber,
Edition = progId.StartsWith("KOMPASLT", StringComparison.OrdinalIgnoreCase) ? "Home/LT" : "Full",
};
}
public void Dispose()
{
// Освобождаем COM-объекты на том же STA-потоке, на котором они созданы.
try
{
// Ограниченное ожидание: не виснем на старте остановки, если STA-поток занят COM-вызовом.
_dispatcher.InvokeAsync(() =>
{
// Quit не вызываем: не «убиваем» КОМПАС пользователя (см. docs/OPEN_QUESTIONS.md).
if (_app is not null && Marshal.IsComObject(_app)) Marshal.ReleaseComObject(_app);
if (_kompas is not null && Marshal.IsComObject(_kompas)) Marshal.ReleaseComObject(_kompas);
_app = null;
_kompas = null;
}).Wait(TimeSpan.FromSeconds(5));
}
catch
{
// Диспетчер остановлен / вызов завис — COM освободит GC-финализатор.
}
}
}