From 3e361bfe006749eafe49f2821cb176155bca5932 Mon Sep 17 00:00:00 2001 From: panot-hong Date: Fri, 17 Sep 2021 02:15:25 +0700 Subject: [PATCH 1/8] Attempt reconnect without creating a new task recursively --- ESCPOS_NET/Printers/BasePrinter.cs | 4 ++-- ESCPOS_NET/Printers/NetworkPrinter.cs | 28 +++++++++++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/ESCPOS_NET/Printers/BasePrinter.cs b/ESCPOS_NET/Printers/BasePrinter.cs index c5d3386..d6cf962 100644 --- a/ESCPOS_NET/Printers/BasePrinter.cs +++ b/ESCPOS_NET/Printers/BasePrinter.cs @@ -42,8 +42,8 @@ public abstract partial class BasePrinter : IPrinter, IDisposable protected ConcurrentQueue WriteBuffer { get; set; } = new ConcurrentQueue(); protected int BytesWrittenSinceLastFlush { get; set; } = 0; - - protected volatile bool IsConnected = true; + + protected virtual bool IsConnected { get; } = true; public string PrinterName { get; protected set; } diff --git a/ESCPOS_NET/Printers/NetworkPrinter.cs b/ESCPOS_NET/Printers/NetworkPrinter.cs index 4d9ca19..69929c9 100644 --- a/ESCPOS_NET/Printers/NetworkPrinter.cs +++ b/ESCPOS_NET/Printers/NetworkPrinter.cs @@ -31,6 +31,8 @@ public class NetworkPrinter : BasePrinter private readonly NetworkPrinterSettings _settings; private TCPConnection _tcpConnection; + protected override bool IsConnected => _tcpConnection?.IsConnected??false; + public NetworkPrinter(NetworkPrinterSettings settings) : base(settings.PrinterName) { _settings = settings; @@ -48,31 +50,42 @@ public NetworkPrinter(NetworkPrinterSettings settings) : base(settings.PrinterNa private void ConnectedEvent(object sender, ClientConnectedEventArgs e) { Logging.Logger?.LogInformation("[{Function}]:[{PrinterName}] Connected successfully to network printer! Connection String: {ConnectionString}", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName, _settings.ConnectionString); - IsConnected = true; + // Close previously created reader and writer if any (for handling reconnect after connection lose (in the future)) + Reader?.Close(); + Writer?.Close(); + Reader = new BinaryReader(_tcpConnection.ReadStream); + Writer = new BinaryWriter(_tcpConnection.WriteStream); InvokeConnect(); } private void DisconnectedEvent(object sender, ClientDisconnectedEventArgs e) { - IsConnected = false; InvokeDisconnect(); Logging.Logger?.LogWarning("[{Function}]:[{PrinterName}] Network printer connection terminated. Attempting to reconnect. Connection String: {ConnectionString}", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName, _settings.ConnectionString); Connect(); } - private void AttemptReconnectInfinitely() + private async ValueTask AttemptReconnectInfinitely() { try { //_tcpConnection.ConnectWithRetries(300000); _tcpConnection.ConnectWithRetries(3000); } - catch (Exception e) + catch (TimeoutException) { //Logging.Logger?.LogWarning("[{Function}]:[{PrinterName}] Network printer unable to connect after 5 minutes. Attempting to reconnect. Settings: {Settings}", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName, JsonSerializer.Serialize(_settings)); - Task.Run(async () => { await Task.Delay(250); Connect(); }); + await Task.Delay(250); + CreateTcpConnection(); + await AttemptReconnectInfinitely(); } } private void Connect() + { + CreateTcpConnection(); + Task.Run(async () => { await AttemptReconnectInfinitely(); }); + } + + private void CreateTcpConnection() { if (_tcpConnection != null) { @@ -86,11 +99,6 @@ private void Connect() // set events _tcpConnection.Connected += ConnectedEvent; _tcpConnection.Disconnected += DisconnectedEvent; - - Reader = new BinaryReader(_tcpConnection.ReadStream); - Writer = new BinaryWriter(_tcpConnection.WriteStream); - - Task.Run(() => { AttemptReconnectInfinitely(); }); } protected override void OverridableDispose() From a1ba7c42c571c25d090d1a24cf10b68e92a8a6e2 Mon Sep 17 00:00:00 2001 From: panot-hong Date: Tue, 21 Sep 2021 01:22:10 +0700 Subject: [PATCH 2/8] support async write with non exception swallow and marshall better producer/consumer write bytes --- ESCPOS_NET/Printers/BasePrinter.cs | 90 ++++++++++++++++++++---------- ESCPOS_NET/Printers/IPrinter.cs | 40 ++++++++++++- 2 files changed, 101 insertions(+), 29 deletions(-) diff --git a/ESCPOS_NET/Printers/BasePrinter.cs b/ESCPOS_NET/Printers/BasePrinter.cs index d6cf962..2c75fcc 100644 --- a/ESCPOS_NET/Printers/BasePrinter.cs +++ b/ESCPOS_NET/Printers/BasePrinter.cs @@ -21,7 +21,6 @@ public abstract partial class BasePrinter : IPrinter, IDisposable //private volatile bool _isMonitoring; private CancellationTokenSource _readCancellationTokenSource; - private CancellationTokenSource _writeCancellationTokenSource; private readonly int _maxBytesPerWrite = 15000; // max byte chunks to write at once. @@ -34,19 +33,23 @@ public abstract partial class BasePrinter : IPrinter, IDisposable //public event EventHandler Idle; protected BinaryWriter Writer { get; set; } - protected BinaryReader Reader { get; set; } protected ConcurrentQueue ReadBuffer { get; set; } = new ConcurrentQueue(); - - protected ConcurrentQueue WriteBuffer { get; set; } = new ConcurrentQueue(); + private readonly BlockingCollection<(byte[] bytes, TaskCompletionSource taskSource)> _writeBuffer = + new BlockingCollection<(byte[] bytes, TaskCompletionSource taskSource)>(); protected int BytesWrittenSinceLastFlush { get; set; } = 0; - + protected virtual bool IsConnected { get; } = true; public string PrinterName { get; protected set; } + /// + /// Timeout in millisecond to wait for the connection to be connected when call WriteAsync function with await. Default is 5000 milliseconds. + /// + public int WriteTimeout { get; set; } = 5000; + protected BasePrinter() { PrinterName = Guid.NewGuid().ToString(); @@ -64,10 +67,9 @@ protected BasePrinter(string printerName) private void Init() { _readCancellationTokenSource = new CancellationTokenSource(); - _writeCancellationTokenSource = new CancellationTokenSource(); Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Initializing Task Threads...", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName); //Task.Factory.StartNew(MonitorPrinterStatusLongRunningTask, _connectivityCancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false); - Task.Factory.StartNew(WriteLongRunningTask, _writeCancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false); + Task.Factory.StartNew(WriteLongRunningTask, TaskCreationOptions.LongRunning).ConfigureAwait(false); Task.Factory.StartNew(ReadLongRunningTask, _readCancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false); // TODO: read and status monitoring probably won't work for fileprinter, should let printer types disable this feature. Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Task Threads started", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName); @@ -89,40 +91,52 @@ protected virtual void Reconnect() } protected virtual async void WriteLongRunningTask() { - while (true) + // Loop when there is a new item in the _writeBuffer, break when _writeBuffer.CompleteAdding() is called (in the dispose) + foreach (var (nextBytes, taskSource) in _writeBuffer.GetConsumingEnumerable()) { - if (_writeCancellationTokenSource != null && _writeCancellationTokenSource.IsCancellationRequested) - { - Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Write Long-Running Task Cancellation was requested.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName); - break; - } + await Task.WhenAny( + Task.Delay(WriteTimeout), + Task.Run(async () => + { + while (!IsConnected) // Await for the connection to the printer get restored + { + await Task.Delay(100); + } + }) + ); - await Task.Delay(100); if (!IsConnected) { + taskSource.SetException(new IOException($"Unrecoverable connectivity error writing to printer.")); continue; } - try { - var didDequeue = WriteBuffer.TryDequeue(out var nextBytes); - if (didDequeue && nextBytes?.Length > 0) + if (nextBytes?.Length > 0) { WriteToBinaryWriter(nextBytes); + taskSource.SetResult(true); + } + else + { + taskSource.SetResult(false); } } catch (IOException ex) { // Thrown if the printer times out the socket connection // default is 90 seconds + taskSource.TrySetException(ex); //Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Swallowing IOException... sometimes happens with network printers. Should get reconnected automatically."); } catch (Exception ex) { - // Swallow the exception + taskSource.TrySetException(ex); //Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Swallowing generic read exception... sometimes happens with serial port printers."); } } + + Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Write Long-Running Task Cancellation was requested.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName); } protected virtual async void ReadLongRunningTask() @@ -151,28 +165,42 @@ protected virtual async void ReadLongRunningTask() DataAvailable(); } } - catch (Exception ex) - { + { // Swallow the exception //Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Swallowing generic read exception... sometimes happens with serial port printers.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName); } } } - public virtual void Write(params byte[][] arrays) + /// + public virtual void Write(params byte[][] byteArrays) { - Write(ByteSplicer.Combine(arrays)); + Write(ByteSplicer.Combine(byteArrays)); } + /// public virtual void Write(byte[] bytes) { - WriteBuffer.Enqueue(bytes); + _ = WriteAsync(bytes); } - protected virtual void WriteToBinaryWriter(byte[] bytes) + /// + public virtual async Task WriteAsync(params byte[][] byteArrays) + { + await WriteAsync(ByteSplicer.Combine(byteArrays)); + } + + /// + public virtual async Task WriteAsync(byte[] bytes) { + var taskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _writeBuffer.Add((bytes, taskSource)); + await taskSource.Task; + } + protected virtual void WriteToBinaryWriter(byte[] bytes) + { if (!IsConnected) { Logging.Logger?.LogInformation("[{Function}]:[{PrinterName}] Attempted to write but printer isn't connected. Attempting to reconnect...", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName); @@ -190,7 +218,6 @@ protected virtual void WriteToBinaryWriter(byte[] bytes) bool hasFlushed = false; while (bytesLeft > 0) { - int count = Math.Min(_maxBytesPerWrite, bytesLeft); try { @@ -227,7 +254,6 @@ protected virtual void Flush(object sender, ElapsedEventArgs e) { try { - BytesWrittenSinceLastFlush = 0; Writer.Flush(); } @@ -237,9 +263,9 @@ protected virtual void Flush(object sender, ElapsedEventArgs e) } } - public virtual void DataAvailable() + private void DataAvailable() { - if (ReadBuffer.Count() % 4 == 0) + if (ReadBuffer.Count % 4 == 0) { var bytes = new byte[4]; for (int i = 0; i < 4; i++) @@ -326,6 +352,14 @@ protected virtual void Dispose(bool disposing) Logging.Logger?.LogDebug(e, "[{Function}]:[{PrinterName}] Dispose Issue during cancellation token cancellation call.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName); } try + { + _writeBuffer.CompleteAdding(); + } + catch (ObjectDisposedException e) + { + Logging.Logger?.LogDebug(e, "[{Function}]:[{PrinterName}] Dispose Issue during closing write buffer.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName); + } + try { Reader?.Close(); } diff --git a/ESCPOS_NET/Printers/IPrinter.cs b/ESCPOS_NET/Printers/IPrinter.cs index e51805d..6bd596b 100644 --- a/ESCPOS_NET/Printers/IPrinter.cs +++ b/ESCPOS_NET/Printers/IPrinter.cs @@ -1,12 +1,50 @@ using System; +using System.Threading.Tasks; namespace ESCPOS_NET { public interface IPrinter { PrinterStatusEventArgs GetStatus(); - void Write(params byte[][] arrays); + /// + /// Write byte array of array to the printer stream. This function discards the and the parameter will still be written + /// to the printer stream when the connection restored if this instance is disposed yet + /// + /// Array of byte array which to be flattened to the overloaded function. + /// + void Write(params byte[][] byteArrays); + /// + /// Write byte array of array to the printer stream. This function discards the and the parameter will still be written + /// to the printer stream when the connection restored if this instance is disposed yet + /// + /// Byte array to write to the printer stream. + /// void Write(byte[] bytes); + /// + /// Write byte array of array to the printer stream. + /// + /// Array of byte array which to be flattened to the overloaded function. + /// + /// + /// await or Wait() this function to await the operation and it would properly capture the exception otherwise the exception will be swallowed. + /// Not await nor Wait() this function would discard the and the parameter will still be written + /// to the printer stream when the connection restored if this instance is disposed yet. + /// + /// The is reach or Attempt to write stream to the disconnected connection + Task WriteAsync(params byte[][] byteArrays); + /// + /// Write byte array of array to the printer stream. + /// + /// Byte array to write to the printer stream. + /// + /// + /// await or Wait() this function to await the operation and it would properly capture the exception otherwise the exception will be swallowed. + /// Not await nor Wait() this function would discard the and the parameter will still be written + /// to the printer stream when the connection restored if this instance is disposed yet. + /// + /// The is reach or Attempt to write stream to the disconnected connection + Task WriteAsync(byte[] bytes); + event EventHandler StatusChanged; event EventHandler Disconnected; event EventHandler Connected; From aae9da03c2148fa3adbdf47525af6f5320abb5bd Mon Sep 17 00:00:00 2001 From: panot-hong Date: Tue, 21 Sep 2021 01:28:44 +0700 Subject: [PATCH 3/8] perform direct tcp connect prior to attempt reconnect which more expensive and also explicitly dispose tcp conn --- ESCPOS_NET/Printers/NetworkPrinter.cs | 20 +++++++++++++++++- ESCPOS_NET/Utils/TCPConnection.cs | 30 +++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/ESCPOS_NET/Printers/NetworkPrinter.cs b/ESCPOS_NET/Printers/NetworkPrinter.cs index 69929c9..7ef55a0 100644 --- a/ESCPOS_NET/Printers/NetworkPrinter.cs +++ b/ESCPOS_NET/Printers/NetworkPrinter.cs @@ -82,7 +82,17 @@ private async ValueTask AttemptReconnectInfinitely() private void Connect() { CreateTcpConnection(); - Task.Run(async () => { await AttemptReconnectInfinitely(); }); + try + { + _tcpConnection.Connect(); + } + catch (SocketException) + { + if (!IsConnected) + { + Task.Run(async () => { await AttemptReconnectInfinitely(); }); + } + } } private void CreateTcpConnection() @@ -103,7 +113,15 @@ private void CreateTcpConnection() protected override void OverridableDispose() { + // Dispose to close tcp connection when the printer object is disposed otherwise + // the tcp connection is held until garbage collected + _tcpConnection?.Dispose(); _tcpConnection = null; } + + ~NetworkPrinter() + { + Dispose(true); + } } } diff --git a/ESCPOS_NET/Utils/TCPConnection.cs b/ESCPOS_NET/Utils/TCPConnection.cs index 55c31f7..73b38e6 100644 --- a/ESCPOS_NET/Utils/TCPConnection.cs +++ b/ESCPOS_NET/Utils/TCPConnection.cs @@ -7,7 +7,7 @@ namespace ESCPOS_NET { - public class TCPConnection + public class TCPConnection : IDisposable { public Stream ReadStream { get; private set; } = new EchoStream(); public Stream WriteStream { get; private set; } @@ -38,12 +38,30 @@ private void DataReceivedEventHandler(object sender, DataReceivedEventArgs e) { ReadStream.Write(e.Data, 0, e.Data.Length); } + /// + /// Establish a connection to the server without retry. SocketException will be thrown after the certain period of attempts. + /// + /// + public void Connect() + { + _client.Connect(); + } public void ConnectWithRetries(int timeoutMs) { _client.ConnectWithRetries(timeoutMs); } + public void Dispose() + { + Dispose(true); + } + ~TCPConnection() + { + Dispose(false); + } + + private void Dispose(bool disposing) { try { @@ -51,9 +69,17 @@ public void ConnectWithRetries(int timeoutMs) _client.Events.Connected -= ConnectedEventHandler; _client.Events.Disconnected -= DisconnectedEventHandler; _client?.Dispose(); + _client = null; + } + catch { } + try + { + WriteStream?.Dispose(); + ReadStream?.Dispose(); + WriteStream = null; + ReadStream = null; } catch { } } - } } From 19ae87d3188440f7367b15dd907b20d84cd154b9 Mon Sep 17 00:00:00 2001 From: panot-hong Date: Tue, 21 Sep 2021 01:55:13 +0700 Subject: [PATCH 4/8] Test between using singleton or short-lived printer object with a bool switch. Also made easy to choose to write with Write or WriteAsync. --- ESCPOS_NET.ConsoleTest/Program.cs | 88 +++++++++++++------ ESCPOS_NET.ConsoleTest/TestLargeByteArrays.cs | 4 +- 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/ESCPOS_NET.ConsoleTest/Program.cs b/ESCPOS_NET.ConsoleTest/Program.cs index f19a60b..a0eca7b 100644 --- a/ESCPOS_NET.ConsoleTest/Program.cs +++ b/ESCPOS_NET.ConsoleTest/Program.cs @@ -13,10 +13,13 @@ internal class Program { private static BasePrinter printer; private static ICommandEmitter e; + /// + /// Indicate whether to test with long-lived printer object or create and dispose every time. + /// + private const bool SINGLETON_PRINTER_OBJECT = false; static void Main(string[] args) { - Console.WriteLine("Welcome to the ESCPOS_NET Test Application!"); Console.Write("Would you like to see all debug messages? (y/n): "); var response = Console.ReadLine().Trim().ToLowerInvariant(); @@ -34,8 +37,9 @@ static void Main(string[] args) Console.WriteLine("2 ) Test Network Printer"); Console.Write("Choice: "); string comPort = ""; - string ip; - string networkPort; + string ip = ""; + string networkPort = ""; + Action createPrinter = null; response = Console.ReadLine(); var valid = new List { "1", "2" }; if (!valid.Contains(response)) @@ -63,7 +67,10 @@ static void Main(string[] args) { baudRate = 115200; } - printer = new SerialPrinter(portName: comPort, baudRate: baudRate); + if (SINGLETON_PRINTER_OBJECT) + printer = new SerialPrinter(portName: comPort, baudRate: baudRate); + else + createPrinter = () => { printer = new SerialPrinter(portName: comPort, baudRate: baudRate); }; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { @@ -73,7 +80,10 @@ static void Main(string[] args) { comPort = "/dev/usb/lp0"; } - printer = new FilePrinter(filePath: comPort, false); + if (SINGLETON_PRINTER_OBJECT) + printer = new FilePrinter(filePath: comPort, false); + else + createPrinter = () => { printer = new FilePrinter(filePath: comPort, false); }; } } else if (choice == 2) @@ -82,7 +92,7 @@ static void Main(string[] args) ip = Console.ReadLine(); if (string.IsNullOrWhiteSpace(ip)) { - ip = "192.168.254.202"; + ip = "127.0.0.1"; // default to local for using TCPPrintServerTest } Console.Write("TCP Port (enter for default 9100): "); networkPort = Console.ReadLine(); @@ -90,7 +100,10 @@ static void Main(string[] args) { networkPort = "9100"; } - printer = new NetworkPrinter(settings: new NetworkPrinterSettings() { ConnectionString = $"{ip}:{networkPort}" }); + if (SINGLETON_PRINTER_OBJECT) + printer = new NetworkPrinter(settings: new NetworkPrinterSettings() { ConnectionString = $"{ip}:{networkPort}" }); + else + createPrinter = () => { printer = new NetworkPrinter(settings: new NetworkPrinterSettings() { ConnectionString = $"{ip}:{networkPort}" }); }; } bool monitor = false; @@ -136,6 +149,11 @@ static void Main(string[] args) continue; } + if (!SINGLETON_PRINTER_OBJECT) + { + createPrinter(); + } + var enumChoice = (Option)choice; if (enumChoice == Option.Exit) { @@ -146,50 +164,50 @@ static void Main(string[] args) if (monitor) { - printer.Write(e.Initialize()); - printer.Write(e.Enable()); - printer.Write(e.EnableAutomaticStatusBack()); + printer.WriteTest(e.Initialize()); + printer.WriteTest(e.Enable()); + printer.WriteTest(e.EnableAutomaticStatusBack()); } Setup(monitor); - printer?.Write(e.PrintLine($"== [ Start {testCases[enumChoice]} ] ==")); + printer?.WriteTest(e.PrintLine($"== [ Start {testCases[enumChoice]} ] ==")); switch (enumChoice) { case Option.SingleLinePrinting: - printer.Write(Tests.SingleLinePrinting(e)); + printer.WriteTest(Tests.SingleLinePrinting(e)); break; case Option.MultiLinePrinting: - printer.Write(Tests.MultiLinePrinting(e)); + printer.WriteTest(Tests.MultiLinePrinting(e)); break; case Option.LineSpacing: - printer.Write(Tests.LineSpacing(e)); + printer.WriteTest(Tests.LineSpacing(e)); break; case Option.BarcodeStyles: - printer.Write(Tests.BarcodeStyles(e)); + printer.WriteTest(Tests.BarcodeStyles(e)); break; case Option.BarcodeTypes: - printer.Write(Tests.BarcodeTypes(e)); + printer.WriteTest(Tests.BarcodeTypes(e)); break; case Option.TwoDimensionCodes: - printer.Write(Tests.TwoDimensionCodes(e)); + printer.WriteTest(Tests.TwoDimensionCodes(e)); break; case Option.TextStyles: - printer.Write(Tests.TextStyles(e)); + printer.WriteTest(Tests.TextStyles(e)); break; case Option.FullReceipt: - printer.Write(Tests.Receipt(e)); + printer.WriteTest(Tests.Receipt(e)); break; case Option.Images: - printer.Write(Tests.Images(e, false)); + printer.WriteTest(Tests.Images(e, false)); break; case Option.LegacyImages: - printer.Write(Tests.Images(e, true)); + printer.WriteTest(Tests.Images(e, true)); break; case Option.LargeByteArrays: try { - printer.Write(Tests.TestLargeByteArrays(e)); + printer.WriteTest(Tests.TestLargeByteArrays(e)); } catch (Exception e) { @@ -197,10 +215,10 @@ static void Main(string[] args) } break; case Option.CashDrawerPin2: - printer.Write(Tests.CashDrawerOpenPin2(e)); + printer.WriteTest(Tests.CashDrawerOpenPin2(e)); break; case Option.CashDrawerPin5: - printer.Write(Tests.CashDrawerOpenPin5(e)); + printer.WriteTest(Tests.CashDrawerOpenPin5(e)); break; default: Console.WriteLine("Invalid entry."); @@ -208,9 +226,10 @@ static void Main(string[] args) } Setup(monitor); - printer?.Write(e.PrintLine($"== [ End {testCases[enumChoice]} ] ==")); - printer?.Write(e.PartialCutAfterFeed(5)); - + printer?.WriteTest(e.PrintLine($"== [ End {testCases[enumChoice]} ] ==")); + printer?.WriteTest(e.PartialCutAfterFeed(5)); + if (!SINGLETON_PRINTER_OBJECT) + printer?.Dispose(); // TODO: also make an automatic runner that runs all tests (command line). } } @@ -262,4 +281,19 @@ private static void Setup(bool enableStatusBackMonitoring) } } } + + internal static class TestExtensions + { + /// + /// Wrapper exception function for ease of switching between Write and WriteAsync function + /// + /// + /// + internal static void WriteTest(this BasePrinter printer, params byte[][] arrays) + { + // Switch to use this if need to test with obsolated Write function + //printer.Write(arrays); + printer.WriteAsync(arrays).Wait(); + } + } } diff --git a/ESCPOS_NET.ConsoleTest/TestLargeByteArrays.cs b/ESCPOS_NET.ConsoleTest/TestLargeByteArrays.cs index aa7b6a4..897fcbc 100644 --- a/ESCPOS_NET.ConsoleTest/TestLargeByteArrays.cs +++ b/ESCPOS_NET.ConsoleTest/TestLargeByteArrays.cs @@ -35,7 +35,7 @@ public static byte[] TestLargeByteArrays(ICommandEmitter e) cube ); MemoryPrinter mp = new MemoryPrinter(); - mp.Write(expectedResult); + mp.WriteTest(expectedResult); var response = mp.GetAllData(); bool hasErrors = false; if (expectedResult.Length != response.Length) @@ -68,7 +68,7 @@ public static byte[] TestLargeByteArrays(ICommandEmitter e) var filename = $"{r.NextDouble().ToString()}.tmp"; using (FilePrinter fp = new FilePrinter(filename, true)) { - fp.Write(expectedResult); + fp.WriteTest(expectedResult); } response = File.ReadAllBytes(filename); hasErrors = false; From 6240ace46d45bef4e6cb3eaf64f66ff5ef1279fb Mon Sep 17 00:00:00 2001 From: panot-hong Date: Tue, 21 Sep 2021 01:57:12 +0700 Subject: [PATCH 5/8] Enhance to support more than one tcp connection. This includes when tcp connection is disconnected and reconnect. --- ESCPOS_NET.TCPPrintServerTest/Program.cs | 38 +++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/ESCPOS_NET.TCPPrintServerTest/Program.cs b/ESCPOS_NET.TCPPrintServerTest/Program.cs index 053ccce..fb75219 100644 --- a/ESCPOS_NET.TCPPrintServerTest/Program.cs +++ b/ESCPOS_NET.TCPPrintServerTest/Program.cs @@ -6,6 +6,7 @@ using System.Net.Sockets; using System.IO; using System.Text; +using System.Threading; namespace TcpEchoServer { @@ -18,23 +19,40 @@ public static void Main() int port = 9100; TcpListener listener = new TcpListener(IPAddress.Loopback, port); listener.Start(); + TcpClient client; - TcpClient client = listener.AcceptTcpClient(); - NetworkStream stream = client.GetStream(); + while (true) + { + // Accept multiple connections with a dedicated thread for each connection + client = listener.AcceptTcpClient(); + ThreadPool.QueueUserWorkItem(TcpClientConnectionHandler, client); + } + } + + private static void TcpClientConnectionHandler(object obj) + { + var tcp = (TcpClient)obj; + NetworkStream stream = tcp.GetStream(); StreamWriter writer = new StreamWriter(stream, Encoding.ASCII) { AutoFlush = true }; StreamReader reader = new StreamReader(stream, Encoding.ASCII); - - while (true) + try { - string inputLine = ""; - while (inputLine != null) + while (true) { - inputLine = reader.ReadLine(); - writer.Write("E"); - Console.WriteLine("Echoing string: " + inputLine); + string inputLine = ""; + while (inputLine != null) + { + inputLine = reader.ReadLine(); + writer.Write("E"); + Console.WriteLine("Echoing string: " + inputLine); + } + Console.WriteLine("Server saw disconnect from client."); } - Console.WriteLine("Server saw disconnect from client."); } + catch(IOException) + { + // connection is closed + } } } } From 752228e688dfb8c1b90c0dd72645660f9b34675d Mon Sep 17 00:00:00 2001 From: panot-hong Date: Sun, 26 Sep 2021 12:18:27 +0700 Subject: [PATCH 6/8] allow non-await write to endless retry --- ESCPOS_NET/Printers/BasePrinter.cs | 76 +++++++++++++++++------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/ESCPOS_NET/Printers/BasePrinter.cs b/ESCPOS_NET/Printers/BasePrinter.cs index 2c75fcc..65e746c 100644 --- a/ESCPOS_NET/Printers/BasePrinter.cs +++ b/ESCPOS_NET/Printers/BasePrinter.cs @@ -3,6 +3,7 @@ using ESCPOS_NET.Utils; using Microsoft.Extensions.Logging; using System; +using System.Collections; using System.Collections.Concurrent; using System.Diagnostics; using System.IO; @@ -90,50 +91,57 @@ protected virtual void Reconnect() // Implemented in the network printer } protected virtual async void WriteLongRunningTask() - { + { // Loop when there is a new item in the _writeBuffer, break when _writeBuffer.CompleteAdding() is called (in the dispose) foreach (var (nextBytes, taskSource) in _writeBuffer.GetConsumingEnumerable()) { - await Task.WhenAny( - Task.Delay(WriteTimeout), - Task.Run(async () => + var writeSuccess = false; + var isAwaitableWrite = taskSource != null; + do + { + await Task.WhenAny( + Task.Delay(WriteTimeout), + Task.Run(async () => + { + while (!IsConnected) // Await for the connection to the printer get restored + { + await Task.Delay(100); + } + }) + ); + + if (!IsConnected) { - while (!IsConnected) // Await for the connection to the printer get restored + taskSource?.SetException(new IOException($"Unrecoverable connectivity error writing to printer.")); + continue; + } + try + { + if (nextBytes?.Length > 0) { - await Task.Delay(100); + WriteToBinaryWriter(nextBytes); + taskSource?.SetResult(true); } - }) - ); - - if (!IsConnected) - { - taskSource.SetException(new IOException($"Unrecoverable connectivity error writing to printer.")); - continue; - } - try - { - if (nextBytes?.Length > 0) + else + { + taskSource?.SetResult(false); + } + writeSuccess = true; + } + catch (IOException ex) { - WriteToBinaryWriter(nextBytes); - taskSource.SetResult(true); + // Thrown if the printer times out the socket connection + // default is 90 seconds + taskSource?.TrySetException(ex); + //Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Swallowing IOException... sometimes happens with network printers. Should get reconnected automatically."); } - else + catch (Exception ex) { - taskSource.SetResult(false); + taskSource?.TrySetException(ex); + //Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Swallowing generic read exception... sometimes happens with serial port printers."); } } - catch (IOException ex) - { - // Thrown if the printer times out the socket connection - // default is 90 seconds - taskSource.TrySetException(ex); - //Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Swallowing IOException... sometimes happens with network printers. Should get reconnected automatically."); - } - catch (Exception ex) - { - taskSource.TrySetException(ex); - //Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Swallowing generic read exception... sometimes happens with serial port printers."); - } + while (!isAwaitableWrite && !writeSuccess); } Logging.Logger?.LogDebug("[{Function}]:[{PrinterName}] Write Long-Running Task Cancellation was requested.", $"{this}.{MethodBase.GetCurrentMethod().Name}", PrinterName); @@ -182,7 +190,7 @@ public virtual void Write(params byte[][] byteArrays) /// public virtual void Write(byte[] bytes) { - _ = WriteAsync(bytes); + _writeBuffer.Add((bytes, null)); } /// From 2615da4ddec65249d2b7650ddaf26893969badeb Mon Sep 17 00:00:00 2001 From: panot-hong Date: Thu, 21 Nov 2024 00:35:16 +0700 Subject: [PATCH 7/8] Expose SerialPort from SerialPrinter --- ESCPOS_NET/Printers/SerialPrinter.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ESCPOS_NET/Printers/SerialPrinter.cs b/ESCPOS_NET/Printers/SerialPrinter.cs index a9a6041..2c3e2e9 100644 --- a/ESCPOS_NET/Printers/SerialPrinter.cs +++ b/ESCPOS_NET/Printers/SerialPrinter.cs @@ -8,6 +8,11 @@ public class SerialPrinter : BasePrinter { private readonly SerialPort _serialPort; + /// + /// Expose to be reused elsewhere as Serial Port will be held open. + /// + public SerialPort SerialPort => _serialPort; + public SerialPrinter(string portName, int baudRate) : base() { From 6db9b057cbf6640e51f0cbc7e5b06e3456fd186f Mon Sep 17 00:00:00 2001 From: panot-hong Date: Thu, 21 Nov 2024 15:08:02 +0700 Subject: [PATCH 8/8] update deps version --- ESCPOS_NET/ESCPOS_NET.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ESCPOS_NET/ESCPOS_NET.csproj b/ESCPOS_NET/ESCPOS_NET.csproj index 1ed71d2..e442415 100644 --- a/ESCPOS_NET/ESCPOS_NET.csproj +++ b/ESCPOS_NET/ESCPOS_NET.csproj @@ -32,10 +32,10 @@ - + - +