Files
desktop-edge-win/ZitiDesktopEdge.Client/ServiceClient/AbstractClient.cs
T
2024-11-25 16:56:47 -05:00

308 lines
13 KiB
C#

/*
Copyright NetFoundry Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Security.Principal;
using System.Security.AccessControl;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using NLog;
using ZitiDesktopEdge.DataStructures;
using System.Reflection;
namespace ZitiDesktopEdge.ServiceClient {
public abstract class AbstractClient {
public event EventHandler<object> OnClientConnected = null;
public event EventHandler<object> OnClientDisconnected;
public event EventHandler<StatusEvent> OnShutdownEvent;
public event EventHandler<object> OnReconnectFailure;
public virtual event EventHandler<Exception> OnCommunicationError;
protected NamedPipeClientStream pipeClient = null;
protected NamedPipeClientStream eventClient = null;
protected StreamWriter ipcWriter = null;
protected StreamReader ipcReader = null;
protected abstract Task ConnectPipesAsync();
protected abstract void ProcessLine(string line);
protected abstract Logger Logger { get; }
protected string Id { get; set; }
protected const string localPipeServer = ".";
protected const int ServiceConnectTimeout = 500;
//protected object namedPipeSyncLock = new object();
protected static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
protected JsonSerializer serializer = new JsonSerializer() { Formatting = Formatting.None };
protected virtual void ClientConnected(object e) {
Connected = true;
Reconnecting = false;
ExpectedShutdown = false;
Logger.Debug("Client connected successfully. Setting UnexpectedShutdown set to false.");
ipcWriter = new StreamWriter(pipeClient);
ipcReader = new StreamReader(pipeClient);
Task.Run(async () => { //hack for now until it's async...
try {
using (StreamReader eventReader = new StreamReader(eventClient)) {
while (true) {
if (eventReader.EndOfStream) {
break;
}
string respAsString = null;
try {
respAsString = await readMessageAsync(eventReader);
try {
ProcessLine(respAsString);
} catch (Exception ex) {
Logger.Warn(ex, "ERROR caught in ProcessLine: {0}", respAsString);
}
} catch (Exception ex) {
Logger.Warn(ex, "ERROR caught in readMessageAsync: {0}", respAsString);
}
}
}
} catch (Exception ex) {
Logger.Debug("unepxected error: " + ex.ToString());
}
// since this thread is always sitting waiting to read
// it should be the only one triggering this event
ClientDisconnected(null);
});
OnClientConnected?.Invoke(this, e);
}
protected virtual void ClientDisconnected(object e) {
Reconnect();
Connected = false;
OnClientDisconnected?.Invoke(this, e);
}
protected virtual void ShutdownEvent(StatusEvent e) {
ExpectedShutdown = true;
OnShutdownEvent?.Invoke(this, e);
}
protected virtual void ReconnectFailureEvent(object e) {
OnReconnectFailure?.Invoke(this, e);
}
protected virtual void CommunicationError(Exception e) {
OnCommunicationError?.Invoke(this, e);
}
async protected Task sendAsync(object objToSend) {
bool retried = false;
while (true) {
try {
var jsonResolver = new ShouldSerializeContractResolver();
var serializerSettings = new JsonSerializerSettings();
serializerSettings.ContractResolver = jsonResolver;
string toSend = JsonConvert.SerializeObject(objToSend, serializerSettings);
if (toSend?.Trim() != null) {
debugServiceCommunication("send", toSend);
if (ipcWriter != null) {
await ipcWriter.WriteAsync(toSend);
await ipcWriter.WriteAsync('\n');
await ipcWriter.FlushAsync();
} else {
throw new IPCException("ipcWriter is null. the target appears to be offline?");
}
} else {
Logger.Debug("NOT sending empty object??? " + objToSend?.ToString());
}
break;
} catch (IOException ioe) {
//almost certainly a problem with the pipe - recreate the pipe... try one more time.
await ConnectPipesAsync();
if (retried) {
//we tried - throw the error...
throw ioe;
} else {
retried = true; //fall back through to the while and try again
}
} catch (MonitorServiceException) {
throw;
} catch (Exception ex) {
//if this fails it's usually because the writer is null/invalid. throwing IOException
//will trigger the pipe to rebuild
throw new IOException("Unexpected error when sending data to service. " + ex.Message);
}
}
}
public bool Reconnecting { get; set; }
public bool Connected { get; set; }
public bool ExpectedShutdown { get; set; }
public AbstractClient(string id) {
this.Id = id;
}
async public Task ConnectAsync() {
//establish the named pipe to the service
await ConnectPipesAsync();
}
public void Reconnect() {
if (Reconnecting) {
Logger.Debug("Already in reconnect mode.");
return;
} else {
Reconnecting = true;
}
Task.Run(async () => {
Logger.Info("service is down. attempting to connect to service...");
DateTime reconnectStart = DateTime.Now;
DateTime logAgainAfter = reconnectStart + TimeSpan.FromSeconds(1);
while (true) {
try {
await Task.Delay(2500);
await ConnectPipesAsync();
if (Connected) {
Logger.Debug("Connected to the service - exiting reconect loop");
Connected = true;
Reconnecting = false;
return;
} else {
//ClientDisconnected(null);
}
} catch (Exception) {
try {
ReconnectFailureEvent("reconnect failure");
} catch (Exception) {
// don't care - just catch it and continue... it's a timeout...
}
var now = DateTime.Now;
if (now > logAgainAfter) {
Logger.Trace("Reconnect failed. Trying again...");
var duration = now - reconnectStart;
if (duration > TimeSpan.FromHours(1)) {
Logger.Info("reconnect has not completed and has been running for {0} hours", duration.TotalHours);
logAgainAfter += TimeSpan.FromHours(1);
} else if (duration > TimeSpan.FromMinutes(1)) {
Logger.Info("reconnect has not completed and has been running for {0} minutes", duration.TotalMinutes);
logAgainAfter += TimeSpan.FromMinutes(1);
} else {
logAgainAfter += TimeSpan.FromSeconds(1);
}
}
}
}
});
}
protected void debugServiceCommunication(string direction, string msg) {
#if DEBUGSVCCOM
Logger.Warn(msg);
#else
Logger.Trace(msg);
Logger.Warn("{}-{}: {}", direction, Id, msg);
#endif
}
async protected Task<T> readAsync<T>(StreamReader reader) where T : SvcResponse {
string respAsString = await readMessageAsync(reader);
T resp = (T)serializer.Deserialize(new StringReader(respAsString), typeof(T));
return resp;
}
async public Task<string> readMessageAsync(StreamReader reader) {
try {
int emptyCount = 1; //just a stop gap in case something crazy happens in the communication
string respAsString = await reader.ReadLineAsync();
debugServiceCommunication("read", respAsString);
while (string.IsNullOrEmpty(respAsString?.Trim())) {
debugServiceCommunication("read", "Received empty payload - continuing to read until a payload is received");
//now how'd that happen...
respAsString = await reader.ReadLineAsync();
debugServiceCommunication("read", respAsString);
emptyCount++;
if (emptyCount > 5) {
Logger.Debug("are we there yet? " + reader.EndOfStream);
//that's just too many...
return null;
}
}
return respAsString;
} catch (IOException ioe) {
//almost certainly a problem with the pipe
Logger.Error(ioe, "io error in read: " + ioe.Message);
ClientDisconnected(null);
throw ioe;
} catch (Exception ee) {
//almost certainly a problem with the pipe
Logger.Error(ee, "unexpected error in read: " + ee.Message);
ClientDisconnected(null);
throw ee;
}
}
async public Task WaitForConnectionAsync() {
while (Reconnecting || !Connected) {
await Task.Delay(100);
}
}
}
public class ShouldSerializeContractResolver : DefaultContractResolver {
public static readonly ShouldSerializeContractResolver Instance = new ShouldSerializeContractResolver();
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) {
JsonProperty property = base.CreateProperty(member, memberSerialization);
if (property.DeclaringType == typeof(Identity) && property.PropertyName == "MfaLastUpdatedTime") {
property.ShouldSerialize =
instance => {
Identity identity = (Identity)instance;
return identity != null && identity.MfaLastUpdatedTime != DateTime.MinValue;
};
}
return property;
}
}
public class MonitorServiceException : Exception {
public MonitorServiceException() { }
public MonitorServiceException(string message) : base(message) { }
public MonitorServiceException(string message, Exception source) : base(message, source) { }
}
public class IPCException : Exception {
public IPCException() { }
public IPCException(string message) : base(message) { }
public IPCException(string message, Exception source) : base(message, source) { }
}
}