using System.Text; using Opc.Ua; using Opc.Ua.Client; using OpcUaManager.Models; // Microsoft.AspNetCore.Http.ISession 과 Opc.Ua.Client.ISession 충돌 해소 using OpcUaISession = Opc.Ua.Client.ISession; // Microsoft.AspNetCore.Http.StatusCodes 와 Opc.Ua.StatusCodes 충돌 해소 using OpcStatusCodes = Opc.Ua.StatusCodes; namespace OpcUaManager.Services; /// /// OPC UA 세션을 싱글톤으로 관리합니다. /// v1.5.374.70 API 기준으로 수정되었습니다. /// public class OpcSessionService : IAsyncDisposable { private readonly ILogger _logger; private readonly CertService _certService; private OpcUaISession? _session; private ApplicationConfiguration? _appConfig; private readonly SemaphoreSlim _lock = new(1, 1); public bool IsConnected => _session?.Connected == true; public string SessionId => _session?.SessionId?.ToString() ?? string.Empty; public OpcSessionService(ILogger logger, CertService certService) { _logger = logger; _certService = certService; } public async Task ConnectAsync(ConnectRequest req) { await _lock.WaitAsync(); try { if (IsConnected) await InternalCloseAsync(); string endpointUrl = $"opc.tcp://{req.ServerIp}:{req.Port}"; // ── 1. 클라이언트 인증서 로드 ──────────────────────────── System.Security.Cryptography.X509Certificates.X509Certificate2? clientCert = null; if (!string.IsNullOrWhiteSpace(req.PfxPath) && File.Exists(req.PfxPath)) clientCert = _certService.LoadPfx(req.PfxPath, req.PfxPassword); string appUri = string.IsNullOrWhiteSpace(req.ApplicationUri) ? $"urn:{System.Net.Dns.GetHostName()}:{req.ApplicationName}" : req.ApplicationUri; // ── 2. ApplicationConfiguration ─────────────────────────── _appConfig = new ApplicationConfiguration { ApplicationName = req.ApplicationName, ApplicationType = ApplicationType.Client, ApplicationUri = appUri, SecurityConfiguration = new SecurityConfiguration { ApplicationCertificate = clientCert != null ? new CertificateIdentifier { Certificate = clientCert } : new CertificateIdentifier(), TrustedPeerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = Path.GetFullPath("pki/trusted") }, TrustedIssuerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = Path.GetFullPath("pki/issuers") }, RejectedCertificateStore = new CertificateTrustList { StoreType = "Directory", StorePath = Path.GetFullPath("pki/rejected") }, AutoAcceptUntrustedCertificates = true, AddAppCertToTrustedStore = true }, TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = req.SessionTimeoutMs } }; // FIX CS1061: ValidateAsync → Validate (v1.5.374.70에서 동기 메서드) _appConfig.Validate(ApplicationType.Client); // FIX CS0104: StatusCodes → OpcStatusCodes 별칭 사용 _appConfig.CertificateValidator.CertificateValidation += (_, e) => { if (e.Error.StatusCode != OpcStatusCodes.Good) e.Accept = true; }; // ── 3. Endpoint Discovery ───────────────────────────────── _logger.LogInformation("Endpoint Discovery: {Url}", endpointUrl); var endpointConfig = EndpointConfiguration.Create(_appConfig); // FIX CS0117: DiscoveryClient.CreateAsync 없음 → 동기 Create 사용 EndpointDescriptionCollection endpoints; using (var discovery = DiscoveryClient.Create(new Uri(endpointUrl), endpointConfig)) { endpoints = discovery.GetEndpoints(null); } var selected = endpoints .OrderByDescending(e => e.SecurityLevel) .FirstOrDefault(e => e.SecurityPolicyUri.Contains(req.SecurityPolicy)) ?? endpoints.OrderByDescending(e => e.SecurityLevel).First(); var endpoint = new ConfiguredEndpoint(null, selected, endpointConfig); // FIX CS1503: UserIdentity(user, byte[]) → UserIdentity(user, string) // v1.5.374.70 에서 password 파라미터가 string 으로 변경됨 var identity = new UserIdentity(req.UserName, req.Password); // ── 5. Session.Create ───────────────────────────────────── #pragma warning disable CS0618 _session = await Session.Create( _appConfig, endpoint, false, "OpcUaManagerSession", (uint)req.SessionTimeoutMs, identity, null); #pragma warning restore CS0618 string secMode = selected.SecurityMode.ToString(); _logger.LogInformation("OPC UA 연결 완료 | SessionId={Id} | Security={Sec}", _session.SessionId, secMode); return new ConnectResult { Success = true, Message = "연결 성공", SessionId = _session.SessionId.ToString(), EndpointUrl = endpointUrl, SecurityMode = secMode }; } catch (Exception ex) { _logger.LogError(ex, "OPC UA 연결 실패"); return new ConnectResult { Success = false, Message = ex.Message }; } finally { _lock.Release(); } } public async Task DisconnectAsync() { await _lock.WaitAsync(); try { await InternalCloseAsync(); return true; } finally { _lock.Release(); } } private async Task InternalCloseAsync() { if (_session != null) { try { await _session.CloseAsync(); } catch { /* ignore */ } _session.Dispose(); _session = null; } } public async Task<(object? Value, string StatusCode)> ReadValueAsync(string nodeId) { if (!IsConnected || _session == null) throw new InvalidOperationException("세션이 연결되어 있지 않습니다."); var result = await _session.ReadValueAsync(nodeId); return (result.Value, result.StatusCode.ToString()); } public Session? GetRawSession() => _session as Session; public async ValueTask DisposeAsync() { await InternalCloseAsync(); _lock.Dispose(); GC.SuppressFinalize(this); } }