diff --git a/Packages/Tracking/CHANGELOG.md b/Packages/Tracking/CHANGELOG.md index cdc993a4d..73351ac3e 100644 --- a/Packages/Tracking/CHANGELOG.md +++ b/Packages/Tracking/CHANGELOG.md @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [docs-website]: https://docs.ultraleap.com/unity-api/ "Ultraleap Docs" +## [Next] + +### Fixed +- Fixed a number of File Descriptor and native handle leaks in `Connection` that could leads to resource exhaustion on repeated editor play/stop. +- Fixed errorneous "LeapProvider not assigned" log messages during teardown. + +### Changed +- Deprecated `Dispose()` on `Connection` and made this a thin shim to `Stop()` to enable better cleanup of resources. +- Added timeout to `Connection.Stop()` to reduce hang time on teardown. + ## [7.3.0] - 25/02/2026 ### Added diff --git a/Packages/Tracking/Core/Runtime/Plugins/LeapCSharp/Connection.cs b/Packages/Tracking/Core/Runtime/Plugins/LeapCSharp/Connection.cs index 2c6b681df..f0474a59f 100644 --- a/Packages/Tracking/Core/Runtime/Plugins/LeapCSharp/Connection.cs +++ b/Packages/Tracking/Core/Runtime/Plugins/LeapCSharp/Connection.cs @@ -17,6 +17,11 @@ namespace LeapInternal public class Connection { + // Timeout used both for the PollConnection wait inside the worker thread and + // for the Join wait when stopping it. They match because Stop is essentially + // waiting for one final Poll cycle to complete after CloseConnection signals it. + private const uint DEFAULT_TIMEOUT_MILLISECONDS = 150; + public struct Key { public readonly int connectionId; @@ -59,6 +64,28 @@ static Connection() long palmOffset = Marshal.OffsetOf(typeof(LEAP_HAND), "palm").ToInt64(); _handPositionOffset = Marshal.OffsetOf(typeof(LEAP_PALM), "position").ToInt64() + palmOffset; _handOrientationOffset = Marshal.OffsetOf(typeof(LEAP_PALM), "orientation").ToInt64() + palmOffset; + + // Stop every pooled connection on domain unload / process exit so native + // LeapC handles and worker threads are released deterministically. + // Registered once here rather than per-Start() to avoid handler accumulation. + AppDomain.CurrentDomain.DomainUnload += (s, e) => StopAll(); + AppDomain.CurrentDomain.ProcessExit += (s, e) => StopAll(); + } + + private static void StopAll() + { + foreach (var conn in connectionDictionary.Values) + { + try + { + conn.Stop(); + } + catch + { + // Ignore all errors during shutdown. + } + } + connectionDictionary.Clear(); } public Key ConnectionKey { get; private set; } @@ -143,34 +170,14 @@ public event EventHandler LeapConnection public Action LeapBeginProfilingBlock; public Action LeapEndProfilingBlock; - private bool _disposed = false; - private bool _loggedNullDeviceWarningForGetInterpolatedFrame = false; private bool _loggedNullDeviceWarningForGetInterpolatedFrameSize = false; - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + [Obsolete("Dispose is deprecated; Call Stop() instead.")] + public void Dispose() => Stop(); - // Protected implementation of Dispose pattern. - protected virtual void Dispose(bool disposing) - { - if (_disposed) - return; - - Stop(); - LeapC.DestroyConnection(_leapConnection); - _leapConnection = IntPtr.Zero; - - _disposed = true; - } - - ~Connection() - { - Dispose(false); - } + [Obsolete("Dispose is deprecated; Call Stop() instead.")] + protected virtual void Dispose(bool disposing) => Stop(); private Connection(Key connectionKey) { @@ -184,15 +191,21 @@ private Connection(Key connectionKey) public void Start(string serverNamespace = "Leap Service", bool multiDeviceAware = true, bool enableFiducialMarkers = false) { - LEAP_CONNECTION_CONFIG config = new LEAP_CONNECTION_CONFIG(); - config.server_namespace = Marshal.StringToHGlobalAnsi(serverNamespace); - config.flags = 0; - if (multiDeviceAware) - config.flags |= (uint)eLeapConnectionFlag.eLeapConnectionFlag_MultipleDevicesAware; - if (enableFiducialMarkers) - config.flags |= (uint)eLeapConnectionFlag.eLeapConnectionFlag_FiducialTracking; - config.size = (uint)Marshal.SizeOf(config); - Start(config); + LEAP_CONNECTION_CONFIG config = new LEAP_CONNECTION_CONFIG + { + server_namespace = Marshal.StringToHGlobalAnsi(serverNamespace), + flags = (multiDeviceAware ? (uint)eLeapConnectionFlag.eLeapConnectionFlag_MultipleDevicesAware : 0u) + | (enableFiducialMarkers ? (uint)eLeapConnectionFlag.eLeapConnectionFlag_FiducialTracking : 0u), + size = (uint)Marshal.SizeOf() + }; + try + { + Start(config); + } + finally + { + Marshal.FreeHGlobal(config.server_namespace); + } } public void Start(LEAP_CONNECTION_CONFIG config) @@ -247,7 +260,6 @@ public void Start(LEAP_CONNECTION_CONFIG config) LeapC.SetAllocator(_leapConnection, ref _pLeapAllocator); _isRunning = true; - AppDomain.CurrentDomain.DomainUnload += (arg1, arg2) => Dispose(true); _polster = new Thread(new ThreadStart(this.processMessages)); _polster.Name = "LeapC Worker"; @@ -257,26 +269,25 @@ public void Start(LEAP_CONNECTION_CONFIG config) public void Stop() { - if (!_isRunning) - return; - _isRunning = false; - //Very important to close the connection before we try to join the - //worker thread! The call to PollConnection can sometimes block, - //despite the timeout, causing an attempt to join the thread waiting - //forever and preventing the connection from stopping. - // - //It seems that closing the connection causes PollConnection to - //unblock in these cases, so just make sure to close the connection - //before trying to join the worker thread. - LeapC.CloseConnection(_leapConnection); - - _polster.Join(); + if (_leapConnection != IntPtr.Zero) + { + // Close the connection before joining the worker thread. PollConnection + // can block past its timeout; closing the connection unblocks it so the + // join doesn't hang. + LeapC.CloseConnection(_leapConnection); + _polster?.Join((int)DEFAULT_TIMEOUT_MILLISECONDS); + _polster = null; + + // Always destroy the connection to avoid leaking native handle. + LeapC.DestroyConnection(_leapConnection); + _leapConnection = IntPtr.Zero; + } } /// - /// Returns the version of the currently installed Tracking Service. + /// Returns the version of the currently installed Tracking Service. /// Might return 0.0.0 if no device is connected or it cannot get the current version. /// /// the current tracking service version @@ -308,9 +319,7 @@ private void processMessages() } LEAP_CONNECTION_MESSAGE _msg = new LEAP_CONNECTION_MESSAGE(); - uint timeout = 150; - - result = LeapC.PollConnection(_leapConnection, timeout, ref _msg); + result = LeapC.PollConnection(_leapConnection, DEFAULT_TIMEOUT_MILLISECONDS, ref _msg); if (result != eLeapRS.eLeapRS_Success) { @@ -407,6 +416,11 @@ private void processMessages() } } //while running } + catch (ThreadAbortException) + { + // Expected to occur during shutdown. + _isRunning = false; + } catch (Exception e) { Logger.Log("Exception: " + e); @@ -1005,48 +1019,48 @@ static public eLeapPolicyFlag FlagForPolicy(Controller.PolicyFlag singlePolicy) /// public static bool IsConnectionAvailable(string serverNamespace = "Leap Service") { - LEAP_CONNECTION_CONFIG config = new LEAP_CONNECTION_CONFIG(); - config.server_namespace = Marshal.StringToHGlobalAnsi(serverNamespace); - config.flags = 0; - config.size = (uint)Marshal.SizeOf(config); - - IntPtr tempConnection; - - eLeapRS result; - - result = LeapC.CreateConnection(ref config, out tempConnection); - - if (result != eLeapRS.eLeapRS_Success || tempConnection == IntPtr.Zero) + LEAP_CONNECTION_CONFIG config = new LEAP_CONNECTION_CONFIG { - LeapC.CloseConnection(tempConnection); - return false; - } + server_namespace = Marshal.StringToHGlobalAnsi(serverNamespace), + flags = 0, + size = (uint)Marshal.SizeOf() + }; - result = LeapC.OpenConnection(tempConnection); - - if (result != eLeapRS.eLeapRS_Success) + IntPtr tempConnection = IntPtr.Zero; + try { - LeapC.CloseConnection(tempConnection); - return false; - } + if (LeapC.CreateConnection(ref config, out tempConnection) != eLeapRS.eLeapRS_Success + || tempConnection == IntPtr.Zero) + { + return false; + } + + if (LeapC.OpenConnection(tempConnection) != eLeapRS.eLeapRS_Success) + { + return false; + } - LEAP_CONNECTION_MESSAGE _msg = new LEAP_CONNECTION_MESSAGE(); - uint timeout = 150; - result = LeapC.PollConnection(tempConnection, timeout, ref _msg); + LEAP_CONNECTION_MESSAGE _msg = new LEAP_CONNECTION_MESSAGE(); + LeapC.PollConnection(tempConnection, DEFAULT_TIMEOUT_MILLISECONDS, ref _msg); - LEAP_CONNECTION_INFO pInfo = new LEAP_CONNECTION_INFO(); - pInfo.size = (uint)Marshal.SizeOf(pInfo); - result = LeapC.GetConnectionInfo(tempConnection, ref pInfo); + LEAP_CONNECTION_INFO pInfo = new LEAP_CONNECTION_INFO + { + size = (uint)Marshal.SizeOf() + }; + LeapC.GetConnectionInfo(tempConnection, ref pInfo); - if (pInfo.status == eLeapConnectionStatus.eLeapConnectionStatus_Connected) - { - LeapC.CloseConnection(tempConnection); - return true; + return pInfo.status == eLeapConnectionStatus.eLeapConnectionStatus_Connected; } + finally + { + if (tempConnection != IntPtr.Zero) + { + LeapC.CloseConnection(tempConnection); + LeapC.DestroyConnection(tempConnection); + } - LeapC.CloseConnection(tempConnection); - - return false; + Marshal.FreeHGlobal(config.server_namespace); + } } /// @@ -1203,11 +1217,11 @@ public FailedDeviceList FailedDevices /// /// Subscribes to the events coming from an individual device - /// + /// /// If this is not called, only the primary device will be subscribed. - /// Will automatically unsubscribe the primary device if this is called - /// on a secondary device, but not a primary one. - /// + /// Will automatically unsubscribe the primary device if this is called + /// on a secondary device, but not a primary one. + /// /// @since 4.1 /// public void SubscribeToDeviceEvents(Device device) @@ -1218,9 +1232,9 @@ public void SubscribeToDeviceEvents(Device device) /// /// Unsubscribes from the events coming from an individual device - /// + /// /// This can be called safely, even if the device has not been subscribed. - /// + /// /// @since 4.1 /// public void UnsubscribeFromDeviceEvents(Device device) @@ -1262,7 +1276,7 @@ public UnityEngine.Vector3 PixelToRectilinearEx(IntPtr deviceHandle, /// /// Converts from image-space pixel coordinates to camera-space rectilinear coordinates - /// + /// /// Also allows specifying a specific device handle and calibration type. /// public UnityEngine.Vector3 PixelToRectilinearEx(IntPtr deviceHandle, @@ -1294,7 +1308,7 @@ public UnityEngine.Vector3 RectilinearToPixel(Image.CameraType camera, UnityEngi /// /// Converts from camera-space rectilinear coordinates to image-space pixel coordinates - /// + /// /// Also allows specifying a specific device handle and calibration type. /// public UnityEngine.Vector3 RectilinearToPixelEx(IntPtr deviceHandle, diff --git a/Packages/Tracking/Core/Runtime/Plugins/LeapCSharp/Controller.cs b/Packages/Tracking/Core/Runtime/Plugins/LeapCSharp/Controller.cs index 5dac907de..23c20f6f8 100644 --- a/Packages/Tracking/Core/Runtime/Plugins/LeapCSharp/Controller.cs +++ b/Packages/Tracking/Core/Runtime/Plugins/LeapCSharp/Controller.cs @@ -41,7 +41,6 @@ public class Controller : IController { Connection _connection; - bool _disposed = false; string _serverNamespace = "Leap Service"; bool _supportsMultipleDevices = true; bool _supportsFiducialMarkers = false; @@ -430,12 +429,10 @@ public void Dispose() // Protected implementation of Dispose pattern. protected virtual void Dispose(bool disposing) { - if (_disposed) - { - return; - } - _connection.Dispose(); - _disposed = true; + _connection.LeapInit -= OnInit; + _connection.LeapConnection -= OnConnect; + _connection.LeapConnectionLost -= OnDisconnect; + _connection.Stop(); } /// diff --git a/Packages/Tracking/Core/Runtime/Scripts/Utils/HandUtils.cs b/Packages/Tracking/Core/Runtime/Scripts/Utils/HandUtils.cs index 313afe336..acc691c39 100644 --- a/Packages/Tracking/Core/Runtime/Scripts/Utils/HandUtils.cs +++ b/Packages/Tracking/Core/Runtime/Scripts/Utils/HandUtils.cs @@ -36,6 +36,9 @@ public static class Hands /// If the search gets to LeapProviders, return providers that are not sources of static data - e.g. hand pose viewers private static void AssignBestLeapProvider(bool preferLiveLeapProviderOverStaticHandPoseProviders = false) { + // True only if never assigned; bypasses Unity's destroyed-equals-null check. + bool wasNeverAssigned = ReferenceEquals(s_provider, null); + // Fall through to the best available Leap Provider if none is assigned if (s_provider == null) { @@ -80,7 +83,10 @@ private static void AssignBestLeapProvider(bool preferLiveLeapProviderOverStatic } } - Debug.Log("LeapProvider was not assigned. Auto assigning: " + s_provider); + if (wasNeverAssigned && s_provider != null) + { + Debug.Log("LeapProvider was not assigned. Auto assigning: " + s_provider); + } } ///