삽질하다 도저히 문제 파악이 안돼서 opcUaManager로 분리 테스트 중
This commit is contained in:
180
opcUaManager/Services/OpcSessionService.cs
Normal file
180
opcUaManager/Services/OpcSessionService.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user