@@ -38,7 +38,11 @@ public sealed class WriteCommand : FocasCommandBase
|
||||
Address: Address,
|
||||
DataType: DataType,
|
||||
Writable: true);
|
||||
var options = BuildOptions([tag]);
|
||||
// The CLI is a per-invocation operator tool; it bypasses the server-side
|
||||
// FocasDriverOptions.Writes.Enabled gate by enabling writes locally for this single
|
||||
// process. Configure-the-server code paths still respect the safer-by-default flag —
|
||||
// see docs/Driver.FOCAS.Cli.md "Writes" subsection (issue #268, plan PR F4-a).
|
||||
var options = BuildOptions([tag], writesEnabled: true);
|
||||
|
||||
var parsed = ParseValue(Value, DataType);
|
||||
|
||||
|
||||
@@ -41,9 +41,12 @@ public abstract class FocasCommandBase : DriverCommandBase
|
||||
/// + the tag list a subclass supplies. Probe disabled; the default
|
||||
/// <see cref="FwlibFocasClientFactory"/> attempts <c>Fwlib32.dll</c> P/Invoke, which
|
||||
/// throws <see cref="DllNotFoundException"/> at first call when the DLL is absent —
|
||||
/// surfaced through the driver as <c>BadCommunicationError</c>.
|
||||
/// surfaced through the driver as <c>BadCommunicationError</c>. Pass
|
||||
/// <paramref name="writesEnabled"/> = <c>true</c> to bypass the F4-a driver-level
|
||||
/// write gate for the lifetime of this CLI invocation (issue #268).
|
||||
/// </summary>
|
||||
protected FocasDriverOptions BuildOptions(IReadOnlyList<FocasTagDefinition> tags) => new()
|
||||
protected FocasDriverOptions BuildOptions(
|
||||
IReadOnlyList<FocasTagDefinition> tags, bool writesEnabled = false) => new()
|
||||
{
|
||||
Devices = [new FocasDeviceOptions(
|
||||
HostAddress: HostAddress,
|
||||
@@ -52,6 +55,7 @@ public abstract class FocasCommandBase : DriverCommandBase
|
||||
Tags = tags,
|
||||
Timeout = Timeout,
|
||||
Probe = new FocasProbeOptions { Enabled = false },
|
||||
Writes = new FocasWritesOptions { Enabled = writesEnabled },
|
||||
};
|
||||
|
||||
protected string DriverInstanceId => $"focas-cli-{CncHost}:{CncPort}";
|
||||
|
||||
@@ -482,6 +482,18 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
ArgumentNullException.ThrowIfNull(writes);
|
||||
var results = new WriteResult[writes.Count];
|
||||
|
||||
// Driver-level master switch (issue #268, plan PR F4-a). When the deployment hasn't
|
||||
// explicitly opted into writes, every batch entry short-circuits to BadNotWritable
|
||||
// before we touch the wire. The status text "writes disabled at driver level" is
|
||||
// surfaced through the resilience pipeline + Admin diagnostics so operators can tell
|
||||
// the driver-level gate apart from a per-tag Writable=false rejection.
|
||||
if (!_options.Writes.Enabled)
|
||||
{
|
||||
for (var i = 0; i < writes.Count; i++)
|
||||
results[i] = new WriteResult(FocasStatusMapper.BadNotWritable);
|
||||
return results;
|
||||
}
|
||||
|
||||
for (var i = 0; i < writes.Count; i++)
|
||||
{
|
||||
var w = writes[i];
|
||||
|
||||
@@ -73,7 +73,10 @@ public static class FocasDriverFactoryExtensions
|
||||
Address: t.Address ?? throw new InvalidOperationException(
|
||||
$"FOCAS tag '{t.Name}' in '{driverInstanceId}' missing Address"),
|
||||
DataType: ParseDataType(t.DataType, t.Name!, driverInstanceId),
|
||||
Writable: t.Writable ?? true,
|
||||
// Per-tag Writable defaults to false post-F4-a (issue #268). A config-DB row
|
||||
// with Writable null means "not opted in" — operators must explicitly flip
|
||||
// the flag per tag before writes flow.
|
||||
Writable: t.Writable ?? false,
|
||||
WriteIdempotent: t.WriteIdempotent ?? false))]
|
||||
: [],
|
||||
Probe = new FocasProbeOptions
|
||||
@@ -83,6 +86,13 @@ public static class FocasDriverFactoryExtensions
|
||||
Timeout = TimeSpan.FromMilliseconds(dto.Probe?.TimeoutMs ?? 2_000),
|
||||
},
|
||||
Timeout = TimeSpan.FromMilliseconds(dto.TimeoutMs ?? 2_000),
|
||||
// Driver-level write opt-in (issue #268, plan PR F4-a). Default false — config rows
|
||||
// that omit the section keep the safer-by-default read-only posture; flipping it on
|
||||
// requires an explicit deployment-time choice.
|
||||
Writes = new FocasWritesOptions
|
||||
{
|
||||
Enabled = dto.Writes?.Enabled ?? false,
|
||||
},
|
||||
};
|
||||
|
||||
var clientFactory = BuildClientFactory(dto, driverInstanceId);
|
||||
@@ -170,6 +180,12 @@ public static class FocasDriverFactoryExtensions
|
||||
public List<FocasDeviceDto>? Devices { get; init; }
|
||||
public List<FocasTagDto>? Tags { get; init; }
|
||||
public FocasProbeDto? Probe { get; init; }
|
||||
public FocasWritesDto? Writes { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class FocasWritesDto
|
||||
{
|
||||
public bool? Enabled { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class FocasDeviceDto
|
||||
|
||||
@@ -28,6 +28,32 @@ public sealed class FocasDriverOptions
|
||||
/// <see cref="FocasAlarmProjectionMode.ActivePlusHistory"/>.
|
||||
/// </summary>
|
||||
public FocasAlarmProjectionOptions AlarmProjection { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Driver-level write opt-in (issue #268, plan PR F4-a). Defaults to
|
||||
/// <c>Enabled = false</c> — the driver short-circuits every <c>IWritable.WriteAsync</c>
|
||||
/// call to <see cref="FocasStatusMapper.BadNotWritable"/> until the deployment explicitly
|
||||
/// flips this on. Combined with the per-tag <see cref="FocasTagDefinition.Writable"/>
|
||||
/// gate (also default-off), every CNC write requires two opt-ins.
|
||||
/// </summary>
|
||||
public FocasWritesOptions Writes { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Driver-level write controls (issue #268, plan PR F4-a). Per the F4-a decision record
|
||||
/// writes ship behind a flag with a safe default: an operator who pulls the FOCAS driver
|
||||
/// into production without touching <c>Writes.Enabled</c> gets read-only behaviour, and
|
||||
/// even with the flag flipped on each individual tag must still set
|
||||
/// <see cref="FocasTagDefinition.Writable"/> = <c>true</c>.
|
||||
/// </summary>
|
||||
public sealed record FocasWritesOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Driver-level master switch. Default <c>false</c> — every write returns
|
||||
/// <see cref="FocasStatusMapper.BadNotWritable"/> with the status text
|
||||
/// <c>"writes disabled at driver level"</c>.
|
||||
/// </summary>
|
||||
public bool Enabled { get; init; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -141,12 +167,22 @@ public sealed record FocasDeviceOptions(
|
||||
/// <c>X0.0</c> / <c>R100</c> / <c>PARAM:1815/0</c> / <c>MACRO:500</c> /
|
||||
/// <c>DIAG:1031</c> / <c>DIAG:280/2</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <paramref name="Writable"/> defaults to <c>false</c> per issue #268 / plan PR F4-a — a
|
||||
/// newly-onboarded tag is read-only until the deployment explicitly opts it in, matching
|
||||
/// the driver-level <see cref="FocasWritesOptions.Enabled"/> safer-by-default posture.
|
||||
/// <paramref name="WriteIdempotent"/> is plumbed through the
|
||||
/// <see cref="Core.Resilience.CapabilityInvoker.ExecuteWriteAsync"/> retry path at the
|
||||
/// server layer (see <see cref="Core.Abstractions.WriteIdempotentAttribute"/>); a
|
||||
/// <c>true</c> value lets the Polly pipeline retry on transient failures while
|
||||
/// <c>false</c> (the default) disables retry per decisions #44/#45.
|
||||
/// </remarks>
|
||||
public sealed record FocasTagDefinition(
|
||||
string Name,
|
||||
string DeviceHostAddress,
|
||||
string Address,
|
||||
FocasDataType DataType,
|
||||
bool Writable = true,
|
||||
bool Writable = false,
|
||||
bool WriteIdempotent = false);
|
||||
|
||||
public sealed class FocasProbeOptions
|
||||
|
||||
Reference in New Issue
Block a user