From b10b55c3f5733ba1d09c485738ff0f2e24439065 Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Tue, 10 Mar 2026 09:03:07 -0600 Subject: [PATCH] Fix deadlocks, TCP stability, and ASCOM registration issues --- ASCOM.Driver/OpenAstroTracker/LocalServer.cs | 2 +- .../OpenAstroTracker/OpenAstroTracker.csproj | 24 ++++---- .../OpenAstroTracker/SharedResources.cs | 4 ++ ASCOM.Driver/TelescopeDriver/Driver.cs | 33 ++++++++++- .../TcpCommunicationHandler.cs | 59 ++++++++++++++----- .../OatmealTelescopeCommandHandlers.cs | 4 +- 6 files changed, 93 insertions(+), 33 deletions(-) diff --git a/ASCOM.Driver/OpenAstroTracker/LocalServer.cs b/ASCOM.Driver/OpenAstroTracker/LocalServer.cs index f4fa7f4..ea563a5 100644 --- a/ASCOM.Driver/OpenAstroTracker/LocalServer.cs +++ b/ASCOM.Driver/OpenAstroTracker/LocalServer.cs @@ -397,7 +397,7 @@ private static void RegisterObjects() key.CreateSubKey("Programmable"); using (RegistryKey key2 = key.CreateSubKey("LocalServer32")) { - key2.SetValue(null, Application.ExecutablePath); + key2.SetValue(null, "\"" + Application.ExecutablePath + "\""); } } // diff --git a/ASCOM.Driver/OpenAstroTracker/OpenAstroTracker.csproj b/ASCOM.Driver/OpenAstroTracker/OpenAstroTracker.csproj index fbdd447..832f8be 100644 --- a/ASCOM.Driver/OpenAstroTracker/OpenAstroTracker.csproj +++ b/ASCOM.Driver/OpenAstroTracker/OpenAstroTracker.csproj @@ -42,7 +42,7 @@ DEBUG;TRACE prompt 4 - AnyCPU + x86 MinimumRecommendedRules.ruleset @@ -62,37 +62,37 @@ - packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Astrometry.dll + ..\packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Astrometry.dll - packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Attributes.dll + ..\packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Attributes.dll - packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Cache.dll + ..\packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Cache.dll - packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Controls.dll + ..\packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Controls.dll - packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.DeviceInterfaces.dll + ..\packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.DeviceInterfaces.dll - packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.DriverAccess.dll + ..\packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.DriverAccess.dll - packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Exceptions.dll + ..\packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Exceptions.dll - packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Internal.Extensions.dll + ..\packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Internal.Extensions.dll - packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.SettingsProvider.dll + ..\packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.SettingsProvider.dll - packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Utilities.dll + ..\packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Utilities.dll - packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Utilities.Video.dll + ..\packages\ASCOM.Platform.6.4.2\lib\net40\ASCOM.Utilities.Video.dll diff --git a/ASCOM.Driver/OpenAstroTracker/SharedResources.cs b/ASCOM.Driver/OpenAstroTracker/SharedResources.cs index a09c77b..fb1ca40 100644 --- a/ASCOM.Driver/OpenAstroTracker/SharedResources.cs +++ b/ASCOM.Driver/OpenAstroTracker/SharedResources.cs @@ -250,6 +250,10 @@ public static string SendMessage(string message) else { LogMessage(LoggingFlags.Serial, $"SendMessage Nr{messageNr,0:0000} - Not connected or Empty Message: " + message); + if (!SharedSerial.Connected) + { + throw new ASCOM.NotConnectedException($"SendMessage called while serial port is disconnected. Command: {message}"); + } } LogMessage(LoggingFlags.Serial, $"SendMessage Nr{messageNr,0:0000} - Releasing lock"); } diff --git a/ASCOM.Driver/TelescopeDriver/Driver.cs b/ASCOM.Driver/TelescopeDriver/Driver.cs index bd7e547..e7c29b9 100644 --- a/ASCOM.Driver/TelescopeDriver/Driver.cs +++ b/ASCOM.Driver/TelescopeDriver/Driver.cs @@ -83,8 +83,23 @@ public Telescope() /// ''' the new settings are saved, otherwise the old values are reloaded. /// ''' THIS IS THE ONLY PLACE WHERE SHOWING USER INTERFACE IS ALLOWED! /// ''' + [System.Runtime.InteropServices.DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + public void SetupDialog() { + // When started by COM (-embedding), Windows denies this process foreground + // privilege, so SetupDialogForm.ShowDialog() opens but never gets focus. + // Call SetForegroundWindow on the main form's handle to regain foreground + // status before showing the modal setup dialog. SetupDialog() runs on the + // main STA thread, so we can call these APIs directly. + var mainForm = System.Windows.Forms.Application.OpenForms.Count > 0 + ? System.Windows.Forms.Application.OpenForms[0] : null; + if (mainForm != null) + { + mainForm.WindowState = System.Windows.Forms.FormWindowState.Normal; + SetForegroundWindow(mainForm.Handle); + } using (var f = new SetupDialogForm(Profile, this, (s) => this.LogMessage(LoggingFlags.Setup, s))) { if (f.ShowDialog() == DialogResult.OK) @@ -93,6 +108,9 @@ public void SetupDialog() SharedResources.SetTraceFlags(Profile.TraceFlags); } } + // Re-minimize frmMain after setup dialog is dismissed + if (mainForm != null && Server.StartedByCOM) + mainForm.WindowState = System.Windows.Forms.FormWindowState.Minimized; } public ArrayList SupportedActions @@ -1353,19 +1371,28 @@ private void CheckConnected(string message) throw new NotConnectedException(message); } - private int PollUntilZero(string command) + private int PollUntilZero(string command, int maxAttempts = 120) { // Takes a command to be sent via CommandString, and resends every 1000ms until a 0 is returned. Returns 0 only when complete. + // maxAttempts caps the wait to prevent hanging indefinitely if the mount stalls or communication fails (default: 120s). string retVal = ""; - while (retVal != "0") + int attempts = 0; + while (retVal != "0" && attempts < maxAttempts) { retVal = CommandString(command); - LogMessage(LoggingFlags.Scope, $"PollUntilZero - Command: {command}, Response: {retVal}"); + LogMessage(LoggingFlags.Scope, $"PollUntilZero - Command: {command}, Response: {retVal}, Attempt: {attempts + 1}/{maxAttempts}"); if (retVal == "0") break; + attempts++; Thread.Sleep(1000); } + if (attempts >= maxAttempts) + { + LogMessage(LoggingFlags.Scope, $"PollUntilZero - Timed out after {maxAttempts} attempts for command: {command}"); + throw new System.TimeoutException($"Mount did not complete operation within {maxAttempts} seconds. Command: {command}"); + } + return System.Convert.ToInt32(retVal); } diff --git a/OATCommunications/CommunicationHandlers/TcpCommunicationHandler.cs b/OATCommunications/CommunicationHandlers/TcpCommunicationHandler.cs index 3922287..216be27 100644 --- a/OATCommunications/CommunicationHandlers/TcpCommunicationHandler.cs +++ b/OATCommunications/CommunicationHandlers/TcpCommunicationHandler.cs @@ -1,4 +1,4 @@ -using OATCommunications.ClientAdapters; +using OATCommunications.ClientAdapters; using OATCommunications.Utilities; using System; using System.Collections.Generic; @@ -16,6 +16,7 @@ public class TcpCommunicationHandler : CommunicationHandler private IPAddress _ip; private int _port; private TcpClient _client; + private NetworkStream _stream; private List _available; private Action _addCallback; @@ -67,12 +68,17 @@ protected override void RunJob(Job job) while ((attempt < 4) && (_client != null)) { Log.WriteLine("TCP: [{0}] Attempt {1} to send command.", command, attempt); - if (!_client.Connected) + + // Only (re)connect when the stream has been closed due to an error, or on first use. + if (_stream == null) { try { _client = new TcpClient(); _client.Connect(_ip, _port); + _client.ReceiveTimeout = 1000; + _client.SendTimeout = 1000; + _stream = _client.GetStream(); } catch (Exception e) { @@ -82,21 +88,19 @@ protected override void RunJob(Job job) } } - _client.ReceiveTimeout = 1000; - _client.SendTimeout = 1000; - string error = String.Empty; - var stream = _client.GetStream(); var bytes = Encoding.ASCII.GetBytes(command); try { - stream.Write(bytes, 0, bytes.Length); + _stream.Write(bytes, 0, bytes.Length); Log.WriteLine("TCP: [{0}] Sent command!", command); } catch (Exception e) { Log.WriteLine("TCP: [{0}] Unable to write command to stream: {1}", command, e.Message); + _stream.Close(); + _stream = null; job.OnFulFilled(new CommandResponse("", false, $"Failed to send message: {e.Message}")); return; } @@ -110,13 +114,12 @@ protected override void RunJob(Job job) Log.WriteLine("TCP: [{0}] No reply needed to command", command); break; - case ResponseType.DoubleFullResponse: case ResponseType.DigitResponse: case ResponseType.FullResponse: { Log.WriteLine("TCP: [{0}] Expecting a {1} reply to command, waiting...", command, job.ResponseType.ToString()); var response = new byte[256]; - var respCount = stream.Read(response, 0, response.Length); + var respCount = _stream.Read(response, 0, response.Length); respString = Encoding.ASCII.GetString(response, 0, respCount); Log.WriteLine("TCP: [{0}] Received reply to command -> [{1}], trimming", command, respString); int hashPos = respString.IndexOf('#'); @@ -128,22 +131,43 @@ protected override void RunJob(Job job) attempt = 10; } break; + + case ResponseType.DoubleFullResponse: + { + Log.WriteLine("TCP: [{0}] Expecting a DoubleFullResponse reply to command, waiting...", command); + var response = new byte[256]; + var respCount = _stream.Read(response, 0, response.Length); + respString = Encoding.ASCII.GetString(response, 0, respCount); + Log.WriteLine("TCP: [{0}] Received first reply to command -> [{1}], trimming", command, respString); + int hashPos = respString.IndexOf('#'); + if (hashPos > 0) + { + respString = respString.Substring(0, hashPos); + } + Log.WriteLine("TCP: [{0}] Returning first reply to command -> [{1}]", command, respString); + // Read and discard the second response + var response2 = new byte[256]; + _stream.Read(response2, 0, response2.Length); + attempt = 10; + } + break; } } catch (Exception e) { Log.WriteLine("TCP: [{0}] Failed to read reply to command. {1} thrown", command, e.GetType().Name); - if (job.ResponseType != ResponseType.NoResponse) - { - respString = "0#"; - } + _stream.Close(); + _stream = null; + respString = string.Empty; } - stream.Close(); attempt++; + // Stream is intentionally left open for the next command. } - job.OnFulFilled(new CommandResponse(respString)); + bool succeeded = job.ResponseType == ResponseType.NoResponse || !string.IsNullOrEmpty(respString); + job.OnFulFilled(new CommandResponse(respString, succeeded, succeeded ? string.Empty : $"Failed to read reply to [{command}]")); + } public override bool Connected @@ -185,6 +209,11 @@ public override void Disconnect() waitQuit.WaitOne(); Log.WriteLine("TCP: Closing port."); + if (_stream != null) + { + _stream.Close(); + _stream = null; + } _client.Close(); _client = null; Log.WriteLine("TCP: Disconnected..."); diff --git a/OATCommunications/TelescopeCommandHandlers/OatmealTelescopeCommandHandlers.cs b/OATCommunications/TelescopeCommandHandlers/OatmealTelescopeCommandHandlers.cs index 5fa9dcd..eec474c 100644 --- a/OATCommunications/TelescopeCommandHandlers/OatmealTelescopeCommandHandlers.cs +++ b/OATCommunications/TelescopeCommandHandlers/OatmealTelescopeCommandHandlers.cs @@ -43,6 +43,7 @@ public async Task RefreshMountState() MountState.Declination = GetCompactDec(parts[6]); success = true; } + doneEvent.Set(); }); await doneEvent.WaitAsync(); @@ -273,6 +274,7 @@ public async Task Slew(TelescopePosition position) SendCommand($":MS#,n", (moveResult) => { success = success && moveResult.Success && moveResult.Data == "1"; + doneEvent.Set(); }); await doneEvent.WaitAsync(); @@ -361,8 +363,6 @@ public async Task SetLocation(double lat, double lon, double altitudeInMet bool success = false; AsyncAutoResetEvent doneEvent = new AsyncAutoResetEvent(); - - await doneEvent.WaitAsync(); // Longitude success = await SetSiteLongitude((float)lon) == "1";