295150751f
The Test Run sandbox and Monaco analysis modelled a script API that had drifted from the site runtime's ScriptGlobals, so real scripts failed to compile in Test Run. Realign both to the runtime surface (Instance/Scripts/ExternalSystem/Attributes/Children/Parent) and drop the duplicate ScriptHost stub so the two cannot diverge again. - Script calls (Scripts.CallShared, Instance.CallScript, Route.To().Call) accept an anonymous object instead of a hand-built dictionary, via a shared ScriptArgs normalizer; existing dictionary calls still compile. - Test Run can optionally bind to a deployed instance, so Instance/ Attributes/CallScript route to it cross-site; adds site-side RouteToGetAttributes/RouteToSetAttributes handlers. - Adds Test Run panels to the API method and template script editors. - Fixes the TestDatabaseQuery seed script, which queried a table that never existed. Also commits unrelated in-progress work already in the tree: the health monitoring report loop, site streaming changes, and the Admin/Design data-connection and SMTP page reorganization.
166 lines
5.5 KiB
C#
166 lines
5.5 KiB
C#
using ScadaLink.Commons.Interfaces.Services;
|
|
using ScadaLink.Commons.Messages.InboundApi;
|
|
using ScadaLink.Commons.Types;
|
|
using ScadaLink.Communication;
|
|
|
|
namespace ScadaLink.InboundAPI;
|
|
|
|
/// <summary>
|
|
/// WP-4: Route.To() helper for cross-site calls from inbound API scripts.
|
|
/// Resolves instance to site, routes via CommunicationService, blocks until response or timeout.
|
|
/// Site unreachable returns error (no store-and-forward).
|
|
/// </summary>
|
|
public class RouteHelper
|
|
{
|
|
private readonly IInstanceLocator _instanceLocator;
|
|
private readonly CommunicationService _communicationService;
|
|
|
|
public RouteHelper(
|
|
IInstanceLocator instanceLocator,
|
|
CommunicationService communicationService)
|
|
{
|
|
_instanceLocator = instanceLocator;
|
|
_communicationService = communicationService;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a route target for the specified instance.
|
|
/// </summary>
|
|
public RouteTarget To(string instanceCode)
|
|
{
|
|
return new RouteTarget(instanceCode, _instanceLocator, _communicationService);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// WP-4: Represents a route target (an instance) for cross-site calls.
|
|
/// </summary>
|
|
public class RouteTarget
|
|
{
|
|
private readonly string _instanceCode;
|
|
private readonly IInstanceLocator _instanceLocator;
|
|
private readonly CommunicationService _communicationService;
|
|
|
|
internal RouteTarget(
|
|
string instanceCode,
|
|
IInstanceLocator instanceLocator,
|
|
CommunicationService communicationService)
|
|
{
|
|
_instanceCode = instanceCode;
|
|
_instanceLocator = instanceLocator;
|
|
_communicationService = communicationService;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls a script on the remote instance. Synchronous from API caller's
|
|
/// perspective. <paramref name="parameters"/> may be a dictionary or an
|
|
/// anonymous object (<c>new { name = "Bob" }</c>) — see <see cref="ScriptArgs"/>.
|
|
/// </summary>
|
|
public async Task<object?> Call(
|
|
string scriptName,
|
|
object? parameters = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var siteId = await ResolveSiteAsync(cancellationToken);
|
|
var correlationId = Guid.NewGuid().ToString();
|
|
|
|
var request = new RouteToCallRequest(
|
|
correlationId, _instanceCode, scriptName, ScriptArgs.Normalize(parameters), DateTimeOffset.UtcNow);
|
|
|
|
var response = await _communicationService.RouteToCallAsync(
|
|
siteId, request, cancellationToken);
|
|
|
|
if (!response.Success)
|
|
{
|
|
throw new InvalidOperationException(
|
|
response.ErrorMessage ?? "Remote script call failed");
|
|
}
|
|
|
|
return response.ReturnValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a single attribute value from the remote instance.
|
|
/// </summary>
|
|
public async Task<object?> GetAttribute(
|
|
string attributeName,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var result = await GetAttributes(new[] { attributeName }, cancellationToken);
|
|
return result.TryGetValue(attributeName, out var value) ? value : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets multiple attribute values from the remote instance (batch read).
|
|
/// </summary>
|
|
public async Task<IReadOnlyDictionary<string, object?>> GetAttributes(
|
|
IEnumerable<string> attributeNames,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var siteId = await ResolveSiteAsync(cancellationToken);
|
|
var correlationId = Guid.NewGuid().ToString();
|
|
|
|
var request = new RouteToGetAttributesRequest(
|
|
correlationId, _instanceCode, attributeNames.ToList(), DateTimeOffset.UtcNow);
|
|
|
|
var response = await _communicationService.RouteToGetAttributesAsync(
|
|
siteId, request, cancellationToken);
|
|
|
|
if (!response.Success)
|
|
{
|
|
throw new InvalidOperationException(
|
|
response.ErrorMessage ?? "Remote attribute read failed");
|
|
}
|
|
|
|
return response.Values;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a single attribute value on the remote instance.
|
|
/// </summary>
|
|
public async Task SetAttribute(
|
|
string attributeName,
|
|
string value,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await SetAttributes(
|
|
new Dictionary<string, string> { { attributeName, value } },
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets multiple attribute values on the remote instance (batch write).
|
|
/// </summary>
|
|
public async Task SetAttributes(
|
|
IReadOnlyDictionary<string, string> attributeValues,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var siteId = await ResolveSiteAsync(cancellationToken);
|
|
var correlationId = Guid.NewGuid().ToString();
|
|
|
|
var request = new RouteToSetAttributesRequest(
|
|
correlationId, _instanceCode, attributeValues, DateTimeOffset.UtcNow);
|
|
|
|
var response = await _communicationService.RouteToSetAttributesAsync(
|
|
siteId, request, cancellationToken);
|
|
|
|
if (!response.Success)
|
|
{
|
|
throw new InvalidOperationException(
|
|
response.ErrorMessage ?? "Remote attribute write failed");
|
|
}
|
|
}
|
|
|
|
private async Task<string> ResolveSiteAsync(CancellationToken cancellationToken)
|
|
{
|
|
var siteId = await _instanceLocator.GetSiteIdForInstanceAsync(_instanceCode, cancellationToken);
|
|
if (siteId == null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Instance '{_instanceCode}' not found or has no assigned site");
|
|
}
|
|
|
|
return siteId;
|
|
}
|
|
}
|