diff --git a/src/Serein.Cli/Services/Interaction/CommandPromptCallbacks.Items.cs b/src/Serein.Cli/Services/Interaction/CommandPromptCallbacks.Items.cs index b247f4eb..133f6094 100644 --- a/src/Serein.Cli/Services/Interaction/CommandPromptCallbacks.Items.cs +++ b/src/Serein.Cli/Services/Interaction/CommandPromptCallbacks.Items.cs @@ -35,7 +35,7 @@ private IEnumerable GetServerCompletionItem() (kv) => new CompletionItem(kv.Key, getExtendedDescription: (_) => GetDescription(kv)) ); - static Task GetDescription(KeyValuePair kv) + static Task GetDescription(KeyValuePair kv) { var stringBuilder = new StringBuilder(); stringBuilder.AppendLine(kv.Value.Configuration.Name); diff --git a/src/Serein.Cli/Services/Interaction/Handlers/ServerHandler.cs b/src/Serein.Cli/Services/Interaction/Handlers/ServerHandler.cs index a2fc5f17..980207f7 100644 --- a/src/Serein.Cli/Services/Interaction/Handlers/ServerHandler.cs +++ b/src/Serein.Cli/Services/Interaction/Handlers/ServerHandler.cs @@ -61,7 +61,7 @@ public override void Invoke(IReadOnlyList args) ); } - if (!_serverManager.Servers.TryGetValue(id, out Server? server)) + if (!_serverManager.Servers.TryGetValue(id, out ServerBase? server)) { throw new InvalidArgumentException("指定的服务器不存在"); } @@ -133,7 +133,7 @@ public override void Invoke(IReadOnlyList args) } } - private void LogServerInfo(Server server) + private void LogServerInfo(ServerBase server) { var stringBuilder = new StringBuilder(); stringBuilder.AppendLine($"{server.Configuration.Name}({server.Id})"); diff --git a/src/Serein.Cli/Services/Interaction/ServerSwitcher.cs b/src/Serein.Cli/Services/Interaction/ServerSwitcher.cs index fd33cee7..c47994ba 100644 --- a/src/Serein.Cli/Services/Interaction/ServerSwitcher.cs +++ b/src/Serein.Cli/Services/Interaction/ServerSwitcher.cs @@ -84,7 +84,7 @@ public void Initialize() private void LogToConsole(object? sender, ServerOutputEventArgs e) { - if (sender is not Server server) + if (sender is not ServerBase server) { return; } diff --git a/src/Serein.Core/Models/Plugins/Net/PluginBase.Events.cs b/src/Serein.Core/Models/Plugins/Net/PluginBase.Events.cs index 0fea21c2..c0b8df9b 100644 --- a/src/Serein.Core/Models/Plugins/Net/PluginBase.Events.cs +++ b/src/Serein.Core/Models/Plugins/Net/PluginBase.Events.cs @@ -4,34 +4,29 @@ using System.Text.Json.Nodes; using System.Threading.Tasks; using Serein.Core.Models.Network.Connection.OneBot.Packets; +using Serein.Core.Services.Servers; using Serein.Core.Utils.Extensions; namespace Serein.Core.Models.Plugins.Net; public abstract partial class PluginBase { - protected virtual Task OnServerStarting(Services.Servers.Server server) => - Task.FromResult(true); + protected virtual Task OnServerStarting(ServerBase server) => Task.FromResult(true); - protected virtual Task OnServerStarted(Services.Servers.Server server) => Task.CompletedTask; + protected virtual Task OnServerStarted(ServerBase server) => Task.CompletedTask; - protected virtual Task OnServerStopping(Services.Servers.Server server) => - Task.FromResult(true); + protected virtual Task OnServerStopping(ServerBase server) => Task.FromResult(true); - protected virtual Task OnServerExited( - Services.Servers.Server server, - int exitcode, - DateTime exitTime - ) => Task.CompletedTask; + protected virtual Task OnServerExited(ServerBase server, int exitcode, DateTime exitTime) => + Task.CompletedTask; - protected virtual Task OnServerOutput(Services.Servers.Server server, string line) => + protected virtual Task OnServerOutput(ServerBase server, string line) => Task.FromResult(true); - protected virtual Task OnServerRawOutput(Services.Servers.Server server, string line) => + protected virtual Task OnServerRawOutput(ServerBase server, string line) => Task.FromResult(true); - protected virtual Task OnServerInput(Services.Servers.Server server, string line) => - Task.CompletedTask; + protected virtual Task OnServerInput(ServerBase server, string line) => Task.CompletedTask; protected virtual Task OnGroupIncreased() => Task.CompletedTask; @@ -62,13 +57,13 @@ internal Task Invoke(Event @event, params object[] args) switch (@event) { case Event.ServerStarted: - return OnServerStarted(args.First().OfType()); + return OnServerStarted(args.First().OfType()); case Event.ServerStarting: - return OnServerStarting(args.First().OfType()); + return OnServerStarting(args.First().OfType()); case Event.ServerStopping: - return OnServerStopping(args.First().OfType()); + return OnServerStopping(args.First().OfType()); case Event.GroupMessageReceived: return OnGroupMessageReceived(args.First().OfType()); @@ -89,7 +84,7 @@ internal Task Invoke(Event @event, params object[] args) } return OnServerOutput( - args.First().OfType(), + args.First().OfType(), args.Last().OfType() ); @@ -100,7 +95,7 @@ internal Task Invoke(Event @event, params object[] args) } return OnServerRawOutput( - args.First().OfType(), + args.First().OfType(), args.Last().OfType() ); @@ -111,14 +106,14 @@ internal Task Invoke(Event @event, params object[] args) } return OnServerInput( - args.First().OfType(), + args.First().OfType(), args.Last().OfType() ); case Event.ServerExited: if ( args.Length != 3 - || args[0] is not Services.Servers.Server server + || args[0] is not ServerBase server || args[1] is not int code || args[2] is not DateTime time ) diff --git a/src/Serein.Core/Models/Server/Configuration.cs b/src/Serein.Core/Models/Server/Configuration.cs index 196137cc..91285fde 100644 --- a/src/Serein.Core/Models/Server/Configuration.cs +++ b/src/Serein.Core/Models/Server/Configuration.cs @@ -25,7 +25,7 @@ public class Configuration : NotifyPropertyChangedModelBase public EncodingMap.EncodingType OutputEncoding { get; set; } - public OutputStyle OutputStyle { get; set; } + public OutputStyle OutputStyle { get; set; } = OutputStyle.RawText; public short PortIPv4 { get; set; } = 19132; @@ -34,4 +34,10 @@ public class Configuration : NotifyPropertyChangedModelBase public bool StartWhenSettingUp { get; set; } public bool UseUnicodeChars { get; set; } + + public bool UsePty { get; set; } + + public int? TerminalWidth { get; set; } = 150; + + public int? TerminalHeight { get; set; } = 80; } diff --git a/src/Serein.Core/Models/Server/ServerInfo.cs b/src/Serein.Core/Models/Server/ServerInfo.cs index df4e858e..9a90b619 100644 --- a/src/Serein.Core/Models/Server/ServerInfo.cs +++ b/src/Serein.Core/Models/Server/ServerInfo.cs @@ -3,7 +3,7 @@ namespace Serein.Core.Models.Server; -internal class ServerInfo : NotifyPropertyChangedModelBase, IServerInfo +public class ServerInfo : NotifyPropertyChangedModelBase, IServerInfo { public string? FileName { get; internal set; } diff --git a/src/Serein.Core/Models/Server/ServersUpdatedEventArgs.cs b/src/Serein.Core/Models/Server/ServersUpdatedEventArgs.cs index 385bb0cb..ad65c89e 100644 --- a/src/Serein.Core/Models/Server/ServersUpdatedEventArgs.cs +++ b/src/Serein.Core/Models/Server/ServersUpdatedEventArgs.cs @@ -1,14 +1,12 @@ using System; +using Serein.Core.Services.Servers; namespace Serein.Core.Models.Server; -public class ServersUpdatedEventArgs( - ServersUpdatedType type, - string id, - Services.Servers.Server server -) : EventArgs +public class ServersUpdatedEventArgs(ServersUpdatedType type, string id, ServerBase server) + : EventArgs { public string Id { get; } = id; public ServersUpdatedType Type { get; } = type; - public Services.Servers.Server Server { get; } = server; + public ServerBase Server { get; } = server; } diff --git a/src/Serein.Core/Serein.Core.csproj b/src/Serein.Core/Serein.Core.csproj index 41b21e93..9d76a77d 100644 --- a/src/Serein.Core/Serein.Core.csproj +++ b/src/Serein.Core/Serein.Core.csproj @@ -25,6 +25,7 @@ + diff --git a/src/Serein.Core/Services/Commands/CommandParser.cs b/src/Serein.Core/Services/Commands/CommandParser.cs index 4cb5fbe6..4434ba13 100644 --- a/src/Serein.Core/Services/Commands/CommandParser.cs +++ b/src/Serein.Core/Services/Commands/CommandParser.cs @@ -312,7 +312,7 @@ public string ApplyVariables( private object? GetServerVariables(string input, string? id = null) { var i = input.IndexOf('@'); - Server? server; + ServerBase? server; if (i < 0) { @@ -334,7 +334,7 @@ public string ApplyVariables( return !_serverManager.Servers.TryGetValue(id, out server) ? null : Switch(key, server); - static object? Switch(string key, Server? server) + static object? Switch(string key, ServerBase? server) { return server is null ? null diff --git a/src/Serein.Core/Services/Commands/CommandRunner.cs b/src/Serein.Core/Services/Commands/CommandRunner.cs index 415324bb..c5d847b9 100644 --- a/src/Serein.Core/Services/Commands/CommandRunner.cs +++ b/src/Serein.Core/Services/Commands/CommandRunner.cs @@ -82,7 +82,7 @@ public async Task RunAsync(Command command, CommandContext? commandContext = nul break; case CommandType.InputServer: - Server? server = null; + ServerBase? server = null; if (!string.IsNullOrEmpty(argumentStr)) { _serverManager.Value.Servers.TryGetValue(argumentStr, out server); diff --git a/src/Serein.Core/Services/Network/WebApi/BroadcastWebSocketModule.cs b/src/Serein.Core/Services/Network/WebApi/BroadcastWebSocketModule.cs index e8690e3c..d599e76d 100644 --- a/src/Serein.Core/Services/Network/WebApi/BroadcastWebSocketModule.cs +++ b/src/Serein.Core/Services/Network/WebApi/BroadcastWebSocketModule.cs @@ -129,7 +129,7 @@ private void OnServersUpdate(object? sender, ServersUpdatedEventArgs e) private void NotifyOutput(object? sender, ServerOutputEventArgs e) { - if (sender is not Server server) + if (sender is not ServerBase server) { return; } @@ -162,7 +162,7 @@ private void NotifyOutput(object? sender, ServerOutputEventArgs e) private void NotifyStatusChanged(object? sender, EventArgs e) { - if (sender is not Server server) + if (sender is not ServerBase server) { return; } diff --git a/src/Serein.Core/Services/Plugins/EventDispatcher.cs b/src/Serein.Core/Services/Plugins/EventDispatcher.cs index 197230b9..bef42a19 100644 --- a/src/Serein.Core/Services/Plugins/EventDispatcher.cs +++ b/src/Serein.Core/Services/Plugins/EventDispatcher.cs @@ -75,7 +75,15 @@ params object[] args { foreach ((_, var jsPlugin) in _jsPluginLoader.Plugins) { - tasks.Add(Task.Run(() => jsPlugin.Invoke(@event, cancellationToken, args))); + if (cancellationToken.IsCancellationRequested) + { + break; + } + + if (jsPlugin.IsEnabled) + { + tasks.Add(Task.Run(() => jsPlugin.Invoke(@event, cancellationToken, args))); + } } } @@ -94,17 +102,20 @@ params object[] args break; } - try - { - tasks.Add(plugin.Invoke(@event, args)); - } - catch (Exception e) + if (plugin.IsEnabled) { - _pluginLogger.Log( - LogLevel.Error, - name, - $"触发事件{name}时出现异常:\n{e.GetDetailString()}" - ); + try + { + tasks.Add(plugin.Invoke(@event, args)); + } + catch (Exception e) + { + _pluginLogger.Log( + LogLevel.Error, + name, + $"触发事件{name}时出现异常:\n{e.GetDetailString()}" + ); + } } } diff --git a/src/Serein.Core/Services/Plugins/Js/Properties/ServerProperty.cs b/src/Serein.Core/Services/Plugins/Js/Properties/ServerProperty.cs index 924b4469..f641c664 100644 --- a/src/Serein.Core/Services/Plugins/Js/Properties/ServerProperty.cs +++ b/src/Serein.Core/Services/Plugins/Js/Properties/ServerProperty.cs @@ -13,7 +13,7 @@ internal ServerProperty(ServerManager servers) _serverManager = servers; } - public Server this[string id] => _serverManager.Servers[id]; + public ServerBase this[string id] => _serverManager.Servers[id]; public string[] Ids => _serverManager.Servers.Keys.ToArray(); diff --git a/src/Serein.Core/Services/Servers/Server.cs b/src/Serein.Core/Services/Servers/Server.cs index 5fc1e0cb..eafb44a7 100644 --- a/src/Serein.Core/Services/Servers/Server.cs +++ b/src/Serein.Core/Services/Servers/Server.cs @@ -1,55 +1,20 @@ using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Serein.Core.Models.Plugins; using Serein.Core.Models.Server; -using Serein.Core.Models.Settings; using Serein.Core.Services.Commands; using Serein.Core.Services.Data; using Serein.Core.Services.Plugins; using Serein.Core.Utils; -using Serein.Core.Utils.Extensions; namespace Serein.Core.Services.Servers; -public sealed class Server +public sealed class Server : ServerBase { - [JsonIgnore] - public string Id { get; } - public RestartStatus RestartStatus { get; private set; } - public bool Status => _serverProcess is not null && !_serverProcess.HasExited; - public int? Pid => _serverProcess?.Id; - public IServerInfo Info => _serverInfo; - public IReadOnlyList CommandHistory => _commandHistory; - public int CommandHistoryIndex { get; internal set; } - public Configuration Configuration { get; } - public ServerPluginManager PluginManager { get; } - - private CancellationTokenSource? _restartCancellationTokenSource; + public override bool Status => _process is not null && !_process.HasExited; + public override int? Pid => _process?.Id; private BinaryWriter? _inputWriter; - private Process? _serverProcess; - private TimeSpan _prevProcessCpuTime = TimeSpan.Zero; - private bool _isTerminated; - - private readonly LogWriter _logWriter; - private readonly Matcher _matcher; - private readonly EventDispatcher _eventDispatcher; - private readonly ReactionTrigger _reactionManager; - private readonly ILogger _logger; - private readonly SettingProvider _settingProvider; - private readonly List _commandHistory; - private readonly List _cache; - private readonly System.Timers.Timer _updateTimer; - private readonly ServerInfo _serverInfo; - public event EventHandler? ServerStatusChanged; - public event EventHandler? ServerOutput; internal Server( string id, @@ -61,45 +26,20 @@ internal Server( EventDispatcher eventDispatcher, ReactionTrigger reactionManager ) + : base( + id, + matcher, + logger, + writerLogger, + configuration, + settingManager, + eventDispatcher, + reactionManager + ) { } + + protected override void StartProcess() { - Id = id; - _logWriter = new(writerLogger, string.Format(PathConstants.ServerLogDirectory, id)); - _logger = logger; - Configuration = configuration; - _settingProvider = settingManager; - _matcher = matcher; - _eventDispatcher = eventDispatcher; - _reactionManager = reactionManager; - _commandHistory = []; - _cache = []; - _updateTimer = new(2000) { AutoReset = true }; - _updateTimer.Elapsed += (_, _) => UpdateInfo(); - _serverInfo = new(); - PluginManager = new(this); - - ServerStatusChanged += (_, _) => UpdateInfo(); - } - - public void Start() - { - _logger.LogDebug("Id={}: 请求启动", Id); - - if (Status) - { - throw new InvalidOperationException("服务器已在运行"); - } - - if (string.IsNullOrEmpty(Configuration.FileName)) - { - throw new InvalidOperationException("启动文件为空"); - } - - if (!_eventDispatcher.Dispatch(Event.ServerStarting, this)) - { - return; - } - - _serverProcess = Process.Start( + _process = Process.Start( new ProcessStartInfo { FileName = Configuration.FileName, @@ -114,259 +54,30 @@ public void Start() Arguments = Configuration.Argument, } ); - _serverProcess!.EnableRaisingEvents = true; - _isTerminated = false; - RestartStatus = RestartStatus.None; - _serverInfo.OutputLines = 0; - _serverInfo.InputLines = 0; - _serverInfo.StartTime = _serverProcess.StartTime; - _serverInfo.ExitTime = null; - _serverInfo.FileName = File.Exists(Configuration.FileName) - ? Path.GetFileName(Configuration.FileName) - : Configuration.FileName; - _commandHistory.Clear(); - _prevProcessCpuTime = TimeSpan.Zero; - - _inputWriter = new(_serverProcess.StandardInput.BaseStream); - - _serverProcess.BeginOutputReadLine(); - _serverProcess.BeginErrorReadLine(); - - _serverProcess.Exited += OnExit; - _serverProcess.ErrorDataReceived += OnOutputDataReceived; - _serverProcess.OutputDataReceived += OnOutputDataReceived; - - ServerStatusChanged?.Invoke(this, EventArgs.Empty); - _restartCancellationTokenSource?.Cancel(); - ServerOutput?.Invoke( - this, - new(ServerOutputType.Information, $"“{Configuration.FileName}”启动中") - ); - _reactionManager.TriggerAsync(ReactionType.ServerStart, new(Id)); - _eventDispatcher.Dispatch(Event.ServerStarted, this); - _updateTimer.Start(); - - _logger.LogDebug("Id={}: 正在启动", Id); - - if (Configuration.SaveLog) - { - _logWriter.WriteAsync(DateTime.Now.ToString("T") + " 服务器已启动"); - } - } - - public void Stop() - { - _logger.LogDebug("Id={}: 请求关闭", Id); - - if (CancelRestart()) - { - return; - } - - if (!Status || _serverProcess is null) - { - throw new InvalidOperationException("服务器未运行"); - } - - if (!_eventDispatcher.Dispatch(Event.ServerStopping, this)) - { - return; - } + _process!.EnableRaisingEvents = true; - if (Configuration.StopCommands.Length == 0) - { - if ( - SereinApp.Type is AppType.Lite or AppType.Plus - && Environment.OSVersion.Platform == PlatformID.Win32NT - ) - { - ServerOutput?.Invoke( - this, - new( - ServerOutputType.Information, - "当前未设置关服命令,将发送Ctrl+C事件作为替代" - ) - ); - - NativeMethods.AttachConsole((uint)_serverProcess.Id); - NativeMethods.GenerateConsoleCtrlEvent( - NativeMethods.CtrlTypes.CTRL_C_EVENT, - (uint)_serverProcess.Id - ); - NativeMethods.FreeConsole(); - - _logger.LogDebug("Id={}: 发送Ctrl+C事件", Id); - } - else - { - throw new NotSupportedException("关服命令为空"); - } - } + _inputWriter = new(_process.StandardInput.BaseStream); - foreach (string command in Configuration.StopCommands) - { - if (!string.IsNullOrEmpty(command)) - { - Input(command); - } - } + _process.BeginOutputReadLine(); + _process.BeginErrorReadLine(); - _logger.LogDebug("Id={}: 正在关闭", Id); - } + _process.Exited += OnExit; + _process.ErrorDataReceived += OnOutputDataReceived; + _process.OutputDataReceived += OnOutputDataReceived; - internal void InputFromCommand(string command, EncodingMap.EncodingType? encodingType = null) - { - if (Status) - { - Input(command, encodingType, false); - } - else if (command == "start") - { - Start(); - } - else if (command == "stop") - { - RestartStatus = RestartStatus.None; - _restartCancellationTokenSource?.Cancel(); - } + OnServerStatusChanged(); } - public void Input(string command) + protected override void TerminateProcess() { - Input(command, null, false); - } - - internal void Input(string command, EncodingMap.EncodingType? encodingType, bool fromUser) - { - _logger.LogDebug( - "Id={}: command='{}'; encodingType={}; fromUser={}", - Id, - command, - encodingType, - fromUser - ); - - if (_inputWriter is null || !Status) - { - return; - } - - if (!_eventDispatcher.Dispatch(Event.ServerInput, this, command)) - { - return; - } - - _inputWriter.Write( - EncodingMap - .GetEncoding(encodingType ?? Configuration.InputEncoding) - .GetBytes( - (Configuration.UseUnicodeChars ? command.ToUnicode() : command) - + Configuration.LineTerminator - ) - ); - _inputWriter.Flush(); - - _serverInfo.InputLines++; - - if (fromUser && !string.IsNullOrEmpty(command)) - { - if ( - _commandHistory.Count > 0 && _commandHistory[^1] != command - || _commandHistory.Count == 0 - ) - { - _commandHistory.Add(command); - } - - if (SereinApp.Type != AppType.Cli) - { - ServerOutput?.Invoke(this, new(ServerOutputType.InputCommand, command)); - } - } - - CommandHistoryIndex = CommandHistory.Count; - - _matcher.QueueServerInputLine(Id, command); - } - - public void RequestRestart() - { - _logger.LogDebug("Id={}: 请求重启", Id); - - if (RestartStatus != RestartStatus.None) - { - throw new InvalidOperationException("正在等待重启"); - } - - Stop(); - - RestartStatus = RestartStatus.Waiting; - } - - public void Terminate() - { - _logger.LogDebug("Id={}: 请求强制结束", Id); - - if (CancelRestart()) - { - return; - } - - if (!Status) - { - throw new InvalidOperationException("服务器未运行"); - } - - _serverProcess?.Kill(true); - _isTerminated = true; + _process?.Kill(true); } private void OnExit(object? sender, EventArgs e) { - _updateTimer.Stop(); - var exitCode = _serverProcess?.ExitCode ?? 0; - _logger.LogDebug("Id={}: 进程(PID={})退出:{}", Id, _serverProcess?.Id, exitCode); - - if (Configuration.SaveLog) - { - _logWriter.WriteAsync(DateTime.Now.ToString("T") + " 进程退出:" + exitCode); - } - - ServerOutput?.Invoke( - this, - new( - ServerOutputType.Information, - $"进程已退出,退出代码为 {exitCode} (0x{exitCode:x8})" - ) - ); - - if ( - RestartStatus == RestartStatus.Waiting - || RestartStatus == RestartStatus.None - && exitCode != 0 - && Configuration.AutoRestart - && !_isTerminated - ) - { - Task.Run(WaitAndRestart); - } - - _serverInfo.ExitTime = _serverProcess?.ExitTime; - _serverProcess = null; - - ServerStatusChanged?.Invoke(this, EventArgs.Empty); - - if (!_eventDispatcher.Dispatch(Event.ServerExited, this, exitCode, DateTime.Now)) - { - return; - } - - _reactionManager.TriggerAsync( - exitCode == 0 - ? ReactionType.ServerExitedNormally - : ReactionType.ServerExitedUnexpectedly, - new(Id) - ); + OnServerExit(_process?.ExitCode ?? 0); + _info.ExitTime = _process?.ExitTime; + _process = null; } private void OnOutputDataReceived(object? sender, DataReceivedEventArgs e) @@ -376,122 +87,17 @@ private void OnOutputDataReceived(object? sender, DataReceivedEventArgs e) return; } - _logger.LogDebug("Id={}: 输出'{}'", Id, e.Data); - if (Configuration.SaveLog) - { - _logWriter.WriteAsync(e.Data); - } - - _serverInfo.OutputLines++; - - ServerOutput?.Invoke(this, new(ServerOutputType.Raw, e.Data)); - - if (!_eventDispatcher.Dispatch(Event.ServerRawOutput, this, e.Data)) - { - return; - } - - var filtered = OutputFilter.Clear(e.Data); - - if (!_eventDispatcher.Dispatch(Event.ServerOutput, this, filtered)) - { - return; - } - - if ( - _settingProvider.Value.Application.PattenForEnableMatchingMuiltLines.Any( - filtered.Contains - ) - ) - { - _cache.Add(filtered); - _matcher.QueueServerOutputLine(Id, string.Join('\n', _cache)); - } - else - { - _cache.Clear(); - } - - _matcher.QueueServerOutputLine(Id, filtered); - } - - private bool CancelRestart() - { - if ( - _restartCancellationTokenSource is not null - && !_restartCancellationTokenSource.IsCancellationRequested - ) - { - _restartCancellationTokenSource.Cancel(); - _logger.LogDebug("Id={}: 取消重启", Id); - ServerOutput?.Invoke(this, new(ServerOutputType.Information, "重启已取消")); - return true; - } - - return false; + OnServerOutput(e.Data); } - private void WaitAndRestart() + protected override void WriteLine(byte[] bytes) { - RestartStatus = RestartStatus.Preparing; - _restartCancellationTokenSource?.Dispose(); - _restartCancellationTokenSource = new(); - - ServerOutput?.Invoke( - this, - new( - ServerOutputType.Information, - $"将在五秒后({DateTime.Now.AddSeconds(5):T})重启服务器" - ) - ); - - Task.Delay(5000, _restartCancellationTokenSource.Token) - .ContinueWith( - (task) => - { - RestartStatus = RestartStatus.None; - if (!task.IsCanceled) - { - try - { - Start(); - } - catch (Exception e) - { - ServerOutput?.Invoke(this, new(ServerOutputType.Error, e.Message)); - } - } - } - ); - } - - private async Task UpdateInfo() - { - if (!Status || _serverProcess is null) + if (_inputWriter is null || !Status) { - _serverInfo.Argument = null; - _serverInfo.FileName = null; - _serverInfo.StartTime = null; - _serverInfo.Stat = null; - _serverInfo.OutputLines = 0; - _serverInfo.InputLines = 0; - _serverInfo.CPUUsage = 0; return; } - _serverInfo.CPUUsage = (int)( - (_serverProcess.TotalProcessorTime - _prevProcessCpuTime).TotalMilliseconds - / 2000 - / Environment.ProcessorCount - * 100 - ); - _prevProcessCpuTime = _serverProcess.TotalProcessorTime; - - if (Configuration.PortIPv4 >= 0) - { - await Task.Run( - () => _serverInfo.Stat = new("127.0.0.1", (ushort)Configuration.PortIPv4) - ); - } + _inputWriter.Write(bytes); + _inputWriter.Flush(); } } diff --git a/src/Serein.Core/Services/Servers/ServerBase.cs b/src/Serein.Core/Services/Servers/ServerBase.cs new file mode 100644 index 00000000..84ab396e --- /dev/null +++ b/src/Serein.Core/Services/Servers/ServerBase.cs @@ -0,0 +1,469 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Serein.Core.Models.Plugins; +using Serein.Core.Models.Server; +using Serein.Core.Models.Settings; +using Serein.Core.Services.Commands; +using Serein.Core.Services.Data; +using Serein.Core.Services.Plugins; +using Serein.Core.Utils; +using Serein.Core.Utils.Extensions; + +namespace Serein.Core.Services.Servers; + +public abstract class ServerBase +{ + [JsonIgnore] + public string Id { get; } + public RestartStatus RestartStatus { get; protected set; } + public abstract bool Status { get; } + public abstract int? Pid { get; } + public IServerInfo Info => _info; + public IReadOnlyList CommandHistory => _commandHistory; + public int CommandHistoryIndex { get; internal set; } + public Configuration Configuration { get; } + public ServerPluginManager PluginManager { get; } + + protected CancellationTokenSource? _restartCancellationTokenSource; + protected bool _isTerminated; + protected Process? _process; + protected TimeSpan _prevProcessCpuTime = TimeSpan.Zero; + protected readonly LogWriter _logWriter; + protected readonly Matcher _matcher; + protected readonly EventDispatcher _eventDispatcher; + protected readonly ReactionTrigger _reactionManager; + protected readonly ILogger _logger; + protected readonly SettingProvider _settingProvider; + protected readonly List _commandHistory; + protected readonly List _cache; + protected readonly System.Timers.Timer _updateTimer; + protected readonly ServerInfo _info; + public event EventHandler? ServerStatusChanged; + public event EventHandler? ServerOutput; + + protected ServerBase( + string id, + Matcher matcher, + ILogger logger, + ILogger writerLogger, + Configuration configuration, + SettingProvider settingManager, + EventDispatcher eventDispatcher, + ReactionTrigger reactionManager + ) + { + Id = id; + _logWriter = new(writerLogger, string.Format(PathConstants.ServerLogDirectory, id)); + _logger = logger; + Configuration = configuration; + _settingProvider = settingManager; + _matcher = matcher; + _eventDispatcher = eventDispatcher; + _reactionManager = reactionManager; + _commandHistory = []; + _cache = []; + _updateTimer = new(2000) { AutoReset = true }; + _updateTimer.Elapsed += (_, _) => UpdateInfo(); + _info = new(); + PluginManager = new(this); + + ServerStatusChanged += (_, _) => UpdateInfo(); + } + + protected abstract void StartProcess(); + + protected abstract void TerminateProcess(); + + protected abstract void WriteLine(byte[] bytes); + + protected void OnServerStatusChanged() + { + ServerStatusChanged?.Invoke(this, EventArgs.Empty); + + if (Status) + { + WriteInfoLine($"“{Configuration.FileName}”启动中"); + } + } + + protected void OnServerExit(int exitCode) + { + _updateTimer.Stop(); + _logger.LogDebug("Id={}: 进程(PID={})退出:{}", Id, Pid, exitCode); + + if (Configuration.SaveLog) + { + _logWriter.WriteAsync(DateTime.Now.ToString("T") + " 进程退出:" + exitCode); + } + + WriteInfoLine($"进程已退出,退出代码为 {exitCode} (0x{exitCode:x8})"); + + if ( + RestartStatus == RestartStatus.Waiting + || RestartStatus == RestartStatus.None + && exitCode != 0 + && Configuration.AutoRestart + && !_isTerminated + ) + { + Task.Run(WaitAndRestart); + } + + ServerStatusChanged?.Invoke(this, EventArgs.Empty); + + if (!_eventDispatcher.Dispatch(Event.ServerExited, this, exitCode, DateTime.Now)) + { + return; + } + + _reactionManager.TriggerAsync( + exitCode == 0 + ? ReactionType.ServerExitedNormally + : ReactionType.ServerExitedUnexpectedly, + new(Id) + ); + } + + protected void WriteInfoLine(string line) + { + ServerOutput?.Invoke(this, new(ServerOutputType.Information, line)); + } + + protected void WriteErrorLine(string line) + { + ServerOutput?.Invoke(this, new(ServerOutputType.Error, line)); + _logger.LogError("Failed to start server with Pty: {}", line); + } + + protected void OnServerOutput(string line) + { + _logger.LogDebug("Id={}: 输出'{}'", Id, line); + if (Configuration.SaveLog) + { + _logWriter.WriteAsync(line); + } + + _info.OutputLines++; + + ServerOutput?.Invoke(this, new(ServerOutputType.Raw, line)); + + if (!_eventDispatcher.Dispatch(Event.ServerRawOutput, this, line)) + { + return; + } + + var filtered = OutputFilter.Clear(line); + + if (!_eventDispatcher.Dispatch(Event.ServerOutput, this, filtered)) + { + return; + } + + if ( + _settingProvider.Value.Application.PattenForEnableMatchingMuiltLines.Any( + filtered.Contains + ) + ) + { + _cache.Add(filtered); + _matcher.QueueServerOutputLine(Id, string.Join('\n', _cache)); + } + else + { + _cache.Clear(); + } + + _matcher.QueueServerOutputLine(Id, filtered); + } + + public void Start() + { + _logger.LogDebug("Id={}: 请求启动", Id); + + if (Status) + { + throw new InvalidOperationException("服务器已在运行"); + } + + if (string.IsNullOrEmpty(Configuration.FileName)) + { + throw new InvalidOperationException("启动文件为空"); + } + + if (!_eventDispatcher.Dispatch(Event.ServerStarting, this)) + { + return; + } + + StartProcess(); + + RestartStatus = RestartStatus.None; + + _info.OutputLines = 0; + _info.InputLines = 0; + _info.StartTime = DateTime.Now; + _info.ExitTime = null; + _info.FileName = File.Exists(Configuration.FileName) + ? Path.GetFileName(Configuration.FileName) + : Configuration.FileName; + + _isTerminated = false; + _prevProcessCpuTime = TimeSpan.Zero; + _commandHistory.Clear(); + _restartCancellationTokenSource?.Cancel(); + + _reactionManager.TriggerAsync(ReactionType.ServerStart, new(Id)); + _eventDispatcher.Dispatch(Event.ServerStarted, this); + _updateTimer.Start(); + + _logger.LogDebug("Id={}: 正在启动", Id); + + if (Configuration.SaveLog) + { + _logWriter.WriteAsync(DateTime.Now.ToString("T") + " 服务器已启动"); + } + } + + public void Stop() + { + _logger.LogDebug("Id={}: 请求关闭", Id); + + if (CancelRestart()) + { + return; + } + + if (!Status) + { + throw new InvalidOperationException("服务器未运行"); + } + + if (!_eventDispatcher.Dispatch(Event.ServerStopping, this)) + { + return; + } + + if (Configuration.StopCommands.Length == 0) + { + if ( + SereinApp.Type is AppType.Lite or AppType.Plus + && Environment.OSVersion.Platform == PlatformID.Win32NT + && Pid is not null + ) + { + WriteInfoLine("当前未设置关服命令,将发送Ctrl+C事件作为替代"); + + var pid = (uint)Pid.Value; + + NativeMethods.AttachConsole(pid); + NativeMethods.GenerateConsoleCtrlEvent(NativeMethods.CtrlTypes.CTRL_C_EVENT, pid); + NativeMethods.FreeConsole(); + + _logger.LogDebug("Id={}: 尝试发送Ctrl+C事件", Id); + } + else + { + throw new NotSupportedException("关服命令为空"); + } + } + + foreach (string command in Configuration.StopCommands) + { + if (!string.IsNullOrEmpty(command)) + { + Input(command); + } + } + + _logger.LogDebug("Id={}: 正在关闭", Id); + } + + internal void InputFromCommand(string command, EncodingMap.EncodingType? encodingType = null) + { + if (Status) + { + Input(command, encodingType, false); + } + else if (command == "start") + { + Start(); + } + else if (command == "stop") + { + RestartStatus = RestartStatus.None; + _restartCancellationTokenSource?.Cancel(); + } + } + + public void Input(string command) + { + Input(command, null, false); + } + + internal void Input(string command, EncodingMap.EncodingType? encodingType, bool fromUser) + { + if (!Status) + { + return; + } + + _logger.LogDebug( + "Id={}: command='{}'; encodingType={}; fromUser={}", + Id, + command, + encodingType, + fromUser + ); + + if (!_eventDispatcher.Dispatch(Event.ServerInput, this, command)) + { + return; + } + + WriteLine( + EncodingMap + .GetEncoding(encodingType ?? Configuration.InputEncoding) + .GetBytes( + (Configuration.UseUnicodeChars ? command.ToUnicode() : command) + + Configuration.LineTerminator + ) + ); + + _info.InputLines++; + + if (fromUser && !string.IsNullOrEmpty(command)) + { + if ( + _commandHistory.Count > 0 && _commandHistory[^1] != command + || _commandHistory.Count == 0 + ) + { + _commandHistory.Add(command); + } + + if (SereinApp.Type != AppType.Cli) + { + ServerOutput?.Invoke(this, new(ServerOutputType.InputCommand, command)); + } + } + + CommandHistoryIndex = CommandHistory.Count; + + _matcher.QueueServerInputLine(Id, command); + } + + public void Terminate() + { + _logger.LogDebug("Id={}: 请求强制结束", Id); + + if (CancelRestart()) + { + return; + } + + if (!Status) + { + throw new InvalidOperationException("服务器未运行"); + } + + TerminateProcess(); + _isTerminated = true; + } + + protected bool CancelRestart() + { + if ( + _restartCancellationTokenSource is not null + && !_restartCancellationTokenSource.IsCancellationRequested + ) + { + _restartCancellationTokenSource.Cancel(); + _logger.LogDebug("Id={}: 取消重启", Id); + WriteInfoLine("重启已取消"); + return true; + } + + return false; + } + + public void RequestRestart() + { + _logger.LogDebug("Id={}: 请求重启", Id); + + if (RestartStatus != RestartStatus.None) + { + throw new InvalidOperationException("正在等待重启"); + } + + Stop(); + + RestartStatus = RestartStatus.Waiting; + } + + private void WaitAndRestart() + { + RestartStatus = RestartStatus.Preparing; + _restartCancellationTokenSource?.Dispose(); + _restartCancellationTokenSource = new(); + + WriteInfoLine($"将在五秒后({DateTime.Now.AddSeconds(5):T})重启服务器"); + + Task.Delay(5000, _restartCancellationTokenSource.Token) + .ContinueWith( + (task) => + { + RestartStatus = RestartStatus.None; + if (!task.IsCanceled) + { + try + { + Start(); + } + catch (Exception e) + { + ServerOutput?.Invoke(this, new(ServerOutputType.Error, e.Message)); + } + } + } + ); + } + + private async Task UpdateInfo() + { + if (!Status && _process is null) + { + _info.Argument = null; + _info.FileName = null; + _info.StartTime = null; + _info.Stat = null; + _info.OutputLines = 0; + _info.InputLines = 0; + _info.CPUUsage = 0; + return; + } + else if (_process is null) + { + return; + } + + _info.CPUUsage = (int)( + (_process.TotalProcessorTime - _prevProcessCpuTime).TotalMilliseconds + / 2000 + / Environment.ProcessorCount + * 100 + ); + _prevProcessCpuTime = _process.TotalProcessorTime; + + if (Configuration.PortIPv4 >= 0) + { + await Task.Run(() => _info.Stat = new("127.0.0.1", (ushort)Configuration.PortIPv4)); + } + } +} diff --git a/src/Serein.Core/Services/Servers/ServerManager.cs b/src/Serein.Core/Services/Servers/ServerManager.cs index af7a7d14..0379a230 100644 --- a/src/Serein.Core/Services/Servers/ServerManager.cs +++ b/src/Serein.Core/Services/Servers/ServerManager.cs @@ -63,7 +63,7 @@ public static void ValidateId(string? id) } } - public IReadOnlyDictionary Servers => _servers; + public IReadOnlyDictionary Servers => _servers; public event EventHandler? ServersUpdated; private readonly ILogger _serverlogger; @@ -73,7 +73,7 @@ public static void ValidateId(string? id) private readonly EventDispatcher _eventDispatcher; private readonly ReactionTrigger _reactionManager; private readonly Matcher _matcher; - private readonly Dictionary _servers = []; + private readonly Dictionary _servers = []; public ServerManager( ILogger serverlogger, @@ -99,20 +99,31 @@ ReactionTrigger reactionManager public bool AnyRunning => _servers.Any(static (kv) => kv.Value.Status); - public Server Add(string id, Configuration configuration) + public ServerBase Add(string id, Configuration configuration) { ValidateId(id); - var server = new Server( - id, - _matcher, - _serverlogger, - _logWriterLogger, - configuration, - _settingProvider, - _eventDispatcher, - _reactionManager - ); + ServerBase server = configuration.UsePty + ? new ServerWithPty( + id, + _matcher, + _serverlogger, + _logWriterLogger, + configuration, + _settingProvider, + _eventDispatcher, + _reactionManager + ) + : new Server( + id, + _matcher, + _serverlogger, + _logWriterLogger, + configuration, + _settingProvider, + _eventDispatcher, + _reactionManager + ); _servers.Add(id, server); ServersUpdated?.Invoke(this, new(ServersUpdatedType.Added, id, server)); diff --git a/src/Serein.Core/Services/Servers/ServerPluginManager.cs b/src/Serein.Core/Services/Servers/ServerPluginManager.cs index 5ca570bf..c717e559 100644 --- a/src/Serein.Core/Services/Servers/ServerPluginManager.cs +++ b/src/Serein.Core/Services/Servers/ServerPluginManager.cs @@ -43,11 +43,11 @@ public sealed class ServerPluginManager public IReadOnlyList Plugins => _plugins; public string? CurrentPluginsDirectory { get; private set; } - private readonly Server _server; + private readonly ServerBase _server; private readonly List _plugins; private bool _updating; - internal ServerPluginManager(Server server) + internal ServerPluginManager(ServerBase server) { _server = server; _plugins = []; diff --git a/src/Serein.Core/Services/Servers/ServerWithPty.cs b/src/Serein.Core/Services/Servers/ServerWithPty.cs new file mode 100644 index 00000000..b0183407 --- /dev/null +++ b/src/Serein.Core/Services/Servers/ServerWithPty.cs @@ -0,0 +1,148 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Pty.Net; +using Serein.Core.Models.Server; +using Serein.Core.Services.Commands; +using Serein.Core.Services.Data; +using Serein.Core.Services.Plugins; +using Serein.Core.Utils; + +namespace Serein.Core.Services.Servers; + +public class ServerWithPty( + string id, + Matcher matcher, + ILogger logger, + ILogger writerLogger, + Configuration configuration, + SettingProvider settingManager, + EventDispatcher eventDispatcher, + ReactionTrigger reactionManager +) + : ServerBase( + id, + matcher, + logger, + writerLogger, + configuration, + settingManager, + eventDispatcher, + reactionManager + ) +{ + private CancellationTokenSource _cancellationTokenSource = new(); + private IPtyConnection? _ptyConnection; + private StreamReader? _streamReader; + private bool _isPreparing; + + public override bool Status => _ptyConnection is not null && !_isPreparing; + + public override int? Pid => _ptyConnection?.Pid; + + protected override void StartProcess() + { + if (_cancellationTokenSource.IsCancellationRequested) + { + _cancellationTokenSource = new(); + } + + _isPreparing = true; + WriteInfoLine("正在使用虚拟终端启动服务器进程。若出现问题请尝试关闭虚拟终端功能。"); + PtyProvider + .SpawnAsync( + new() + { + Name = Id, + App = Configuration.FileName, + CommandLine = [Configuration.Argument], + Cwd = + Path.GetDirectoryName(Configuration.FileName) + ?? Directory.GetCurrentDirectory(), + ForceWinPty = Environment.OSVersion.Platform == PlatformID.Win32NT, + + Rows = + SereinApp.Type == AppType.Cli + && Environment.OSVersion.Platform == PlatformID.Win32NT + && Configuration.TerminalHeight is null + ? Console.WindowHeight + : Configuration.TerminalHeight ?? 80, + Cols = + SereinApp.Type == AppType.Cli + && Environment.OSVersion.Platform == PlatformID.Win32NT + && Configuration.TerminalHeight is null + ? Console.WindowHeight + : Configuration.TerminalWidth ?? 150, + }, + _cancellationTokenSource.Token + ) + .ContinueWith( + (task) => + { + if (task.IsFaulted) + { + if (Configuration.SaveLog) + { + _logWriter.WriteAsync(task.Exception.ToString()); + } + WriteErrorLine(task.Exception.Message); + return; + } + + _isPreparing = false; + _ptyConnection = task.Result; + _process = Process.GetProcessById(_ptyConnection.Pid); + _ptyConnection.ProcessExited += (_, e) => + { + _ptyConnection = null; + _process = null; + OnServerExit(e.ExitCode); + _cancellationTokenSource.Cancel(); + }; + _streamReader = new( + _ptyConnection.ReaderStream, + EncodingMap.GetEncoding(Configuration.OutputEncoding) + ); + + OnServerStatusChanged(); + ReadLineLoop(); + } + ); + } + + private async Task ReadLineLoop() + { + while ( + Status && !_cancellationTokenSource.IsCancellationRequested && _streamReader is not null + ) + { + try + { + var line = await _streamReader.ReadLineAsync(_cancellationTokenSource.Token); + if (line is not null) + { + OnServerOutput(line); + } + } + catch (OperationCanceledException) + { + break; + } + } + + _streamReader?.Dispose(); + } + + protected override void TerminateProcess() + { + _process?.Kill(true); + } + + protected override void WriteLine(byte[] bytes) + { + _ptyConnection?.WriterStream.Write(bytes); + } +} diff --git a/src/Serein.Core/Utils/OutputFilter.cs b/src/Serein.Core/Utils/OutputFilter.cs index fc78bccc..19f8ac12 100644 --- a/src/Serein.Core/Utils/OutputFilter.cs +++ b/src/Serein.Core/Utils/OutputFilter.cs @@ -27,16 +27,16 @@ public static string RemoveControlChars(string input) return stringBuilder.ToString(); } - private static readonly Regex ColorCharsPattern = GetColorCharsPattern(); + private static readonly Regex ANSIEscapePattern = GetANSIEscapePattern(); /// /// 移除颜色字符 /// /// 输入 /// 移除后的文本 - public static string RemoveColorChars(string input) + public static string RemoveANSIEscapeChars(string input) { - return ColorCharsPattern.Replace(input, string.Empty); + return ANSIEscapePattern.Replace(input, string.Empty); } /// @@ -46,9 +46,9 @@ public static string RemoveColorChars(string input) /// 移除后的文本 public static string Clear(string input) { - return RemoveColorChars(RemoveControlChars(input)); + return RemoveANSIEscapeChars(RemoveControlChars(input)); } - [GeneratedRegex(@"\x1B\[[0-9;]*[ABCDEFGHJKSTfmnsulh]")] - private static partial Regex GetColorCharsPattern(); + [GeneratedRegex(@"\x1B\[[0-9;\?=]*[ABCDEFGHJKSTfmnsulh]")] + private static partial Regex GetANSIEscapePattern(); } diff --git a/src/Serein.Lite/Ui/Servers/ConfigurationEditor.Designer.cs b/src/Serein.Lite/Ui/Servers/ConfigurationEditor.Designer.cs index ca1cf707..41c2776c 100644 --- a/src/Serein.Lite/Ui/Servers/ConfigurationEditor.Designer.cs +++ b/src/Serein.Lite/Ui/Servers/ConfigurationEditor.Designer.cs @@ -64,6 +64,7 @@ private void InitializeComponent() ConfirmButton = new System.Windows.Forms.Button(); ErrorProvider = new System.Windows.Forms.ErrorProvider(components); ToolTip = new System.Windows.Forms.ToolTip(components); + UsePtyCheckBox = new System.Windows.Forms.CheckBox(); IdLabel = new System.Windows.Forms.Label(); NameLabel = new System.Windows.Forms.Label(); FileNameLabel = new System.Windows.Forms.Label(); @@ -266,6 +267,7 @@ private void InitializeComponent() // // InputAndOutputTabPage // + InputAndOutputTabPage.Controls.Add(UsePtyCheckBox); InputAndOutputTabPage.Controls.Add(LineTerminatorTextBox); InputAndOutputTabPage.Controls.Add(LineTerminatorLabel); InputAndOutputTabPage.Controls.Add(UseUnicodeCharsCheckBox); @@ -445,6 +447,17 @@ private void InitializeComponent() // ErrorProvider.ContainerControl = this; // + // UsePtyCheckBox + // + UsePtyCheckBox.AutoSize = true; + UsePtyCheckBox.Location = new System.Drawing.Point(365, 194); + UsePtyCheckBox.Name = "UsePtyCheckBox"; + UsePtyCheckBox.Size = new System.Drawing.Size(310, 35); + UsePtyCheckBox.TabIndex = 11; + UsePtyCheckBox.Text = "使用虚拟终端(实验性)"; + ToolTip.SetToolTip(UsePtyCheckBox, "使用虚拟终端输入和输出\r\n· 用于解决一些控制台无输入或输出的问题\r\n· 在编辑服务器配置时修改此项需重启Serein方可生效\r\n· 可能因系统版本不同而有不同的效果 \r\n· 这是一个实验性选项,后续版本中可能会发生变化"); + UsePtyCheckBox.UseVisualStyleBackColor = true; + // // ConfigurationEditor // AcceptButton = ConfirmButton; @@ -500,5 +513,6 @@ private void InitializeComponent() private System.Windows.Forms.TextBox NameTextBox; private System.Windows.Forms.ErrorProvider ErrorProvider; private System.Windows.Forms.ToolTip ToolTip; + private System.Windows.Forms.CheckBox UsePtyCheckBox; } } diff --git a/src/Serein.Lite/Ui/Servers/ConfigurationEditor.cs b/src/Serein.Lite/Ui/Servers/ConfigurationEditor.cs index 3c5a3229..171d4064 100644 --- a/src/Serein.Lite/Ui/Servers/ConfigurationEditor.cs +++ b/src/Serein.Lite/Ui/Servers/ConfigurationEditor.cs @@ -54,6 +54,7 @@ private void SyncData() InputEncondingComboBox.SelectedIndex = (int)_configuration.InputEncoding; OutputEncondingComboBox.SelectedIndex = (int)_configuration.OutputEncoding; UseUnicodeCharsCheckBox.Checked = _configuration.UseUnicodeChars; + UsePtyCheckBox.Checked = _configuration.UsePty; } private void ConfirmButton_Click(object sender, EventArgs e) @@ -94,6 +95,7 @@ private void ConfirmButton_Click(object sender, EventArgs e) || string.IsNullOrWhiteSpace(NameTextBox.Text) ? "未命名" : NameTextBox.Text; + _configuration.UsePty = UsePtyCheckBox.Checked; _configuration.FileName = FileNameTextBox.Text; _configuration.Argument = ArgumentTextBox.Text; _configuration.AutoRestart = AutoRestartCheckBox.Checked; diff --git a/src/Serein.Lite/Ui/Servers/Panel.Designer.cs b/src/Serein.Lite/Ui/Servers/Panel.Designer.cs index 7761d71b..b5bc9db5 100644 --- a/src/Serein.Lite/Ui/Servers/Panel.Designer.cs +++ b/src/Serein.Lite/Ui/Servers/Panel.Designer.cs @@ -28,6 +28,7 @@ protected override void Dispose(bool disposing) /// private void InitializeComponent() { + components = new System.ComponentModel.Container(); System.Windows.Forms.TableLayoutPanel MainTableLayoutPanel; System.Windows.Forms.GroupBox InfoGroupBox; System.Windows.Forms.TableLayoutPanel InformationTableLayoutPanel; @@ -56,6 +57,7 @@ private void InitializeComponent() ConsoleBrowser = new Controls.ConsoleWebBrowser(); InputTextBox = new System.Windows.Forms.TextBox(); EnterButton = new System.Windows.Forms.Button(); + ToolTipProvider = new System.Windows.Forms.ToolTip(components); MainTableLayoutPanel = new System.Windows.Forms.TableLayoutPanel(); InfoGroupBox = new System.Windows.Forms.GroupBox(); InformationTableLayoutPanel = new System.Windows.Forms.TableLayoutPanel(); @@ -475,5 +477,6 @@ private void InitializeComponent() private System.Windows.Forms.Label PlayerCountDynamicLabel; private System.Windows.Forms.Label CPUPercentDynamicLabel; private Serein.Lite.Ui.Controls.ConsoleWebBrowser ConsoleBrowser; + private System.Windows.Forms.ToolTip ToolTipProvider; } } diff --git a/src/Serein.Lite/Ui/Servers/Panel.cs b/src/Serein.Lite/Ui/Servers/Panel.cs index c34ff126..009c30eb 100644 --- a/src/Serein.Lite/Ui/Servers/Panel.cs +++ b/src/Serein.Lite/Ui/Servers/Panel.cs @@ -11,26 +11,26 @@ namespace Serein.Lite.Ui.Servers; public partial class Panel : UserControl { - private readonly Server _server; + private readonly ServerBase _server; private readonly MainForm _mainForm; private readonly System.Timers.Timer _timer; private readonly object _lock = new(); private readonly Lazy _pluginManagerForm; - public Panel(Server server, MainForm mainForm) + public Panel(ServerBase server, MainForm mainForm) { InitializeComponent(); Dock = DockStyle.Fill; _timer = new(1000); - _timer.Elapsed += (_, _) => Invoke(UpdateInfoLabels); + _timer.Elapsed += (_, _) => Invoke(UpdateInfo); _server = server; _mainForm = mainForm; _server.ServerOutput += OnServerOutput; _server.ServerStatusChanged += (_, _) => { - Invoke(UpdateInfoLabels); + Invoke(UpdateInfo); if (_server.Status) { @@ -207,7 +207,7 @@ private void EnterCommand() } } - private void UpdateInfoLabels() + private void UpdateInfo() { StatusDynamicLabel.Text = _server.Status ? "运行中" : "未启动"; VersionDynamicLabel.Text = _server.Status ? _server.Info.Stat?.Version ?? "-" : "-"; @@ -222,6 +222,10 @@ private void UpdateInfoLabels() CPUPercentDynamicLabel.Text = _server.Status ? _server.Info.CPUUsage.ToString("N2") + "%" : "-"; + + ToolTipProvider.SetToolTip(VersionDynamicLabel, VersionDynamicLabel.Text); + ToolTipProvider.SetToolTip(PlayerCountDynamicLabel, PlayerCountDynamicLabel.Text); + ToolTipProvider.SetToolTip(RunTimeDynamicLabel, RunTimeDynamicLabel.Text); } protected override void OnLoad(EventArgs e) diff --git a/src/Serein.Lite/Ui/Servers/Panel.resx b/src/Serein.Lite/Ui/Servers/Panel.resx index 37a90d5b..9f50f92d 100644 --- a/src/Serein.Lite/Ui/Servers/Panel.resx +++ b/src/Serein.Lite/Ui/Servers/Panel.resx @@ -1,7 +1,7 @@