Implement EnsureTagAsync (live-verified) + DeleteTagAsync (DelT semantics partial)

New SDK surface:
  HistorianClient.EnsureTagAsync(HistorianTagDefinition)
  HistorianClient.DeleteTagAsync(string tagName)

Plumbing:
  src/AVEVA.Historian.Client/Models/HistorianTagDefinition.cs
    Public input model — TagName/Description/EngineeringUnit/DataType/MinEU/MaxEU.
    Currently only HistorianDataType.Float is live-verified.

  src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs
    SerializeAnalogCTagMetadata produces 146-byte payload byte-for-byte
    identical to the captured native EnsT2(Float) request.
    SerializeDeleteTagNames produces ushort 0x6751 + ushort 1 + uint count
    + per-tag (uint charCount + UTF-16 chars).

  src/AVEVA.Historian.Client/Wcf/HistorianWcfTagWriteOrchestrator.cs
    Both EnsT2 and DelT run the full Stat-priming chain captured for the
    analog flow (UpdC3 + Stat.GetV ×3 + Stat.GETHI ×2 + 7× GetSystemParameter
    + Trx.GetV + Retr.GetV).

  src/AVEVA.Historian.Client/Wcf/HistorianWcfTagClient.cs
    MapDataType extended to accept tag-origin marker 0xC7 (SDK-created tags).

Tests:
  5 golden-byte tests (HistorianTagWriteProtocolTests):
    SerializeAnalogCTagMetadata byte-for-byte match against captured 146-byte fixture
    SerializeAnalogCTagMetadata produces different bytes for different inputs
    SerializeDeleteTagNames single-tag matches captured shape
    SerializeDeleteTagNames multi-tag appends each
    SerializeDeleteTagNames empty list throws

  1 live integration test (gated by HISTORIAN_WRITE_SANDBOX_TAG):
    EnsureTagAsync_AndDeleteTagAsync_RoundTrip_AgainstLocalHistorian
    EnsureTagAsync creates the sandbox tag, GetTagMetadataAsync reads it
    back. 130/130 tests pass.

Harness improvements:
  --write-delete-after now runs DelT independently of AddStreamedValue
  outcome.
  HistorianTagStatusList constructed correctly for DeleteTags reflection
  call (previous StringCollection attempt failed with TypeMismatch).

