391 lines
12 KiB
C#
391 lines
12 KiB
C#
using System.Security;
|
||
using System.Windows.Input;
|
||
|
||
namespace Zcbot.WindowsNode;
|
||
|
||
internal sealed class MainWindowViewModel : ObservableObject, IDisposable
|
||
{
|
||
private readonly IStartupRegistration startupRegistration;
|
||
private readonly IDiagnosticService diagnosticService;
|
||
private readonly AsyncCommand registerCommand;
|
||
private readonly AsyncCommand reconnectCommand;
|
||
private readonly AsyncCommand resetIdentityCommand;
|
||
private string statusText = "尚未注册";
|
||
private string statusDetail = "等待节点状态";
|
||
private string identityText = "尚未建立节点身份";
|
||
private string serverUrl = "http://127.0.0.1:8765";
|
||
private string nodeName = Environment.MachineName.ToLowerInvariant();
|
||
private string enrollmentCode = string.Empty;
|
||
private string operationMessage = string.Empty;
|
||
private string statusTone = "neutral";
|
||
private bool isRegistered;
|
||
private bool isBusy;
|
||
private bool isExiting;
|
||
private string currentPage = "overview";
|
||
private bool isStartupEnabled;
|
||
private NodeConfig? currentConfig;
|
||
|
||
internal MainWindowViewModel(
|
||
IStartupRegistration startupRegistration,
|
||
IDiagnosticService diagnosticService,
|
||
SoftwarePageViewModel softwarePage,
|
||
JobPageViewModel jobPage,
|
||
AnsysAcceptanceViewModel acceptancePage,
|
||
DataRootPageViewModel dataRootPage)
|
||
{
|
||
this.startupRegistration = startupRegistration;
|
||
this.diagnosticService = diagnosticService;
|
||
SoftwarePage = softwarePage;
|
||
JobPage = jobPage;
|
||
AcceptancePage = acceptancePage;
|
||
DataRootPage = dataRootPage;
|
||
try
|
||
{
|
||
isStartupEnabled = startupRegistration.IsEnabled;
|
||
}
|
||
catch (Exception exception) when (
|
||
exception is UnauthorizedAccessException or IOException or SecurityException)
|
||
{
|
||
operationMessage = $"无法读取自动启动设置:{exception.Message}";
|
||
}
|
||
|
||
CopyDiagnosticsCommand = new RelayCommand(CopyDiagnostics, () => IsRegistered);
|
||
HideCommand = new RelayCommand(() => HideRequested?.Invoke(), () => !IsExiting);
|
||
ExitCommand = new AsyncCommand(
|
||
() => ExitRequested?.Invoke() ?? Task.CompletedTask,
|
||
() => !IsExiting);
|
||
ShowOverviewCommand = new RelayCommand(() => Navigate("overview"));
|
||
ShowSoftwareCommand = new AsyncCommand(async () =>
|
||
{
|
||
Navigate("software");
|
||
await SoftwarePage.RefreshAsync();
|
||
AcceptancePage.RefreshStatus();
|
||
});
|
||
ShowJobsCommand = new RelayCommand(() =>
|
||
{
|
||
Navigate("jobs");
|
||
JobPage.Refresh();
|
||
});
|
||
ShowSettingsCommand = new RelayCommand(() =>
|
||
{
|
||
Navigate("settings");
|
||
DataRootPage.Refresh();
|
||
});
|
||
registerCommand = new AsyncCommand(RegisterAsync, CanRegister);
|
||
reconnectCommand = new AsyncCommand(ReconnectAsync, () => IsRegistered && !IsBusy);
|
||
resetIdentityCommand = new AsyncCommand(ResetIdentityAsync, () => IsRegistered && !IsBusy);
|
||
RegisterCommand = registerCommand;
|
||
ReconnectCommand = reconnectCommand;
|
||
ResetIdentityCommand = resetIdentityCommand;
|
||
}
|
||
|
||
internal event Func<string, bool>? CopyDiagnosticsRequested;
|
||
internal event Action? HideRequested;
|
||
internal event Func<Task>? ExitRequested;
|
||
internal event Func<EnrollOptions, Task>? RegisterRequested;
|
||
internal event Func<Task<bool>>? ReconnectRequested;
|
||
internal event Func<Task<bool>>? ResetIdentityRequested;
|
||
|
||
public string StatusText
|
||
{
|
||
get => statusText;
|
||
private set => SetProperty(ref statusText, value);
|
||
}
|
||
|
||
public string StatusDetail
|
||
{
|
||
get => statusDetail;
|
||
private set => SetProperty(ref statusDetail, value);
|
||
}
|
||
|
||
public string IdentityText
|
||
{
|
||
get => identityText;
|
||
private set => SetProperty(ref identityText, value);
|
||
}
|
||
|
||
public string ServerUrl
|
||
{
|
||
get => serverUrl;
|
||
set
|
||
{
|
||
if (SetProperty(ref serverUrl, value)) registerCommand.RaiseCanExecuteChanged();
|
||
}
|
||
}
|
||
|
||
public string NodeName
|
||
{
|
||
get => nodeName;
|
||
set
|
||
{
|
||
if (SetProperty(ref nodeName, value)) registerCommand.RaiseCanExecuteChanged();
|
||
}
|
||
}
|
||
|
||
public string EnrollmentCode
|
||
{
|
||
get => enrollmentCode;
|
||
set
|
||
{
|
||
if (SetProperty(ref enrollmentCode, value)) registerCommand.RaiseCanExecuteChanged();
|
||
}
|
||
}
|
||
|
||
public string OperationMessage
|
||
{
|
||
get => operationMessage;
|
||
private set
|
||
{
|
||
if (SetProperty(ref operationMessage, value))
|
||
{
|
||
OnPropertyChanged(nameof(HasOperationMessage));
|
||
}
|
||
}
|
||
}
|
||
|
||
public bool HasOperationMessage => !string.IsNullOrWhiteSpace(OperationMessage);
|
||
|
||
public string StatusTone
|
||
{
|
||
get => statusTone;
|
||
private set => SetProperty(ref statusTone, value);
|
||
}
|
||
|
||
public bool IsRegistered
|
||
{
|
||
get => isRegistered;
|
||
private set
|
||
{
|
||
if (SetProperty(ref isRegistered, value))
|
||
{
|
||
OnPropertyChanged(nameof(IsRegistrationVisible));
|
||
RaiseOperationCanExecuteChanged();
|
||
((RelayCommand)CopyDiagnosticsCommand).RaiseCanExecuteChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
public bool IsRegistrationVisible => !IsRegistered;
|
||
|
||
public bool IsBusy
|
||
{
|
||
get => isBusy;
|
||
private set
|
||
{
|
||
if (SetProperty(ref isBusy, value)) RaiseOperationCanExecuteChanged();
|
||
}
|
||
}
|
||
|
||
public bool IsExiting
|
||
{
|
||
get => isExiting;
|
||
private set
|
||
{
|
||
if (SetProperty(ref isExiting, value))
|
||
{
|
||
((RelayCommand)HideCommand).RaiseCanExecuteChanged();
|
||
((AsyncCommand)ExitCommand).RaiseCanExecuteChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
public bool IsOverviewPage => currentPage == "overview";
|
||
|
||
public bool IsSoftwarePage => currentPage == "software";
|
||
|
||
public bool IsJobsPage => currentPage == "jobs";
|
||
|
||
public bool IsSettingsPage => currentPage == "settings";
|
||
|
||
public string PageTitle => currentPage switch
|
||
{
|
||
"software" => "专业软件",
|
||
"jobs" => "本机任务",
|
||
"settings" => "运行设置",
|
||
_ => "节点概览",
|
||
};
|
||
|
||
public bool IsStartupEnabled
|
||
{
|
||
get => isStartupEnabled;
|
||
set
|
||
{
|
||
if (value == isStartupEnabled) return;
|
||
try
|
||
{
|
||
startupRegistration.SetEnabled(value);
|
||
SetProperty(ref isStartupEnabled, value);
|
||
OperationMessage = value
|
||
? "已启用登录 Windows 后自动启动"
|
||
: "已关闭登录 Windows 后自动启动";
|
||
}
|
||
catch (Exception exception) when (
|
||
exception is UnauthorizedAccessException or IOException or SecurityException)
|
||
{
|
||
OperationMessage = $"自动启动设置失败:{exception.Message}";
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
public ICommand CopyDiagnosticsCommand { get; }
|
||
public ICommand HideCommand { get; }
|
||
public ICommand ExitCommand { get; }
|
||
public ICommand ShowOverviewCommand { get; }
|
||
public ICommand ShowSoftwareCommand { get; }
|
||
public ICommand ShowJobsCommand { get; }
|
||
public ICommand ShowSettingsCommand { get; }
|
||
public ICommand RegisterCommand { get; }
|
||
public ICommand ReconnectCommand { get; }
|
||
public ICommand ResetIdentityCommand { get; }
|
||
|
||
public SoftwarePageViewModel SoftwarePage { get; }
|
||
public JobPageViewModel JobPage { get; }
|
||
public AnsysAcceptanceViewModel AcceptancePage { get; }
|
||
public DataRootPageViewModel DataRootPage { get; }
|
||
|
||
internal void ApplyStatus(NodeStatus status, NodeConfig? config = null)
|
||
{
|
||
StatusText = status.State switch
|
||
{
|
||
NodeState.Online => "节点在线",
|
||
NodeState.Connecting => "正在连接",
|
||
NodeState.AuthenticationRequired => "需要重新注册",
|
||
NodeState.Offline => "节点离线",
|
||
NodeState.Stopped => "节点已停止",
|
||
_ => "尚未注册",
|
||
};
|
||
StatusTone = status.State switch
|
||
{
|
||
NodeState.Online => "success",
|
||
NodeState.Connecting => "warning",
|
||
NodeState.NotRegistered or NodeState.AuthenticationRequired => "error",
|
||
_ => "neutral",
|
||
};
|
||
StatusDetail = $"{status.Message} {status.ChangedAt:HH:mm:ss}";
|
||
IsRegistered = config is not null;
|
||
currentConfig = config;
|
||
IdentityText = config is null
|
||
? "尚未建立节点身份"
|
||
: $"节点:{config.NodeName}\nNode ID:{config.NodeId}\n服务:{config.ServerUrl}";
|
||
if (config is not null)
|
||
{
|
||
ServerUrl = config.ServerUrl.AbsoluteUri.TrimEnd('/');
|
||
NodeName = config.NodeName;
|
||
EnrollmentCode = string.Empty;
|
||
}
|
||
}
|
||
|
||
internal void BeginExit()
|
||
{
|
||
SoftwarePage.CancelInstall();
|
||
IsExiting = true;
|
||
StatusText = "正在退出";
|
||
StatusTone = "neutral";
|
||
StatusDetail = "正在等待本机任务与后台进程安全收尾";
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
SoftwarePage.Dispose();
|
||
AcceptancePage.Dispose();
|
||
}
|
||
|
||
private bool CanRegister() =>
|
||
!IsRegistered
|
||
&& !IsBusy
|
||
&& !string.IsNullOrWhiteSpace(ServerUrl)
|
||
&& !string.IsNullOrWhiteSpace(NodeName)
|
||
&& !string.IsNullOrWhiteSpace(EnrollmentCode);
|
||
|
||
private void CopyDiagnostics()
|
||
{
|
||
if (currentConfig is null) return;
|
||
OperationMessage = CopyDiagnosticsRequested?.Invoke(
|
||
diagnosticService.Build(currentConfig)) == true
|
||
? "诊断信息已复制;内容不包含 Node Token。"
|
||
: "无法写入剪贴板,请稍后重试。";
|
||
}
|
||
|
||
private async Task RegisterAsync()
|
||
{
|
||
if (RegisterRequested is null) return;
|
||
await RunOperationAsync(async () =>
|
||
{
|
||
var options = EnrollOptions.Parse([
|
||
"--server", ServerUrl,
|
||
"--name", NodeName,
|
||
"--code", EnrollmentCode]);
|
||
await RegisterRequested(options);
|
||
OperationMessage = "注册成功,正在连接节点";
|
||
}, "正在注册节点…");
|
||
}
|
||
|
||
private async Task ReconnectAsync()
|
||
{
|
||
if (ReconnectRequested is null) return;
|
||
await RunOperationAsync(async () =>
|
||
{
|
||
OperationMessage = await ReconnectRequested()
|
||
? "已开始重新连接"
|
||
: "本机尚未注册";
|
||
}, "正在等待任务收尾并重新连接…");
|
||
}
|
||
|
||
private async Task ResetIdentityAsync()
|
||
{
|
||
if (ResetIdentityRequested is null) return;
|
||
await RunOperationAsync(async () =>
|
||
{
|
||
if (await ResetIdentityRequested())
|
||
{
|
||
OperationMessage = "本机身份已清除,请使用新注册码注册";
|
||
}
|
||
else
|
||
{
|
||
OperationMessage = "已取消清除本机身份";
|
||
}
|
||
}, "正在清除本机身份…");
|
||
}
|
||
|
||
private async Task RunOperationAsync(Func<Task> operation, string progress)
|
||
{
|
||
IsBusy = true;
|
||
OperationMessage = progress;
|
||
try
|
||
{
|
||
await operation();
|
||
}
|
||
catch (Exception exception) when (
|
||
exception is NodeConfigurationException
|
||
or HttpRequestException
|
||
or IOException
|
||
or UnauthorizedAccessException)
|
||
{
|
||
OperationMessage = exception.Message;
|
||
}
|
||
finally
|
||
{
|
||
IsBusy = false;
|
||
}
|
||
}
|
||
|
||
private void RaiseOperationCanExecuteChanged()
|
||
{
|
||
registerCommand.RaiseCanExecuteChanged();
|
||
reconnectCommand.RaiseCanExecuteChanged();
|
||
resetIdentityCommand.RaiseCanExecuteChanged();
|
||
}
|
||
|
||
private void Navigate(string page)
|
||
{
|
||
if (currentPage == page) return;
|
||
currentPage = page;
|
||
OnPropertyChanged(nameof(IsOverviewPage));
|
||
OnPropertyChanged(nameof(IsSoftwarePage));
|
||
OnPropertyChanged(nameof(IsJobsPage));
|
||
OnPropertyChanged(nameof(IsSettingsPage));
|
||
OnPropertyChanged(nameof(PageTitle));
|
||
}
|
||
}
|