Compare commits

..

1 commit

Author SHA1 Message Date
Jonathan Arndt
f8738a9053 feat(messenger): implement client-specific event messaging for tech password validation 2026-08-19 12:38:53 -07:00
3 changed files with 66 additions and 29 deletions

View file

@ -151,8 +151,6 @@ namespace PepperDash.Core
// Thread-safety lock for state changes // Thread-safety lock for state changes
private readonly object _stateLock = new object(); private readonly object _stateLock = new object();
private volatile bool _isProgramStopping;
private bool disconnectLogged = false; private bool disconnectLogged = false;
/// <summary> /// <summary>
@ -209,11 +207,13 @@ namespace PepperDash.Core
{ {
if (programEventType == eProgramStatusEventType.Stopping) if (programEventType == eProgramStatusEventType.Stopping)
{ {
_isProgramStopping = true; if (client != null)
{
this.LogDebug("Program stopping. Closing connection"); this.LogDebug("Program stopping. Closing connection");
Disconnect(); Disconnect();
} }
} }
}
/// <summary> /// <summary>
/// Connect method /// Connect method
@ -228,12 +228,6 @@ namespace PepperDash.Core
return; return;
} }
if (_isProgramStopping)
{
this.LogDebug("Skipping connect because program is stopping");
return;
}
ConnectEnabled = true; ConnectEnabled = true;
try try
@ -293,7 +287,13 @@ namespace PepperDash.Core
} }
catch (SshConnectionException e) catch (SshConnectionException e)
{ {
var ie = e.InnerException; // The details are inside, when present - remote can close the connection with no inner exception at all var ie = e.InnerException; // The details are inside!!
if (ie is SocketException)
{
this.LogError("CONNECTION failure: Cannot reach host");
this.LogVerbose(ie, "Exception details: ");
}
if (ie is System.Net.Sockets.SocketException socketException) if (ie is System.Net.Sockets.SocketException socketException)
{ {
@ -301,20 +301,20 @@ namespace PepperDash.Core
Hostname, Port); Hostname, Port);
this.LogVerbose(socketException, "SocketException details: "); this.LogVerbose(socketException, "SocketException details: ");
} }
else if (ie is SshAuthenticationException) if (ie is SshAuthenticationException)
{ {
this.LogError("Authentication failure for username {userName}", Username); this.LogError("Authentication failure for username {userName}", Username);
this.LogVerbose(ie, "AuthenticationException details: "); this.LogVerbose(ie, "AuthenticationException details: ");
} }
else else
{ {
this.LogError("Error on connect: {error}", ie?.Message ?? e.Message); this.LogError("Error on connect: {error}", ie.Message);
this.LogVerbose(ie ?? e, "Exception details: "); this.LogVerbose(ie, "Exception details: ");
} }
disconnectLogged = true; disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect && ConnectEnabled && !_isProgramStopping) if (AutoReconnect)
{ {
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); StartReconnectTimer();
@ -326,7 +326,7 @@ namespace PepperDash.Core
disconnectLogged = true; disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect && ConnectEnabled && !_isProgramStopping) if (AutoReconnect)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); StartReconnectTimer();
@ -338,7 +338,7 @@ namespace PepperDash.Core
this.LogVerbose(e, "Exception details: "); this.LogVerbose(e, "Exception details: ");
disconnectLogged = true; disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect && ConnectEnabled && !_isProgramStopping) if (AutoReconnect)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); StartReconnectTimer();
@ -473,7 +473,7 @@ namespace PepperDash.Core
{ {
connectLock.Release(); connectLock.Release();
} }
if (AutoReconnect && ConnectEnabled && !_isProgramStopping) if (AutoReconnect && ConnectEnabled)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); StartReconnectTimer();
@ -516,11 +516,8 @@ namespace PepperDash.Core
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim()); this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
{
StartReconnectTimer(); StartReconnectTimer();
} }
}
catch (Exception ex) catch (Exception ex)
{ {
this.LogException(ex, "Exception sending text: '{message}'", text); this.LogException(ex, "Exception sending text: '{message}'", text);
@ -552,11 +549,8 @@ namespace PepperDash.Core
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes)); this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
{
StartReconnectTimer(); StartReconnectTimer();
} }
}
catch (Exception ex) catch (Exception ex)
{ {
this.LogException(ex, "Exception sending {message}", ComTextHelper.GetEscapedText(bytes)); this.LogException(ex, "Exception sending {message}", ComTextHelper.GetEscapedText(bytes));

View file

@ -11,6 +11,15 @@ namespace PepperDash.Essentials.AppServer.Messengers
{ {
private readonly ITechPassword _room; private readonly ITechPassword _room;
// Captures the id of the client whose /validateTechPassword request is in flight, so the
// TechPasswordValidateResult handler below can reply to only that client instead of
// broadcasting to every connected panel. Relies on ValidateTechPassword firing the event
// synchronously (true for all known ITechPassword implementations); if a future
// implementation validates asynchronously, _pendingClientId will already be null when the
// handler runs and this degrades to the previous broadcast behavior.
private readonly object _pendingLock = new object();
private string _pendingClientId;
public ITechPasswordMessenger(string key, string messagePath, ITechPassword room) public ITechPasswordMessenger(string key, string messagePath, ITechPassword room)
: base(key, messagePath, room as IKeyName) : base(key, messagePath, room as IKeyName)
{ {
@ -28,7 +37,12 @@ namespace PepperDash.Essentials.AppServer.Messengers
{ {
var password = content.Value<string>("password"); var password = content.Value<string>("password");
lock (_pendingLock)
{
_pendingClientId = id;
_room.ValidateTechPassword(password); _room.ValidateTechPassword(password);
_pendingClientId = null;
}
}); });
AddAction("/setTechPassword", (id, content) => AddAction("/setTechPassword", (id, content) =>
@ -45,12 +59,18 @@ namespace PepperDash.Essentials.AppServer.Messengers
_room.TechPasswordValidateResult += (sender, args) => _room.TechPasswordValidateResult += (sender, args) =>
{ {
string clientId;
lock (_pendingLock)
{
clientId = _pendingClientId;
}
var evt = new ITechPasswordEventMessage var evt = new ITechPasswordEventMessage
{ {
IsValid = args.IsValid IsValid = args.IsValid
}; };
PostEventMessage(evt, "passwordValidationResult"); PostEventMessage(evt, "passwordValidationResult", clientId);
}; };
} }

View file

@ -405,5 +405,28 @@ namespace PepperDash.Essentials.AppServer.Messengers
}); });
} }
/// <summary>
/// Helper for posting an event message to a single client. A null/empty clientId falls back
/// to the existing broadcast behavior of the other PostEventMessage overloads.
/// </summary>
/// <param name="message"></param>
/// <param name="eventType"></param>
/// <param name="clientId">Client id that will direct the message back to only that client</param>
protected void PostEventMessage(DeviceEventMessageBase message, string eventType, string clientId)
{
message.Key = _device.Key;
message.Name = _device.Name;
message.EventType = eventType;
AppServerController?.SendMessageObject(new MobileControlMessage
{
Type = $"/event{MessagePath}/{eventType}",
ClientId = clientId,
Content = JToken.FromObject(message),
});
}
} }
} }