281 lines
8.2 KiB
C#
281 lines
8.2 KiB
C#
namespace Zcbot.WindowsNode;
|
|
|
|
internal enum NodeShutdownMode
|
|
{
|
|
WaitForJobs,
|
|
CancelJobs,
|
|
}
|
|
|
|
internal interface INodeConnection
|
|
{
|
|
Task RunAsync(CancellationToken cancellationToken);
|
|
void CancelActiveJobsForExit();
|
|
}
|
|
|
|
internal sealed class NodeApplicationController : IDisposable
|
|
{
|
|
private readonly INodeConfigStore store;
|
|
private readonly NodePaths paths;
|
|
private readonly Func<NodeConfig, Action<NodeStatus>, INodeConnection> connectionFactory;
|
|
private readonly Func<EnrollOptions, INodeConfigStore, CancellationToken, Task> enroll;
|
|
private readonly Func<string, string, CancellationToken, Task<NodeDataMigrationResult>> migrate;
|
|
private readonly Action<string> saveDataRoot;
|
|
private readonly SemaphoreSlim lifecycleGate = new(1, 1);
|
|
private readonly object stopSync = new();
|
|
private CancellationTokenSource? connectionStop;
|
|
private Task? connectionTask;
|
|
private INodeConnection? connection;
|
|
private Task? stopTask;
|
|
private bool disposed;
|
|
|
|
internal NodeApplicationController(
|
|
INodeConfigStore store,
|
|
NodePaths paths,
|
|
Func<NodeConfig, Action<NodeStatus>, INodeConnection> connectionFactory,
|
|
Func<EnrollOptions, INodeConfigStore, CancellationToken, Task> enroll,
|
|
Func<string, string, CancellationToken, Task<NodeDataMigrationResult>> migrate,
|
|
Action<string> saveDataRoot)
|
|
{
|
|
this.store = store;
|
|
this.paths = paths;
|
|
this.connectionFactory = connectionFactory;
|
|
this.enroll = enroll;
|
|
this.migrate = migrate;
|
|
this.saveDataRoot = saveDataRoot;
|
|
}
|
|
|
|
internal event Action<NodeStatus>? StatusChanged;
|
|
|
|
internal NodeConfig? CurrentConfig { get; private set; }
|
|
|
|
internal NodeStatus CurrentStatus { get; private set; } =
|
|
NodeStatus.Create(NodeState.NotRegistered, "尚未注册");
|
|
|
|
internal int ActiveJobCount => new JobMonitorService(paths.JobsDirectory)
|
|
.ReadSnapshots()
|
|
.Count(item => item.IsActive);
|
|
|
|
internal NodePaths Paths => paths;
|
|
|
|
internal void Start()
|
|
{
|
|
ObjectDisposedException.ThrowIf(disposed, this);
|
|
if (!store.Exists)
|
|
{
|
|
Publish(CurrentStatus);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
CurrentConfig = store.Load();
|
|
Publish(NodeStatus.Create(NodeState.Connecting, "正在连接 zcbot…"));
|
|
StartConnection();
|
|
}
|
|
catch (NodeConfigurationException exception)
|
|
{
|
|
Publish(NodeStatus.Create(NodeState.AuthenticationRequired, exception.Message));
|
|
}
|
|
}
|
|
|
|
internal async Task RegisterAsync(
|
|
EnrollOptions options,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ObjectDisposedException.ThrowIf(disposed, this);
|
|
await lifecycleGate.WaitAsync(cancellationToken);
|
|
try
|
|
{
|
|
Publish(NodeStatus.Create(NodeState.Connecting, "正在注册节点…"));
|
|
try
|
|
{
|
|
await enroll(options, store, cancellationToken);
|
|
CurrentConfig = store.Load();
|
|
Publish(NodeStatus.Create(NodeState.Connecting, "注册成功,正在连接…"));
|
|
StartConnection();
|
|
}
|
|
catch
|
|
{
|
|
Publish(NodeStatus.Create(NodeState.NotRegistered, "注册失败,请检查配置"));
|
|
throw;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lifecycleGate.Release();
|
|
}
|
|
}
|
|
|
|
internal async Task<bool> ReconnectAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
ObjectDisposedException.ThrowIf(disposed, this);
|
|
await lifecycleGate.WaitAsync(cancellationToken);
|
|
try
|
|
{
|
|
if (CurrentConfig is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Publish(NodeStatus.Create(NodeState.Connecting, "等待本机任务收尾后重连…"));
|
|
await StopConnectionAsync();
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
StartConnection();
|
|
return true;
|
|
}
|
|
finally
|
|
{
|
|
lifecycleGate.Release();
|
|
}
|
|
}
|
|
|
|
internal async Task ResetIdentityAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
ObjectDisposedException.ThrowIf(disposed, this);
|
|
await lifecycleGate.WaitAsync(cancellationToken);
|
|
try
|
|
{
|
|
await StopConnectionAsync();
|
|
store.DeleteLocalIdentity();
|
|
CurrentConfig = null;
|
|
Publish(NodeStatus.Create(
|
|
NodeState.NotRegistered,
|
|
"本机身份已清除,请使用新注册码注册"));
|
|
}
|
|
finally
|
|
{
|
|
lifecycleGate.Release();
|
|
}
|
|
}
|
|
|
|
internal async Task<NodeDataMigrationResult> MigrateDataRootAsync(
|
|
string targetRoot,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ObjectDisposedException.ThrowIf(disposed, this);
|
|
await lifecycleGate.WaitAsync(cancellationToken);
|
|
try
|
|
{
|
|
await StopConnectionAsync();
|
|
try
|
|
{
|
|
var result = await migrate(paths.RootDirectory, targetRoot, cancellationToken);
|
|
saveDataRoot(result.TargetDirectory);
|
|
return result;
|
|
}
|
|
catch
|
|
{
|
|
StartConnection();
|
|
throw;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lifecycleGate.Release();
|
|
}
|
|
}
|
|
|
|
internal Task StopAsync(NodeShutdownMode mode)
|
|
{
|
|
lock (stopSync)
|
|
{
|
|
ObjectDisposedException.ThrowIf(disposed, this);
|
|
return stopTask ??= StopOnceAsync(mode);
|
|
}
|
|
}
|
|
|
|
private async Task StopOnceAsync(NodeShutdownMode mode)
|
|
{
|
|
await lifecycleGate.WaitAsync();
|
|
try
|
|
{
|
|
var activeJobs = ActiveJobCount;
|
|
Publish(NodeStatus.Create(
|
|
NodeState.Stopped,
|
|
mode == NodeShutdownMode.CancelJobs
|
|
? "正在取消本机任务并退出…"
|
|
: activeJobs > 0 ? "等待本机任务完成后退出…" : "正在退出…"));
|
|
if (mode == NodeShutdownMode.CancelJobs)
|
|
{
|
|
connection?.CancelActiveJobsForExit();
|
|
}
|
|
await StopConnectionAsync();
|
|
}
|
|
finally
|
|
{
|
|
lifecycleGate.Release();
|
|
}
|
|
}
|
|
|
|
private void StartConnection()
|
|
{
|
|
if (CurrentConfig is null || connectionTask is { IsCompleted: false })
|
|
{
|
|
return;
|
|
}
|
|
|
|
connectionStop?.Dispose();
|
|
connectionStop = new CancellationTokenSource();
|
|
connection = connectionFactory(CurrentConfig, Publish);
|
|
connectionTask = RunConnectionAsync(connection, connectionStop.Token);
|
|
}
|
|
|
|
private async Task RunConnectionAsync(
|
|
INodeConnection activeConnection,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await activeConnection.RunAsync(cancellationToken);
|
|
}
|
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
{
|
|
Publish(NodeStatus.Create(NodeState.Stopped, "连接已停止"));
|
|
}
|
|
catch (NodeConfigurationException exception)
|
|
{
|
|
Publish(NodeStatus.Create(NodeState.AuthenticationRequired, exception.Message));
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Publish(NodeStatus.Create(NodeState.Offline, $"节点异常:{exception.Message}"));
|
|
}
|
|
finally
|
|
{
|
|
if (ReferenceEquals(connection, activeConnection))
|
|
{
|
|
connection = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task StopConnectionAsync()
|
|
{
|
|
connectionStop?.Cancel();
|
|
if (connectionTask is not null)
|
|
{
|
|
await connectionTask;
|
|
}
|
|
connectionTask = null;
|
|
connection = null;
|
|
}
|
|
|
|
private void Publish(NodeStatus status)
|
|
{
|
|
CurrentStatus = status;
|
|
StatusChanged?.Invoke(status);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (disposed)
|
|
{
|
|
return;
|
|
}
|
|
disposed = true;
|
|
connectionStop?.Cancel();
|
|
connectionStop?.Dispose();
|
|
lifecycleGate.Dispose();
|
|
}
|
|
}
|