diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..3ba13e0ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/new-issue-template.md b/.github/ISSUE_TEMPLATE/new-issue-template.md new file mode 100644 index 000000000..da570829e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-issue-template.md @@ -0,0 +1,19 @@ +--- +name: New issue template +about: You should choose this one +title: '' +labels: '' +assignees: '' + +--- + +**DO NOT ASK FOR USAGE HELP HERE.** +Please see [this page](https://quickfixengine.org/n/support/) +to learn where you should ask for help. + +Github issues are **only** for engine bugs and feature requests, +and only a couple people are notified. If you ask for regular usage help here, +we will close your issue without helping. + +Ok? If you have an engine bug or feature request, +then please delete this message and proceed with your report. Thanks! diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 000000000..6418cfc11 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,31 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: .NET + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Unit Test + run: dotnet test --no-build --verbosity normal UnitTests + - name: Acceptance + run: dotnet test --no-build --verbosity normal AcceptanceTest + diff --git a/Examples/Executor/Examples.Executor.csproj b/Examples/Executor/Examples.Executor.csproj index 76a96de46..920387f58 100644 --- a/Examples/Executor/Examples.Executor.csproj +++ b/Examples/Executor/Examples.Executor.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net6.0 Executor Executor Copyright © Connamara Systems, LLC 2011 diff --git a/Examples/Executor/Executor.cs b/Examples/Executor/Executor.cs old mode 100755 new mode 100644 diff --git a/Examples/Executor/executor.cfg b/Examples/Executor/executor.cfg old mode 100755 new mode 100644 diff --git a/Examples/FixToJson/Examples.FixToJson.csproj b/Examples/FixToJson/Examples.FixToJson.csproj index 1bf674782..2a3da6645 100644 --- a/Examples/FixToJson/Examples.FixToJson.csproj +++ b/Examples/FixToJson/Examples.FixToJson.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net6.0 FixToJson FixToJson Copyright © Connamara Systems, LLC 2022 diff --git a/Examples/README.txt b/Examples/README.md similarity index 56% rename from Examples/README.txt rename to Examples/README.md index 16446c4e1..ac9ea18f6 100644 --- a/Examples/README.txt +++ b/Examples/README.md @@ -1,13 +1,14 @@ Example Applications ==================== -These example applications demonstrate how to use the QuickFIX/n library to -build your own FIX applications. There are 3 QuickFIX/N example applications: +How-to Examples +--------------- +Three of these example applications demonstrate how to use the QuickFIX/n library to +build your own FIX applications. They are: -1. The SimpleAcceptor demonstrates a barebones acceptor application. -2. The Executor takes orders over a FIX session and executes them. -3. The TradeClient is a command line client that sends orders. - (NOT FOR USE WITH COMMERCIAL FIX INTERFACES! It won't work!) +* **SimpleAcceptor:** demonstrates a barebones acceptor application. +* **Executor:** takes orders over a FIX session and "executes" them. +* **TradeClient:** a command-line client that sends orders. _(NOT FOR USE WITH COMMERCIAL FIX INTERFACES! It won't work!)_ TradeClient and Executor can be configured to send and execute orders with each other. @@ -16,39 +17,36 @@ TradeClient can also be configured to connect to SimpleAcceptor, though TradeClient will not hear any application-level responses from SimpleAcceptor. -Each app is meant to be run from its target dir, e.g. Examples\Executor\bin\Debug\net461 -or Examples\Executor\bin\Debug\netcoreapp2.0. The instructions below assume net461, but -apply equally to netcoreapp2.0. +To run each, go into its directory and use `dotnet run `, e.g. +* `Examples/SimpleAcceptor> dotnet run simpleacc.cfg` +* `Examples/Executor> dotnet run executor.cfg` +* `Examples/TradeClient> dotnet run tradeclient.cfg` -SimpleAcceptor -============== +### SimpleAcceptor The SimpleAcceptor example shows you how to create a simple acceptor server. -It will let initiators connect to it, and logs all admin and application +It will let initiators connect to it, and logs all admin and application level messages to the screen. It does not process these messages. -Program.cs demonstrates how to setup and start a new acceptor object +Program.cs demonstrates how to setup and start a new acceptor object from the Session settings file. SimpleAcceptorApp.cs implements the Application interface and is where you would handle all your application level logic. Configure the SimpleAcceptor by modifying SimpleAcceptor/simpleacc.cfg This configuration file defines the FIX Sessions the acceptor will -handle. For more information - http://quickfixn.org/tutorial/configuration +handle. For more information: +http://quickfixn.org/tutorial/configuration -Build QuickFIX/n first by running build.bat -Then start the SimpleAcceptor by opening a command prompt at -quickfixn/Examples/SimpleAcceptor/bin/Release/net461 and running: -Examples.SimpleAcceptor.exe simpleacc.cfg -Executor -======== +### Executor -The Executor example takes incoming orders and executes them. Executor demonstrates -how to create an acceptor to crack messages and execute orders. -The Executor class inherits MessageCracker and implements Application. -For more information on how message cracking works - +The Executor example takes incoming orders and executes them. Executor demonstrates +how to create an acceptor to crack messages and execute orders. +The Executor class inherits MessageCracker and implements Application. + +For more information on how message cracking works: http://quickfixn.org/tutorial/receiving-messages The OnMessage callbacks show you how to get field values from the NewOrderSingle @@ -59,12 +57,8 @@ the counterparty will reject your message. The Executor is configured with the executor.cfg file. -Build QuickFIX/n by running build.bat, then start the Executor by opening a command -prompt at quickfixn/Examples/Executor/bin/Release/net461 and running: -Executor.exe executor.cfg -TradeClient -=========== +### TradeClient The TradeClient is a command line example that shows how to create different FIX message types and versions. You can create new order singles, cancel order requests, @@ -76,6 +70,14 @@ demonstration of how to complete certain tasks in your own application. TradeClient is configured with the tradeclient.cfg file. -Build QuickFIX/n by running build.bat, then start TradeClient by opening a command -prompt at quickfixn/Examples/TradeClient/bin/Release/net461 and running -TradeClient.exe tradeclient.cfg + +FIX/Json Examples +----------------- + +These demonstrate the FIX-to-Json and Json-to-FIX conversions. They are pretty simple. + + +Standalone/SerilogLog +--------------------- + +See the README in that directory. diff --git a/Examples/SimpleAcceptor/Examples.SimpleAcceptor.csproj b/Examples/SimpleAcceptor/Examples.SimpleAcceptor.csproj index ef0bb7352..b1df00be5 100644 --- a/Examples/SimpleAcceptor/Examples.SimpleAcceptor.csproj +++ b/Examples/SimpleAcceptor/Examples.SimpleAcceptor.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net6.0 SimpleAcceptor SimpleAcceptor Copyright © Connamara Systems, LLC 2011 diff --git a/Examples/Standalone/SerilogLog/README.txt b/Examples/Standalone/SerilogLog/README.md similarity index 77% rename from Examples/Standalone/SerilogLog/README.txt rename to Examples/Standalone/SerilogLog/README.md index 2864c8476..2a30dd399 100644 --- a/Examples/Standalone/SerilogLog/README.txt +++ b/Examples/Standalone/SerilogLog/README.md @@ -7,16 +7,18 @@ and limited total log size. The solution contains 2 projects: -1. The SerilogLog is a sample implementation of ILog and ILogFactory. -NOTE: this is a sample, NOT a finished ready-to-use product. -Follow comments in the source code to adjust the example to -your actual requirements. +1. The SerilogLog is a sample implementation of ILog and ILogFactory. + _NOTE: this is a sample, **NOT** a finished ready-to-use product. + Follow comments in the source code to adjust the example to + your actual requirements._ 2. The UnitTests are proof that limits for log size are working. The projects are dependent on QuickFix.dll (ILog and ILogFactory interfaces). Assuming the standard directory structure, the reference path is: -...\QuickFIXn\bin\Release\netstandard2.0\QuickFix.dll + + \QuickFIXn\bin\Debug\net6.0\QuickFix.dll + You need to build QuickFIXn release configuration to have the DLL available. Alternatively, you can change the reference in the both projects. @@ -24,8 +26,10 @@ If you copy/paste the source code in your project, you need to have references to QuickFix.dll and NuGet Serilog.Sinks.File installed. Usage example: + ILogFactory logFactory = new SerilogLogFactory(settings); ThreadedSocketAcceptor _acceptor = new ThreadedSocketAcceptor(executorApp, storeFactory, settings, logFactory); -- all sessions created by this `_acceptor` will be using SerilogLog + +All sessions created by this `_acceptor` will be using SerilogLog for messages and events logging. diff --git a/Examples/Standalone/SerilogLog/SerilogLog.csproj b/Examples/Standalone/SerilogLog/SerilogLog.csproj index 9fcef905c..8e03b5169 100644 --- a/Examples/Standalone/SerilogLog/SerilogLog.csproj +++ b/Examples/Standalone/SerilogLog/SerilogLog.csproj @@ -16,7 +16,7 @@ - ..\..\..\QuickFIXn\bin\Release\netstandard2.0\QuickFix.dll + ..\..\..\QuickFIXn\bin\Debug\net6.0\QuickFix.dll diff --git a/Examples/Standalone/SerilogLog/UnitTests/UnitTests.csproj b/Examples/Standalone/SerilogLog/UnitTests/UnitTests.csproj index eb48059aa..9b23f968b 100644 --- a/Examples/Standalone/SerilogLog/UnitTests/UnitTests.csproj +++ b/Examples/Standalone/SerilogLog/UnitTests/UnitTests.csproj @@ -18,7 +18,7 @@ - ..\..\..\..\QuickFIXn\bin\Release\netstandard2.0\QuickFix.dll + ..\..\..\..\QuickFIXn\bin\Debug\net6.0\QuickFix.dll diff --git a/Examples/TradeClient/Examples.TradeClient.csproj b/Examples/TradeClient/Examples.TradeClient.csproj index c58a18fa3..353b2aa14 100644 --- a/Examples/TradeClient/Examples.TradeClient.csproj +++ b/Examples/TradeClient/Examples.TradeClient.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net6.0 TradeClient TradeClient Copyright © Connamara Systems, LLC 2011 diff --git a/Examples/TradeClient/tradeclient.cfg b/Examples/TradeClient/tradeclient.cfg index f04834103..fb57c7eb2 100644 --- a/Examples/TradeClient/tradeclient.cfg +++ b/Examples/TradeClient/tradeclient.cfg @@ -18,5 +18,6 @@ ResetOnDisconnect=Y BeginString=FIX.4.4 SenderCompID=CLIENT1 TargetCompID=EXECUTOR +# use this instead to connect to SimpleAcceptor +#TargetCompID=SIMPLE HeartBtInt=30 - diff --git a/QuickFIXn.sln b/QuickFIXn.sln index 593a72fd7..df35f468d 100644 --- a/QuickFIXn.sln +++ b/QuickFIXn.sln @@ -19,7 +19,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.TradeClient", "Exa EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FD384F84-3F83-4BF9-9238-82C70B74C2EA}" ProjectSection(SolutionItems) = preProject - acceptance_test.ps1 = acceptance_test.ps1 appveyor.yml = appveyor.yml CONTRIBUTING.md = CONTRIBUTING.md LICENSE = LICENSE diff --git a/QuickFIXn/AbstractInitiator.cs b/QuickFIXn/AbstractInitiator.cs index f1f4d8c55..86ea84ae7 100644 --- a/QuickFIXn/AbstractInitiator.cs +++ b/QuickFIXn/AbstractInitiator.cs @@ -7,41 +7,33 @@ namespace QuickFix public abstract class AbstractInitiator : IInitiator { // from constructor - private IApplication _app = null; - private IMessageStoreFactory _storeFactory = null; - private SessionSettings _settings = null; - private ILogFactory _logFactory = null; - private IMessageFactory _msgFactory = null; - - private object sync_ = new object(); - private Dictionary sessions_ = new Dictionary(); - private HashSet sessionIDs_ = new HashSet(); - private HashSet pending_ = new HashSet(); - private HashSet connected_ = new HashSet(); - private HashSet disconnected_ = new HashSet(); - private bool isStopped_ = true; + private readonly IApplication _app; + private readonly IMessageStoreFactory _storeFactory; + private readonly SessionSettings _settings; + private readonly ILogFactory _logFactory; + private readonly IMessageFactory _msgFactory; + + private object sync_ = new(); + private Dictionary sessions_ = new(); + private HashSet sessionIDs_ = new(); + private HashSet pending_ = new(); + private HashSet connected_ = new(); + private HashSet disconnected_ = new(); private Thread thread_; - private SessionFactory sessionFactory_ = null; + private SessionFactory sessionFactory_; #region Properties - public bool IsStopped - { - get { return isStopped_; } - } + public bool IsStopped { get; private set; } = true; #endregion - public AbstractInitiator(IApplication app, IMessageStoreFactory storeFactory, SessionSettings settings) - : this(app, storeFactory, settings, null, null) - { } - - public AbstractInitiator(IApplication app, IMessageStoreFactory storeFactory, SessionSettings settings, ILogFactory logFactory) - : this(app, storeFactory, settings, logFactory, null) - { } - public AbstractInitiator( - IApplication app, IMessageStoreFactory storeFactory, SessionSettings settings, ILogFactory logFactory, IMessageFactory messageFactory) + IApplication app, + IMessageStoreFactory storeFactory, + SessionSettings settings, + ILogFactory? logFactory, + IMessageFactory? messageFactory) { _app = app; _storeFactory = storeFactory; @@ -71,7 +63,7 @@ public void Start() throw new ConfigError("No sessions defined for initiator"); // start it up - isStopped_ = false; + IsStopped = false; OnConfigure(_settings); thread_ = new Thread(new ThreadStart(OnStart)); thread_.Start(); @@ -205,7 +197,7 @@ public void Stop(bool force) SetDisconnected(Session.LookupSession(sessionID).SessionID); } - isStopped_ = true; + IsStopped = true; OnStop(); // Give OnStop() time to finish its business diff --git a/QuickFIXn/Fields/Converters/DateTimeConverter.cs b/QuickFIXn/Fields/Converters/DateTimeConverter.cs index ca9c83128..cf504f3f1 100644 --- a/QuickFIXn/Fields/Converters/DateTimeConverter.cs +++ b/QuickFIXn/Fields/Converters/DateTimeConverter.cs @@ -71,8 +71,8 @@ private static System.DateTime ConvertFromNanoString(string str, string[] format { // GMT offset int n = dec.Contains('+') ? dec.IndexOf('+') : dec.IndexOf('-'); - kind = System.DateTimeKind.Utc; - offset = int.Parse(dec.Substring(n + 1)); + kind = System.DateTimeKind.Unspecified; + offset = int.Parse(dec.Substring(n)); dec = dec.Substring(0, n); } else @@ -87,7 +87,7 @@ private static System.DateTime ConvertFromNanoString(string str, string[] format // apply GMT offset if (offset != 0) { - d = new System.DateTimeOffset(d).ToOffset(System.TimeSpan.FromHours(offset)).DateTime; + d = new System.DateTimeOffset(d, System.TimeSpan.FromHours(offset)).UtcDateTime; } long ticks = frac / NanosecondsPerTick; diff --git a/QuickFIXn/Parser.cs b/QuickFIXn/Parser.cs index 60b20a327..d23b3548c 100755 --- a/QuickFIXn/Parser.cs +++ b/QuickFIXn/Parser.cs @@ -1,4 +1,7 @@ using System; +using System.Diagnostics; +using System.Globalization; +using System.Text; namespace QuickFix { @@ -7,144 +10,184 @@ namespace QuickFix /// public class Parser { - private byte[] buffer_ = new byte[512]; - int usedBufferLength = 0; + private readonly Encoding _encoding; + private readonly byte[] _beginStringBytes; + private readonly byte[] _bodyLengthBytes; + private readonly byte[] _checkSumBytes; + + private byte[] _buffer = new byte[512]; + private int _usedBufferLength = 0; + private int _bufferStartIndex = 0; + + public Parser() : this(CharEncoding.DefaultEncoding) + { } + + public Parser(Encoding encoding) + { + _encoding = encoding; + _beginStringBytes = encoding.GetBytes("8="); + _bodyLengthBytes = encoding.GetBytes('\u0001' + "9="); + _checkSumBytes = encoding.GetBytes('\u0001' + "10="); + } public void AddToStream(byte[] data, int bytesAdded) + => AddToStream(data.AsSpan(0, bytesAdded)); + + public void AddToStream(Span data) { - if (buffer_.Length < usedBufferLength + bytesAdded) - Array.Resize(ref buffer_, (usedBufferLength + bytesAdded)); - Buffer.BlockCopy(data, 0, buffer_, usedBufferLength , bytesAdded); - usedBufferLength += bytesAdded; + // We attempt to copy the new bytes into the existing buffer. + if (data.TryCopyTo(_buffer.AsSpan(_bufferStartIndex + _usedBufferLength))) + { + _usedBufferLength += data.Length; + } + else + { + // There is not enough space at the end of the buffer. + // If the new length is less than the length of the existing buffer, + // then we can just move the existing data to the start of the buffer + // and copy the new bytes in successfully. Otherwise we allocate a + // larger buffer and copy everything in. + // This avoids allocating a new array in all but the last case. + int requiredLength = _usedBufferLength + data.Length; + byte[] buffer = (uint)requiredLength <= _buffer.Length ? _buffer : new byte[requiredLength * 2]; // Allocate double to reduce subsequent resizes + + _buffer.AsSpan(_bufferStartIndex, _usedBufferLength).CopyTo(buffer); + data.CopyTo(buffer.AsSpan(_usedBufferLength)); + _buffer = buffer; + _bufferStartIndex = 0; + _usedBufferLength = requiredLength; + } } public bool ReadFixMessage(out string msg) { msg = ""; - if(buffer_.Length < 2) - return false; - - int pos = 0; - pos = IndexOf(buffer_, "8=", 0); - if(-1 == pos) + Span buffer = _buffer.AsSpan(_bufferStartIndex, _usedBufferLength); + + int pos; + + if ((pos = buffer.IndexOf(_beginStringBytes)) < 0) + { + // BeginString (e.g. 8=) not yet found return false; + } - buffer_ = Remove(buffer_, pos); - pos = 0; + // Discard everything in the buffer up to the first BeginString tag + _bufferStartIndex += pos; + _usedBufferLength -= pos; - int length = 0; + buffer = _buffer.AsSpan(_bufferStartIndex, _usedBufferLength); - try + Debug.Assert(buffer.StartsWith(_beginStringBytes)); + + if (!ExtractLength(out int bodyLength, out int bytesConsumed, buffer)) { - if (!ExtractLength(out length, out pos, buffer_)) - return false; + // BodyLength tag and value (e.g. |9=YY|) not yet found + return false; + } - // pos is at first character past the BodyLength field (tag 9) + buffer = buffer.Slice(--bytesConsumed); - pos += length; - if (buffer_.Length < pos) - return false; + // buffer starts at the terminating SOH of the BodyLength (9) field + // e.g. + // 8=XX|9=YY|...... + // ^ + // | - pos = IndexOf(buffer_, "\x01" + "10=", pos - 1); - if (-1 == pos) - return false; // no tag 10 received yet - pos += 4; // pos now just after "10=" + Debug.Assert(buffer[0] == 1); + Debug.Assert(bodyLength >= 0); - pos = IndexOf(buffer_, "\x01", pos); - if (-1 == pos) - return false; - pos += 1; + if (buffer.Length < bodyLength) + { + return false; + } + + buffer = buffer.Slice(bodyLength); + bytesConsumed += bodyLength; - msg = Substring(buffer_, 0, pos); - buffer_ = Remove(buffer_, pos); - return true; + if ((pos = buffer.IndexOf(_checkSumBytes)) < 0) + { + // CheckSum (e.g. |10=) not yet found + return false; } - catch (MessageParseError e) + + Debug.Assert(_buffer.AsSpan(_bufferStartIndex + bytesConsumed + pos).StartsWith(_checkSumBytes)); + + buffer = buffer.Slice(pos + _checkSumBytes.Length); + bytesConsumed += pos + _checkSumBytes.Length; + + // buffer starts at the first byte of the CheckSum value + // e.g. + // 8=XX|9=YY|.........|10=...... + // ^ + // | + + if ((pos = buffer.IndexOf((byte)1)) < 0) { - if ((length > 0) && (pos <= buffer_.Length)) - buffer_ = Remove(buffer_, pos); - else - buffer_ = Remove(buffer_, buffer_.Length); - throw e; + // No terminating SOH found yet + return false; } + + Debug.Assert(_buffer[_bufferStartIndex + bytesConsumed + pos] == 1); + + bytesConsumed += pos + 1; // +1 to include the terminating SOH + + msg = _encoding.GetString(_buffer, _bufferStartIndex, bytesConsumed); + + // Discard this message in the buffer + _bufferStartIndex += bytesConsumed; + _usedBufferLength -= bytesConsumed; + + return true; } - public bool ExtractLength(out int length, out int pos, string buf) + public bool ExtractLength(out int bodyLength, out int bytesConsumed, string buf) { - return ExtractLength(out length, out pos, CharEncoding.DefaultEncoding.GetBytes(buf)); + return ExtractLength(out bodyLength, out bytesConsumed, _encoding.GetBytes(buf)); } - public bool ExtractLength(out int length, out int pos, byte[] buf) + public bool ExtractLength(out int bodyLength, out int bytesConsumed, Span buffer) { - length = 0; - pos = 0; + bodyLength = 0; + bytesConsumed = 0; - if (buf.Length < 1) - return false; + int pos; - int startPos = IndexOf(buf, "\x01" + "9=", 0); - if(-1 == startPos) + if ((pos = buffer.IndexOf(_bodyLengthBytes)) < 0) + { + // No BodyLength tag (|9=) found yet return false; - startPos +=3; + } - int endPos = IndexOf(buf, "\x01", startPos); - if(-1 == endPos) - return false; + bytesConsumed = pos + _bodyLengthBytes.Length; - string strLength = Substring(buf, startPos, endPos - startPos); - try - { - length = Fields.Converters.IntConverter.Convert(strLength); - if(length < 0) - throw new MessageParseError("Invalid BodyLength (" + length + ")"); - } - catch(FieldConvertError e) + buffer = buffer.Slice(bytesConsumed); + + if ((pos = buffer.IndexOf((byte)1)) < 0) { - throw new MessageParseError(e.Message, e); + // No terminating SOH found yet + bytesConsumed = 0; + return false; } - pos = endPos + 1; - return true; - } + // The longest string representation of an Int32 with NumberStyles.None is 10. + Span bodyLengthChars = stackalloc char[10]; + int charsWritten = _encoding.GetChars(buffer.Slice(0, pos), bodyLengthChars); - private int IndexOf(byte[] arrayToSearchThrough, string stringPatternToFind, int offset) - { - byte[] patternToFind = CharEncoding.DefaultEncoding.GetBytes(stringPatternToFind); - if (patternToFind.Length > arrayToSearchThrough.Length) - return -1; - for (int i = offset; i <= arrayToSearchThrough.Length - patternToFind.Length; i++) + if (!int.TryParse(bodyLengthChars.Slice(0, charsWritten), NumberStyles.None, CultureInfo.InvariantCulture, out bodyLength)) { - bool found = true; - for (int j = 0; j < patternToFind.Length; j++) - { - if (arrayToSearchThrough[i + j] != patternToFind[j]) - { - found = false; - break; - } - } - if (found) - { - return i; - } + // Bad BodyLength value. Discard the data in the buffer up to this point. + bytesConsumed += pos + 1; // +1 to include the terminating SOH + _bufferStartIndex += bytesConsumed; + _usedBufferLength -= bytesConsumed; + bytesConsumed = 0; + throw new MessageParseError($"Invalid BodyLength value \"{bodyLengthChars.Slice(0, charsWritten)}\""); } - return -1; - } - private byte[] Remove(byte[] array, int count) - { - byte[] returnByte = new byte[array.Length - count]; - Buffer.BlockCopy(array, count, returnByte, 0, array.Length - count); - usedBufferLength -= count; - return returnByte; - } + bytesConsumed += pos + 1; // +1 to include the terminating SOH - private string Substring(byte[] array, int startIndex, int length) - { - byte[] returnByte = new byte[length]; - Buffer.BlockCopy(array, startIndex, returnByte, 0, length); - return CharEncoding.DefaultEncoding.GetString(returnByte); + return true; } } } diff --git a/QuickFIXn/Session.cs b/QuickFIXn/Session.cs index ff32b2c40..fe481f2c6 100755 --- a/QuickFIXn/Session.cs +++ b/QuickFIXn/Session.cs @@ -743,7 +743,7 @@ protected void NextLogon(Message logon) int heartBtInt = logon.GetInt(Fields.Tags.HeartBtInt); state_.HeartBtInt = heartBtInt; GenerateLogon(logon); - this.Log.OnEvent("Responding to logon request"); + this.Log.OnEvent($"Responding to logon request; heartbeat is {heartBtInt} seconds"); } state_.SentReset = false; diff --git a/QuickFIXn/SocketReader.cs b/QuickFIXn/SocketReader.cs index 641c12d43..f433d0f22 100755 --- a/QuickFIXn/SocketReader.cs +++ b/QuickFIXn/SocketReader.cs @@ -222,8 +222,6 @@ protected bool HandleNewSession(string msg) return false; } qfSession_.Log.OnEvent(qfSession_.SessionID + " Socket Reader " + GetHashCode() + " accepting session " + qfSession_.SessionID + " from " + tcpClient_.Client.RemoteEndPoint); - // FIXME do this here? qfSession_.HeartBtInt = QuickFix.Fields.Converters.IntConverter.Convert(message.GetField(Fields.Tags.HeartBtInt)); /// FIXME - qfSession_.Log.OnEvent(qfSession_.SessionID + " Acceptor heartbeat set to " + qfSession_.HeartBtInt + " seconds"); qfSession_.SetResponder(responder_); return true; } diff --git a/QuickFIXn/ThreadedSocketAcceptor.cs b/QuickFIXn/ThreadedSocketAcceptor.cs index bc6e55c62..c040dd381 100755 --- a/QuickFIXn/ThreadedSocketAcceptor.cs +++ b/QuickFIXn/ThreadedSocketAcceptor.cs @@ -11,15 +11,13 @@ namespace QuickFix /// public class ThreadedSocketAcceptor : IAcceptor { - - - private Dictionary sessions_ = new Dictionary(); + private Dictionary sessions_ = new(); private SessionSettings settings_; - private Dictionary socketDescriptorForAddress_ = new Dictionary(); + private Dictionary socketDescriptorForAddress_ = new(); private SessionFactory sessionFactory_; private bool isStarted_ = false; private bool _disposed = false; - private object sync_ = new object(); + private object sync_ = new(); #region Constructors @@ -56,18 +54,18 @@ public ThreadedSocketAcceptor( IApplication application, IMessageStoreFactory storeFactory, SessionSettings settings, - ILogFactory logFactory, - IMessageFactory messageFactory) + ILogFactory? logFactory, + IMessageFactory? messageFactory) { - logFactory = logFactory ?? new NullLogFactory(); - messageFactory = messageFactory ?? new DefaultMessageFactory(); - SessionFactory sf = new SessionFactory(application, storeFactory, logFactory, messageFactory); + ILogFactory lf = logFactory ?? new NullLogFactory(); + IMessageFactory mf = messageFactory ?? new DefaultMessageFactory(); + SessionFactory sf = new SessionFactory(application, storeFactory, lf, mf); try { CreateSessions(settings, sf); } - catch (System.Exception e) + catch (Exception e) { throw new ConfigError(e.Message, e); } @@ -377,7 +375,7 @@ public bool RemoveSession(SessionID sessionID, bool terminateActiveSession) /// Any override should call base.Dispose(disposing). /// /// - protected virtual void Dispose(bool disposing) + protected void Dispose(bool disposing) { if (_disposed) return; if (disposing) @@ -404,6 +402,7 @@ public void Dispose() Dispose(true); GC.SuppressFinalize(this); } + ~ThreadedSocketAcceptor() => Dispose(false); } } diff --git a/QuickFIXn/Transport/SocketInitiator.cs b/QuickFIXn/Transport/SocketInitiator.cs index da2db3961..8539a08a3 100755 --- a/QuickFIXn/Transport/SocketInitiator.cs +++ b/QuickFIXn/Transport/SocketInitiator.cs @@ -22,22 +22,19 @@ public class SocketInitiator : AbstractInitiator private volatile bool shutdownRequested_ = false; private DateTime lastConnectTimeDT = DateTime.MinValue; private int reconnectInterval_ = 30; - private SocketSettings socketSettings_ = new SocketSettings(); - private Dictionary threads_ = new Dictionary(); - private Dictionary sessionToHostNum_ = new Dictionary(); - private object sync_ = new object(); + private SocketSettings socketSettings_ = new(); + private Dictionary threads_ = new(); + private Dictionary sessionToHostNum_ = new(); + private object sync_ = new(); #endregion - public SocketInitiator(IApplication application, IMessageStoreFactory storeFactory, SessionSettings settings) - : this(application, storeFactory, settings, null) - { } - - public SocketInitiator(IApplication application, IMessageStoreFactory storeFactory, SessionSettings settings, ILogFactory logFactory) - : base(application, storeFactory, settings, logFactory) - { } - - public SocketInitiator(IApplication application, IMessageStoreFactory storeFactory, SessionSettings settings, ILogFactory logFactory, IMessageFactory messageFactory) + public SocketInitiator( + IApplication application, + IMessageStoreFactory storeFactory, + SessionSettings settings, + ILogFactory? logFactory = null, + IMessageFactory? messageFactory = null) : base(application, storeFactory, settings, logFactory, messageFactory) { } @@ -80,7 +77,7 @@ public static void SocketInitiatorThreadStart(object socketInitiatorThread) exceptionEvent = $"Unexpected exception: {ex}"; } - if (exceptionEvent != null) + if (exceptionEvent is not null) { if (t.Session.Disposed) { @@ -253,10 +250,8 @@ protected override void DoConnect(SessionID sessionID, Dictionary settings) AddThread(t); } - catch (System.Exception e) - { - if (null != session) - session.Log.OnEvent(e.Message); + catch (System.Exception e) { + session?.Log.OnEvent(e.Message); } } diff --git a/README.md b/README.md index eea04a14c..534529a86 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ For more information: AcceptanceTest logs are output to `bin/Debug/net6.0/log`. + Credits ------- diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0015a74ed..6012cf81d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -11,8 +11,16 @@ What's New **CAUTION: There are breaking changes between 1.10 and 1.11! Please review the 1.11.0 notes below.** ### NEXT RELEASE + +**Breaking change** +* #768 - span-ify parser (Rob-Hague) - makes a change to QuickFix.Parser interface, which isn't likely to affect users + +**Non-breaking changes** * #400 - added DDTool, a C#-based codegen, and deleted Ruby-based generator (gbirchmeier) * #811 - convert AT platform to be NUnit-based, get rid of Ruby runner (Rob-Hague) +* #813 - fix incorrect logging of acceptor heartbeat (gbirchmeier) +* #815 - update broken/neglected example apps & docs (gbirchmeier) +* #764 - fix positive UTC offset parsing in DateTimeConverter (Rob-Hague) ### v1.11.2: * same as v1.11.1, but I fixed the readme in the pushed nuget packages diff --git a/UnitTests/ConverterTests.cs b/UnitTests/ConverterTests.cs index d808c6be0..14d138059 100644 --- a/UnitTests/ConverterTests.cs +++ b/UnitTests/ConverterTests.cs @@ -178,7 +178,7 @@ public void TestNanosecondPrecision() Assert.That(DateTimeConverter.ConvertToDateTime("20021201-11:03:05.231116500Z", TimeStampPrecision.Nanosecond), Is.EqualTo(dt)); // convert nanosecond time with non-UTC positive offset time zone to full DateTime - Assert.That(DateTimeConverter.ConvertToDateTime("20021201-06:03:05.231116500+05", TimeStampPrecision.Nanosecond), Is.EqualTo(dt)); + Assert.That(DateTimeConverter.ConvertToDateTime("20021201-16:03:05.231116500+05", TimeStampPrecision.Nanosecond), Is.EqualTo(dt)); // convert nanosecond time with non-UTC negative offset time zone to full DateTime Assert.That(DateTimeConverter.ConvertToDateTime("20021201-08:03:05.231116500-03", TimeStampPrecision.Nanosecond), Is.EqualTo(dt)); diff --git a/UnitTests/ParserTest.cs b/UnitTests/ParserTest.cs index a31c190f8..8aaca064b 100755 --- a/UnitTests/ParserTest.cs +++ b/UnitTests/ParserTest.cs @@ -1,6 +1,8 @@ using NUnit.Framework; using QuickFix; using System; +using System.Collections.Generic; +using System.Linq; namespace UnitTests { @@ -10,6 +12,7 @@ public class ParserTest const string normalLength = "8=FIX.4.2\x01" + "9=12\x01" + "35=A\x01" + " 108=30\x01" + "10=31\x01"; const string badLength = "8=FIX.4.2\x01" + "9=A\x01" + "35=A\x01" + "108=30\x01" + "10=31\x01"; const string negativeLength = "8=FIX.4.2\x01" + "9=-1\x01" + "35=A\x01" + "108=30\x01" + "10=31\x01"; + const string zeroLength = "8=FIX.4.2\x01" + "9=0\x01" + "35=A\x01" + "108=30\x01" + "10=31\x01"; const string incomplete_1 = "8=FIX.4.2"; const string incomplete_2 = "8=FIX.4.2\x01" + "9=12"; @@ -29,64 +32,92 @@ public void ExtractLength() Assert.AreEqual(12, len); Assert.AreEqual(15, pos); - pos = 0; - Assert.Throws(delegate { parser.ExtractLength(out len, out pos, badLength); }); + Assert.True(parser.ExtractLength(out len, out pos, zeroLength)); + Assert.AreEqual(0, len); + Assert.AreEqual(14, pos); + Assert.Throws(delegate { parser.ExtractLength(out len, out pos, badLength); }); Assert.AreEqual(0, pos); + Assert.Throws(delegate { parser.ExtractLength(out len, out pos, negativeLength); }); + Assert.AreEqual(0, pos); + Assert.False(parser.ExtractLength(out len, out pos, incomplete_1)); Assert.AreEqual(0, pos); - parser.ExtractLength(out len, out pos, incomplete_1); - parser.ExtractLength(out len, out pos, incomplete_2); + Assert.False(parser.ExtractLength(out len, out pos, incomplete_2)); Assert.AreEqual(0, pos); Assert.False(parser.ExtractLength(out len, out pos, "")); + Assert.AreEqual(0, pos); } [Test] - public void ReadCompleteFixMessages() + [TestCase(100, 1)] + [TestCase(10, 10)] + public void ReadCompleteFixMessages(int batchSize, int numBatches) { const string fixMsg1 = "8=FIX.4.2\x01" + "9=12\x01" + "35=A\x01" + "108=30\x01" + "10=31\x01"; const string fixMsg2 = "8=FIX.4.2\x01" + "9=17\x01" + "35=4\x01" + "36=88\x01" + "123=Y\x01" + "10=34\x01"; const string fixMsg3 = "8=FIX.4.2\x01" + "9=19\x01" + "35=A\x01" + "108=30\x01" + "9710=8\x01" + "10=31\x01"; Parser parser = new Parser(); - byte[] combined = StrToBytes(fixMsg1 + fixMsg2 + fixMsg3); - parser.AddToStream(combined, combined.Length); - - string readFixMsg1; - Assert.True(parser.ReadFixMessage(out readFixMsg1)); - Assert.AreEqual(fixMsg1, readFixMsg1); - string readFixMsg2; - Assert.True(parser.ReadFixMessage(out readFixMsg2)); - Assert.AreEqual(fixMsg2, readFixMsg2); - - string readFixMsg3; - Assert.True(parser.ReadFixMessage(out readFixMsg3)); - Assert.AreEqual(fixMsg3, readFixMsg3); + for (int batchNum = 0; batchNum < numBatches; batchNum++) + { + List batch = new(); + + for (int i = 0; i < batchSize; i++) + { + string message = (i % 3) switch + { + 0 => fixMsg1, + 1 => fixMsg2, + _ => fixMsg3 + }; + + batch.Add(message); + parser.AddToStream(CharEncoding.DefaultEncoding.GetBytes(message)); + } + + for (int i = 0; i < batchSize; i++) + { + Assert.True(parser.ReadFixMessage(out string message)); + Assert.AreEqual(batch[i], message); + } + + Assert.False(parser.ReadFixMessage(out _)); + } } [Test] public void ReadPartialFixMessage() { - string partFixMsg1 = "8=FIX.4.2\x01" + "9=17\x01" + "35=4\x01" + "36="; - string partFixMsg2 = "88\x01" + "123=Y\x01" + "10=34\x01"; - - byte[] partBytes1 = CharEncoding.DefaultEncoding.GetBytes(partFixMsg1); - byte[] partBytes2 = CharEncoding.DefaultEncoding.GetBytes(partFixMsg2); - - Parser parser = new Parser(); - string readPartFixMsg; - - parser.AddToStream(partBytes1, partBytes1.Length); - Assert.False(parser.ReadFixMessage(out readPartFixMsg)); - - parser.AddToStream(partBytes2, partBytes2.Length); - Assert.True(parser.ReadFixMessage(out readPartFixMsg)); - - Assert.AreEqual(partFixMsg1 + partFixMsg2, readPartFixMsg); + List messageParts = new() + { + "abcdef8", // Junk + "8", // No BeginString found yet + "=FIX.4.2", // No BodyLength tag found yet + '\x01' + "9=17", // No BodyLength terminating SOH found yet + '\x01' + "35=4", // Message smaller than BodyLength value + '\x01' + "36=88\x01" + "123=Y\x01" + "10", // no CheckSum tag found yet + "=34", // No CheckSum terminating SOH found yet + "\x01" + }; + + Parser parser = new(); + + for(int i = 0; i < messageParts.Count - 1; i++) + { + parser.AddToStream(CharEncoding.DefaultEncoding.GetBytes(messageParts[i])); + Assert.False(parser.ReadFixMessage(out _)); + } + + string expectedMessage = string.Join("", messageParts.Skip(1)); + + parser.AddToStream(CharEncoding.DefaultEncoding.GetBytes(messageParts[^1])); + Assert.True(parser.ReadFixMessage(out string actualMessage)); + Assert.AreEqual(expectedMessage, actualMessage); } [Test] @@ -95,13 +126,14 @@ public void ReadFixMessageWithBadLength() byte[] fixMsg = StrToBytes("8=TEST\x01" + "9=TEST\x01" + "35=TEST\x01" + "49=SS1\x01" + "56=RORE\x01" + "34=3\x01" + "52=20050222-16:45:53\x01" + "10=TEST\x01"); Parser parser = new Parser(); - parser.AddToStream(fixMsg, fixMsg.Length); + parser.AddToStream(fixMsg); + parser.AddToStream(StrToBytes(normalLength)); - string readFixMsg; - Assert.Throws(delegate { parser.ReadFixMessage(out readFixMsg); }); + Assert.Throws(delegate { parser.ReadFixMessage(out _); }); // nothing thrown now because the previous call removes bad data from buffer: - Assert.DoesNotThrow(delegate { parser.ReadFixMessage(out readFixMsg); }); + Assert.True(parser.ReadFixMessage(out string readFixMsg)); + Assert.AreEqual(normalLength, readFixMsg); } [Test] diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index e592f21ba..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: 1.0.{build} - -image: -- Visual Studio 2022 - -install: -- dir C:\ -- set PATH=C:\Ruby26-x64\bin;%PATH% -- ruby -v -- pwsh --Version -- pwsh .\scripts\Generate-Message-Sources.ps1 - -before_test: -- ruby -v - -build_script: -- ps: dotnet build -c Release --framework net6.0 - -test_script: -- dotnet test -c Release --no-build --no-restore UnitTests -l trx -- dotnet test UnitTests -- dotnet test AcceptanceTest