Files
dbserver/opcUaManager/Services/OpcSessionService.cs

181 lines
7.2 KiB
C#

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;
/// <summary>
/// OPC UA 세션을 싱글톤으로 관리합니다.
/// v1.5.374.70 API 기준으로 수정되었습니다.
/// </summary>
public class OpcSessionService : IAsyncDisposable
{
private readonly ILogger<OpcSessionService> _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<OpcSessionService> logger, CertService certService)
{
_logger = logger;
_certService = certService;
}
public async Task<ConnectResult> 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<bool> 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);
}
}