diff --git a/.gitignore b/.gitignore index ef8008092..76310e0ed 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ upload_to_s3.rb *~ *.suo .vs/ +.vscode/ # ReSharper is a .NET coding add-in _ReSharper*/ diff --git a/AcceptanceTest/at_xml_to_nunit_xml.rb b/AcceptanceTest/at_xml_to_nunit_xml.rb index def1845ba..53c01a07e 100644 --- a/AcceptanceTest/at_xml_to_nunit_xml.rb +++ b/AcceptanceTest/at_xml_to_nunit_xml.rb @@ -58,7 +58,6 @@ class Fixture attr_accessor :name, :total, :errors, :failures, :cases def self.create_from_acceptance_test_xml xml_filename - require 'rubygems' require 'nokogiri' fixture = Fixture.new diff --git a/AcceptanceTest/debug/at/quickfix_net.dll b/AcceptanceTest/debug/at/quickfix_net.dll deleted file mode 100755 index d99aa1604..000000000 Binary files a/AcceptanceTest/debug/at/quickfix_net.dll and /dev/null differ diff --git a/AcceptanceTest/debug/at/quickfix_net.pdb b/AcceptanceTest/debug/at/quickfix_net.pdb deleted file mode 100755 index 76c8c8191..000000000 Binary files a/AcceptanceTest/debug/at/quickfix_net.pdb and /dev/null differ diff --git a/AcceptanceTest/debug/at/quickfix_net_messages.dll b/AcceptanceTest/debug/at/quickfix_net_messages.dll deleted file mode 100755 index da92e6c67..000000000 Binary files a/AcceptanceTest/debug/at/quickfix_net_messages.dll and /dev/null differ diff --git a/AcceptanceTest/debug/at/quickfix_net_messages.pdb b/AcceptanceTest/debug/at/quickfix_net_messages.pdb deleted file mode 100755 index 608700f3a..000000000 Binary files a/AcceptanceTest/debug/at/quickfix_net_messages.pdb and /dev/null differ diff --git a/AcceptanceTest/debug/pt/quickfix_net.dll b/AcceptanceTest/debug/pt/quickfix_net.dll deleted file mode 100755 index d99aa1604..000000000 Binary files a/AcceptanceTest/debug/pt/quickfix_net.dll and /dev/null differ diff --git a/AcceptanceTest/debug/pt/quickfix_net.pdb b/AcceptanceTest/debug/pt/quickfix_net.pdb deleted file mode 100755 index 76c8c8191..000000000 Binary files a/AcceptanceTest/debug/pt/quickfix_net.pdb and /dev/null differ diff --git a/AcceptanceTest/debug/pt/quickfix_net_messages.dll b/AcceptanceTest/debug/pt/quickfix_net_messages.dll deleted file mode 100755 index da92e6c67..000000000 Binary files a/AcceptanceTest/debug/pt/quickfix_net_messages.dll and /dev/null differ diff --git a/AcceptanceTest/debug/pt/quickfix_net_messages.pdb b/AcceptanceTest/debug/pt/quickfix_net_messages.pdb deleted file mode 100755 index 608700f3a..000000000 Binary files a/AcceptanceTest/debug/pt/quickfix_net_messages.pdb and /dev/null differ diff --git a/AcceptanceTest/runat.ps1 b/AcceptanceTest/runat.ps1 index c64ec4913..55c37d676 100644 --- a/AcceptanceTest/runat.ps1 +++ b/AcceptanceTest/runat.ps1 @@ -120,6 +120,7 @@ AppDataDictionary=..\spec\fix\FIX50SP1.xml "@ | Out-File 'cfg\at.cfg' -Encoding utf8 # Start the acceptor... + # (this runs the exe or dll in AcceptanceTest/bin/) $AtProcess = Start-Process -FilePath dotnet.exe -ArgumentList "run -c $Configuration -f $Framework --no-build --no-restore -- $Conf" -NoNewWindow -PassThru if ($UseWsl -and $UseWsl.IsPresent) { diff --git a/New-Release.ps1 b/New-Release.ps1 index 3bcc8c74d..a57f93ae9 100644 --- a/New-Release.ps1 +++ b/New-Release.ps1 @@ -27,7 +27,7 @@ Write-Host "`nTag Version: $TagVersion`n" if ($UseWslRuby -and $UseWslRuby.IsPresent) { wsl ruby scripts\update_assembly_version.rb $TagVersion QuickFIXn\QuickFix.csproj Messages\FIX40\QuickFix.FIX40.csproj Messages\FIX41\QuickFix.FIX41.csproj Messages\FIX42\QuickFix.FIX42.csproj Messages\FIX43\QuickFix.FIX43.csproj Messages\FIX44\QuickFix.FIX44.csproj Messages\FIX50\QuickFix.FIX50.csproj Messages\FIX50SP1\QuickFix.FIX50SP1.csproj Messages\FIX50SP2\QuickFix.FIX50SP2.csproj } else { - .\ruby scripts\update_assembly_version.rb $TagVersion QuickFIXn\QuickFix.csproj Messages\FIX40\QuickFix.FIX40.csproj Messages\FIX41\QuickFix.FIX41.csproj Messages\FIX42\QuickFix.FIX42.csproj Messages\FIX43\QuickFix.FIX43.csproj Messages\FIX44\QuickFix.FIX44.csproj Messages\FIX50\QuickFix.FIX50.csproj Messages\FIX50SP1\QuickFix.FIX50SP1.csproj Messages\FIX50SP2\QuickFix.FIX50SP2.csproj + ruby scripts\update_assembly_version.rb $TagVersion QuickFIXn\QuickFix.csproj Messages\FIX40\QuickFix.FIX40.csproj Messages\FIX41\QuickFix.FIX41.csproj Messages\FIX42\QuickFix.FIX42.csproj Messages\FIX43\QuickFix.FIX43.csproj Messages\FIX44\QuickFix.FIX44.csproj Messages\FIX50\QuickFix.FIX50.csproj Messages\FIX50SP1\QuickFix.FIX50SP1.csproj Messages\FIX50SP2\QuickFix.FIX50SP2.csproj } $ExitCode = $LASTEXITCODE @@ -38,22 +38,22 @@ if ($ExitCode -eq 0) { Exit $ExitCode } -.\git add QuickFIXn\QuickFix.csproj -.\git add Messages\FIX40\QuickFix.FIX40.csproj -.\git add Messages\FIX41\QuickFix.FIX41.csproj -.\git add Messages\FIX42\QuickFix.FIX42.csproj -.\git add Messages\FIX43\QuickFix.FIX43.csproj -.\git add Messages\FIX44\QuickFix.FIX44.csproj -.\git add Messages\FIX50\QuickFix.FIX50.csproj -.\git add Messages\FIX50SP1\QuickFix.FIX50SP1.csproj -.\git add Messages\FIX50SP2\QuickFix.FIX50SP2.csproj -.\git commit -m "version number for version %TAG_VERSION%" +git add QuickFIXn\QuickFix.csproj +git add Messages\FIX40\QuickFix.FIX40.csproj +git add Messages\FIX41\QuickFix.FIX41.csproj +git add Messages\FIX42\QuickFix.FIX42.csproj +git add Messages\FIX43\QuickFix.FIX43.csproj +git add Messages\FIX44\QuickFix.FIX44.csproj +git add Messages\FIX50\QuickFix.FIX50.csproj +git add Messages\FIX50SP1\QuickFix.FIX50SP1.csproj +git add Messages\FIX50SP2\QuickFix.FIX50SP2.csproj +git commit -m "version number for version %TAG_VERSION%" Write-Host '* Version number committed.' -.\git tag -a $TagVersion -m 'Release version $TagVersion' +git tag -a $TagVersion -m 'Release version $TagVersion' Write-Host '* Created tag.' -.\git checkout $TagVersion +git checkout $TagVersion $ExitCode = $LASTEXITCODE if ($ExitCode -eq 0) { Write-Host "* Checked out tag $TagVersion" @@ -65,7 +65,7 @@ if ($ExitCode -eq 0) { if ($UsWslRuby -and $UseWslRuby.IsPresent) { wsl ruby generator/generate.rb } else { - .\ruby generator/generate.rb + ruby generator/generate.rb } $ExitCode = $LASTEXITCODE @@ -91,17 +91,17 @@ if ($ExitCode -eq 0) { # DO NOT remove quotes around *.nupkg. Due to a bug in older versions of the .NET SDK, # without the surrounding quotes, only the first package will be pushed. -if ($BuildTarget -ieq 'pack') { - dotnet nuget push '*.nupkg' -s 'https://api.nuget.org/v3/index.json' -k $NuGetApiKey 'tmp\NuGet' -} - -$ExitCode = $LASTEXITCODE -if ($ExitCode -eq 0) { - Write-Host '* Pushed QuickFIX/n NuGet packages to NuGet.org' -} else { - Write-Error 'A problem occurred while pushing NuGet packages to NuGet.org' - Exit $ExitCode -} +#if ($BuildTarget -ieq 'pack') { +# dotnet nuget push '*.nupkg' -s 'https://api.nuget.org/v3/index.json' -k $NuGetApiKey 'tmp\NuGet' +#} + +#$ExitCode = $LASTEXITCODE +#if ($ExitCode -eq 0) { +# Write-Host '* Pushed QuickFIX/n NuGet packages to NuGet.org' +#} else { +# Write-Error 'A problem occurred while pushing NuGet packages to NuGet.org' +# Exit $ExitCode +#} @( $QFDir @@ -140,7 +140,7 @@ Write-Host '* Created zip.' if ($UseWslRuby -and $UseWslRuby.IsPresent) { wsl ruby scripts\s3_upload.rb "$QFDir.zip" $AwsAccessKey $AwsSecretKey } else { - .\ruby scripts\s3_upload.rb "$QFDir.zip" $AwsAccessKey $AwsSecretKey + ruby scripts\s3_upload.rb "$QFDir.zip" $AwsAccessKey $AwsSecretKey } $ExitCode = $LASTEXITCODE @@ -154,7 +154,7 @@ if ($ExitCode -eq 0) { Get-ChildItem -Path 'tmp' -Recurse | Remove-Item -Recurse -Force Write-Host '* Removed ''tmp'' directory.' -.\git checkout master +git checkout master Write-Host '* Changed back to master.' Write-Host "`nSuccessfully created QuickFIX/n $TagVersion" -ForegroundColor Green @@ -164,4 +164,4 @@ if ($NuGetApiKey) { } Write-Host "You must commit the new tag and deploy the website" -Exit 0 \ No newline at end of file +Exit 0 diff --git a/QuickFIXn/DefaultMessageFactory.cs b/QuickFIXn/DefaultMessageFactory.cs index e73e27002..e168f74bd 100644 --- a/QuickFIXn/DefaultMessageFactory.cs +++ b/QuickFIXn/DefaultMessageFactory.cs @@ -188,7 +188,7 @@ private static ICollection GetAppDomainAssemblies() var assemblies = AppDomain .CurrentDomain .GetAssemblies() - .Where(assembly => !assembly.IsDynamic) + .Where(assembly => !assembly.IsDynamic && assembly.GetName().Name.StartsWith("QuickFix")) .ToList(); return assemblies; } diff --git a/QuickFIXn/FileLog.cs b/QuickFIXn/FileLog.cs index 43401a07d..98c1ee2d6 100755 --- a/QuickFIXn/FileLog.cs +++ b/QuickFIXn/FileLog.cs @@ -43,23 +43,97 @@ private void Init(string fileLogPath, string prefix) eventLog_.AutoFlush = true; } + public const string WILDCARD_FILE_PREFIX = "DS_"; // Dynamic Session + public const string WILDCARD_REPLACEMENT = "(ANY)"; + /// + /// Calculates unique filename prefix from SessionID. + /// Handles wildcards in SessionID fields + /// + /// Filename prefix unique for SessioID public static string Prefix(SessionID sessionID) { - System.Text.StringBuilder prefix = new System.Text.StringBuilder(sessionID.BeginString) - .Append('-').Append(sessionID.SenderCompID); + + bool hasWildcard = false; + + System.Text.StringBuilder prefix = new System.Text.StringBuilder(); + if (Values.WILDCARD_VALUE.Equals(sessionID.BeginString)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT).Append('-'); + } + else prefix.Append(sessionID.BeginString).Append('-'); + if (Values.WILDCARD_VALUE.Equals(sessionID.SenderCompID)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.SenderCompID); + + if (SessionID.IsSet(sessionID.SenderSubID)) - prefix.Append('_').Append(sessionID.SenderSubID); + { + prefix.Append('_'); + if (Values.WILDCARD_VALUE.Equals(sessionID.SenderSubID)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.SenderSubID); + } if (SessionID.IsSet(sessionID.SenderLocationID)) - prefix.Append('_').Append(sessionID.SenderLocationID); - prefix.Append('-').Append(sessionID.TargetCompID); + { + prefix.Append('_'); + if (Values.WILDCARD_VALUE.Equals(sessionID.SenderLocationID)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.SenderLocationID); + } + prefix.Append('-'); + if (Values.WILDCARD_VALUE.Equals(sessionID.TargetCompID)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.TargetCompID); + if (SessionID.IsSet(sessionID.TargetSubID)) - prefix.Append('_').Append(sessionID.TargetSubID); + { + prefix.Append('_'); + if (Values.WILDCARD_VALUE.Equals(sessionID.TargetSubID)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.TargetSubID); + } if (SessionID.IsSet(sessionID.TargetLocationID)) - prefix.Append('_').Append(sessionID.TargetLocationID); - + { + prefix.Append('_'); + if (Values.WILDCARD_VALUE.Equals(sessionID.TargetLocationID)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.TargetLocationID); + } + // SessionQualifier now is not allowed for acceptor, so cannot be a wildcard, + // still should not hurt to implement wildcard logic for it if (SessionID.IsSet(sessionID.SessionQualifier)) - prefix.Append('-').Append(sessionID.SessionQualifier); - + { + prefix.Append('-'); + if (Values.WILDCARD_VALUE.Equals(sessionID.SessionQualifier)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.SessionQualifier); + } + // To guarantee unique file name, + // even if someone uses WILDCARD_REPLACEMENT as a part if SessionID + if (hasWildcard) return WILDCARD_FILE_PREFIX + prefix.ToString(); + // No wildcard(s) - no additional prefix return prefix.ToString(); } diff --git a/QuickFIXn/FileStore.cs b/QuickFIXn/FileStore.cs index e8f885a32..78740ecdb 100755 --- a/QuickFIXn/FileStore.cs +++ b/QuickFIXn/FileStore.cs @@ -35,23 +35,97 @@ public MsgDef(long index, int size) System.Collections.Generic.Dictionary offsets_ = new Dictionary(); + public const string WILDCARD_FILE_PREFIX = "DS_"; // Dynamic Session + public const string WILDCARD_REPLACEMENT = "(ANY)"; + /// + /// Calculates unique filename prefix from SessionID. + /// Handles wildcards in SessionID fields + /// + /// Filename prefix unique for SessioID public static string Prefix(SessionID sessionID) { - System.Text.StringBuilder prefix = new System.Text.StringBuilder(sessionID.BeginString) - .Append('-').Append(sessionID.SenderCompID); + + bool hasWildcard = false; + + System.Text.StringBuilder prefix = new System.Text.StringBuilder(); + if (Values.WILDCARD_VALUE.Equals(sessionID.BeginString)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT).Append('-'); + } + else prefix.Append(sessionID.BeginString).Append('-'); + if (Values.WILDCARD_VALUE.Equals(sessionID.SenderCompID)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.SenderCompID); + + if (SessionID.IsSet(sessionID.SenderSubID)) - prefix.Append('_').Append(sessionID.SenderSubID); + { + prefix.Append('_'); + if (Values.WILDCARD_VALUE.Equals(sessionID.SenderSubID)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.SenderSubID); + } if (SessionID.IsSet(sessionID.SenderLocationID)) - prefix.Append('_').Append(sessionID.SenderLocationID); - prefix.Append('-').Append(sessionID.TargetCompID); + { + prefix.Append('_'); + if (Values.WILDCARD_VALUE.Equals(sessionID.SenderLocationID)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.SenderLocationID); + } + prefix.Append('-'); + if (Values.WILDCARD_VALUE.Equals(sessionID.TargetCompID)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.TargetCompID); + if (SessionID.IsSet(sessionID.TargetSubID)) - prefix.Append('_').Append(sessionID.TargetSubID); + { + prefix.Append('_'); + if (Values.WILDCARD_VALUE.Equals(sessionID.TargetSubID)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.TargetSubID); + } if (SessionID.IsSet(sessionID.TargetLocationID)) - prefix.Append('_').Append(sessionID.TargetLocationID); - + { + prefix.Append('_'); + if (Values.WILDCARD_VALUE.Equals(sessionID.TargetLocationID)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.TargetLocationID); + } + // SessionQualifier now is not allowed for acceptor, so cannot be a wildcard, + // still should not hurt to implement wildcard logic for it if (SessionID.IsSet(sessionID.SessionQualifier)) - prefix.Append('-').Append(sessionID.SessionQualifier); - + { + prefix.Append('-'); + if (Values.WILDCARD_VALUE.Equals(sessionID.SessionQualifier)) + { + hasWildcard = true; + prefix.Append(WILDCARD_REPLACEMENT); + } + else prefix.Append(sessionID.SessionQualifier); + } + // To guarantee unique file name, + // even if someone uses WILDCARD_REPLACEMENT as a part if SessionID + if (hasWildcard) return WILDCARD_FILE_PREFIX + prefix.ToString(); + // No wildcard(s) - no additional prefix return prefix.ToString(); } diff --git a/QuickFIXn/Session.cs b/QuickFIXn/Session.cs old mode 100755 new mode 100644 index 5e1f7a799..4711a4980 --- a/QuickFIXn/Session.cs +++ b/QuickFIXn/Session.cs @@ -312,6 +312,19 @@ public Session( this.Log.OnEvent("Created session"); } + private IAcceptor _acceptor = null; + private QuickFix.Dictionary _settings = null; + public Session( + bool isInitiator, IApplication app, IMessageStoreFactory storeFactory, SessionID sessID, DataDictionaryProvider dataDictProvider, + SessionSchedule sessionSchedule, int heartBtInt, ILogFactory logFactory, IMessageFactory msgFactory, string senderDefaultApplVerID, + IAcceptor acceptor, QuickFix.Dictionary settings) + :this(isInitiator, app, storeFactory, sessID, dataDictProvider,sessionSchedule, heartBtInt, logFactory, msgFactory, senderDefaultApplVerID) + { + _acceptor = acceptor; + _settings = settings; + } + + #region Static Methods /// @@ -330,6 +343,69 @@ public static Session LookupSession(SessionID sessionID) return result; } + /// + /// Looks up a Session by its SessionID, firstly by strings equality, and then by wildcard matching. + /// In the latter case, creates a new session using the found wildcard session as a template. + /// + /// the Session if found or created, otherwise returns null + public static Session LookupOrCreateDynamicSession(SessionID sessionID) + { + Session result = LookupSession(sessionID); + if (null != result) return result; + // If the sessionID would match a "template" sessions with wildcards + // => create the new session based on the template + lock (sessions_) + { + foreach (KeyValuePair kv in sessions_) + { + Session session = kv.Value; + if (session.IsMatching(sessionID)) return session.CreateFromTemplate(sessionID); + } + } + return null; + } + + private bool IsMatching(SessionID sessionID) + { + // (SessionID is "*" AND sessionID is set) OR (SessionID == sessionID) + return ((SessionID.BeginString.Equals(Values.WILDCARD_VALUE) && !sessionID.BeginString.Equals(SessionID.NOT_SET)) || SessionID.BeginString.Equals(sessionID.BeginString)) + && ((SessionID.SenderCompID.Equals(Values.WILDCARD_VALUE) && !sessionID.SenderCompID.Equals(SessionID.NOT_SET)) || SessionID.SenderCompID.Equals(sessionID.SenderCompID)) + && ((SessionID.SenderSubID.Equals(Values.WILDCARD_VALUE) && !sessionID.SenderSubID.Equals(SessionID.NOT_SET)) || SessionID.SenderSubID.Equals(sessionID.SenderSubID)) + && ((SessionID.SenderLocationID.Equals(Values.WILDCARD_VALUE) && !sessionID.SenderLocationID.Equals(SessionID.NOT_SET)) || SessionID.SenderLocationID.Equals(sessionID.SenderLocationID)) + && ((SessionID.TargetCompID.Equals(Values.WILDCARD_VALUE) && !sessionID.TargetCompID.Equals(SessionID.NOT_SET)) || SessionID.TargetCompID.Equals(sessionID.TargetCompID)) + && ((SessionID.TargetSubID.Equals(Values.WILDCARD_VALUE) && !sessionID.TargetSubID.Equals(SessionID.NOT_SET)) || SessionID.TargetSubID.Equals(sessionID.TargetSubID)) + && ((SessionID.TargetLocationID.Equals(Values.WILDCARD_VALUE) && !sessionID.TargetLocationID.Equals(SessionID.NOT_SET)) || SessionID.TargetLocationID.Equals(sessionID.TargetLocationID)); + } + + private Session CreateFromTemplate(SessionID sessionID) + { + if(null == _acceptor) + { + Log.OnEvent($"ERROR adding session {sessionID}: template {SessionID} created with null acceptor"); + return null; + } + Dictionary dict = ReplaceWildcards(sessionID); + if (!_acceptor.AddSession(sessionID, dict)) + { + Log.OnEvent($"ERROR adding session {sessionID} from template {SessionID}"); + return null; + } + return LookupSession(sessionID); + } + + private QuickFix.Dictionary ReplaceWildcards(SessionID sessionID) + { + QuickFix.Dictionary actualSettings = new QuickFix.Dictionary(_settings); + if (Values.WILDCARD_VALUE.Equals(SessionID.BeginString)) actualSettings.SetString(SessionSettings.BEGINSTRING, sessionID.BeginString); + if (Values.WILDCARD_VALUE.Equals(SessionID.SenderCompID)) actualSettings.SetString(SessionSettings.SENDERCOMPID, sessionID.SenderCompID); + if (Values.WILDCARD_VALUE.Equals(SessionID.SenderSubID)) actualSettings.SetString(SessionSettings.SENDERSUBID, sessionID.SenderSubID); + if (Values.WILDCARD_VALUE.Equals(SessionID.SenderLocationID)) actualSettings.SetString(SessionSettings.SENDERLOCID, sessionID.SenderLocationID); + if (Values.WILDCARD_VALUE.Equals(SessionID.TargetCompID)) actualSettings.SetString(SessionSettings.TARGETCOMPID, sessionID.TargetCompID); + if (Values.WILDCARD_VALUE.Equals(SessionID.TargetSubID)) actualSettings.SetString(SessionSettings.TARGETSUBID, sessionID.TargetSubID); + if (Values.WILDCARD_VALUE.Equals(SessionID.TargetLocationID)) actualSettings.SetString(SessionSettings.TARGETLOCID, sessionID.TargetLocationID); + return actualSettings; + } + /// /// Looks up a Session by its SessionID /// @@ -630,6 +706,8 @@ internal void Next(MessageBuilder msgBuilder) if (MsgType.LOGON.Equals(msgType)) NextLogon(message); + else if (MsgType.LOGOUT.Equals(msgType)) + NextLogout(message); else if (!IsLoggedOn) Disconnect(string.Format("Received msg type '{0}' when not logged on", msgType)); else if (MsgType.HEARTBEAT.Equals(msgType)) @@ -638,8 +716,6 @@ internal void Next(MessageBuilder msgBuilder) NextTestRequest(message); else if (MsgType.SEQUENCE_RESET.Equals(msgType)) NextSequenceReset(message); - else if (MsgType.LOGOUT.Equals(msgType)) - NextLogout(message); else if (MsgType.RESEND_REQUEST.Equals(msgType)) NextResendRequest(message); else diff --git a/QuickFIXn/SessionFactory.cs b/QuickFIXn/SessionFactory.cs index d498f8096..ebb1fbde6 100755 --- a/QuickFIXn/SessionFactory.cs +++ b/QuickFIXn/SessionFactory.cs @@ -45,6 +45,13 @@ static private bool DetectIfInitiator(QuickFix.Dictionary settings) throw new ConfigError("Invalid ConnectionType"); } + private IAcceptor _acceptor = null; + public SessionFactory(IApplication app, IMessageStoreFactory storeFactory, ILogFactory logFactory, IMessageFactory messageFactory, IAcceptor acceptor) + : this(app, storeFactory, logFactory, messageFactory) + { + _acceptor = acceptor; + } + public Session Create(SessionID sessionID, QuickFix.Dictionary settings) { bool isInitiator = SessionFactory.DetectIfInitiator(settings); @@ -109,7 +116,9 @@ public Session Create(SessionID sessionID, QuickFix.Dictionary settings) heartBtInt, logFactory_, sessionMsgFactory, - senderDefaultApplVerId); + senderDefaultApplVerId, + _acceptor, + settings); if (settings.Has(SessionSettings.SEND_REDUNDANT_RESENDREQUESTS)) session.SendRedundantResendRequests = settings.GetBool(SessionSettings.SEND_REDUNDANT_RESENDREQUESTS); diff --git a/QuickFIXn/SessionSettings.cs b/QuickFIXn/SessionSettings.cs index 7f87d8364..baf3485ac 100755 --- a/QuickFIXn/SessionSettings.cs +++ b/QuickFIXn/SessionSettings.cs @@ -254,21 +254,50 @@ public override string ToString() protected void Validate(QuickFix.Dictionary dictionary) { + // Firstly check to validate connection type + string connectionType = dictionary.GetString(CONNECTION_TYPE); + if (!"initiator".Equals(connectionType) && !"acceptor".Equals(connectionType)) + { + throw new ConfigError(CONNECTION_TYPE + " must be 'initiator' or 'acceptor'"); + } + // Wildcards in session name parameters are valid only for acceptor + ValidateWildcardMandatory(dictionary, BEGINSTRING); + ValidateWildcardMandatory(dictionary, SENDERCOMPID); + ValidateWildcardOptional(dictionary, SENDERSUBID); + ValidateWildcardOptional(dictionary, SENDERLOCID); + ValidateWildcardMandatory(dictionary, TARGETCOMPID); + ValidateWildcardOptional(dictionary, TARGETSUBID); + ValidateWildcardOptional(dictionary, TARGETLOCID); string beginString = dictionary.GetString(BEGINSTRING); + // BeginString can only be one of the predefined values if (beginString != Values.BeginString_FIX40 && beginString != Values.BeginString_FIX41 && beginString != Values.BeginString_FIX42 && beginString != Values.BeginString_FIX43 && beginString != Values.BeginString_FIX44 && - beginString != Values.BeginString_FIXT11) + beginString != Values.BeginString_FIXT11 && + beginString != Values.WILDCARD_VALUE) { - throw new ConfigError(BEGINSTRING + " (" + beginString + ") must be FIX.4.0 to FIX.4.4 or FIXT.1.1"); + throw new ConfigError($"{BEGINSTRING} ({ beginString }) must be FIX.4.0 to FIX.4.4 or FIXT.1.1 or wildcard '{Values.WILDCARD_VALUE}' (only for acceptor)"); } + } - string connectionType = dictionary.GetString(CONNECTION_TYPE); - if (!"initiator".Equals(connectionType) && !"acceptor".Equals(connectionType)) + private void ValidateWildcardMandatory(Dictionary dictionary, string parameterName) + { + if (Values.WILDCARD_VALUE.Equals(dictionary.GetString(parameterName)) + && (!"acceptor".Equals(dictionary.GetString(CONNECTION_TYPE)))) { - throw new ConfigError(CONNECTION_TYPE + " must be 'initiator' or 'acceptor'"); + throw new ConfigError($"{parameterName} could be a wildcard ({Values.WILDCARD_VALUE}) only in 'acceptor' configuration"); + } + } + + private void ValidateWildcardOptional(Dictionary dictionary, string parameterName) + { + if (dictionary.Has(parameterName) && + Values.WILDCARD_VALUE.Equals(dictionary.GetString(parameterName)) + && (!"acceptor".Equals(dictionary.GetString(CONNECTION_TYPE)))) + { + throw new ConfigError($"{parameterName} could be a wildcard ({Values.WILDCARD_VALUE}) only in 'acceptor' configuration"); } } } diff --git a/QuickFIXn/SocketReader.cs b/QuickFIXn/SocketReader.cs index d6aa07b4c..e5e6f4985 100755 --- a/QuickFIXn/SocketReader.cs +++ b/QuickFIXn/SocketReader.cs @@ -142,7 +142,7 @@ protected void OnMessageFoundInternal(string msg) { if (null == qfSession_) { - qfSession_ = Session.LookupSession(Message.GetReverseSessionID(msg)); + qfSession_ = Session.LookupOrCreateDynamicSession(Message.GetReverseSessionID(msg)); if (null == qfSession_) { this.Log("ERROR: Disconnecting; received message for unknown session: " + msg); diff --git a/QuickFIXn/ThreadedSocketAcceptor.cs b/QuickFIXn/ThreadedSocketAcceptor.cs index bb6d5ae36..aa42b95f2 100755 --- a/QuickFIXn/ThreadedSocketAcceptor.cs +++ b/QuickFIXn/ThreadedSocketAcceptor.cs @@ -61,7 +61,7 @@ public ThreadedSocketAcceptor( { logFactory = logFactory ?? new NullLogFactory(); messageFactory = messageFactory ?? new DefaultMessageFactory(); - SessionFactory sf = new SessionFactory(application, storeFactory, logFactory, messageFactory); + SessionFactory sf = new SessionFactory(application, storeFactory, logFactory, messageFactory, this); try { @@ -101,6 +101,7 @@ private void CreateSessions(SessionSettings settings, SessionFactory sessionFact } if (0 == socketDescriptorForAddress_.Count) + // If empty => no listers will be started => this instance could never accept a connection throw new ConfigError("No acceptor sessions found in SessionSettings."); } diff --git a/QuickFIXn/Transport/StreamFactory.cs b/QuickFIXn/Transport/StreamFactory.cs index f90a02ff4..39a79e7b5 100644 --- a/QuickFIXn/Transport/StreamFactory.cs +++ b/QuickFIXn/Transport/StreamFactory.cs @@ -349,13 +349,13 @@ private bool ValidateClientCertificate(object sender, X509Certificate certificat /// /// Perform certificate validation common for both server and client. /// - /// The remtoe certificate to validate. + /// The remote certificate to validate. /// The SSL policy errors supplied by .Net. /// Enhanced key usage, which the remote computers certificate should contain. /// true if the certificate should be treated as trusted; otherwise false private bool VerifyRemoteCertificate(X509Certificate certificate, SslPolicyErrors sslPolicyErrors, string enhancedKeyUsage) { - // Accept without looking at if the certificat is valid if validation is disabled + // Accept without looking at if the certificate is valid if validation is disabled if (socketSettings_.ValidateCertificates == false) return true; diff --git a/QuickFIXn/Values.cs b/QuickFIXn/Values.cs index 8521df8a4..ca14de9ae 100755 --- a/QuickFIXn/Values.cs +++ b/QuickFIXn/Values.cs @@ -10,5 +10,6 @@ public class Values public const string BeginString_FIX42 = "FIX.4.2"; public const string BeginString_FIX41 = "FIX.4.1"; public const string BeginString_FIX40 = "FIX.4.0"; + public const string WILDCARD_VALUE = "*"; } } diff --git a/README.md b/README.md index 4c9b5782c..8bfa6928f 100644 --- a/README.md +++ b/README.md @@ -13,22 +13,20 @@ This project requires the following: **To build and run tests** -* [Ruby (1.9.3 recommended)](http://rubyinstaller.org/) (used to generate message and field classes from the DataDictionary xml file) +* [Ruby (2.5+ recommended)](http://rubyinstaller.org/) (used to generate message and field classes from the DataDictionary xml file) * From the command-line: dotnet 2.0.0 or higher * From Visual Studio: version 2017 or higher Code Generation --------------- -To regenerate the message and field class source from the Data Dictionaries, you need Ruby and the Nokogiri gem: +To regenerate the message and field class source from the Data Dictionaries, +you need Ruby and the Nokogiri gem: ``` -gem install nokogiri -v 1.6.8.1 -generate.bat +gem install nokogiri +ruby generate\generate.bat ``` -(Nokogiri versions 1.7+ require Ruby 2.0, so we must use this older version.) - - Build ----- To build the project, run: @@ -92,9 +90,11 @@ To run one particular acceptance test, e.g. fix42\14e_IncorrectEnumValue.def: ``` cd AcceptanceTest -runat.ps1 release 5003 definitions\server\fix42\14e_IncorrectEnumValue.def cfg\at_42.cfg +pwsh runat.ps1 release 5003 definitions\server\fix42\14e_IncorrectEnumValue.def cfg\at_42.cfg netcoreapp2.1 ``` +The final param must be "net461" or "netcoreapp2.1". + (See acceptance_test.ps1 for the proper port numbers and config files to use in the above command.) The test results will then be available in AcceptanceTests\TestResults.xml and diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0ed7cf61e..492f41bc2 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -9,6 +9,10 @@ What's New ---------- ### NEXT VERSION: +* (patch) #630 - Make DefaultMessageFactory reflect only over QF/n assemblies (gbirchmeier) +* (patch) #643 - add 1156 (ApplExtId) to FIXT11.xml (gbirchmeier) + +### v1.10.0: * (patch) #505 - Fix ObjectDisposedException when SocketInitiator is stopped before connection attempt fails (musashibg) * (patch) #571 - StreamFactory fix: unexpected null from GetProxy (mkoca & mcmillab) * (patch) #547 - Implement Refresh() method in FileStore (roederja2) diff --git a/RELEASE_README.md b/RELEASE_README.md new file mode 100644 index 000000000..c78b52486 --- /dev/null +++ b/RELEASE_README.md @@ -0,0 +1,36 @@ +QuickFIX/n +========== +Thank you for using QuickFIX/n! +Here's an overview of what's in this zip package. + +Introduction +------------ +QuickFIX/n implements the FIX protocol on .NET. +For more information, visit [http://www.quickfixn.org] (http://www.quickfixn.org) + +File and Directory Information +------------------------------ +* RELEASE_NOTES.md - release notes +* README.md - this file +* LICENSE - license information +* bin - Contains the QuickFix.dll and message libraries for different target platforms +* config - Contains sample config files for an acceptor or initiator +* spec - Contains the various FIX data dictionaries + +Getting Started +--------------- +The directory bin/netstandard2.0 contains +* the QF/n core, QuickFix.dll +* a message-definition dll for each FIX version (e.g. QuickFix.FIX40.dll, QuickFix.FIX41.dll, ...) + +**Everyone needs QuickFix.dll**, so copy that into your project and reference it. + +Now you'll also need a message-definition dll. Most users will only need one. +(In rare cases it's possible to have an app that makes multiple connections +to different counterparties using different versions. +If that's you, chances are you already know what you're doing.) + +So copy your selected message-definition dll into your project and reference it. + +Then read the [documentation](http://quickfixn.org/tutorial/creating-an-application) and get coding! + diff --git a/UnitTests/FileLogTests.cs b/UnitTests/FileLogTests.cs index 7fd950341..6bce45711 100644 --- a/UnitTests/FileLogTests.cs +++ b/UnitTests/FileLogTests.cs @@ -36,6 +36,16 @@ public void testPrefix() Assert.AreEqual("FIX.4.3-sender-target-foo", QuickFix.FileLog.Prefix(someSessionIDWithQualifier)); } + [Test] + public void testPrefixWildcards() + { + QuickFix.SessionID someSessionID = new QuickFix.SessionID("FIX.4.4", QuickFix.Values.WILDCARD_VALUE, "target"); + QuickFix.SessionID someSessionIDWithQualifier = new QuickFix.SessionID(QuickFix.Values.WILDCARD_VALUE, "sender", QuickFix.Values.WILDCARD_VALUE, "foo"); + + Assert.AreEqual($"{QuickFix.FileLog.WILDCARD_FILE_PREFIX}FIX.4.4-{QuickFix.FileLog.WILDCARD_REPLACEMENT}-target", QuickFix.FileLog.Prefix(someSessionID)); + Assert.AreEqual($"{QuickFix.FileLog.WILDCARD_FILE_PREFIX}{QuickFix.FileLog.WILDCARD_REPLACEMENT}-sender-{QuickFix.FileLog.WILDCARD_REPLACEMENT}-foo", QuickFix.FileLog.Prefix(someSessionIDWithQualifier)); + } + [Test] public void testPrefixForSubsAndLocation() { @@ -46,6 +56,17 @@ public void testPrefixForSubsAndLocation() Assert.That(QuickFix.FileLog.Prefix(sessionIDWithSubsNoLocation), Is.EqualTo("FIX.4.2-SENDERCOMP_SENDERSUB-TARGETCOMP_TARGETSUB")); } + [Test] + public void testPrefixForSubsAndLocationWildcards() + { + QuickFix.SessionID sessionIDWithSubsAndLocation = new QuickFix.SessionID("FIX.4.2", "SENDERCOMP", "SENDERSUB", QuickFix.Values.WILDCARD_VALUE, + "TARGETCOMP", "TARGETSUB", QuickFix.Values.WILDCARD_VALUE); + Assert.That(QuickFix.FileLog.Prefix(sessionIDWithSubsAndLocation), Is.EqualTo($"{QuickFix.FileLog.WILDCARD_FILE_PREFIX}FIX.4.2-SENDERCOMP_SENDERSUB_{QuickFix.FileLog.WILDCARD_REPLACEMENT}-TARGETCOMP_TARGETSUB_{QuickFix.FileLog.WILDCARD_REPLACEMENT}")); + + QuickFix.SessionID sessionIDWithSubsNoLocation = new QuickFix.SessionID("FIX.4.2", "SENDERCOMP", QuickFix.Values.WILDCARD_VALUE, "TARGETCOMP", QuickFix.Values.WILDCARD_VALUE); + Assert.That(QuickFix.FileLog.Prefix(sessionIDWithSubsNoLocation), Is.EqualTo($"{QuickFix.FileLog.WILDCARD_FILE_PREFIX}FIX.4.2-SENDERCOMP_{QuickFix.FileLog.WILDCARD_REPLACEMENT}-TARGETCOMP_{QuickFix.FileLog.WILDCARD_REPLACEMENT}")); + } + [Test] public void testGeneratedFileName() { @@ -74,6 +95,34 @@ public void testGeneratedFileName() Assert.That(System.IO.File.Exists(Path.Combine(logDirectory, "FIX.4.2-SENDERCOMP-TARGETCOMP.messages.current.log"))); } + [Test] + public void testGeneratedFileNameWildcards() + { + var logDirectory = Path.Combine(TestContext.CurrentContext.TestDirectory, "log"); + + if (System.IO.Directory.Exists(logDirectory)) + System.IO.Directory.Delete(logDirectory, true); + + QuickFix.SessionID sessionID = new QuickFix.SessionID("FIX.4.2", QuickFix.Values.WILDCARD_VALUE, QuickFix.Values.WILDCARD_VALUE); + QuickFix.SessionSettings settings = new QuickFix.SessionSettings(); + + QuickFix.Dictionary config = new QuickFix.Dictionary(); + config.SetString(QuickFix.SessionSettings.CONNECTION_TYPE, "acceptor"); + config.SetString(QuickFix.SessionSettings.FILE_LOG_PATH, logDirectory); + + settings.Set(sessionID, config); + + QuickFix.FileLogFactory factory = new QuickFix.FileLogFactory(settings); + log = (QuickFix.FileLog)factory.Create(sessionID); + + log.OnEvent("some event"); + log.OnIncoming("some incoming"); + log.OnOutgoing("some outgoing"); + + Assert.That(System.IO.File.Exists(Path.Combine(logDirectory, $"{QuickFix.FileLog.WILDCARD_FILE_PREFIX}FIX.4.2-{QuickFix.FileLog.WILDCARD_REPLACEMENT}-{QuickFix.FileLog.WILDCARD_REPLACEMENT}.event.current.log"))); + Assert.That(System.IO.File.Exists(Path.Combine(logDirectory, $"{QuickFix.FileLog.WILDCARD_FILE_PREFIX}FIX.4.2-{QuickFix.FileLog.WILDCARD_REPLACEMENT}-{QuickFix.FileLog.WILDCARD_REPLACEMENT}.messages.current.log"))); + } + [Test] public void testThrowsIfNoConfig() { diff --git a/UnitTests/FileStoreWildcardTests.cs b/UnitTests/FileStoreWildcardTests.cs new file mode 100644 index 000000000..2a56fd251 --- /dev/null +++ b/UnitTests/FileStoreWildcardTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NUnit.Framework; +using System.Threading; + +namespace UnitTests +{ + [TestFixture] + class FileStoreWildcardTests + { + QuickFix.FileStore store; + QuickFix.FileStoreFactory factory; + + QuickFix.SessionSettings settings; + QuickFix.SessionID sessionID; + + private string storeDirectory; + + [SetUp] + public void setup() + { + storeDirectory = Path.Combine(TestContext.CurrentContext.TestDirectory, "store"); + + if (System.IO.Directory.Exists(storeDirectory)) + System.IO.Directory.Delete(storeDirectory, true); + + sessionID = new QuickFix.SessionID(QuickFix.Values.WILDCARD_VALUE, QuickFix.Values.WILDCARD_VALUE, QuickFix.Values.WILDCARD_VALUE); + + QuickFix.Dictionary config = new QuickFix.Dictionary(); + config.SetString(QuickFix.SessionSettings.CONNECTION_TYPE, "acceptor"); + config.SetString(QuickFix.SessionSettings.FILE_STORE_PATH, storeDirectory); + + settings = new QuickFix.SessionSettings(); + settings.Set(sessionID, config); + factory = new QuickFix.FileStoreFactory(settings); + + store = (QuickFix.FileStore)factory.Create(sessionID); + } + + [TearDown] + public void teardown() + { + store.Dispose(); + Directory.Delete(storeDirectory, true); + } + + [Test] + public void testPrefixForSessionWithSubsAndLocWildcards() + { + QuickFix.SessionID sessionIDWithSubsAndLocation = new QuickFix.SessionID("FIX.4.2", "SENDERCOMP", "SENDERSUB", QuickFix.Values.WILDCARD_VALUE, "TARGETCOMP", "TARGETSUB", QuickFix.Values.WILDCARD_VALUE); + Assert.That(QuickFix.FileStore.Prefix(sessionIDWithSubsAndLocation), Is.EqualTo($"{QuickFix.FileStore.WILDCARD_FILE_PREFIX}FIX.4.2-SENDERCOMP_SENDERSUB_{QuickFix.FileStore.WILDCARD_REPLACEMENT}-TARGETCOMP_TARGETSUB_{QuickFix.FileStore.WILDCARD_REPLACEMENT}")); + + QuickFix.SessionID sessionIDWithSubsNoLocation = new QuickFix.SessionID(QuickFix.Values.WILDCARD_VALUE, QuickFix.Values.WILDCARD_VALUE, QuickFix.Values.WILDCARD_VALUE, + QuickFix.Values.WILDCARD_VALUE, QuickFix.Values.WILDCARD_VALUE); + Assert.That(QuickFix.FileStore.Prefix(sessionIDWithSubsNoLocation), Is.EqualTo($"{QuickFix.FileStore.WILDCARD_FILE_PREFIX}{QuickFix.FileStore.WILDCARD_REPLACEMENT}-{QuickFix.FileStore.WILDCARD_REPLACEMENT}_{QuickFix.FileStore.WILDCARD_REPLACEMENT}-{QuickFix.FileStore.WILDCARD_REPLACEMENT}_{QuickFix.FileStore.WILDCARD_REPLACEMENT}")); + } + + [Test] + public void generateFileNamesTest() + { + Assert.That(System.IO.File.Exists(Path.Combine(storeDirectory, $"{QuickFix.FileStore.WILDCARD_FILE_PREFIX}{QuickFix.FileStore.WILDCARD_REPLACEMENT}-{QuickFix.FileStore.WILDCARD_REPLACEMENT}-{QuickFix.FileStore.WILDCARD_REPLACEMENT}.seqnums"))); + Assert.That(System.IO.File.Exists(Path.Combine(storeDirectory, $"{QuickFix.FileStore.WILDCARD_FILE_PREFIX}{QuickFix.FileStore.WILDCARD_REPLACEMENT}-{QuickFix.FileStore.WILDCARD_REPLACEMENT}-{QuickFix.FileStore.WILDCARD_REPLACEMENT}.body"))); + Assert.That(System.IO.File.Exists(Path.Combine(storeDirectory, $"{QuickFix.FileStore.WILDCARD_FILE_PREFIX}{QuickFix.FileStore.WILDCARD_REPLACEMENT}-{QuickFix.FileStore.WILDCARD_REPLACEMENT}-{QuickFix.FileStore.WILDCARD_REPLACEMENT}.header"))); + Assert.That(System.IO.File.Exists(Path.Combine(storeDirectory, $"{QuickFix.FileStore.WILDCARD_FILE_PREFIX}{QuickFix.FileStore.WILDCARD_REPLACEMENT}-{QuickFix.FileStore.WILDCARD_REPLACEMENT}-{QuickFix.FileStore.WILDCARD_REPLACEMENT}.session"))); + } + + } +} diff --git a/UnitTests/SessionSettingsTest.cs b/UnitTests/SessionSettingsTest.cs index 585eea473..66d1b7125 100755 --- a/UnitTests/SessionSettingsTest.cs +++ b/UnitTests/SessionSettingsTest.cs @@ -143,6 +143,46 @@ public void LoadSettingsWithDefaultSectionLast() Assert.That(settings.Get(session6).GetLong("VALUE"), Is.EqualTo(6)); } + [Test] + public void LoadSettingsWithWildcards() + { + string configuration = new System.Text.StringBuilder() + .AppendLine("[DEFAULT]") + .AppendLine("ConnectionType=acceptor") + .AppendLine("BeginString=*") + .AppendLine("SenderCompID=*") + .AppendLine("TargetCompID=*") + .AppendLine("SessionQualifier=*") + .AppendLine("SomeValue=whatever") + .AppendLine("Empty=") + .ToString(); + SessionSettings settings = new SessionSettings(new System.IO.StringReader(configuration)); + Assert.That(settings.Get().GetString("BeginString"), Is.EqualTo("*")); + Assert.That(settings.Get().GetString("SenderCompID"), Is.EqualTo("*")); + Assert.That(settings.Get().GetString("TargetCompID"), Is.EqualTo("*")); + Assert.That(settings.Get().GetString("SessionQualifier"), Is.EqualTo("*")); + + configuration = new System.Text.StringBuilder() + .AppendLine("[SESSION]") + .AppendLine("ConnectionType=initiator") + .AppendLine("BeginString=*") + .AppendLine("SenderCompID=*") + .AppendLine("TargetCompID=*") + .ToString(); + ConfigError ex = Assert.Throws(delegate { _ = new SessionSettings(new System.IO.StringReader(configuration)); }); + Assert.That(ex.Message, Is.EqualTo("Configuration failed: BeginString could be a wildcard (*) only in 'acceptor' configuration")); + + configuration = new System.Text.StringBuilder() + .AppendLine("[SESSION]") + .AppendLine("ConnectionType=initiator") + .AppendLine("BeginString=FIX.4.0") + .AppendLine("SenderCompID=*") + .AppendLine("TargetCompID=*") + .ToString(); + ex = Assert.Throws(delegate { _ = new SessionSettings(new System.IO.StringReader(configuration)); }); + Assert.That(ex.Message, Is.EqualTo("Configuration failed: SenderCompID could be a wildcard (*) only in 'acceptor' configuration")); + } + [Test] public void DuplicateSession() { diff --git a/UnitTests/SessionTemplateTest.cs b/UnitTests/SessionTemplateTest.cs new file mode 100644 index 000000000..b4eed5346 --- /dev/null +++ b/UnitTests/SessionTemplateTest.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Net; +using System.Net.Sockets; +using System.Text.RegularExpressions; + +using NUnit.Framework; +using QuickFix; +using QuickFix.Transport; + +namespace UnitTests +{ + [TestFixture] + class SessionTemplateTest + { + public class TestApplication : IApplication + { + Action _logonNotify; + Action _logoffNotify; + public TestApplication(Action logonNotify, Action logoffNotify) + { + _logonNotify = logonNotify; + _logoffNotify = logoffNotify; + } + public void FromAdmin(Message message, SessionID sessionID) + { } + + public void FromApp(Message message, SessionID sessionID) + { } + + public void OnCreate(SessionID sessionID) { } + public void OnLogout(SessionID sessionID) + { + _logoffNotify(sessionID.TargetCompID); + } + public void OnLogon(SessionID sessionID) + { + _logonNotify(sessionID.TargetCompID); + } + + public void ToAdmin(Message message, SessionID sessionID) { } + public void ToApp(Message message, SessionID sessionID) { } + } + class SocketState + { + public SocketState(Socket s) + { + _socket = s; + } + public Socket _socket; + public byte[] _rxBuffer = new byte[1024]; + public string _messageFragment = string.Empty; + public string _exMessage; + } + + const string Host = "127.0.0.1"; + const int ConnectPort = 55100; + const int AcceptPort = 55101; + const int AcceptPort2 = 55102; + const string ServerCompID = "dummy"; + const string StaticInitiatorCompID = "ini01"; + const string StaticAcceptorCompID = "acc01"; + const string StaticAcceptorCompID2 = "acc02"; + + const string FIXMessageEnd = @"\x0110=\d{3}\x01"; + const string FIXMessageDelimit = @"(8=FIX|\A).*?(" + FIXMessageEnd + @"|\z)"; + + string _logPath; + SocketInitiator _initiator; + ThreadedSocketAcceptor _acceptor; + Dictionary _sessions; + HashSet _loggedOnCompIDs; + Socket _listenSocket; + + Dictionary CreateAcceptorConfig() + { + Dictionary settings = new Dictionary(); + settings.SetString(SessionSettings.CONNECTION_TYPE, "acceptor"); + settings.SetString(SessionSettings.USE_DATA_DICTIONARY, "N"); + settings.SetString(SessionSettings.START_TIME, "12:00:00"); + settings.SetString(SessionSettings.END_TIME, "12:00:00"); + settings.SetString(SessionSettings.HEARTBTINT, "300"); + return settings; + } + + SessionID CreateSessionID(string targetCompID) + { + return new SessionID(QuickFix.Values.BeginString_FIX42, ServerCompID, targetCompID); + } + + void LogonCallback(string compID) + { + lock (_loggedOnCompIDs) + { + _loggedOnCompIDs.Add(compID); + Monitor.Pulse(_loggedOnCompIDs); + } + } + void LogoffCallback(string compID) + { + lock (_loggedOnCompIDs) + { + _loggedOnCompIDs.Remove(compID); + Monitor.Pulse(_loggedOnCompIDs); + } + } + + void StartAcceptor(SessionID sessionID) + { + TestApplication application = new TestApplication(LogonCallback, LogoffCallback); + IMessageStoreFactory storeFactory = new MemoryStoreFactory(); + SessionSettings settings = new SessionSettings(); + Dictionary defaults = new Dictionary(); + defaults.SetString(QuickFix.SessionSettings.FILE_LOG_PATH, _logPath); + + // Put IP endpoint settings into default section to verify that that defaults get merged into + // session-specific settings not only for static sessions, but also for dynamic ones + defaults.SetString(SessionSettings.SOCKET_ACCEPT_HOST, Host); + defaults.SetString(SessionSettings.SOCKET_ACCEPT_PORT, AcceptPort.ToString()); + + settings.Set(defaults); + ILogFactory logFactory = new FileLogFactory(settings); + + settings.Set(sessionID, CreateAcceptorConfig()); + + _acceptor = new ThreadedSocketAcceptor(application, storeFactory, settings, logFactory); + _acceptor.Start(); + } + + void StartListener() + { + var address = IPAddress.Parse(Host); + var listenEndpoint = new IPEndPoint(address, ConnectPort); + _listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _listenSocket.Bind(listenEndpoint); + _listenSocket.Listen(10); + _listenSocket.BeginAccept(new AsyncCallback(ProcessInboundConnect), null); + } + + void ProcessInboundConnect(IAsyncResult ar) + { + Socket handler = null; + try + { + handler = _listenSocket.EndAccept(ar); + } + catch + { + _listenSocket = null; // Assume listener has been closed + } + + if (handler != null) + { + ReceiveAsync(new SocketState(handler)); + _listenSocket.BeginAccept(new AsyncCallback(ProcessInboundConnect), null); + } + } + + void ProcessRXData(IAsyncResult ar) + { + SocketState socketState = (SocketState)ar.AsyncState; + int bytesReceived = 0; + try + { + bytesReceived = socketState._socket.EndReceive(ar); + } + catch (Exception ex) + { + socketState._exMessage = ex.InnerException == null ? ex.Message : ex.InnerException.Message; + } + + if (bytesReceived == 0) + { + socketState._socket.Close(); + lock (socketState._socket) + Monitor.Pulse(socketState._socket); + return; + } + string msgText = CharEncoding.DefaultEncoding.GetString(socketState._rxBuffer, 0, bytesReceived); + foreach (Match m in Regex.Matches(msgText, FIXMessageDelimit)) + { + socketState._messageFragment += m.Value; + if (Regex.IsMatch(socketState._messageFragment, FIXMessageEnd)) + { + Message message = new Message(socketState._messageFragment); + socketState._messageFragment = string.Empty; + string targetCompID = message.Header.GetString(QuickFix.Fields.Tags.TargetCompID); + if (message.Header.GetString(QuickFix.Fields.Tags.MsgType) == QuickFix.Fields.MsgType.LOGON) + lock (_sessions) + { + _sessions[targetCompID] = socketState; + Monitor.Pulse(_sessions); + } + } + } + ReceiveAsync(socketState); + } + + void ReceiveAsync(SocketState socketState) + { + socketState._socket.BeginReceive(socketState._rxBuffer, 0, socketState._rxBuffer.Length, SocketFlags.None, new AsyncCallback(ProcessRXData), socketState); ; + } + + Socket ConnectToEngine() + { + return ConnectToEngine(AcceptPort); + } + + Socket ConnectToEngine(int port) + { + var address = IPAddress.Parse(Host); + var endpoint = new IPEndPoint(address, port); + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + try + { + socket.Connect(endpoint); + ReceiveAsync(new SocketState(socket)); + return socket; + } + catch (Exception ex) + { + Assert.Fail(string.Format("Failed to connect: {0}", ex.Message)); + return null; + } + } + + Socket GetSocket(string compID) + { + lock (_sessions) + return _sessions[compID]._socket; + } + + bool WaitForLogonStatus(string targetCompID) + { + lock (_loggedOnCompIDs) + { + if (!_loggedOnCompIDs.Contains(targetCompID)) + Monitor.Wait(_loggedOnCompIDs, 10000); + return _loggedOnCompIDs.Contains(targetCompID); + } + } + + bool WaitForLogonMessage(string targetCompID) + { + lock (_sessions) + { + if (!_sessions.ContainsKey(targetCompID)) + Monitor.Wait(_sessions, 10000); + return _sessions.ContainsKey(targetCompID); + } + } + + bool WaitForDisconnect(Socket s) + { + lock (s) + { + if (s.Connected) + Monitor.Wait(s, 10000); + return !s.Connected; + } + } + + bool WaitForDisconnect(string compID) + { + return WaitForDisconnect(GetSocket(compID)); + } + + bool HasReceivedMessage(string compID) + { + lock (_sessions) + return _sessions.ContainsKey(compID); + } + + bool IsLoggedOn(string compID) + { + lock (_loggedOnCompIDs) + return _loggedOnCompIDs.Contains(compID); + } + + void SendInitiatorLogon(string senderCompID) + { + SendLogon(GetSocket(senderCompID), senderCompID); + } + + void SendLogon(Socket s, string senderCompID) + { + SendLogon(s, senderCompID, ServerCompID); + } + + void SendLogon(Socket s, string senderCompID, string targetCompID) + { + var msg = new QuickFix.FIX42.Logon(); + msg.Header.SetField(new QuickFix.Fields.TargetCompID(targetCompID)); + msg.Header.SetField(new QuickFix.Fields.SenderCompID(senderCompID)); + msg.Header.SetField(new QuickFix.Fields.MsgSeqNum(1)); + msg.Header.SetField(new QuickFix.Fields.SendingTime(System.DateTime.UtcNow)); + msg.SetField(new QuickFix.Fields.HeartBtInt(300)); + // Simple logon message + s.Send(CharEncoding.DefaultEncoding.GetBytes(msg.ToString())); + } + + void ClearLogs() + { + if (System.IO.Directory.Exists(_logPath)) + try + { + System.IO.Directory.Delete(_logPath, true); + } + catch { } + } + + [SetUp] + public void Setup() + { + _logPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "log"); + _sessions = new Dictionary(); + _loggedOnCompIDs = new HashSet(); + ClearLogs(); + } + + [TearDown] + public void TearDown() + { + if (_listenSocket != null) + _listenSocket.Close(); + if (_initiator != null) + _initiator.Stop(true); + if (_acceptor != null) + _acceptor.Stop(true); + + _initiator = null; + _acceptor = null; + + Thread.Sleep(500); + ClearLogs(); + } + + [Test] + public void DynamicAcceptor() + { + // Starting Acceptor with template for sessions + SessionID templateSessionID = new SessionID(QuickFix.Values.BeginString_FIX42, "DYN_SERVER", "*"); + StartAcceptor(templateSessionID); + + // Ensure we can log using the defined template + var socket01 = ConnectToEngine(); + SendLogon(socket01, "ANY_CLIENT", "DYN_SERVER"); + Assert.IsTrue(WaitForLogonStatus("ANY_CLIENT"), "Failed to logon templated acceptor session"); + + // Ensure we can log using the defined template with the other client CompID + var socket011 = ConnectToEngine(); + SendLogon(socket011, "ANY_OTHER_CLIENT", "DYN_SERVER"); + Assert.IsTrue(WaitForLogonStatus("ANY_OTHER_CLIENT"), "Failed to logon templated acceptor session"); + + // Ensure that attempt to log on as not matching the template does not work + var socket02 = ConnectToEngine(); + SendLogon(socket02, "NEW_CLIENT", "NOT_CONFIGURED"); + Assert.IsTrue(WaitForDisconnect(socket02), "Server failed to disconnect unconfigured CompID"); + Assert.False(HasReceivedMessage("NEW_CLIENT"), "Unexpected message received for unconfigured CompID"); + + // Add the new session template to acceptor and ensure that we can now log on + SessionID secondTemplateSessionID = new SessionID(QuickFix.Values.BeginString_FIX42, "DYN_2ND_SERVER", "*"); + var sessionConfig = CreateAcceptorConfig(); + Assert.IsTrue(_acceptor.AddSession(secondTemplateSessionID, sessionConfig), "Failed to add dynamic session template to acceptor"); + var socket03 = ConnectToEngine(); + SendLogon(socket03, "NEW_CLIENT", "DYN_2ND_SERVER"); + Assert.IsTrue(WaitForLogonStatus("NEW_CLIENT"), "Failed to logon dynamically added acceptor template session"); + SessionID dynamicDessionID = new SessionID(Values.BeginString_FIX42, "DYN_2ND_SERVER", "NEW_CLIENT"); + + // Ensure that we can't add the same session again + Assert.IsFalse(_acceptor.AddSession(secondTemplateSessionID, sessionConfig), "Added dynamic session template twice"); + + // Template session can be deleted even after logon, as the new (non-template) sessions are created for those + Assert.IsTrue(_acceptor.RemoveSession(secondTemplateSessionID, false), "Failed to remove active session template"); + // Now that dynamic acceptor is logged on, ensure that unforced attempt to remove session fails + Assert.IsFalse(_acceptor.RemoveSession(dynamicDessionID, false), "Unexpected success removing active session"); + Assert.IsTrue(socket03.Connected, "Unexpected loss of connection"); + + // Ensure that forced attempt to remove session dynamic session succeeds, even though it is in logged on state + Assert.IsTrue(_acceptor.RemoveSession(dynamicDessionID, true), "Failed to remove active session"); + Assert.IsTrue(WaitForDisconnect(socket03), "Socket still connected after session removed"); + Assert.IsFalse(IsLoggedOn("NEW_CLIENT"), "Session still logged on after being removed"); + } + } +} diff --git a/UnitTests/ThreadedSocketAcceptorTests.cs b/UnitTests/ThreadedSocketAcceptorTests.cs index 229a65108..7fce32804 100644 --- a/UnitTests/ThreadedSocketAcceptorTests.cs +++ b/UnitTests/ThreadedSocketAcceptorTests.cs @@ -28,14 +28,14 @@ public class ThreadedSocketAcceptorTests BeginString = FIX.4.4 "; - private static SessionSettings CreateSettings() + private static SessionSettings CreateSettings(string config) { - return new SessionSettings(new StringReader(Config)); + return new SessionSettings(new StringReader(config)); } - private static ThreadedSocketAcceptor CreateAcceptor() + private static ThreadedSocketAcceptor CreateAcceptor(string config) { - var settings = CreateSettings(); + var settings = CreateSettings(config); return new ThreadedSocketAcceptor( new NullApplication(), new FileStoreFactory(settings), @@ -46,16 +46,41 @@ private static ThreadedSocketAcceptor CreateAcceptor() [Test] public void TestRecreation() { - StartStopAcceptor(); - StartStopAcceptor(); - StartStopAcceptor(); + StartStopAcceptor(Config); + StartStopAcceptor(Config); + StartStopAcceptor(Config); } - private static void StartStopAcceptor() + private static void StartStopAcceptor(string config) { - var acceptor = CreateAcceptor(); + var acceptor = CreateAcceptor(config); acceptor.Start(); acceptor.Dispose(); } + + private static string ConfigWildcards = $@" +[DEFAULT] +StartTime = 00:00:00 +EndTime = 23:59:59 +ConnectionType = acceptor +SocketAcceptHost = 127.0.0.1 +SocketAcceptPort = 10000 +FileStorePath = {TestContext.CurrentContext.TestDirectory}\store +FileLogPath = {TestContext.CurrentContext.TestDirectory}\log +UseDataDictionary = N + +[SESSION] +SenderCompID = * +TargetCompID = * +BeginString = FIX.4.4 +"; + [Test] + public void TestRecreationWildcards() + { + StartStopAcceptor(ConfigWildcards); + StartStopAcceptor(ConfigWildcards); + StartStopAcceptor(ConfigWildcards); + } + } } diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index 0fd9c5d9e..fe02bb5fb 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -17,7 +17,7 @@ - + diff --git a/appveyor.yml b/appveyor.yml index 363cb2987..a796ff77d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,10 +1,17 @@ version: 1.0.{build} install: +- dir C:\ +- set PATH=C:\Ruby26-x64\bin;%PATH% +- ruby -v - ps: | - gem install --source http://rubygems.org nokogiri -v 1.6.8.1 + ruby -v + gem install nokogiri ruby generator\generate.rb +before_test: +- ruby -v + build_script: - ps: dotnet build -c Release diff --git a/build.bat b/build.bat index 186eacd47..ffdaf6b5c 100755 --- a/build.bat +++ b/build.bat @@ -4,7 +4,8 @@ set CONFIGURATION=%1 if "%1" == "" set CONFIGURATION=Release -pwsh -v >nul +REM pwsh is in PowerShell 6+ +pwsh -v >nul 2>&1 if "%ERRORLEVEL%" == "0" ( pwsh .\build.ps1 %CONFIGURATION% ) else ( diff --git a/spec/fix/FIXT11.xml b/spec/fix/FIXT11.xml index 15054a8a2..d14e46243 100644 --- a/spec/fix/FIXT11.xml +++ b/spec/fix/FIXT11.xml @@ -30,6 +30,7 @@ + @@ -309,5 +310,6 @@ +