diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..a5a9d5fe --- /dev/null +++ b/.editorconfig @@ -0,0 +1,213 @@ +# EditorConfig for Visual Studio 2022: https://learn.microsoft.com/en-us/visualstudio/ide/create-portable-custom-editor-options?view=vs-2022 + +# This is a top-most .editorconfig file +root = true + +#===================================================== +# +# nanoFramework specific settings +# +# +#===================================================== +[*] +# Generic EditorConfig settings +end_of_line = crlf +charset = utf-8-bom + +# Visual Studio spell checker +spelling_languages = en-us +spelling_checkable_types = strings,identifiers,comments +spelling_error_severity = information +spelling_exclusion_path = spelling_exclusion.dic + +#===================================================== +# +# Settings copied from the .NET runtime +# +# https://github.com/dotnet/runtime +# +#===================================================== +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +# Generated code +[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] +generated_code = true + +# C# files +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true:none +csharp_preserve_single_line_statements = false:none +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = false:none +csharp_style_prefer_switch_expression = true:suggestion +dotnet_style_readonly_field = true:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_collection_expression = when_types_exactly_match +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +csharp_prefer_simple_default_expression = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Other features +csharp_style_prefer_index_operator = false:none +csharp_style_prefer_range_operator = false:none +csharp_style_pattern_local_over_anonymous_function = false:none + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# License header +file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +[*.{csproj,vbproj,proj,nativeproj,locproj}] +charset = utf-8 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# YAML config files +[*.{yml,yaml}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd,bat}] +end_of_line = crlf \ No newline at end of file diff --git a/nanoFramework.Tools.DebugLibrary.Net/PortDefinitions/PortBase.cs b/nanoFramework.Tools.DebugLibrary.Net/PortDefinitions/PortBase.cs index 8a63ed73..b4a9cd1f 100644 --- a/nanoFramework.Tools.DebugLibrary.Net/PortDefinitions/PortBase.cs +++ b/nanoFramework.Tools.DebugLibrary.Net/PortDefinitions/PortBase.cs @@ -1,18 +1,20 @@ -// -// Copyright (c) .NET Foundation and Contributors -// See LICENSE file in the project root for full license information. -// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using nanoFramework.Tools.Debugger.PortComposite; using nanoFramework.Tools.Debugger.PortSerial; using nanoFramework.Tools.Debugger.PortTcpIp; -using System.Collections.Generic; namespace nanoFramework.Tools.Debugger { // write intellisense documentation for this class public abstract partial class PortBase : PortMessageBase { + protected PortBase() + { + NanoFrameworkDevices = NanoFrameworkDevices.Instance; + } #region creating serial instances diff --git a/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/NanoFrameworkDevices.cs b/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/NanoFrameworkDevices.cs index 985eddf2..d16d6965 100644 --- a/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/NanoFrameworkDevices.cs +++ b/nanoFramework.Tools.DebugLibrary.Shared/DeviceConfiguration/NanoFrameworkDevices.cs @@ -1,4 +1,7 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; using System.Collections.ObjectModel; namespace nanoFramework.Tools.Debugger @@ -10,7 +13,13 @@ public class NanoFrameworkDevices : ObservableCollection public static NanoFrameworkDevices Instance { - get { return _instance.Value; } + get + { + lock (_instance) + { + return _instance.Value; + } + } } private NanoFrameworkDevices() { } diff --git a/nanoFramework.Tools.DebugLibrary.Shared/NFDevice/GlobalExclusiveDeviceAccess.cs b/nanoFramework.Tools.DebugLibrary.Shared/NFDevice/GlobalExclusiveDeviceAccess.cs new file mode 100644 index 00000000..ccb76b2c --- /dev/null +++ b/nanoFramework.Tools.DebugLibrary.Shared/NFDevice/GlobalExclusiveDeviceAccess.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using nanoFramework.Tools.Debugger.PortTcpIp; + +namespace nanoFramework.Tools.Debugger.NFDevice +{ + /// + /// Code that wants to access a device should use this system-wide exclusive access while + /// communicating to a device to prevent that another nanoFramework tool also wants to + /// communicate with the device. + /// + public static class GlobalExclusiveDeviceAccess + { + #region Fields + /// + /// Base name for the system-wide mutex that controls access to a device connected to a COM port. + /// + private const string MutexBaseName = "276545121198496AADD346A60F14EF8D_"; + #endregion + + #region Methods + /// + /// Communicate with a serial device and ensure the code to be executed as exclusive access to the device. + /// + /// The serial port the device is connected to. + /// Code to execute while having exclusive access to the device + /// Maximum time in milliseconds to wait for exclusive access + /// Cancellation token that can be cancelled to stop/abort running the . + /// This method does not stop/abort execution of after it has been started. + /// Indicates whether the has been executed. Returns false if exclusive access + /// cannot be obtained within , or if was cancelled + /// before the has been started. + public static bool CommunicateWithDevice(string serialPort, Action communication, int millisecondsTimeout = Timeout.Infinite, CancellationToken? cancellationToken = null) + { + return DoCommunicateWithDevice(serialPort, communication, millisecondsTimeout, cancellationToken); + } + + /// + /// Communicate with a device accessible via the network and ensure the code to be executed as exclusive access to the device. + /// + /// The network address the device is connected to. + /// Code to execute while having exclusive access to the device + /// Maximum time in milliseconds to wait for exclusive access + /// Cancellation token that can be cancelled to stop/abort running the . + /// This method does not stop/abort execution of after it has been started. + /// Indicates whether the has been executed. Returns false if exclusive access + /// cannot be obtained within , or if was cancelled + /// before the has been started. + public static bool CommunicateWithDevice(NetworkDeviceInformation address, Action communication, int millisecondsTimeout = Timeout.Infinite, CancellationToken? cancellationToken = null) + { + return DoCommunicateWithDevice($"{address.Host}:{address.Port}", communication, millisecondsTimeout, cancellationToken); + } + #endregion + + #region Implementation + private static bool DoCommunicateWithDevice(string connectionKey, Action communication, int millisecondsTimeout, CancellationToken? cancellationToken) + { + for (bool retry = true; retry;) + { + retry = false; + + var waitHandles = new List(); + var mutex = new Mutex(false, $"{MutexBaseName}{connectionKey}"); + waitHandles.Add(mutex); + + CancellationTokenSource timeOutToken = null; + if (millisecondsTimeout > 0 && millisecondsTimeout != Timeout.Infinite) + { + timeOutToken = new CancellationTokenSource(millisecondsTimeout); + waitHandles.Add(timeOutToken.Token.WaitHandle); + } + + if (cancellationToken.HasValue) + { + waitHandles.Add(cancellationToken.Value.WaitHandle); + } + + try + { + if (WaitHandle.WaitAny(waitHandles.ToArray()) == 0) + { + communication(); + return true; + } + } + catch (AbandonedMutexException) + { + // While this process is waiting on a mutex, the process that owned the mutex has been terminated + // without properly releasing the mutex. + // Try again, if this is the only remaining process it will re-create the mutex and get exclusive access. + retry = true; + } + finally + { + mutex.ReleaseMutex(); + timeOutToken?.Dispose(); + } + } + + return false; + } + #endregion + } +} diff --git a/nanoFramework.Tools.DebugLibrary.Shared/PortComposite/PortCompositeDeviceManager.cs b/nanoFramework.Tools.DebugLibrary.Shared/PortComposite/PortCompositeDeviceManager.cs index 0b163c1a..8e861cea 100644 --- a/nanoFramework.Tools.DebugLibrary.Shared/PortComposite/PortCompositeDeviceManager.cs +++ b/nanoFramework.Tools.DebugLibrary.Shared/PortComposite/PortCompositeDeviceManager.cs @@ -1,10 +1,9 @@ -// -// Copyright (c) .NET Foundation and Contributors -// See LICENSE file in the project root for full license information. -// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; @@ -20,8 +19,6 @@ public PortCompositeDeviceManager( IEnumerable ports, bool startDeviceWatchers = true) { - NanoFrameworkDevices = NanoFrameworkDevices.Instance; - _ports.AddRange(ports); SubscribeToPortEvents(); @@ -52,21 +49,27 @@ private void OnLogMessageAvailable(object sender, StringEventArgs e) private void OnPortDeviceEnumerationCompleted(object sender, EventArgs e) { - DeviceEnumerationCompleted?.Invoke(this, EventArgs.Empty); + IsDevicesEnumerationComplete = (from p in _ports + where p.IsDevicesEnumerationComplete + select p).Any(); + if (IsDevicesEnumerationComplete) + { + DeviceEnumerationCompleted?.Invoke(this, EventArgs.Empty); + } } /// - /// This API is not available in - public override void AddDevice(string deviceId) + /// This API is not available in PortCompositeDeviceManager. + public override NanoDeviceBase AddDevice(string deviceId) { - _ports.ForEach(p => - { - p.AddDevice(deviceId); - }); + // None of the Port*Manager has a check whether deviceId matches the ID handled by the manager, + // so we don't know how to add a device here. + throw new NotImplementedException(); } public override void StartDeviceWatchers() { + IsDevicesEnumerationComplete = false; _ports.ForEach(p => p.StartDeviceWatchers()); } @@ -77,6 +80,7 @@ public override void StopDeviceWatchers() public override void ReScanDevices() { + IsDevicesEnumerationComplete = false; Task.Run(() => { _ports.ForEach(p => p.ReScanDevices()); diff --git a/nanoFramework.Tools.DebugLibrary.Shared/PortDefinitions/IPort.cs b/nanoFramework.Tools.DebugLibrary.Shared/PortDefinitions/IPort.cs index 53a4e813..0a90788f 100644 --- a/nanoFramework.Tools.DebugLibrary.Shared/PortDefinitions/IPort.cs +++ b/nanoFramework.Tools.DebugLibrary.Shared/PortDefinitions/IPort.cs @@ -1,7 +1,5 @@ -// -// Copyright (c) .NET Foundation and Contributors -// See LICENSE file in the project root for full license information. -// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using System; @@ -9,6 +7,12 @@ namespace nanoFramework.Tools.Debugger { public interface IPort { + /// + /// Gets the Instance ID of the port that is unique among all ports + /// (regardless of the type of port). + /// + string InstanceId { get; } + int AvailableBytes { get; } int SendBuffer(byte[] buffer); diff --git a/nanoFramework.Tools.DebugLibrary.Shared/PortDefinitions/PortBase.cs b/nanoFramework.Tools.DebugLibrary.Shared/PortDefinitions/PortBase.cs index 330cbd61..4e480ae4 100644 --- a/nanoFramework.Tools.DebugLibrary.Shared/PortDefinitions/PortBase.cs +++ b/nanoFramework.Tools.DebugLibrary.Shared/PortDefinitions/PortBase.cs @@ -1,7 +1,5 @@ -// -// Copyright (c) .NET Foundation and Contributors -// See LICENSE file in the project root for full license information. -// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; @@ -52,13 +50,15 @@ public string PersistName /// public bool IsDevicesEnumerationComplete { get; internal set; } = false; - public NanoFrameworkDevices NanoFrameworkDevices { get; protected set; } + public NanoFrameworkDevices NanoFrameworkDevices { get; } /// - /// Adds a new device to list of NanoFrameworkDevices. + /// Adds a new device to list of NanoFrameworkDevices. /// - /// The serial port name where the device is connected. - public abstract void AddDevice(string deviceId); + /// The unique ID (based on the connection properties) of the device. + /// The device with the unique ID that is added or (if it was already discovered before) retrieved + /// from the list of devices. Returns if no device has been added. + public abstract NanoDeviceBase AddDevice(string deviceId); /// /// Starts the device watchers. diff --git a/nanoFramework.Tools.DebugLibrary.Shared/PortSerial/DeviceWatcher.cs b/nanoFramework.Tools.DebugLibrary.Shared/PortSerial/DeviceWatcher.cs index a01d5e44..69d7d92e 100644 --- a/nanoFramework.Tools.DebugLibrary.Shared/PortSerial/DeviceWatcher.cs +++ b/nanoFramework.Tools.DebugLibrary.Shared/PortSerial/DeviceWatcher.cs @@ -1,11 +1,17 @@ -using Microsoft.Win32; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32; +using nanoFramework.Tools.Debugger.NFDevice; namespace nanoFramework.Tools.Debugger.PortSerial { @@ -43,6 +49,17 @@ public class DeviceWatcher : IDisposable /// public event EventDeviceRemoved Removed; + /// + /// Represents a delegate method that is used to handle the AllNewDevicesAdded event. + /// + /// The object that raised the event. + public delegate void EventAllNewDevicesAdded(object sender); + + /// + /// Raised when all newly discovered devices have been added + /// + public event EventAllNewDevicesAdded AllNewDevicesAdded; + /// /// Gets or sets the status of the device watcher. /// @@ -60,13 +77,15 @@ public DeviceWatcher(PortSerialManager owner) /// /// Starts the device watcher. /// - public void Start() + /// The collection of serial ports to ignore when searching for devices. + /// Changes in the collection after the start of the device watcher are taken into account. + public void Start(ICollection portsToExclude = null) { if (!_started) { _threadWatch = new Thread(() => { - StartWatcher(); + StartWatcher(portsToExclude ?? []); }) { IsBackground = true, @@ -77,21 +96,30 @@ public void Start() } } - private void StartWatcher() + private void StartWatcher(ICollection portsToExclude) { _ownerManager.OnLogMessageAvailable($"PortSerial device watcher started @ Thread {_threadWatch.ManagedThreadId} [ProcessID: {Process.GetCurrentProcess().Id}]"); - _ports = new List(); + _ports = []; _started = true; + int newPortsDetected = 0; + var newPortsDetectedLock = new object(); + Status = DeviceWatcherStatus.Started; while (_started) { try { - var ports = GetPortNames(); + var ports = new List(); + lock (portsToExclude) + { + ports.AddRange(from p in GetPortNames() + where !portsToExclude.Contains(p) + select p); + } // check for ports that departed List portsToRemove = new(); @@ -120,10 +148,36 @@ private void StartWatcher() if (!_ports.Contains(port)) { _ports.Add(port); - Added?.Invoke(this, port); + if (Added is not null) + { + if (PortSerialManager.GetRegisteredDevice(port) is null) + { + lock (newPortsDetectedLock) + { + newPortsDetected++; + } + + Task.Run(async () => + { + // Force true async running + await Task.Yield(); + GlobalExclusiveDeviceAccess.CommunicateWithDevice( + port, + () => Added.Invoke(this, port) + ); + lock (newPortsDetectedLock) + { + if (--newPortsDetected == 0) + { + AllNewDevicesAdded?.Invoke(this); + } + } + + }); + } + } } } - Thread.Sleep(200); } #if DEBUG @@ -145,17 +199,16 @@ private void StartWatcher() /// /// Gets the list of serial ports. /// - /// The list of serial ports. - public List GetPortNames() + /// The list of serial ports that may be connected to a nanoDevice. + public static List GetPortNames() { - return RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? GetPortNames_Linux() : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? GetPortNames_OSX() : RuntimeInformation.IsOSPlatform(OSPlatform.Create("FREEBSD")) ? GetPortNames_FreeBSD() : GetPortNames_Windows(); } - private List GetPortNames_Linux() + private static List GetPortNames_Linux() { List ports = new List(); @@ -176,7 +229,7 @@ private List GetPortNames_Linux() return ports; } - private List GetPortNames_OSX() + private static List GetPortNames_OSX() { List ports = new List(); @@ -213,7 +266,7 @@ private List GetPortNames_OSX() return ports; } - private List GetPortNames_FreeBSD() + private static List GetPortNames_FreeBSD() { List ports = new List(); @@ -236,13 +289,49 @@ private List GetPortNames_FreeBSD() return ports; } - private List GetPortNames_Windows() + private static List GetPortNames_Windows() { const string FindFullPathPattern = @"\\\\\?\\([\w]*)#([\w&]*)#([\w&]*)"; const string RegExPattern = @"\\Device\\([a-zA-Z]*)(\d)"; List portNames = new List(); try { + // discard known system and other rogue devices + bool IsSpecialPort(string deviceFullPath) + { + if (deviceFullPath is not null) + { + // make it upper case for comparison + string deviceFULLPATH = deviceFullPath.ToUpperInvariant(); + + if ( + deviceFULLPATH.StartsWith(@"\\?\ACPI") || + + // reported in https://github.com/nanoframework/Home/issues/332 + // COM ports from Broadcom 20702 Bluetooth adapter + deviceFULLPATH.Contains(@"VID_0A5C+PID_21E1") || + + // reported in https://nanoframework.slack.com/archives/C4MGGBH1P/p1531660736000055?thread_ts=1531659631.000021&cid=C4MGGBH1P + // COM ports from Broadcom 20702 Bluetooth adapter + deviceFULLPATH.Contains(@"VID&00010057_PID&0023") || + + // reported in Discord channel + deviceFULLPATH.Contains(@"VID&0001009E_PID&400A") || + + // this seems to cover virtual COM ports from Bluetooth devices + deviceFULLPATH.Contains("BTHENUM") || + + // this seems to cover virtual COM ports by ELTIMA + deviceFULLPATH.Contains("EVSERIAL") + ) + { + // don't even bother with this one + return true; + } + } + return false; + } + // Gets the list of supposed open ports RegistryKey allPorts = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DEVICEMAP\SERIALCOMM"); RegistryKey deviceFullPaths = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\COM Name Arbiter\Devices"); @@ -271,7 +360,8 @@ private List GetPortNames_Windows() if (portKeyInfo != null) { string portName = (string)allPorts.GetValue(port); - if (portName != null) + if (portName != null + && !IsSpecialPort((string)deviceFullPaths.GetValue(portName))) { portNames.Add(portName); } @@ -281,29 +371,32 @@ private List GetPortNames_Windows() else { string portName = (string)allPorts.GetValue(port); - if (portName != null) + string deviceFullPath = (string)deviceFullPaths.GetValue(portName); + if (deviceFullPath != null) { + if (IsSpecialPort(deviceFullPath)) + { + // don't even bother with this one + continue; + } + // Get the full qualified name of the device - string deviceFullPath = (string)deviceFullPaths.GetValue(portName); - if (deviceFullPath != null) + var devicePathDetail = Regex.Match(deviceFullPath.Replace("+", "&"), FindFullPathPattern); + if ((devicePathDetail.Success) && (devicePathDetail.Groups.Count == 4)) { - var devicePathDetail = Regex.Match(deviceFullPath.Replace("+", "&"), FindFullPathPattern); - if ((devicePathDetail.Success) && (devicePathDetail.Groups.Count == 4)) - { - string devicePath = deviceFullPath.Split('#')[1]; + string devicePath = deviceFullPath.Split('#')[1]; - RegistryKey device = Registry.LocalMachine.OpenSubKey($"SYSTEM\\CurrentControlSet\\Enum\\{devicePathDetail.Groups[1]}\\{devicePath}\\{devicePathDetail.Groups[3]}"); - if (device != null) + RegistryKey device = Registry.LocalMachine.OpenSubKey($"SYSTEM\\CurrentControlSet\\Enum\\{devicePathDetail.Groups[1]}\\{devicePath}\\{devicePathDetail.Groups[3]}"); + if (device != null) + { + string service = (string)device.GetValue("Service"); + if (service != null) { - string service = (string)device.GetValue("Service"); - if (service != null) + activePorts = Registry.LocalMachine.OpenSubKey($"SYSTEM\\CurrentControlSet\\Services\\{service}\\Enum"); + if (activePorts != null) { - activePorts = Registry.LocalMachine.OpenSubKey($"SYSTEM\\CurrentControlSet\\Services\\{service}\\Enum"); - if (activePorts != null) - { - // If the device is still plugged, it should appear as valid here, if not present, it means, the device has been disconnected - portNames.Add(portName); - } + // If the device is still plugged, it should appear as valid here, if not present, it means, the device has been disconnected + portNames.Add(portName); } } } diff --git a/nanoFramework.Tools.DebugLibrary.Shared/PortSerial/PortSerialManager.cs b/nanoFramework.Tools.DebugLibrary.Shared/PortSerial/PortSerialManager.cs index af9b38a8..f5110570 100644 --- a/nanoFramework.Tools.DebugLibrary.Shared/PortSerial/PortSerialManager.cs +++ b/nanoFramework.Tools.DebugLibrary.Shared/PortSerial/PortSerialManager.cs @@ -1,11 +1,6 @@ -// -// Copyright (c) .NET Foundation and Contributors -// See LICENSE file in the project root for full license information. -// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Win32; -using nanoFramework.Tools.Debugger.WireProtocol; -using Polly; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -14,6 +9,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using nanoFramework.Tools.Debugger.WireProtocol; +using Polly; namespace nanoFramework.Tools.Debugger.PortSerial { @@ -27,10 +24,6 @@ public partial class PortSerialManager : PortBase // counter of device watchers completed private int _deviceWatchersCompletedCount = 0; - // counter of device watchers completed - private int _newDevicesCount = 0; - private readonly object _newDevicesCountLock = new object(); - private readonly Random _delay = new Random(DateTime.Now.Millisecond); private readonly ConcurrentDictionary _devicesCache = new ConcurrentDictionary(); @@ -40,9 +33,12 @@ public partial class PortSerialManager : PortBase /// /// Creates an Serial debug client /// + /// Indicates whether to start the device watcher. + /// The collection of serial ports to ignore when searching for devices. + /// Changes in the collection after the start of the device watcher are taken into account. + /// public PortSerialManager(bool startDeviceWatchers = true, List portExclusionList = null, int bootTime = 3000) { - NanoFrameworkDevices = NanoFrameworkDevices.Instance; _deviceWatcher = new(this); BootTime = bootTime; @@ -66,8 +62,6 @@ public PortSerialManager(bool startDeviceWatchers = true, List portExclu public override void ReScanDevices() { - _newDevicesCount = 0; - // need to reset this here to have intimidate effect IsDevicesEnumerationComplete = false; @@ -101,6 +95,7 @@ private void InitializeDeviceWatchers() { _deviceWatcher.Added += OnDeviceAdded; _deviceWatcher.Removed += OnDeviceRemoved; + _deviceWatcher.AllNewDevicesAdded += ProcessDeviceEnumerationComplete; } public void StartSerialDeviceWatchers() @@ -116,11 +111,10 @@ private void StartDeviceWatchersInternal() { // Start all device watchers - _deviceWatcher.Start(); + _deviceWatcher.Start(PortExclusionList); _watchersStarted = true; - _deviceWatchersCompletedCount = 0; IsDevicesEnumerationComplete = false; } @@ -146,8 +140,13 @@ private void StopDeviceWatchersInternal() private void NanoFrameworkDevicesRemoveAllSerial() { + List devicesToRemove; + // also clear nanoFramework devices list - var devicesToRemove = NanoFrameworkDevices.Select(nanoDevice => ((NanoDevice)nanoDevice).DeviceId).ToList(); + lock (NanoFrameworkDevices) + { + devicesToRemove = NanoFrameworkDevices.Select(nanoDevice => ((NanoDevice)nanoDevice).DeviceId).ToList(); + } foreach (var deviceId in devicesToRemove) { @@ -158,140 +157,153 @@ private void NanoFrameworkDevicesRemoveAllSerial() #endregion #region Methods to manage device list add, remove, etc + /// + /// Get the device that communicates via the serial port, provided it has been added to the + /// list of known devices. + /// + /// The port name of the device to get. + /// The that communicates via the serial port, or if the device is not found. + public static NanoDeviceBase GetRegisteredDevice(string portName) + { + if (!string.IsNullOrWhiteSpace(portName)) + { + var devices = NanoFrameworkDevices.Instance; + lock (devices) + { + return devices.FirstOrDefault(d => (d as NanoDevice)?.DeviceId == portName); + } + } + return null; + } /// /// Adds a new device to list of NanoFrameworkDevices. /// /// The serial port name where the device is connected. - public override void AddDevice(string deviceId) + /// The device with the unique ID that is added or (if it was already discovered before) retrieved + /// from the list of devices. Returns if no device has been added. + public override NanoDeviceBase AddDevice(string deviceId) { - AddDeviceToListAsync(deviceId); + return AddDeviceToListAsync(deviceId); } /// - /// Creates a and adds it to the list of devices. + /// Creates a and adds it to the list of devices. /// /// The AQS used to find this device - private void AddDeviceToListAsync(string deviceId) + private NanoDeviceBase AddDeviceToListAsync(string deviceId) { // search the nanoFramework device list for a device with a matching interface ID var nanoFrameworkDeviceMatch = FindNanoFrameworkDevice(deviceId); // Add the device if it's new - if (nanoFrameworkDeviceMatch == null) + if (nanoFrameworkDeviceMatch is null) { OnLogMessageAvailable(NanoDevicesEventSource.Log.CandidateDevice(deviceId)); - if (nanoFrameworkDeviceMatch == null) - { - // Create a new element for this device and... - var newNanoFrameworkDevice = new NanoDevice(); - newNanoFrameworkDevice.DeviceId = deviceId; - newNanoFrameworkDevice.ConnectionPort = new PortSerial(this, newNanoFrameworkDevice); - newNanoFrameworkDevice.Transport = TransportType.Serial; + // Create a new element for this device and... + var newNanoFrameworkDevice = new NanoDevice(); + newNanoFrameworkDevice.DeviceId = deviceId; + newNanoFrameworkDevice.ConnectionPort = new PortSerial(this, newNanoFrameworkDevice); + newNanoFrameworkDevice.Transport = TransportType.Serial; - var connectResult = newNanoFrameworkDevice.ConnectionPort.ConnectDevice(); + var connectResult = newNanoFrameworkDevice.ConnectionPort.ConnectDevice(); - if (connectResult == ConnectPortResult.Unauthorized) + if (connectResult == ConnectPortResult.Unauthorized) + { + OnLogMessageAvailable(NanoDevicesEventSource.Log.UnauthorizedAccessToDevice(deviceId)); + } + else if (connectResult == ConnectPortResult.Connected) + { + if (CheckValidNanoFrameworkSerialDevice(newNanoFrameworkDevice)) { - OnLogMessageAvailable(NanoDevicesEventSource.Log.UnauthorizedAccessToDevice(deviceId)); + //add device to the collection + NanoFrameworkDeviceAdd(newNanoFrameworkDevice); + + OnLogMessageAvailable(NanoDevicesEventSource.Log.ValidDevice($"{newNanoFrameworkDevice.Description}")); + nanoFrameworkDeviceMatch = newNanoFrameworkDevice; } - else if (connectResult == ConnectPortResult.Connected) + else { - if (CheckValidNanoFrameworkSerialDevice(newNanoFrameworkDevice)) - { - //add device to the collection - NanoFrameworkDeviceAdd(newNanoFrameworkDevice); + // disconnect + newNanoFrameworkDevice.Disconnect(); - OnLogMessageAvailable(NanoDevicesEventSource.Log.ValidDevice($"{newNanoFrameworkDevice.Description}")); - } - else - { - // disconnect - newNanoFrameworkDevice.Disconnect(); + // devices powered by the USB cable and that feature a serial converter (like an FTDI chip) + // are still booting when the USB enumeration event raises + // so need to give them enough time for the boot sequence to complete before trying to communicate with them - // devices powered by the USB cable and that feature a serial converter (like an FTDI chip) - // are still booting when the USB enumeration event raises - // so need to give them enough time for the boot sequence to complete before trying to communicate with them + // Failing to connect to debugger engine on first attempt occurs frequently on dual USB devices like ESP32 WROVER KIT. + // Seems to be something related with both devices using the same USB endpoint + // Another reason is that an ESP32 takes around 3 seconds to complete the boot sequence and launch the CLR. + // Until then the device will look non responsive or invalid to the detection mechanism that we're using. + // A nice workaround for this seems to be adding an extra random wait so the comms are not simultaneous. - // Failing to connect to debugger engine on first attempt occurs frequently on dual USB devices like ESP32 WROVER KIT. - // Seems to be something related with both devices using the same USB endpoint - // Another reason is that an ESP32 takes around 3 seconds to complete the boot sequence and launch the CLR. - // Until then the device will look non responsive or invalid to the detection mechanism that we're using. - // A nice workaround for this seems to be adding an extra random wait so the comms are not simultaneous. - - int delay; - lock (_delay) - { - delay = _delay.Next(200, 600); - } + int delay; + lock (_delay) + { + delay = _delay.Next(200, 600); + } - Thread.Sleep(BootTime + delay); + Thread.Sleep(BootTime + delay); - OnLogMessageAvailable(NanoDevicesEventSource.Log.CheckingValidDevice($" {newNanoFrameworkDevice.DeviceId} *** 2nd attempt ***")); + OnLogMessageAvailable(NanoDevicesEventSource.Log.CheckingValidDevice($" {newNanoFrameworkDevice.DeviceId} *** 2nd attempt ***")); - connectResult = newNanoFrameworkDevice.ConnectionPort.ConnectDevice(); + connectResult = newNanoFrameworkDevice.ConnectionPort.ConnectDevice(); - if (connectResult == ConnectPortResult.Unauthorized) - { - OnLogMessageAvailable(NanoDevicesEventSource.Log.UnauthorizedAccessToDevice(deviceId)); - } - else if (connectResult == ConnectPortResult.Connected) + if (connectResult == ConnectPortResult.Unauthorized) + { + OnLogMessageAvailable(NanoDevicesEventSource.Log.UnauthorizedAccessToDevice(deviceId)); + } + else if (connectResult == ConnectPortResult.Connected) + { + if (CheckValidNanoFrameworkSerialDevice(newNanoFrameworkDevice, true)) { - if (CheckValidNanoFrameworkSerialDevice(newNanoFrameworkDevice, true)) - { - NanoFrameworkDeviceAdd(newNanoFrameworkDevice); + NanoFrameworkDeviceAdd(newNanoFrameworkDevice); - OnLogMessageAvailable(NanoDevicesEventSource.Log.ValidDevice($"{newNanoFrameworkDevice.Description}")); - } - else - { - OnLogMessageAvailable(NanoDevicesEventSource.Log.QuitDevice(deviceId)); - } + OnLogMessageAvailable(NanoDevicesEventSource.Log.ValidDevice($"{newNanoFrameworkDevice.Description}")); } else { OnLogMessageAvailable(NanoDevicesEventSource.Log.QuitDevice(deviceId)); } } + else + { + OnLogMessageAvailable(NanoDevicesEventSource.Log.QuitDevice(deviceId)); + } } - else - { - OnLogMessageAvailable(NanoDevicesEventSource.Log.QuitDevice(deviceId)); - } - - // subtract devices count - lock (_newDevicesCountLock) - { - _newDevicesCount--; - } - - - // check if we are done processing arriving devices - if (_newDevicesCount == 0) - { - ProcessDeviceEnumerationComplete(); - } + } + else + { + OnLogMessageAvailable(NanoDevicesEventSource.Log.QuitDevice(deviceId)); } } + return nanoFrameworkDeviceMatch; } /// - /// add device to the collection (if new) + /// Adds a device to the collection (if new). /// - /// new NanoSerialDevice + /// The new private void NanoFrameworkDeviceAdd(NanoDevice newNanoFrameworkDevice) { - if (newNanoFrameworkDevice != null && NanoFrameworkDevices.OfType>().Count(i => i.DeviceId == newNanoFrameworkDevice.DeviceId) == 0) + lock (NanoFrameworkDevices) { - //add device to the collection - NanoFrameworkDevices.Add(newNanoFrameworkDevice); + if (newNanoFrameworkDevice != null && NanoFrameworkDevices.OfType>().Count(i => i.DeviceId == newNanoFrameworkDevice.DeviceId) == 0) + { + //add device to the collection + NanoFrameworkDevices.Add(newNanoFrameworkDevice); + } } } public override void DisposeDevice(string instanceId) { - var deviceToDispose = NanoFrameworkDevices.FirstOrDefault(nanoDevice => ((NanoDevice)nanoDevice).DeviceId == instanceId); + NanoDeviceBase deviceToDispose; + lock (NanoFrameworkDevices) + { + deviceToDispose = NanoFrameworkDevices.FirstOrDefault(nanoDevice => ((NanoDevice)nanoDevice).DeviceId == instanceId); + } if (deviceToDispose != null) { @@ -306,24 +318,46 @@ private void RemoveDeviceFromList(string deviceId) { OnLogMessageAvailable(NanoDevicesEventSource.Log.DeviceDeparture(deviceId)); - // get devices and remove them from collection - NanoFrameworkDevices.OfType>() - .Where(i => i.DeviceId == deviceId).ToList() - .ForEach(RemoveNanoFrameworkDevices); + List> devices; + lock (NanoFrameworkDevices) + { + // get devices + devices = NanoFrameworkDevices.OfType>() + .Where(i => i.DeviceId == deviceId).ToList(); + } + + // remove them from collection + devices.ForEach(RemoveNanoFrameworkDevices); } private void RemoveNanoFrameworkDevices(NanoDevice device) { - NanoFrameworkDevices.Remove(device); - device?.DebugEngine?.StopProcessing(); - device?.DebugEngine?.Dispose(); + if (device is null) + { + return; + } + + lock (NanoFrameworkDevices) + { + NanoFrameworkDevices.Remove(device); + } + + // get rid of debug engine, if that was created + device.DebugEngine?.StopProcessing(); + device.DebugEngine?.Dispose(); + + // disconnect device in order to free port + device.Disconnect(true); } private NanoDeviceBase FindNanoFrameworkDevice(string deviceId) { if (deviceId != null) { - return NanoFrameworkDevices.FirstOrDefault(d => (d as NanoDevice).DeviceId == deviceId); + lock (NanoFrameworkDevices.Instance) + { + return NanoFrameworkDevices.FirstOrDefault(d => (d as NanoDevice)?.DeviceId == deviceId); + } } return null; @@ -333,7 +367,7 @@ private NanoDeviceBase FindNanoFrameworkDevice(string deviceId) /// Remove the device from the device list /// /// - /// + /// private void OnDeviceRemoved(object sender, string serialPort) { RemoveDeviceFromList(serialPort); @@ -344,72 +378,27 @@ private void OnDeviceRemoved(object sender, string serialPort) /// This function will add the device to the listOfDevices /// /// - /// + /// private void OnDeviceAdded(object sender, string serialPort) { // check against exclusion list - if (PortExclusionList.Contains(serialPort)) + bool exclude; + lock (PortExclusionList) { - OnLogMessageAvailable(NanoDevicesEventSource.Log.DroppingDeviceToExclude(serialPort)); - return; + exclude = PortExclusionList.Contains(serialPort); } - - // discard known system and other rogue devices - RegistryKey portKeyInfo = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\COM Name Arbiter\Devices"); - if (portKeyInfo != null) + if (exclude) { - var portInfo = (string)portKeyInfo.GetValue(serialPort); - - if (portInfo != null) - { - Debug.WriteLine($"{nameof(OnDeviceAdded)}: port {serialPort}, portinfo: {portInfo}"); - - // make it upper case for comparison - portInfo = portInfo.ToUpperInvariant(); - - if ( - portInfo.StartsWith(@"\\?\ACPI") || - - // reported in https://github.com/nanoframework/Home/issues/332 - // COM ports from Broadcom 20702 Bluetooth adapter - portInfo.Contains(@"VID_0A5C+PID_21E1") || - - // reported in https://nanoframework.slack.com/archives/C4MGGBH1P/p1531660736000055?thread_ts=1531659631.000021&cid=C4MGGBH1P - // COM ports from Broadcom 20702 Bluetooth adapter - portInfo.Contains(@"VID&00010057_PID&0023") || - - // reported in Discord channel - portInfo.Contains(@"VID&0001009E_PID&400A") || - - // this seems to cover virtual COM ports from Bluetooth devices - portInfo.Contains("BTHENUM") || - - // this seems to cover virtual COM ports by ELTIMA - portInfo.Contains("EVSERIAL") - ) - { - OnLogMessageAvailable(NanoDevicesEventSource.Log.DroppingDeviceToExclude(serialPort)); - - // don't even bother with this one - return; - } - } + OnLogMessageAvailable(NanoDevicesEventSource.Log.DroppingDeviceToExclude(serialPort)); + return; } OnLogMessageAvailable(NanoDevicesEventSource.Log.DeviceArrival(serialPort)); - lock (_newDevicesCountLock) - { - _newDevicesCount++; - } - - Task.Run(() => - { - Policy.Handle() - .WaitAndRetry(10, retryCount => TimeSpan.FromMilliseconds((retryCount * retryCount) * 25), - onRetry: (exception, delay, retryCount, context) => LogRetry(exception, delay, retryCount, context)) - .Execute(() => AddDeviceToListAsync(serialPort)); - }); + Policy.Handle() + .WaitAndRetry(10, retryCount => TimeSpan.FromMilliseconds((retryCount * retryCount) * 25), + onRetry: (exception, delay, retryCount, context) => LogRetry(exception, delay, retryCount, context)) + .Execute(() => AddDeviceToListAsync(serialPort)); } private void LogRetry(Exception exception, TimeSpan delay, object retryCount, object context) @@ -425,12 +414,23 @@ private void LogRetry(Exception exception, TimeSpan delay, object retryCount, ob #region Handlers and events for Device Enumeration Complete - private void ProcessDeviceEnumerationComplete() + private void ProcessDeviceEnumerationComplete(object sender) { - OnLogMessageAvailable(NanoDevicesEventSource.Log.SerialDeviceEnumerationCompleted(NanoFrameworkDevices.Count)); + int count; + lock (NanoFrameworkDevices) + { + if (IsDevicesEnumerationComplete) + { + // Nothing has changed + return; + } + // all watchers have completed enumeration + IsDevicesEnumerationComplete = true; + + count = NanoFrameworkDevices.OfType>().Count(); + } - // all watchers have completed enumeration - IsDevicesEnumerationComplete = true; + OnLogMessageAvailable(NanoDevicesEventSource.Log.SerialDeviceEnumerationCompleted(count)); // fire event that Serial enumeration is complete OnDeviceEnumerationCompleted(); diff --git a/nanoFramework.Tools.DebugLibrary.Shared/PortTcpIp/DeviceWatcher.cs b/nanoFramework.Tools.DebugLibrary.Shared/PortTcpIp/DeviceWatcher.cs index 205f234a..260b4d5c 100644 --- a/nanoFramework.Tools.DebugLibrary.Shared/PortTcpIp/DeviceWatcher.cs +++ b/nanoFramework.Tools.DebugLibrary.Shared/PortTcpIp/DeviceWatcher.cs @@ -1,7 +1,5 @@ -// -// Copyright (c) .NET Foundation and Contributors -// See LICENSE file in the project root for full license information. -// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; @@ -9,6 +7,8 @@ using System.Net.Sockets; using System.Text; using System.Threading; +using System.Threading.Tasks; +using nanoFramework.Tools.Debugger.NFDevice; namespace nanoFramework.Tools.Debugger.PortTcpIp { @@ -148,7 +148,21 @@ private void ProcessDiscoveryMessage(string message) switch (command) { case CommandDeviceStart: - Added?.Invoke(this, new NetworkDeviceInformation(host, port)); + if (Added is not null) + { + var info = new NetworkDeviceInformation(host, port); + if (PortTcpIpManager.GetRegisteredDevice(info) is null) + { + Task.Run(async () => + { + await Task.Yield(); // Force true async running + GlobalExclusiveDeviceAccess.CommunicateWithDevice(info, () => + { + Added.Invoke(this, info); + }); + }); + } + } break; case CommandDeviceStop: diff --git a/nanoFramework.Tools.DebugLibrary.Shared/PortTcpIp/PortTcpIp.cs b/nanoFramework.Tools.DebugLibrary.Shared/PortTcpIp/PortTcpIp.cs index 6677e5f2..071f6fb4 100644 --- a/nanoFramework.Tools.DebugLibrary.Shared/PortTcpIp/PortTcpIp.cs +++ b/nanoFramework.Tools.DebugLibrary.Shared/PortTcpIp/PortTcpIp.cs @@ -1,7 +1,5 @@ -// -// Copyright (c) .NET Foundation and Contributors -// See LICENSE file in the project root for full license information. -// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; @@ -19,7 +17,7 @@ public class PortTcpIp : PortMessageBase, IPort private NanoNetworkDevice NanoNetworkDevice => NanoDevice.Device; - private string InstanceId => NanoDevice.DeviceId; + public string InstanceId => NanoDevice.DeviceId; public override event EventHandler LogMessageAvailable; @@ -140,4 +138,4 @@ private void OnLogMessageAvailable(string message) LogMessageAvailable?.Invoke(this, new StringEventArgs(message)); } } -} \ No newline at end of file +} diff --git a/nanoFramework.Tools.DebugLibrary.Shared/PortTcpIp/PortTcpIpManager.cs b/nanoFramework.Tools.DebugLibrary.Shared/PortTcpIp/PortTcpIpManager.cs index 0227fe24..0402e724 100644 --- a/nanoFramework.Tools.DebugLibrary.Shared/PortTcpIp/PortTcpIpManager.cs +++ b/nanoFramework.Tools.DebugLibrary.Shared/PortTcpIp/PortTcpIpManager.cs @@ -1,10 +1,6 @@ -// -// Copyright (c) .NET Foundation and Contributors -// See LICENSE file in the project root for full license information. -// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. -using nanoFramework.Tools.Debugger.WireProtocol; -using Polly; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -12,6 +8,8 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using nanoFramework.Tools.Debugger.WireProtocol; +using Polly; namespace nanoFramework.Tools.Debugger.PortTcpIp { @@ -27,13 +25,14 @@ public class PortTcpIpManager : PortBase // Network device watchers started flag private bool _watchersStarted = false; - // counter of device watchers completed - private int _newDevicesCount = 0; - /// - /// Internal list with the actual nF Network devices + /// Internal list with the actual nF Network devices. + /// This must be a static list as NanoFrameworkDevices is also global. + /// Take care that all items of _networkDevices are in the NanoFrameworkDevices, + /// and that there are no devices in NanoFrameworkDevices that should be present + /// in _networkDevices but are not (and use NanoFrameworkDevices for locks). /// - private readonly List _networkDevices = new List(); + private static readonly List _networkDevices = new List(); private IEnumerable> _networkNanoFrameworkDevices => NanoFrameworkDevices.Cast>(); @@ -48,8 +47,6 @@ public PortTcpIpManager(bool startDeviceWatchers = true, int discoveryPort = Dis { _deviceWatcher = new DeviceWatcher(this, discoveryPort); - NanoFrameworkDevices = NanoFrameworkDevices.Instance; - Task.Factory.StartNew(() => { InitializeDeviceWatchers(); @@ -126,21 +123,40 @@ private void StopDeviceWatchersInternal() } // Clear the list of devices so we don't have potentially disconnected devices around - ClearDeviceEntries(); - // also clear nanoFramework devices list - var devicesToRemove = _networkNanoFrameworkDevices.Select(nanoDevice => nanoDevice.DeviceId).ToList(); + List devicesToRemove; + lock (NanoFrameworkDevices) + { + devicesToRemove = _networkNanoFrameworkDevices.Select(nanoDevice => nanoDevice.DeviceId).ToList(); + } foreach (var deviceId in devicesToRemove) { // get device... - var device = FindNanoFrameworkDevice(deviceId); - - // ... and remove it from collection - NanoFrameworkDevices.Remove(device); - - device?.DebugEngine?.StopProcessing(); - device?.DebugEngine?.Stop(true); + NanoDeviceBase device; + lock (NanoFrameworkDevices) + { + var deviceEntry = FindDevice(deviceId); + if (deviceEntry is null) + { + // this is not a TcpIp-connected device and is managed by another PortManager + continue; + } + + // ... and remove it from collection + _networkDevices.Remove(deviceEntry); + + device = FindNanoFrameworkDevice(deviceId); + if (device is null) + { + continue; + } + + // ... and remove it from collection + NanoFrameworkDevices.Remove(device); + } + device.DebugEngine?.StopProcessing(); + device.DebugEngine?.Stop(true); } _watchersStarted = false; @@ -150,69 +166,91 @@ private void StopDeviceWatchersInternal() #region Methods to manage device list add, remove, etc + /// + /// Get the device that communicates via the network port, provided it has been added to the + /// list of known devices. + /// + /// The name of the network device. + /// + public static NanoDeviceBase GetRegisteredDevice(NetworkDeviceInformation networkDevice) + { + if (networkDevice is not null) + { + var devices = NanoFrameworkDevices.Instance; + lock (devices) + { + return devices.FirstOrDefault(d => (d as NanoDevice)?.DeviceId == networkDevice.DeviceId); + } + } + return null; + } /// /// Creates a DeviceListEntry for a device and adds it to the list of devices /// - private void AddDeviceToListAsync(NetworkDeviceInformation networkDevice) + private (NanoDeviceBase device, bool isNew) AddDeviceToListAsync(NetworkDeviceInformation networkDevice) { - // search the device list for a device with a matching interface ID - var networkMatch = FindDevice(networkDevice.DeviceId); + bool isNew = false; - // Add the device if it's new - if (networkMatch != null) return; - - OnLogMessageAvailable(NanoDevicesEventSource.Log.CandidateDevice(networkDevice.DeviceId)); + // search the device list for a device with a matching interface ID + NetworkDeviceInformation networkMatch; + NanoDeviceBase nanoFrameworkDeviceMatch; + lock (NanoFrameworkDevices) + { + networkMatch = FindDevice(networkDevice.DeviceId); - // search the nanoFramework device list for a device with a matching interface ID - var nanoFrameworkDeviceMatch = FindNanoFrameworkDevice(networkDevice.DeviceId); + // search the nanoFramework device list for a device with a matching interface ID + nanoFrameworkDeviceMatch = FindNanoFrameworkDevice(networkDevice.DeviceId); + } - if (nanoFrameworkDeviceMatch != null) return; + // Add the device if it's new + if (networkMatch is null && nanoFrameworkDeviceMatch is null) + { + OnLogMessageAvailable(NanoDevicesEventSource.Log.CandidateDevice(networkDevice.DeviceId)); - // Create a new element for this device and... - var newNanoFrameworkDevice = new NanoDevice(); - newNanoFrameworkDevice.DeviceId = networkDevice.DeviceId; - newNanoFrameworkDevice.ConnectionPort = new PortTcpIp(this, newNanoFrameworkDevice, networkDevice); - newNanoFrameworkDevice.Transport = TransportType.TcpIp; + // Create a new element for this device and... + var newNanoFrameworkDevice = new NanoDevice(); + newNanoFrameworkDevice.DeviceId = networkDevice.DeviceId; + newNanoFrameworkDevice.ConnectionPort = new PortTcpIp(this, newNanoFrameworkDevice, networkDevice); + newNanoFrameworkDevice.Transport = TransportType.TcpIp; - var connectResult = newNanoFrameworkDevice.ConnectionPort.ConnectDevice(); + var connectResult = newNanoFrameworkDevice.ConnectionPort.ConnectDevice(); - if (connectResult == ConnectPortResult.Unauthorized) - { - OnLogMessageAvailable( - NanoDevicesEventSource.Log.UnauthorizedAccessToDevice(networkDevice.DeviceId)); - } - else if (connectResult == ConnectPortResult.Connected) - { - if (CheckValidNanoFrameworkNetworkDevice(newNanoFrameworkDevice)) + if (connectResult == ConnectPortResult.Unauthorized) { - //add device to the collection - NanoFrameworkDevices.Add(newNanoFrameworkDevice); - - _networkDevices.Add(networkDevice); - OnLogMessageAvailable( - NanoDevicesEventSource.Log.ValidDevice($"{newNanoFrameworkDevice.Description}")); + NanoDevicesEventSource.Log.UnauthorizedAccessToDevice(networkDevice.DeviceId)); + } + else if (connectResult == ConnectPortResult.Connected) + { + if (CheckValidNanoFrameworkNetworkDevice(newNanoFrameworkDevice)) + { + lock (NanoFrameworkDevices) + { + //add device to the collection + NanoFrameworkDevices.Add(newNanoFrameworkDevice); + _networkDevices.Add(networkDevice); + } + + OnLogMessageAvailable( + NanoDevicesEventSource.Log.ValidDevice($"{newNanoFrameworkDevice.Description}")); + + nanoFrameworkDeviceMatch = newNanoFrameworkDevice; + isNew = true; + } + else + { + // disconnect + newNanoFrameworkDevice.Disconnect(); + } } else { - // disconnect - newNanoFrameworkDevice.Disconnect(); + OnLogMessageAvailable(NanoDevicesEventSource.Log.QuitDevice(networkDevice.DeviceId)); } } - else - { - OnLogMessageAvailable(NanoDevicesEventSource.Log.QuitDevice(networkDevice.DeviceId)); - } - - // subtract devices count - _newDevicesCount--; - // check if we are done processing arriving devices - if (_newDevicesCount == 0) - { - ProcessDeviceEnumerationComplete(); - } + return (nanoFrameworkDeviceMatch, isNew); } public override void DisposeDevice(string instanceId) @@ -226,32 +264,39 @@ public override void DisposeDevice(string instanceId) private void RemoveDeviceFromList(NetworkDeviceInformation networkDevice) { // Removes the device entry from the internal list; therefore the UI - var deviceEntry = FindDevice(networkDevice.DeviceId); - OnLogMessageAvailable(NanoDevicesEventSource.Log.DeviceDeparture(networkDevice.DeviceId)); - _networkDevices.Remove(deviceEntry); - // get device... - var device = FindNanoFrameworkDevice(networkDevice.DeviceId); + NanoDeviceBase device; + lock (NanoFrameworkDevices) + { + var deviceEntry = FindDevice(networkDevice.DeviceId); + if (deviceEntry != null) + { + _networkDevices.Remove(deviceEntry); + } - // ... and remove it from collection - NanoFrameworkDevices.Remove(device); + device = FindNanoFrameworkDevice(networkDevice.DeviceId); + if (device != null) + { + // ... and remove it from collection + NanoFrameworkDevices.Remove(device); + } + } + // get rid of debug engine, if that was created device?.DebugEngine?.StopProcessing(); device?.DebugEngine?.Dispose(); - } - private void ClearDeviceEntries() - { - _networkDevices.Clear(); + // disconnect device + device?.Disconnect(true); } /// /// Searches through the existing list of devices for the first DeviceListEntry that has /// the specified device Id. /// - internal NetworkDeviceInformation FindDevice(string deviceId) => + private NetworkDeviceInformation FindDevice(string deviceId) => _networkDevices.FirstOrDefault(d => d.DeviceId == deviceId); private NanoDeviceBase FindNanoFrameworkDevice(string deviceId) => @@ -272,9 +317,12 @@ private void OnDeviceAdded(object sender, NetworkDeviceInformation networkDevice { OnLogMessageAvailable(NanoDevicesEventSource.Log.DeviceArrival(networkDevice.DeviceId)); - _newDevicesCount++; + var (_, isNew) = AddDeviceToListAsync(networkDevice); - Task.Run(() => { AddDeviceToListAsync(networkDevice); }); + if (isNew && !IsDevicesEnumerationComplete) + { + ProcessDeviceEnumerationComplete(); + } } #endregion @@ -284,11 +332,16 @@ private void OnDeviceAdded(object sender, NetworkDeviceInformation networkDevice private void ProcessDeviceEnumerationComplete() { - OnLogMessageAvailable( - NanoDevicesEventSource.Log.SerialDeviceEnumerationCompleted(NanoFrameworkDevices.Count)); + int count; + lock (NanoFrameworkDevices) + { + IsDevicesEnumerationComplete = true; + count = NanoFrameworkDevices.OfType>().Count(); + } - // all watchers have completed enumeration - IsDevicesEnumerationComplete = true; + // TODO: count are not serial devices + OnLogMessageAvailable( + NanoDevicesEventSource.Log.SerialDeviceEnumerationCompleted(count)); // fire event that Network enumeration is complete OnDeviceEnumerationCompleted(); @@ -494,7 +547,8 @@ internal void OnLogMessageAvailable(string message) LogMessageAvailable?.Invoke(this, new StringEventArgs(message)); } - public override void AddDevice(string deviceId) + /// + public override NanoDeviceBase AddDevice(string deviceId) { // expected format is "tcpip://{Host}:{Port}" @@ -504,9 +558,9 @@ public override void AddDevice(string deviceId) throw new ArgumentException("Invalid tcpip format."); } - AddDeviceToListAsync(new NetworkDeviceInformation( + return AddDeviceToListAsync(new NetworkDeviceInformation( match.Groups["host"].Value, - int.Parse(match.Groups["port"].Value))); + int.Parse(match.Groups["port"].Value))).device; } /// @@ -517,4 +571,4 @@ public override void AddDevice(string deviceId) #endregion } -} \ No newline at end of file +} diff --git a/nanoFramework.Tools.DebugLibrary.Shared/WireProtocol/Engine.cs b/nanoFramework.Tools.DebugLibrary.Shared/WireProtocol/Engine.cs index 858deae8..ecee39d3 100644 --- a/nanoFramework.Tools.DebugLibrary.Shared/WireProtocol/Engine.cs +++ b/nanoFramework.Tools.DebugLibrary.Shared/WireProtocol/Engine.cs @@ -1,12 +1,6 @@ -// -// Copyright (c) .NET Foundation and Contributors -// Portions Copyright (c) Microsoft Corporation. All rights reserved. -// See LICENSE file in the project root for full license information. -// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. -using nanoFramework.Tools.Debugger.Extensions; -using nanoFramework.Tools.Debugger.WireProtocol; -using Polly; using System; using System.Collections; using System.Collections.Generic; @@ -16,6 +10,9 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using nanoFramework.Tools.Debugger.Extensions; +using nanoFramework.Tools.Debugger.WireProtocol; +using Polly; namespace nanoFramework.Tools.Debugger { @@ -77,6 +74,14 @@ public partial class Engine : IDisposable, IControllerHostLocal internal Engine(NanoDeviceBase device) { + lock (_syncReqLockForPort) + { + if (!_syncReqLockForPort.TryGetValue(device.ConnectionPort.InstanceId, out _syncReqLock)) + { + _syncReqLockForPort[device.ConnectionPort.InstanceId] = _syncReqLock = new SemaphoreSlim(1, 1); + } + } + InitializeLocal(device); // default to false @@ -619,9 +624,10 @@ public void Dispose() /// /// Global lock object for synchronizing message request. This ensures there is only one - /// outstanding request at any point of time. + /// outstanding request per device at any point of time. /// - private static readonly SemaphoreSlim _syncReqLock = new SemaphoreSlim(1, 1); + private readonly static Dictionary _syncReqLockForPort = []; + private readonly SemaphoreSlim _syncReqLock; private IncomingMessage PerformSyncRequest( uint command, @@ -821,42 +827,42 @@ public bool ProcessMessage(IncomingMessage message, bool isReply) switch (bp.Cmd) { case Commands.c_Monitor_Ping: + { + // signal that a monitor ping was received + _pingEvent.Set(); + + Commands.Monitor_Ping.Reply cmdReply = new Commands.Monitor_Ping.Reply { - // signal that a monitor ping was received - _pingEvent.Set(); + Source = Commands.Monitor_Ping.c_Ping_Source_Host, + Flags = (StopDebuggerOnConnect ? Commands.Monitor_Ping.c_Ping_DbgFlag_Stop : 0) + }; - Commands.Monitor_Ping.Reply cmdReply = new Commands.Monitor_Ping.Reply - { - Source = Commands.Monitor_Ping.c_Ping_Source_Host, - Flags = (StopDebuggerOnConnect ? Commands.Monitor_Ping.c_Ping_DbgFlag_Stop : 0) - }; - - PerformRequestAsync( - new OutgoingMessage( - _controlller.GetNextSequenceId(), - message, - _controlller.CreateConverter(), - Flags.c_NonCritical, - cmdReply) - ); - - return true; - } + PerformRequestAsync( + new OutgoingMessage( + _controlller.GetNextSequenceId(), + message, + _controlller.CreateConverter(), + Flags.c_NonCritical, + cmdReply) + ); - case Commands.c_Monitor_Message: - { - Commands.Monitor_Message payload = message.Payload as Commands.Monitor_Message; + return true; + } - Debug.Assert(payload != null); + case Commands.c_Monitor_Message: + { + Commands.Monitor_Message payload = message.Payload as Commands.Monitor_Message; - if (payload != null) - { - Task.Factory.StartNew(() => _eventMessage?.Invoke(message, payload.ToString())); - } + Debug.Assert(payload != null); - return true; + if (payload != null) + { + Task.Factory.StartNew(() => _eventMessage?.Invoke(message, payload.ToString())); } + return true; + } + case Commands.c_Debugging_Messaging_Query: Debug.Assert(message.Payload != null); Task.Factory.StartNew(() => RpcReceiveQuery(message, (Commands.Debugging_Messaging_Query)message.Payload)); diff --git a/nanoFramework.Tools.DebugLibrary.Shared/nanoFramework.Tools.DebugLibrary.Net.projitems b/nanoFramework.Tools.DebugLibrary.Shared/nanoFramework.Tools.DebugLibrary.Net.projitems index 7c5cb259..d95bcc7e 100644 --- a/nanoFramework.Tools.DebugLibrary.Shared/nanoFramework.Tools.DebugLibrary.Net.projitems +++ b/nanoFramework.Tools.DebugLibrary.Shared/nanoFramework.Tools.DebugLibrary.Net.projitems @@ -67,6 +67,7 @@ + diff --git a/spelling_exclusion.dic b/spelling_exclusion.dic new file mode 100644 index 00000000..8c0e1f8c --- /dev/null +++ b/spelling_exclusion.dic @@ -0,0 +1 @@ +nano