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; /// /// Подключение к КОМПАС и владение корневыми COM-объектами на время жизни сервера. /// /// Подключается через API5 (), так как он нужен для построения 3D /// (ksPart) и снимков (ksDocument3D), а из него получает современный API7 (). /// Все обращения к COM выполняются на STA-потоке . /// /// [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 _log; private KompasObject? _kompas; // API5 root private IApplication? _app; // API7 application private bool _launchedByUs; public KompasSession(KompasDispatcher dispatcher, ILogger? log = null) { _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); _log = log ?? NullLogger.Instance; } public bool IsConnected => _app is not null; /// Сведения о последнем успешном подключении (null, пока не подключались). public KompasConnectionInfo? LastInfo { get; private set; } /// Показать/скрыть окно КОМПАС. public Task SetVisibleAsync(bool visible, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => Application.Visible = visible, ct); /// Корневой объект API5. Доступен только на STA-потоке после подключения. public KompasObject Kompas => _kompas ?? throw new InvalidOperationException("Нет подключения к КОМПАС. Сначала вызовите ConnectAsync."); /// Приложение API7. Доступно только на STA-потоке после подключения. public IApplication Application => _app ?? throw new InvalidOperationException("Нет подключения к КОМПАС. Сначала вызовите ConnectAsync."); /// Подключиться к КОМПАС: attach к запущенному или launch нового экземпляра. public Task 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-финализатор. } } }