diff --git a/src/samples/InteropTest-gsf/Program.cs b/src/samples/InteropTest-gsf/Program.cs index 9818e31e..cb527269 100644 --- a/src/samples/InteropTest-gsf/Program.cs +++ b/src/samples/InteropTest-gsf/Program.cs @@ -26,7 +26,10 @@ using sttp; using System; using System.Collections.Generic; +using System.Data; using System.IO; +using System.Threading; +using System.Xml; namespace InteropTest; @@ -37,6 +40,12 @@ class Program private static ulong s_processCount; private static bool s_ready; + // Metadata interop-mode state + private static readonly ManualResetEventSlim s_metadataComplete = new(false); + private static volatile bool s_metadataRequested; + private static volatile bool s_metadataHandled; + private static int s_metadataExitCode = 2; // default: timeout / no metadata received + private static void StatusMessage(string message) { // Calls can come from multiple threads, so we impose a simple lock before write to console @@ -58,7 +67,7 @@ static void Main(string[] args) if (args.Length < 2) { Console.WriteLine("Usage:"); - Console.WriteLine(" InteropTest HOSTNAME PORT"); + Console.WriteLine(" InteropTest HOSTNAME PORT [--metadata [--gzip]]"); return; } @@ -71,6 +80,18 @@ static void Main(string[] args) return; } + // Metadata interop mode: request XML metadata, dump its datetime values, and exit with a + // status code (0 = metadata received and parsed, non-zero = deserialization error / timeout). + // This exercises the publisher's xs:dateTime serialization against the .NET DataSet parser. + // Add --gzip to advertise the GZip compression mode (compressed-metadata path); without it + // the .NET-default modes are used (CompressMetadata without GZip -> uncompressed path). + if (args.Length > 2 && args[2].Equals("--metadata", StringComparison.OrdinalIgnoreCase)) + { + bool useGZip = Array.Exists(args, a => a.Equals("--gzip", StringComparison.OrdinalIgnoreCase)); + RunMetadataMode(hostname, port, useGZip); + return; + } + // Initialize the subscribers, maintaining the life-time of SubscriberHandler instances within main DataSubscriber subscriber = new(); subscriber.StatusMessage += (_, e) => StatusMessage(e.Argument); @@ -153,4 +174,121 @@ private static void subscriber_ConnectionTerminated(object sender, EventArgs e) s_export?.Close(); s_export = null; } + + // ---------------------------------------------------------------------------------------------- + // Metadata interop mode + // ---------------------------------------------------------------------------------------------- + + private static void RunMetadataMode(string hostname, ushort port, bool useGZip) + { + DataSubscriber subscriber = new(); + subscriber.StatusMessage += (_, e) => StatusMessage(e.Argument); + subscriber.ProcessException += metadata_ProcessException; + subscriber.ConnectionEstablished += metadata_ConnectionEstablished; + subscriber.ConnectionTerminated += (_, _) => StatusMessage("Connection terminated."); + subscriber.MetaDataReceived += metadata_MetaDataReceived; + subscriber.ConnectionString = $"server={hostname}:{port}"; + + // A correct STTP publisher GZip-compresses metadata only when the subscriber negotiates + // BOTH CompressMetadata (on by default) and the GZip compression mode. With --gzip we + // advertise GZip to exercise the compressed path; without it we use the .NET-default + // operational modes (CompressMetadata without GZip), exercising the uncompressed path. + if (useGZip) + subscriber.CompressionModes |= CompressionModes.GZip; + + StatusMessage($"Metadata mode: {(useGZip ? "GZip (compressed)" : "default (uncompressed)")} negotiation."); + subscriber.Initialize(); + subscriber.Start(); + + // Wait for metadata, a deserialization error, or a timeout. + if (!s_metadataComplete.Wait(TimeSpan.FromSeconds(15))) + { + ErrorMessage("METADATA TIMEOUT: no metadata received within 15 seconds."); + s_metadataExitCode = 2; + } + + try + { + subscriber.Stop(); + } + catch (Exception ex) + { + ErrorMessage($"Error stopping subscriber: {ex.Message}"); + } + + Environment.Exit(s_metadataExitCode); + } + + private static void metadata_ConnectionEstablished(object sender, EventArgs e) + { + StatusMessage("Connection established; requesting metadata..."); + s_metadataRequested = true; + (sender as DataSubscriber)?.RefreshMetadata(); + } + + private static void metadata_ProcessException(object sender, EventArgs e) + { + // A malformed xs:dateTime lexical value in the metadata surfaces here: DataSet.ReadXml + // (via DeserializeMetadata) throws and is re-raised as a "Failed to process publisher + // response packet" exception. Any exception after the metadata request is treated as a + // failure of the exchange. + ErrorMessage($"PROCESS EXCEPTION: {e.Argument.Message}"); + + if (!s_metadataRequested || s_metadataHandled) + return; + + s_metadataHandled = true; + s_metadataExitCode = 1; + s_metadataComplete.Set(); + } + + private static void metadata_MetaDataReceived(object sender, EventArgs e) + { + if (s_metadataHandled) + return; + + s_metadataHandled = true; + + try + { + DataSet metadata = e.Argument; + StatusMessage($"METADATA RECEIVED: {metadata.Tables.Count} table(s)."); + + // Persist the received metadata for inspection. + metadata.WriteXml("metadata-received.xml", XmlWriteMode.WriteSchema); + StatusMessage("Wrote metadata-received.xml"); + + // Dump every DateTime value so the orchestrator can verify the round-trip. Uppercase + // 'F' omits trailing fractional zeros, matching the bare canonical form. + foreach (DataTable table in metadata.Tables) + { + foreach (DataColumn column in table.Columns) + { + if (column.DataType != typeof(DateTime)) + continue; + + foreach (DataRow row in table.Rows) + { + if (row[column] == DBNull.Value) + continue; + + DateTime value = (DateTime)row[column]; + StatusMessage($"DATETIME {table.TableName}.{column.ColumnName} = {value:yyyy-MM-ddTHH:mm:ss.FFFFFFF}"); + } + } + } + + StatusMessage("METADATA OK"); + s_metadataExitCode = 0; + } + catch (Exception ex) + { + ErrorMessage($"METADATA FAILURE while inspecting received data: {ex.Message}"); + s_metadataExitCode = 1; + } + finally + { + s_metadataComplete.Set(); + } + } } \ No newline at end of file