Known DelT gap: SDK's DeleteTagAsync returns true but server-side
cascading deletion does not always complete (the row remains in
Runtime.dbo.Tag). The captured native flow's DelT removes the tag
cleanly (verified via harness --write-delete-after), so something
around the WCF DelT call is missing from our orchestrator. Documented
as known issue with SMC-based cleanup as workaround.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
dohertj2
2026-05-04 08:33:21 -04:00
parent b3d22befd0
commit cfc8d44e3a
9 changed files with 664 additions and 19 deletions
@@ -121,6 +121,42 @@ public sealed class HistorianClient : IAsyncDisposable
return _protocol.GetSystemParameterAsync(name, cancellationToken);
}
/// <summary>
/// Creates or updates the named tag in the Historian Runtime database via
/// <c>EnsureTags2</c>. Currently only <see cref="HistorianDataType.Float"/> is
/// live-verified. Note: writing data values to the new tag (via a separate
/// AddStreamedValue/AddS2 path) is NOT supported by the SDK — see
/// <c>docs/plans/write-commands-reverse-engineering.md</c> for the architectural
/// finding.
/// </summary>
public Task<bool> EnsureTagAsync(HistorianTagDefinition definition, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(definition);
if (!OperatingSystem.IsWindows())
{
throw new ProtocolEvidenceMissingException("EnsureTagAsync requires Windows for the SSPI auth path");
}
return new HistorianWcfTagWriteOrchestrator(_options).EnsureTagAsync(definition, cancellationToken);
}
/// <summary>
/// Deletes the named tag via <c>DeleteTags</c>. **Known issue (2026-05-04):**
/// the SDK's DelT call returns true but the server-side cascading deletion does
/// not always complete (the row remains in <c>Runtime.dbo.Tag</c>). The
/// captured native flow's DelT removes the tag cleanly, so additional priming
/// or a side call between WCF DelT and server cascade is missing. Use the SMC
/// fallback to clean up sandbox tags until this is resolved.
/// </summary>
public Task<bool> DeleteTagAsync(string tagName, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tagName);
if (!OperatingSystem.IsWindows())
{
throw new ProtocolEvidenceMissingException("DeleteTagAsync requires Windows for the SSPI auth path");
}
return new HistorianWcfTagWriteOrchestrator(_options).DeleteTagAsync(tagName, cancellationToken);
}
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
@@ -0,0 +1,31 @@
namespace AVEVA.Historian.Client.Models;
/// <summary>
/// Input model for <see cref="HistorianClient.EnsureTagAsync"/> — the minimal set of
/// fields the SDK currently surfaces for tag creation. Mirrors the analog-Float shape
/// captured from the native wrapper (see
/// <c>docs/plans/write-commands-reverse-engineering.md</c> Phase 2 findings).
/// MinEU/MaxEU are accepted but the underlying CTagMetadata serializer's bytes
/// for the EU range are not yet decoded — non-default values are sent on the wire
/// but the server's interpretation has not been verified.
/// </summary>
public sealed record HistorianTagDefinition
{
/// <summary>Tag name (ASCII; up to 255 chars per server limit).</summary>
public required string TagName { get; init; }
/// <summary>Tag description (free text; up to 255 chars).</summary>
public string? Description { get; init; }
/// <summary>Engineering unit label (e.g. "Seconds", "kPa"). Required for analog tags.</summary>
public string? EngineeringUnit { get; init; }
/// <summary>Native data type. Currently only <see cref="HistorianDataType.Float"/> is live-verified end-to-end.</summary>
public HistorianDataType DataType { get; init; } = HistorianDataType.Float;
/// <summary>Engineering-units lower bound (analog tags). Default 0.</summary>
public double MinEU { get; init; }
/// <summary>Engineering-units upper bound (analog tags). Default 100.</summary>
public double MaxEU { get; init; } = 100.0;
}
@@ -0,0 +1,153 @@
using System.Text;
namespace AVEVA.Historian.Client.Wcf;
/// <remarks>
/// Serializers for the EnsT2 (CTagMetadata) and DelT (tag-name list) write paths.
/// Decoded from native captures landed in
/// <c>artifacts/reverse-engineering/instrumented-wcf-writemessage-writes/bothmessage-write-with-delt-latest.ndjson</c>
/// — see <c>docs/plans/write-commands-reverse-engineering.md</c> Phase 2 findings.
///
/// Per the captured 146-byte analog Float CTagMetadata, the layout is:
/// <code>
/// 9-byte fixed header = 67 03 00 01 00 00 00 04 C6
/// 18 zero bytes (placeholder GUID + 2 bytes; future server-assigned tag id)
/// compact ASCII tag name
/// 16 bytes of 0xFF (sentinel — likely common-event-type GUID equivalent unused for analog)
/// compact ASCII description
/// compact ASCII metadata provider ("MDAS")
/// 6-byte flag block = 02 01 01 00 00 00
/// uint32 storage rate (ms)
/// int64 date-created FILETIME UTC
/// 2-byte separator = 1A 03
/// compact ASCII engineering unit
/// uint32 = 0x2710 (10000 — purpose unclear; observed constant)
/// 8-byte double = 1.0 (likely IntegralDivisor)
/// 5-byte trailer = FE 00 01 01 01
/// </code>
/// MinEU/MaxEU/MinRaw/MaxRaw fields and their wire positions are NOT yet decoded
/// from the captured fixture (the test tag used the defaults). The serializer accepts
/// those parameters from <see cref="Models.HistorianTagDefinition"/> but their wire
/// representation is currently a TODO; for now they are not encoded into the
/// payload — the server uses defaults from the AnalogTag table after creation.
/// </remarks>
internal static class HistorianTagWriteProtocol
{
private const byte CompactAsciiMarker = 0x09;
private static readonly byte[] AnalogHeader =
[
// bytes 0-8 (constant — observed identical across runs)
0x67, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0xC6,
// bytes 9-10: observed `02 01` (purpose unclear — possibly a sub-marker)
0x02, 0x01,
];
private static readonly byte[] AnalogPadding16 = new byte[16];
private static readonly byte[] AnalogPostNamePadding = new byte[16];
static HistorianTagWriteProtocol()
{
// 16 bytes of 0xFF observed between tag name and description.
for (int i = 0; i < AnalogPostNamePadding.Length; i++)
{
AnalogPostNamePadding[i] = 0xFF;
}
}
// After MDAS, the captured layout is:
// `02 01 01 00 00 00` (6 bytes — flag block, observed constant)
// `01` (1 byte — observed constant; purpose unclear)
// uint32 storage rate (4 bytes)
private static readonly byte[] AnalogFlagBlock = [0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01];
private static readonly byte[] AnalogSeparator = [0x1A, 0x03];
private static readonly byte[] AnalogTrailer = [0xFE, 0x00, 0x01, 0x01, 0x01];
private const string MetadataProvider = "MDAS";
private const uint IntegralDivisorMagic = 0x2710u;
private const uint DefaultStorageRateMs = 1000u;
/// <summary>
/// Serializes a CTagMetadata payload for an analog tag (CDataType=Float currently
/// supported). Output matches the byte-for-byte capture for the same inputs.
/// </summary>
/// <param name="tagName">Tag name (ASCII).</param>
/// <param name="description">Tag description (ASCII; null/empty allowed).</param>
/// <param name="engineeringUnit">EU label (ASCII; null/empty allowed).</param>
/// <param name="dateCreatedUtc">DateCreated FILETIME (caller passes <see cref="DateTime.UtcNow"/>).</param>
/// <param name="storageRateMs">StorageRate in milliseconds.</param>
public static byte[] SerializeAnalogCTagMetadata(
string tagName,
string? description,
string? engineeringUnit,
DateTime dateCreatedUtc,
uint storageRateMs = DefaultStorageRateMs)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tagName);
using MemoryStream ms = new();
using BinaryWriter w = new(ms);
w.Write(AnalogHeader); // 9 bytes
w.Write(AnalogPadding16); // 16 bytes (all zero — placeholder GUID + 2)
WriteCompactAscii(w, tagName); // var
w.Write(AnalogPostNamePadding); // 16 bytes of 0xFF
WriteCompactAscii(w, description ?? string.Empty); // var
WriteCompactAscii(w, MetadataProvider); // 7 bytes ("MDAS")
w.Write(AnalogFlagBlock); // 6 bytes
w.Write(storageRateMs); // uint32
w.Write(dateCreatedUtc.ToUniversalTime().ToFileTimeUtc()); // int64
w.Write(AnalogSeparator); // 2 bytes
WriteCompactAscii(w, engineeringUnit ?? string.Empty); // var
w.Write(IntegralDivisorMagic); // uint32 (purpose unclear — captured constant)
w.Write(1.0); // double
w.Write(AnalogTrailer); // 5 bytes
return ms.ToArray();
}
/// <summary>
/// Serializes the tagNames byte buffer for the DelT (DeleteTags) WCF op.
/// Decoded layout from a captured DelT request:
/// <code>
/// ushort header1 = 0x6751
/// ushort header2 = 1
/// uint32 tagCount
/// for each tag: uint32 charCount + charCount × UTF-16 LE chars
/// </code>
/// </summary>
public static byte[] SerializeDeleteTagNames(IReadOnlyList<string> tagNames)
{
ArgumentNullException.ThrowIfNull(tagNames);
if (tagNames.Count == 0)
{
throw new ArgumentException("DeleteTags requires at least one tag name.", nameof(tagNames));
}
using MemoryStream ms = new();
using BinaryWriter w = new(ms);
w.Write((ushort)0x6751);
w.Write((ushort)1);
w.Write(checked((uint)tagNames.Count));
foreach (string name in tagNames)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(tagNames));
w.Write(checked((uint)name.Length));
w.Write(Encoding.Unicode.GetBytes(name));
}
return ms.ToArray();
}
/// <summary>Compact ASCII string: <c>0x09 + UInt16 byteLen + LEN ASCII bytes</c>.</summary>
private static void WriteCompactAscii(BinaryWriter writer, string value)
{
byte[] ascii = Encoding.ASCII.GetBytes(value);
if (ascii.Length > ushort.MaxValue)
{
throw new ArgumentOutOfRangeException(nameof(value), "Compact ASCII strings cannot exceed UInt16 length.");
}
writer.Write(CompactAsciiMarker);
writer.Write((ushort)ascii.Length);
writer.Write(ascii);
}
}
@@ -220,7 +220,10 @@ internal static class HistorianWcfTagClient
/// </summary>
internal static HistorianDataType MapDataType(byte[] nativeDataTypeDescriptor)
{
if (nativeDataTypeDescriptor is not [0x03, 0xCF or 0xC3, _, _])
// byte 1 origin marker: 0xCF = system / built-in tag, 0xC3 = MDAS-routed
// (e.g. OPC UA imported), 0xC7 = SDK-created via EnsT2 (live-verified by the
// EnsureTagAsync round-trip test).
if (nativeDataTypeDescriptor is not [0x03, 0xCF or 0xC3 or 0xC7, _, _])
{
throw new ProtocolEvidenceMissingException(
$"GetTagInfoFromName data type descriptor {Convert.ToHexString(nativeDataTypeDescriptor)}");
@@ -0,0 +1,227 @@
using System.Buffers.Binary;
using System.Runtime.Versioning;
using System.ServiceModel;
using System.ServiceModel.Channels;
using AVEVA.Historian.Client.Models;
using AVEVA.Historian.Client.Wcf.Contracts;
namespace AVEVA.Historian.Client.Wcf;
/// <remarks>
/// Drives the EnsT2 (EnsureTags2) and DelT (DeleteTags) WCF operations end-to-end.
/// Mirrors <see cref="HistorianWcfReadOrchestrator"/> for the reads flow — opens an
/// authenticated session, runs the documented priming chain (UpdC3 + 7×
/// Stat.GetSystemParameter + Trx/Stat/Retr GetV) and then issues the write op.
///
/// AddS2 is intentionally NOT here — it is blocked architecturally per
/// <c>docs/plans/write-commands-reverse-engineering.md</c> Phase 2 findings.
/// </remarks>
[SupportedOSPlatform("windows")]
internal sealed class HistorianWcfTagWriteOrchestrator
{
private readonly HistorianClientOptions _options;
public HistorianWcfTagWriteOrchestrator(HistorianClientOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
}
public Task<bool> EnsureTagAsync(HistorianTagDefinition definition, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(definition);
ArgumentException.ThrowIfNullOrWhiteSpace(definition.TagName, nameof(definition));
if (definition.DataType != HistorianDataType.Float)
{
throw new ProtocolEvidenceMissingException(
$"EnsureTagAsync currently only supports HistorianDataType.Float (analog double); requested {definition.DataType}");
}
return Task.Run(() => EnsureTag(definition), cancellationToken);
}
public Task<bool> DeleteTagAsync(string tagName, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tagName);
return Task.Run(() => DeleteTag(tagName), cancellationToken);
}
private bool EnsureTag(HistorianTagDefinition definition)
{
Guid contextKey = Guid.NewGuid();
var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(_options);
Binding auxBinding = HistorianWcfBindingFactory.CreateAuxiliaryBinding(_options);
EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Status);
EndpointAddress transactionEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Transaction);
EndpointAddress retrievalEndpoint = HistorianWcfBindingFactory.CreatePipeEndpointAddress(_options.Host, HistorianWcfServiceNames.Retrieval);
if (_options.Transport != HistorianTransport.LocalPipe)
{
retrievalEndpoint = HistorianWcfBindingFactory.CreateEndpointAddress(_options.Host, _options.Port, HistorianWcfServiceNames.Retrieval);
}
bool result = false;
HistorianWcfAuthChainHelper.OpenAuthenticatedConnection(
_options, histBinding, histEndpoint, contextKey, CancellationToken.None,
additionalSetup: (historyChannel, context) => result = SendEnsureTags2(
historyChannel, context, definition, auxBinding, statusEndpoint, transactionEndpoint, retrievalEndpoint));
return result;
}
private bool DeleteTag(string tagName)
{
Guid contextKey = Guid.NewGuid();
var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(_options);
Binding auxBinding = HistorianWcfBindingFactory.CreateAuxiliaryBinding(_options);
EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Status);
EndpointAddress transactionEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Transaction);
EndpointAddress retrievalEndpoint = _options.Transport == HistorianTransport.LocalPipe
? HistorianWcfBindingFactory.CreatePipeEndpointAddress(_options.Host, HistorianWcfServiceNames.Retrieval)
: HistorianWcfBindingFactory.CreateEndpointAddress(_options.Host, _options.Port, HistorianWcfServiceNames.Retrieval);
bool result = false;
HistorianWcfAuthChainHelper.OpenAuthenticatedConnection(
_options, histBinding, histEndpoint, contextKey, CancellationToken.None,
additionalSetup: (historyChannel, context) =>
{
RunWritePriming(historyChannel, context, auxBinding, statusEndpoint, transactionEndpoint, retrievalEndpoint);
result = SendDeleteTags(historyChannel, context, tagName);
});
return result;
}
private static bool SendEnsureTags2(
IHistoryServiceContract2 historyChannel,
HistorianWcfAuthChainHelper.OpenConnectionContext context,
HistorianTagDefinition definition,
Binding auxBinding,
EndpointAddress statusEndpoint,
EndpointAddress transactionEndpoint,
EndpointAddress retrievalEndpoint)
{
RunWritePriming(historyChannel, context, auxBinding, statusEndpoint, transactionEndpoint, retrievalEndpoint);
string handle = context.StorageSessionId.ToString("D").ToUpperInvariant();
byte[] payload = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata(
tagName: definition.TagName,
description: definition.Description,
engineeringUnit: definition.EngineeringUnit,
dateCreatedUtc: DateTime.UtcNow);
return historyChannel.EnsureTags2(
handle: handle,
elementCount: 1,
inputBuffer: payload,
outputBuffer: out _,
errorBuffer: out _);
}
/// <summary>
/// Runs the priming chain captured between Open2 and the actual write op (EnsT2 / DelT).
/// Both paths share the same priming per the native flow capture:
/// Stat.GetV ×2 → Stat.GETHI(HistorianVersion) ×2 → UpdC3 → 6 GetSystemParameter →
/// GetSystemParameter("AllowRenameTags") → Trx.GetV → Stat.GetV → Retr.GetV.
/// </summary>
private static void RunWritePriming(
IHistoryServiceContract2 historyChannel,
HistorianWcfAuthChainHelper.OpenConnectionContext context,
Binding auxBinding,
EndpointAddress statusEndpoint,
EndpointAddress transactionEndpoint,
EndpointAddress retrievalEndpoint)
{
string handle = context.StorageSessionId.ToString("D").ToUpperInvariant();
ChannelFactory<IStatusServiceContract2> statusFactory = new(auxBinding, statusEndpoint);
IStatusServiceContract2 statusChannel = statusFactory.CreateChannel();
ChannelFactory<ITransactionServiceContract> transactionFactory = new(auxBinding, transactionEndpoint);
ITransactionServiceContract transactionChannel = transactionFactory.CreateChannel();
ChannelFactory<IRetrievalServiceContract4> retrievalFactory = new(auxBinding, retrievalEndpoint);
IRetrievalServiceContract4 retrievalChannel = retrievalFactory.CreateChannel();
try
{
TryRun(() => statusChannel.GetInterfaceVersion(out _));
TryRun(() => statusChannel.GetInterfaceVersion(out _));
byte[] historianVersionRequest = BuildGetHistorianInfoRequest("HistorianVersion");
TryRun(() => statusChannel.GetHistorianInfo(handle, historianVersionRequest, out _, out _));
TryRun(() => statusChannel.GetHistorianInfo(handle, historianVersionRequest, out _, out _));
byte[] clientStatus = BuildUpdC3ClientStatusBlob();
historyChannel.UpdateClientStatus3(handle, (uint)clientStatus.Length, ref clientStatus, out _, out _, out _, out _);
foreach (string parameterName in NativeStatusParametersBeforeAnalogEnsT2)
{
TryRun(() => statusChannel.GetSystemParameter(context.ClientHandle, parameterName, out _, out _, out _));
}
TryRun(() => statusChannel.GetSystemParameter(context.ClientHandle, "AllowRenameTags", out _, out _, out _));
TryRun(() => transactionChannel.GetInterfaceVersion(out _));
TryRun(() => statusChannel.GetInterfaceVersion(out _));
TryRun(() => retrievalChannel.GetInterfaceVersion(out _));
}
finally
{
CloseSafely(retrievalChannel, retrievalFactory);
CloseSafely(transactionChannel, transactionFactory);
CloseSafely(statusChannel, statusFactory);
}
}
private static bool SendDeleteTags(
IHistoryServiceContract2 historyChannel,
HistorianWcfAuthChainHelper.OpenConnectionContext context,
string tagName)
{
// DelT uses the uint clientHandle, NOT the GUID handle (decoded from wire capture).
byte[] tagNamesBytes = HistorianTagWriteProtocol.SerializeDeleteTagNames([tagName]);
uint statusSize = 0;
byte[] status = [];
return historyChannel.DeleteTags(
handle: context.ClientHandle,
tagNamesSize: checked((uint)tagNamesBytes.Length),
tagNames: tagNamesBytes,
statusSize: ref statusSize,
status: ref status,
errorSize: out _,
errorBuffer: out _);
}
private static readonly string[] NativeStatusParametersBeforeAnalogEnsT2 =
[
"AllowOriginals",
"HistorianPartner",
"HistorianVersion",
"MaxCyclicStorageTimeout",
"RealTimeWindow",
"FutureTimeThreshold",
];
private static void TryRun(Action a) { try { a(); } catch { } }
/// <summary>81-byte UpdC3 status blob captured from native (same as event flow).</summary>
private static byte[] BuildUpdC3ClientStatusBlob()
{
byte[] blob = new byte[81];
blob[0] = 0x02;
blob[1] = 0x01;
blob[77] = 0x1E;
return blob;
}
/// <summary>GETHI request bytes for a parameter-name query (decoded from native).</summary>
private static byte[] BuildGetHistorianInfoRequest(string parameterName)
{
byte[] nameBytes = System.Text.Encoding.Unicode.GetBytes(parameterName);
int payloadLength = nameBytes.Length > 0 ? nameBytes.Length - 1 : 0;
byte[] buffer = new byte[8 + payloadLength];
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(0, 2), 0x6753);
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(2, 2), 0x0002);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), (uint)parameterName.Length);
Buffer.BlockCopy(nameBytes, 0, buffer, 8, payloadLength);
return buffer;
}
private static void CloseSafely(object channel, ICommunicationObject factory)
{
try { if (channel is ICommunicationObject co) { if (co.State == CommunicationState.Faulted) co.Abort(); else co.Close(); } } catch { }
try { if (factory.State == CommunicationState.Faulted) factory.Abort(); else factory.Close(); } catch { }
}
}