Skip to content

fix(sdk): Refactored logic and consolidated connected status control #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Kepware.Api.Test/ApiClient/_TestApiClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ protected void ConfigureConnectedClient(

_httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}/config/v1/doc")
.ReturnsResponse(HttpStatusCode.OK, statusResponse, "application/json");

_httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}/config/v1/project")
.ReturnsResponse(HttpStatusCode.OK, "[]", "application/json");
}

protected Channel CreateTestChannel(string name = "TestChannel", string driver = "Advanced Simulator")
Expand Down
87 changes: 60 additions & 27 deletions Kepware.Api/KepwareApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public partial class KepwareApiClient : IKepwareDefaultValueProvider
private const string ENDPOINT_STATUS = "/config/v1/status";
private const string ENDPOINT_DOC = "/config/v1/doc";
private const string ENDPOINT_ABOUT = "/config/v1/about";
private const string ENDPOINT_PROJECT = "/config/v1/project";

private readonly ILogger<KepwareApiClient> m_logger;
private readonly HttpClient m_httpClient;
Expand Down Expand Up @@ -117,57 +118,90 @@ internal KepwareApiClient(string name, KepwareApiClientOptions options, ILoggerF

#region connection test & product info
/// <summary>
/// Tests the connection to the Kepware server and checks if the server runtime is healthy. Uses the
/// /config/v1/status endpoint for health verification.
/// Tests the connection to the Kepware server and checks if the server runtime is healthy. Also
/// validates authentication credentials.
/// Uses the /config/v1/status endpoint for health verification.
/// Uses the /config/v1/doc endpoint to verify credentials.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the connection was successful.</returns>

public async Task<bool> TestConnectionAsync(CancellationToken cancellationToken = default)
{
bool blnIsConnected = false;
try
{
if (m_isConnected == null) // first time after connection change
if (m_isConnected != true) // already connected
{
m_logger.LogInformation("Connecting to {ClientName}-client at {BaseAddress}...", ClientName, m_httpClient.BaseAddress);
}
var response = await m_httpClient.GetAsync(ENDPOINT_STATUS, cancellationToken).ConfigureAwait(false);

if (response.IsSuccessStatusCode)
// check if the response is successful and contains a healthy status
// if the response is not successful, we assume the connection is not healthy
if (!response.IsSuccessStatusCode)
{
var status = await JsonSerializer.DeserializeAsync(
m_logger.LogWarning("Failed to connect to {ClientName}-client at {BaseAddress}, Reason: {ReasonPhrase}", ClientName, m_httpClient.BaseAddress, response.ReasonPhrase);
m_isConnected = null; // set connection state to null if we cannot connect
return false; // connection failed
}

// Deserialize the response content to check the status
var status = await JsonSerializer.DeserializeAsync(
await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false),
KepJsonContext.Default.ListApiStatus, cancellationToken)
.ConfigureAwait(false);
if (status?.FirstOrDefault()?.Healthy == true)
{
blnIsConnected = true;
}

// Check if the status is healthy
if (status?.FirstOrDefault()?.Healthy == false)
{
m_logger.LogWarning("Failed to connect to {ClientName}-client at {BaseAddress}, Reason: {String}", ClientName, m_httpClient.BaseAddress, "Server Status Check Failed");
m_isConnected = null; // set connection state to null if we cannot connect
return false; // connection failed
}

if (m_isConnected == null || (m_isConnected != null && m_isConnected != blnIsConnected)) // first time after connection change or when connection is lost
// If the connection is already healthy, we can return true immediately
if (m_isConnected == true )
{
if (!blnIsConnected)
{
m_logger.LogWarning("Failed to connect to {ClientName}-client at {BaseAddress}, Reason: {ReasonPhrase}", ClientName, m_httpClient.BaseAddress, response.ReasonPhrase);
}
else
{
var prodInfo = await GetProductInfoAsync(cancellationToken).ConfigureAwait(false);
m_logger.LogInformation("Successfully connected to {ClientName}-client: {ProductName} {ProductVersion} on {BaseAddress}", ClientName, prodInfo?.ProductName, prodInfo?.ProductVersion, m_httpClient.BaseAddress);

m_hasValidCredentials = await TestCredentialsAsync(cancellationToken).ConfigureAwait(false);
}
return true; // connection is healthy
}

// Inital connection attempt or a reconnection due to failure,
// we need to check the product info and credentials
var prodInfo = await GetProductInfoAsync(cancellationToken).ConfigureAwait(false);

// If we cannot get the product info, we assume the connection is not healthy
if (prodInfo == null)
{
m_isConnected = null; // set connection state to null if we cannot get product info
return false;
}

// If we have a valid product info, we can check the credentials
m_hasValidCredentials = await TestCredentialsAsync(cancellationToken).ConfigureAwait(false);

// If we do not have valid credentials, we assume the connection is not healthy
if (m_hasValidCredentials != true)
{
m_isConnected = null; // set connection state to null if we cannot connect or credentials are invalid
m_logger.LogWarning("Connection to {ClientName}-client at {BaseAddress} failed because credentials are invalid", ClientName, m_httpClient.BaseAddress);
return false;
}

m_logger.LogInformation("Successfully connected to {ClientName}-client: {ProductName} {ProductVersion} on {BaseAddress}", ClientName, prodInfo?.ProductName, prodInfo?.ProductVersion, m_httpClient.BaseAddress);

m_isConnected = true; // set connection state to true if we have a valid product info and credentials
return m_isConnected.Value; // return true if we have a valid connection

}
catch (HttpRequestException httpEx)
{
if (m_isConnected == null || m_isConnected == true) // first time after connection change or when connection is lost
m_logger.LogWarning(httpEx, "Failed to connect to {ClientName}-client at {BaseAddress}", ClientName, m_httpClient.BaseAddress);
m_isConnected = null;
}
m_isConnected = blnIsConnected;
return blnIsConnected && m_hasValidCredentials == true;

// If we reach this point, we assume the connection is not healthy
return false;
}

/// <summary>
Expand All @@ -186,7 +220,6 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var prodInfo = JsonSerializer.Deserialize(content, KepJsonContext.Default.ProductInfo);

m_isConnected = true;
return prodInfo;
}
else
Expand All @@ -197,7 +230,6 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false
catch (HttpRequestException httpEx)
{
m_logger.LogWarning(httpEx, "Failed to connect to {ClientName}-client at {BaseAddress}: {Message}", ClientName, m_httpClient.BaseAddress, httpEx.Message);
m_isConnected = null;
}
catch (JsonException jsonEx)
{
Expand All @@ -212,7 +244,8 @@ private async Task<bool> TestCredentialsAsync(CancellationToken cancellationToke
bool hasValidCredentials = false;
try
{
var response = await m_httpClient.GetAsync(ENDPOINT_DOC, cancellationToken).ConfigureAwait(false);

var response = await m_httpClient.GetAsync(ENDPOINT_PROJECT, cancellationToken).ConfigureAwait(false);
hasValidCredentials = response.IsSuccessStatusCode;
if (hasValidCredentials)
{
Expand Down