diff --git a/Source/v2/Meadow.Cli/Meadow.CLI.csproj b/Source/v2/Meadow.Cli/Meadow.CLI.csproj index 9cee05e5..7f9c4f62 100644 --- a/Source/v2/Meadow.Cli/Meadow.CLI.csproj +++ b/Source/v2/Meadow.Cli/Meadow.CLI.csproj @@ -10,7 +10,7 @@ Wilderness Labs, Inc Wilderness Labs, Inc true - 2.0.44.0 + 2.0.46.0 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.CLI/ https://github.com/WildernessLabs/Meadow.CLI diff --git a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs index 9d681764..75a3506c 100644 --- a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs +++ b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs @@ -6,5 +6,5 @@ namespace Meadow.CLI; public static class Constants { - public const string CLI_VERSION = "2.0.44.0"; + public const string CLI_VERSION = "2.0.46.0"; } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 9a607bd3..3c094aa4 100755 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -1,6 +1,5 @@ using System.Buffers; using System.IO.Ports; -using System.Net; using System.Security.Cryptography; namespace Meadow.Hcom; @@ -23,6 +22,7 @@ public partial class SerialConnection : ConnectionBase, IDisposable private ConnectionState _state; private readonly List _listeners = new List(); private readonly Queue _pendingCommands = new Queue(); + private readonly AutoResetEvent _commandEvent = new AutoResetEvent(false); private bool _maintainConnection; private Thread? _connectionManager = null; private readonly List _textList = new List(); @@ -77,7 +77,6 @@ private bool MaintainConnection Name = "HCOM Connection Manager" }; _connectionManager.Start(); - } } } @@ -210,6 +209,7 @@ public override void Detach() var count = _messageCount; _pendingCommands.Enqueue(command); + _commandEvent.Set(); while (timeout-- > 0) { @@ -246,6 +246,7 @@ private void CommandManager() { while (!_isDisposed) { + _commandEvent.WaitOne(); while (_pendingCommands.Count > 0) { Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); @@ -261,8 +262,6 @@ private void CommandManager() // TODO: re-queue on fail? } } - - Thread.Sleep(1000); } } @@ -298,6 +297,7 @@ public void EnqueueRequest(IRequest command) } _pendingCommands.Enqueue(command); + _commandEvent.Set(); } private void EncodeAndSendPacket(byte[] messageBytes, CancellationToken? cancellationToken = null) @@ -1226,16 +1226,16 @@ public override async Task StartDebuggingSession(int port, ILog throw new DeviceNotFoundException(); } + var debuggingServer = new DebuggingServer(this, port, logger); + + logger?.LogDebug("Tell the Debugging Server to Start Listening"); + _ = debuggingServer.StartListening(cancellationToken); + logger?.LogDebug($"Start Debugging on port: {port}"); await Device.StartDebugging(port, logger, cancellationToken); await WaitForMeadowAttach(cancellationToken); - var endpoint = new IPEndPoint(IPAddress.Loopback, port); - var debuggingServer = new DebuggingServer(this, Device, endpoint, logger); - - logger?.LogDebug("Tell the Debugging Server to Start Listening"); - await debuggingServer.StartListening(cancellationToken); return debuggingServer; } @@ -1267,15 +1267,5 @@ public override async Task SendDebuggerData(byte[] debuggerData, uint userData, _lastRequestConcluded = null; EnqueueRequest(command); - - var success = await WaitForResult(() => - { - if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) - { - return true; - } - - return false; - }, cancellationToken); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.ActiveClient.cs b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.ActiveClient.cs new file mode 100644 index 00000000..5849ab1c --- /dev/null +++ b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.ActiveClient.cs @@ -0,0 +1,185 @@ +using System.Buffers; +using System.Collections.Concurrent; +using System.Net.Sockets; +using System.Security.Cryptography; + +namespace Meadow.Hcom; + +public partial class DebuggingServer +{ + private class ActiveClient : IDisposable + { + private readonly IMeadowConnection _connection; + private readonly TcpClient _tcpClient; + private readonly NetworkStream _networkStream; + private readonly CancellationTokenSource _cts; + private readonly Task _receiveVsDebugDataTask; + private readonly Task _receiveMeadowDebugDataTask; + private readonly ILogger? _logger; + private bool _disposed; + private readonly BlockingCollection _debuggerMessages = new(); + private readonly AutoResetEvent _vsDebugDataReady = new(false); + + internal ActiveClient(IMeadowConnection connection, TcpClient tcpClient, ILogger? logger, CancellationToken? cancellationToken) + { + _cts = cancellationToken != null + ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value) + : new CancellationTokenSource(); + + _logger = logger; + _connection = connection; + _tcpClient = tcpClient; + _networkStream = tcpClient.GetStream(); + + _logger?.LogDebug("Starting receive task"); + + _connection.DebuggerMessageReceived += MeadowConnection_DebuggerMessageReceived; + + _receiveVsDebugDataTask = Task.Factory.StartNew(SendToMeadowAsync, _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + _receiveMeadowDebugDataTask = Task.Factory.StartNew(SendToVisualStudio, _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + + private void MeadowConnection_DebuggerMessageReceived(object sender, byte[] e) + { + _debuggerMessages.Add(e); + _vsDebugDataReady.Set(); + } + + private const int RECEIVE_BUFFER_SIZE = 256; + + private async Task SendToMeadowAsync() + { + try + { + using var md5 = MD5.Create(); + var receiveBuffer = ArrayPool.Shared.Rent(RECEIVE_BUFFER_SIZE); + var meadowBuffer = Array.Empty(); + + while (!_cts.Token.IsCancellationRequested) + { + if (_networkStream != null && _networkStream.CanRead) + { + int bytesRead; + do + { + bytesRead = await _networkStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length, _cts.Token); + + if (bytesRead == 0 || _cts.Token.IsCancellationRequested) + { + continue; + } + + var destIndex = meadowBuffer.Length; + Array.Resize(ref meadowBuffer, destIndex + bytesRead); + Array.Copy(receiveBuffer, 0, meadowBuffer, destIndex, bytesRead); + + _logger?.LogTrace("Received {count} bytes from VS, will forward to HCOM/Meadow. {hash}", + meadowBuffer.Length, + BitConverter.ToString(md5.ComputeHash(meadowBuffer)) + .Replace("-", string.Empty) + .ToLowerInvariant()); + + await _connection.SendDebuggerData(meadowBuffer, 0, _cts.Token); + meadowBuffer = Array.Empty(); + } while (_networkStream.DataAvailable); + } + else + { + _logger?.LogInformation("Unable to Read Data from Visual Studio"); + _logger?.LogTrace("Unable to Read Data from Visual Studio"); + } + } + } + catch (IOException ioe) + { + _logger?.LogInformation("Visual Studio has Disconnected"); + _logger?.LogTrace(ioe, "Visual Studio has Disconnected"); + } + catch (ObjectDisposedException ode) + { + _logger?.LogInformation("Visual Studio has stopped debugging"); + _logger?.LogTrace(ode, "Visual Studio has stopped debugging"); + } + catch (Exception ex) + { + _logger?.LogError($"Error receiving data from Visual Studio.\nError: {ex.Message}\nStackTrace:\n{ex.StackTrace}"); + throw; + } + } + + private async Task SendToVisualStudio() + { + try + { + while (!_cts.Token.IsCancellationRequested) + { + if (_networkStream != null && _networkStream.CanWrite) + { + _vsDebugDataReady.WaitOne(); + + while (_debuggerMessages.Count > 0) + { + var byteData = _debuggerMessages.Take(_cts.Token); + + _logger?.LogTrace("Received {count} bytes from Meadow, will forward to VS", byteData.Length); + if (!_tcpClient.Connected) + { + _logger?.LogDebug("Cannot forward data, Visual Studio is not connected"); + return; + } + + await _networkStream.WriteAsync(byteData, 0, byteData.Length, _cts.Token); + _logger?.LogTrace("Forwarded {count} bytes to VS", byteData.Length); + } + } + else + { + _logger?.LogInformation("Unable to Write Data from Visual Studio"); + } + } + } + catch (OperationCanceledException oce) + { + _logger?.LogInformation("Operation Cancelled"); + _logger?.LogTrace(oce, "Operation Cancelled"); + } + catch (Exception ex) + { + _logger?.LogError($"Error sending data to Visual Studio.\nError: {ex.Message}\nStackTrace:\n{ex.StackTrace}"); + + if (!_cts.Token.IsCancellationRequested) + { + throw; + } + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _logger?.LogTrace("Disposing ActiveClient"); + _cts.Cancel(); + try + { + Task.WhenAll(_receiveVsDebugDataTask, _receiveMeadowDebugDataTask).Wait(TimeSpan.FromSeconds(10)); + } + catch (AggregateException ex) + { + _logger?.LogError("Error waiting for tasks to complete during dispose", ex); + } + _tcpClient.Dispose(); + _networkStream.Dispose(); + _cts.Dispose(); + + if (_connection != null) + { + _connection.DebuggerMessageReceived -= MeadowConnection_DebuggerMessageReceived; + } + _disposed = true; + } + } +} diff --git a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs index f23373bf..ce0a097e 100644 --- a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs +++ b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs @@ -1,47 +1,40 @@ -using System.Buffers; -using System.Collections.Concurrent; -using System.Net; +using System.Net; using System.Net.Sockets; -using System.Security.Cryptography; namespace Meadow.Hcom; // This TCP server directly interacts with Visual Studio debugging. // What it receives from Visual Studio it forwards to Meadow. // What it receives from Meadow it forwards to Visual Studio. -public class DebuggingServer : IDisposable +public partial class DebuggingServer : IDisposable { // VS 2019 - 4024 // VS 2017 - 4022 // VS 2015 - 4020 - public IPEndPoint LocalEndpoint { get; private set; } private readonly object _lck = new(); private CancellationTokenSource? _cancellationTokenSource; private readonly ILogger? _logger; - private readonly IMeadowDevice _meadow; private readonly IMeadowConnection _connection; private ActiveClient? _activeClient; - private int _activeClientCount = 0; private readonly TcpListener _listener; - private Task? _listenerTask; - private bool _isReady; + private readonly Task? _listenerTask; public bool Disposed; // Constructor /// /// Create a new DebuggingServer for proxying debug data between VS and Meadow /// - /// The to debug + /// The meadow connection /// The to listen for incoming debugger connections /// The to logging state information - public DebuggingServer(IMeadowConnection connection, IMeadowDevice meadow, IPEndPoint localEndpoint, ILogger? logger) + public DebuggingServer(IMeadowConnection connection, int port, ILogger? logger) { - LocalEndpoint = localEndpoint; + _logger = logger; _connection = connection; - _meadow = meadow; - _logger = logger; - _listener = new TcpListener(LocalEndpoint); + var endPoint = new IPEndPoint(IPAddress.Loopback, port); + + _listener = new TcpListener(endPoint); } /// @@ -51,23 +44,14 @@ public DebuggingServer(IMeadowConnection connection, IMeadowDevice meadow, IPEnd /// A representing the startup operation public async Task StartListening(CancellationToken cancellationToken) { - if (cancellationToken != null) - { - _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - _listenerTask = Task.Factory.StartNew(StartListener, TaskCreationOptions.LongRunning); - var startTimeout = DateTime.UtcNow.AddSeconds(60); - while (DateTime.UtcNow < startTimeout) - { - if (_isReady) - { - return; - } + _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - await Task.Delay(100, cancellationToken); - } + _listener.Start(); + _logger?.LogInformation($"Listening for Visual Studio to connect"); - throw new Exception("DebuggingServer did not start listening within the 60 second timeout."); - } + // This call will wait for the client to connect, before continuing. We shouldn't need a loop. + var tcpClient = await _listener.AcceptTcpClientAsync(); + _activeClient = CreateActiveClient(tcpClient); } /// @@ -86,61 +70,29 @@ public async Task StopListening() } } - private async Task StartListener() - { - try - { - _listener.Start(); - LocalEndpoint = (IPEndPoint)_listener.LocalEndpoint; - _logger?.LogInformation($"Listening for Visual Studio to connect on {LocalEndpoint.Address}:{LocalEndpoint.Port}" + Environment.NewLine); - _isReady = true; - - // This call will wait for the client to connect, before continuing. We shouldn't need a loop. - TcpClient tcpClient = await _listener.AcceptTcpClientAsync(); - OnConnect(tcpClient); - } - catch (SocketException soex) - { - _logger?.LogError("A Socket error occurred. The port may already be in use. Try rebooting to free up the port."); - _logger?.LogError($"Error:\n{soex.Message} \nStack Trace:\n{soex.StackTrace}"); - } - catch (Exception ex) - { - _logger?.LogError("An unhandled exception occurred while listening for debugging connections."); - _logger?.LogError($"Error:\n{ex.Message} \nStack Trace:\n{ex.StackTrace}"); - } - } - - private void OnConnect(TcpClient tcpClient) + private ActiveClient? CreateActiveClient(TcpClient tcpClient) { try { lock (_lck) { _logger?.LogInformation("Visual Studio has Connected" + Environment.NewLine); - if (_activeClientCount > 0 && _activeClient?.Disposed == false) + + if (_activeClient != null) { _logger?.LogDebug("Closing active client"); - Debug.Assert(_activeClientCount == 1); - Debug.Assert(_activeClient != null); - CloseActiveClient(); + _activeClient?.Dispose(); + _activeClient = null; } - _activeClient = new ActiveClient(_connection, tcpClient, _logger, _cancellationTokenSource?.Token); - _activeClientCount++; + return new ActiveClient(_connection, tcpClient, _logger, _cancellationTokenSource?.Token); } } catch (Exception ex) { _logger?.LogError(ex, "An error occurred while connecting to Visual Studio"); } - } - - internal void CloseActiveClient() - { - _activeClient?.Dispose(); - _activeClient = null; - _activeClientCount = 0; + return null; } public void Dispose() @@ -157,192 +109,4 @@ public void Dispose() Disposed = true; } } - - // Embedded class - private class ActiveClient : IDisposable - { - private readonly IMeadowConnection _connection; - private readonly TcpClient _tcpClient; - private readonly NetworkStream _networkStream; - - private readonly CancellationTokenSource _cts; - private readonly Task _receiveVsDebugDataTask; - private readonly Task _receiveMeadowDebugDataTask; - private readonly ILogger? _logger; - public bool Disposed = false; - private readonly BlockingCollection _debuggerMessages = new(); - - // Constructor - internal ActiveClient(IMeadowConnection connection, TcpClient tcpClient, ILogger? logger, CancellationToken? cancellationToken) - { - if (cancellationToken != null) - { - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value); - } - else - { - _cts = new CancellationTokenSource(); - } - - _logger = logger; - _connection = connection; - _tcpClient = tcpClient; - _networkStream = tcpClient.GetStream(); - _logger?.LogDebug("Starting receive task"); - _receiveVsDebugDataTask = Task.Factory.StartNew(SendToMeadowAsync, TaskCreationOptions.LongRunning); - _receiveMeadowDebugDataTask = Task.Factory.StartNew(SendToVisualStudio, TaskCreationOptions.LongRunning); - - _connection.DebuggerMessageReceived += MeadowConnection_DebuggerMessageReceived; - } - - private void MeadowConnection_DebuggerMessageReceived(object sender, byte[] e) - { - _logger?.LogDebug("Debugger Message Received, Adding to collection"); - _debuggerMessages.Add(e); - } - - private const int RECEIVE_BUFFER_SIZE = 256; - - private async Task SendToMeadowAsync() - { - try - { - using var md5 = MD5.Create(); - // Receive from Visual Studio and send to Meadow - var receiveBuffer = ArrayPool.Shared.Rent(RECEIVE_BUFFER_SIZE); - var meadowBuffer = Array.Empty(); - - while (!_cts.IsCancellationRequested) - { - if (_networkStream != null && _networkStream.CanRead) - { - int bytesRead; - do - { - bytesRead = await _networkStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length, _cts.Token); - - if (bytesRead == 0 || _cts.IsCancellationRequested) - { - continue; - } - - var destIndex = meadowBuffer.Length; - Array.Resize(ref meadowBuffer, destIndex + bytesRead); - Array.Copy(receiveBuffer, 0, meadowBuffer, destIndex, bytesRead); - - // Forward the RECIEVE_BUFFER_SIZE chunk to Meadow immediately - _logger?.LogTrace("Received {count} bytes from VS, will forward to HCOM/Meadow. {hash}", - meadowBuffer.Length, - BitConverter.ToString(md5.ComputeHash(meadowBuffer)) - .Replace("-", string.Empty) - .ToLowerInvariant()); - - await _connection.SendDebuggerData(meadowBuffer, 0, _cts.Token); - meadowBuffer = Array.Empty(); - - // Ensure we read all the data in this message before passing it along - // I'm not sure this is actually needed, the whole message should get read at once. - } while (_networkStream.DataAvailable); - } - else - { - // User probably hit stop - _logger?.LogInformation("Unable to Read Data from Visual Studio"); - _logger?.LogTrace("Unable to Read Data from Visual Studio"); - } - } - } - catch (IOException ioe) - { - // VS client probably died - _logger?.LogInformation("Visual Studio has Disconnected" + Environment.NewLine); - _logger?.LogTrace(ioe, "Visual Studio has Disconnected"); - } - catch (ObjectDisposedException ode) - { - // User probably hit stop - _logger?.LogInformation("Visual Studio has stopped debugging" + Environment.NewLine); - _logger?.LogTrace(ode, "Visual Studio has stopped debugging"); - } - catch (Exception ex) - { - _logger?.LogError($"Error receiving data from Visual Studio.{Environment.NewLine}Error: {ex.Message}{Environment.NewLine}StackTrace:{Environment.NewLine}{ex.StackTrace}"); - throw; - } - } - - private async Task SendToVisualStudio() - { - try - { - while (!_cts.IsCancellationRequested) - { - if (_networkStream != null && _networkStream.CanWrite) - { - while (_debuggerMessages.Count > 0) - { - var byteData = _debuggerMessages.Take(_cts.Token); - _logger?.LogTrace("Received {count} bytes from Meadow, will forward to VS", byteData.Length); - if (!_tcpClient.Connected) - { - _logger?.LogDebug("Cannot forward data, Visual Studio is not connected"); - return; - } - - await _networkStream.WriteAsync(byteData, 0, byteData.Length, _cts.Token); - _logger?.LogTrace("Forwarded {count} bytes to VS", byteData.Length); - } - } - else - { - // User probably hit stop - _logger?.LogInformation("Unable to Write Data from Visual Studio"); - } - } - } - catch (OperationCanceledException oce) - { - // User probably hit stop; Removed logging as User doesn't need to see this - // Keeping it as a TODO in case we find a side effect that needs logging. - _logger?.LogInformation("Operation Cancelled"); - _logger?.LogTrace(oce, "Operation Cancelled"); - } - catch (Exception ex) - { - _logger?.LogError($"Error sending data to Visual Studio.{Environment.NewLine}Error: {ex.Message}{Environment.NewLine}StackTrace:{Environment.NewLine}{ex.StackTrace}"); - - if (_cts.IsCancellationRequested) - { - throw; - } - } - } - - public void Dispose() - { - lock (_tcpClient) - { - if (Disposed) - { - return; - } - - _logger?.LogTrace("Disposing ActiveClient"); - _cts.Cancel(false); - _receiveVsDebugDataTask.Wait(TimeSpan.FromSeconds(10)); - _receiveMeadowDebugDataTask.Wait(TimeSpan.FromSeconds(10)); - _receiveVsDebugDataTask?.Dispose(); - _receiveMeadowDebugDataTask?.Dispose(); - _tcpClient.Dispose(); - _networkStream.Dispose(); - _cts.Dispose(); - - if (_connection != null) - { - _connection.DebuggerMessageReceived -= MeadowConnection_DebuggerMessageReceived; - } - Disposed = true; - } - } - } } \ No newline at end of file