From 2da1eac443e0d0c91fdde1ed3d96fa23e9597a14 Mon Sep 17 00:00:00 2001 From: kislovdm Date: Fri, 17 Jul 2026 13:50:22 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MonsterMonitor/Models/AppSettings.cs | 3 +- src/MonsterMonitor/Services/AppController.cs | 7 +- src/MonsterMonitor/Services/AuthMonitor.cs | 99 +++++++--- .../Services/ProcessMonitorService.cs | 23 +-- .../Services/SshTunnelService.cs | 183 +++++++++++------- src/MonsterMonitor/UI/MainForm.cs | 130 +++++++++---- src/MonsterMonitor/UI/SettingsForm.cs | 27 ++- 7 files changed, 321 insertions(+), 151 deletions(-) diff --git a/src/MonsterMonitor/Models/AppSettings.cs b/src/MonsterMonitor/Models/AppSettings.cs index 1a9875e..7c364b0 100644 --- a/src/MonsterMonitor/Models/AppSettings.cs +++ b/src/MonsterMonitor/Models/AppSettings.cs @@ -170,8 +170,9 @@ namespace MonsterMonitor.Models if (string.IsNullOrWhiteSpace(result.SystemPasswordProtected)) { + // Значение по умолчанию применяется только в памяти. + // Раньше здесь вызывался Save() — лишний дисковый I/O на каждом чтении настроек. result.SetSystemPassword("STerra"); - result.Save(); } return result; diff --git a/src/MonsterMonitor/Services/AppController.cs b/src/MonsterMonitor/Services/AppController.cs index 32093e0..4911ca8 100644 --- a/src/MonsterMonitor/Services/AppController.cs +++ b/src/MonsterMonitor/Services/AppController.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using MonsterMonitor.Models; namespace MonsterMonitor.Services @@ -26,12 +27,14 @@ namespace MonsterMonitor.Services _power.PreventSleep(); _processMonitor = new ProcessMonitorService(_log); _sshTunnel = new SshTunnelService(_log); - _authMonitor = new AuthMonitor(settings); + _authMonitor = new AuthMonitor(settings, _log); _ssConfig.EnsureConfig(settings); _processMonitor.Start(settings.SsProcessPath, settings.SsArguments); _sshTunnel.Start(settings); - _ = _authMonitor.StartMonitor(); + _authMonitor.StartMonitor().ContinueWith( + t => _log.Error("Ошибка запуска монитора авторизации: " + t.Exception?.GetBaseException().Message), + TaskContinuationOptions.OnlyOnFaulted); _log.Info("Сервисы приложения запущены."); } diff --git a/src/MonsterMonitor/Services/AuthMonitor.cs b/src/MonsterMonitor/Services/AuthMonitor.cs index 180520d..dc4f85a 100644 --- a/src/MonsterMonitor/Services/AuthMonitor.cs +++ b/src/MonsterMonitor/Services/AuthMonitor.cs @@ -1,5 +1,4 @@ -using System; -using System.Diagnostics; +using System; using System.Runtime.InteropServices; using System.Text; using System.Threading; @@ -9,32 +8,81 @@ using MonsterMonitor.Models; namespace MonsterMonitor.Services { - public class AuthMonitor: IAuthMonitor + public class AuthMonitor : IAuthMonitor { + private const string AuthWindowMarker = "XAuth request"; + [DllImport("user32.dll")] - static extern IntPtr GetForegroundWindow(); + private static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count); + [DllImport("user32.dll")] - static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count); + private static extern int GetWindowTextLength(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); private readonly AppSettings _settings; + private readonly LogService _log; private CancellationTokenSource _monitorCancellation; private Task _monitorTask; - public AuthMonitor(AppSettings settings) + public AuthMonitor(AppSettings settings, LogService log) { _settings = settings; + _log = log; } - private string GetActiveWindowTitle() + private static string GetWindowTitle(IntPtr handle) { - const int nChars = 256; - var buff = new StringBuilder(nChars); - var handle = GetForegroundWindow(); - return GetWindowText(handle, buff, nChars) > 0 ? buff.ToString() : null; + var length = GetWindowTextLength(handle); + if (length <= 0) + { + return null; + } + + var buff = new StringBuilder(length + 1); + return GetWindowText(handle, buff, buff.Capacity) > 0 ? buff.ToString() : null; + } + + private static string GetActiveWindowTitle() + { + return GetWindowTitle(GetForegroundWindow()); + } + + // Ищет окно авторизации через перечисление окон (без перебора всех процессов + // и без утечки хендлов Process). Возвращает первое видимое подходящее окно. + private static IntPtr FindAuthWindow() + { + var found = IntPtr.Zero; + EnumWindows((hWnd, lParam) => + { + if (!IsWindowVisible(hWnd)) + { + return true; + } + + var title = GetWindowTitle(hWnd); + if (title != null && title.IndexOf(AuthWindowMarker, StringComparison.OrdinalIgnoreCase) >= 0) + { + found = hWnd; + return false; // прекращаем перечисление + } + + return true; + }, IntPtr.Zero); + + return found; } public async Task StartMonitor() @@ -75,29 +123,28 @@ namespace MonsterMonitor.Services { try { - Process[] processlist = Process.GetProcesses(); - foreach (Process process in processlist) + var authWindow = FindAuthWindow(); + if (authWindow != IntPtr.Zero) { - if (!string.IsNullOrEmpty(process.MainWindowTitle) && process.MainWindowTitle.Contains("XAuth request")) - { - SetForegroundWindow(process.MainWindowHandle); - } - } + SetForegroundWindow(authWindow); - var activeWindowTitle = GetActiveWindowTitle(); - if (activeWindowTitle?.Contains("XAuth request") == true) - { - var password = _settings.GetSystemPassword(); - if (!string.IsNullOrEmpty(password)) + var activeWindowTitle = GetActiveWindowTitle(); + if (activeWindowTitle?.IndexOf(AuthWindowMarker, StringComparison.OrdinalIgnoreCase) >= 0) { - SendKeys.SendWait(password); - SendKeys.SendWait("{ENTER}"); + var password = _settings.GetSystemPassword(); + if (!string.IsNullOrEmpty(password)) + { + SendKeys.SendWait(password); + SendKeys.SendWait("{ENTER}"); + } } } } catch (Exception ex) { - MessageBox.Show(ex.Message + ex.StackTrace); + // Раньше здесь был MessageBox.Show из фонового потока — при повторяющемся + // исключении он заваливал рабочий стол окнами. Теперь просто пишем в лог. + _log.Warn("Ошибка мониторинга авторизации: " + ex.Message); } try diff --git a/src/MonsterMonitor/Services/ProcessMonitorService.cs b/src/MonsterMonitor/Services/ProcessMonitorService.cs index bf32d5a..7c869de 100644 --- a/src/MonsterMonitor/Services/ProcessMonitorService.cs +++ b/src/MonsterMonitor/Services/ProcessMonitorService.cs @@ -13,6 +13,7 @@ namespace MonsterMonitor.Services private string _path; private string _arguments; private bool _stopping; + private string _lastStartError; public ProcessMonitorService(LogService log) { @@ -43,7 +44,6 @@ namespace MonsterMonitor.Services { if (_process != null) { - _process.Exited -= ProcessOnExited; _process.Dispose(); } _process = new Process @@ -57,27 +57,19 @@ namespace MonsterMonitor.Services }, EnableRaisingEvents = true }; - //_process.Exited += ProcessOnExited; _process.Start(); + _lastStartError = null; _log.Info("Процесс ss запущен."); } catch (Exception ex) { - _log.Error("Ошибка запуска ss: " + ex.Message); - } - } - - private void ProcessOnExited(object sender, EventArgs e) - { - lock (_sync) - { - if (_stopping) + // Watchdog тикает каждые 10с; при неверном пути не флудим лог + // одинаковой ошибкой — пишем её только при изменении. + if (_lastStartError != ex.Message) { - return; + _lastStartError = ex.Message; + _log.Error("Ошибка запуска ss: " + ex.Message); } - - _log.Warn("Процесс ss завершился, запускаю заново."); - EnsureStarted(); } } @@ -118,7 +110,6 @@ namespace MonsterMonitor.Services { if (_process != null) { - _process.Exited -= ProcessOnExited; _process.Dispose(); } _process = null; diff --git a/src/MonsterMonitor/Services/SshTunnelService.cs b/src/MonsterMonitor/Services/SshTunnelService.cs index fcf3678..9471a3e 100644 --- a/src/MonsterMonitor/Services/SshTunnelService.cs +++ b/src/MonsterMonitor/Services/SshTunnelService.cs @@ -12,16 +12,21 @@ namespace MonsterMonitor.Services { private readonly LogService _log; private readonly object _sync = new object(); + + // Сериализует реконнект: перекрывающиеся запросы (из событий и из монитора) + // отбрасываются, а не выполняются параллельно и не копятся в очередь. + private readonly SemaphoreSlim _reconnectGate = new SemaphoreSlim(1, 1); + private SshClient _client; private ForwardedPortRemote _forwardedPort; private SshCommand _heartbeatCommand; private CancellationTokenSource _heartbeatReadTokenSource; private Task _heartbeatReadTask; private CancellationTokenSource _monitorTokenSource; + private Task _monitorTask; private AppSettings _settings; - private bool _disposed; - private bool _reconnectInProgress; - private DateTime _lastHeartbeatUtc = DateTime.MinValue; + private volatile bool _disposed; + private long _lastHeartbeatTicks; private bool _remoteIsWindows; public SshTunnelService(LogService log) @@ -55,6 +60,10 @@ namespace MonsterMonitor.Services var authMethod = new PasswordAuthenticationMethod(_settings.SshUsername, password); var connectionInfo = new ConnectionInfo(_settings.SshHost, _settings.SshPort, _settings.SshUsername, authMethod); + // Ограничиваем блокирующий Connect(), чтобы он не висел бесконечно. + var timeoutSec = Math.Min(60, Math.Max(5, _settings.ReconnectTimeoutSec)); + connectionInfo.Timeout = TimeSpan.FromSeconds(timeoutSec); + _client = new SshClient(connectionInfo); _client.ErrorOccurred += ClientOnErrorOccurred; _client.Connect(); @@ -85,7 +94,7 @@ namespace MonsterMonitor.Services private void StartMonitor() { _monitorTokenSource = new CancellationTokenSource(); - Task.Run(() => MonitorLoop(_monitorTokenSource.Token)); + _monitorTask = Task.Run(() => MonitorLoop(_monitorTokenSource.Token)); } private async Task MonitorLoop(CancellationToken token) @@ -94,18 +103,23 @@ namespace MonsterMonitor.Services { try { - var silenceThresholdSec = Math.Max( - 5, - _settings.MaxPingFailures); - - var lastHeartbeat = _lastHeartbeatUtc; - var isHeartbeatAlive = lastHeartbeat != DateTime.MinValue && - (DateTime.UtcNow - lastHeartbeat).TotalSeconds <= silenceThresholdSec; - - if (!IsConnected() || !isHeartbeatAlive) + // Пропускаем проверку, пока идёт реконнект — иначе получаем лог-флуд + // и лишние пробуждения, дёргающие уже занятый шлюз реконнекта. + if (_reconnectGate.CurrentCount > 0) { - _log.Warn("Нет живого вывода heartbeat-команды на удаленном сервере. Переподключаю SSH."); - await Reconnect().ConfigureAwait(false); + // heartbeat присылается раз в секунду; MaxPingFailures трактуем + // как допустимое число пропущенных ответов (≈ секунд тишины). + var silenceThresholdSec = Math.Max(10, _settings.MaxPingFailures); + + var lastHeartbeatTicks = Interlocked.Read(ref _lastHeartbeatTicks); + var isHeartbeatAlive = lastHeartbeatTicks != 0 && + (DateTime.UtcNow - new DateTime(lastHeartbeatTicks, DateTimeKind.Utc)).TotalSeconds <= silenceThresholdSec; + + if (!IsConnected() || !isHeartbeatAlive) + { + _log.Warn("Нет живого вывода heartbeat-команды на удаленном сервере. Переподключаю SSH."); + await Reconnect().ConfigureAwait(false); + } } } catch (Exception ex) @@ -115,9 +129,9 @@ namespace MonsterMonitor.Services try { - await Task.Delay(TimeSpan.FromSeconds(2), token).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(3), token).ConfigureAwait(false); } - catch (TaskCanceledException) + catch (OperationCanceledException) { break; } @@ -136,39 +150,51 @@ namespace MonsterMonitor.Services { Task.Run(async () => { - await Task.Delay(1000).ConfigureAwait(false); - await Reconnect().ConfigureAwait(false); + try + { + await Task.Delay(1000).ConfigureAwait(false); + await Reconnect().ConfigureAwait(false); + } + catch (Exception ex) + { + // fire-and-forget: гарантированно не роняем процесс необработанным исключением. + _log.Warn("Ошибка отложенного переподключения SSH: " + ex.Message); + } }); } private async Task Reconnect() { - if (_reconnectInProgress || _disposed) + if (_disposed) + { + return; + } + + // Неблокирующая попытка захватить шлюз: если реконнект уже идёт — выходим, + // не создавая второй параллельный Connect() и не накапливая очередь. + if (!await _reconnectGate.WaitAsync(0).ConfigureAwait(false)) { return; } - _reconnectInProgress = true; try { - var timeout = Math.Min(60, Math.Max(5, _settings.ReconnectTimeoutSec)); + if (_disposed) + { + return; + } + _log.Warn("Перезапуск SSH-соединения..."); - var reconnectTask = Task.Run(() => + // Connect() ограничен ConnectionInfo.Timeout, поэтому не зависнет навсегда. + // Ждём завершения задачи (без брошенного WhenAny) — нет орфанных SshClient. + await Task.Run(() => { DisconnectCore(); Connect(); - }); + }).ConfigureAwait(false); - var completed = await Task.WhenAny(reconnectTask, Task.Delay(TimeSpan.FromSeconds(timeout))).ConfigureAwait(false); - if (completed != reconnectTask) - { - _log.Error("Переподключение превысило таймаут " + timeout + "с."); - } - else - { - _log.Info("SSH-соединение восстановлено."); - } + _log.Info("SSH-соединение восстановлено."); } catch (Exception ex) { @@ -176,46 +202,51 @@ namespace MonsterMonitor.Services } finally { - _reconnectInProgress = false; + _reconnectGate.Release(); } } private void DisconnectCore() { - StopRemoteHeartbeatNoLock(); + lock (_sync) + { + StopRemoteHeartbeatNoLock(); - try - { - if (_forwardedPort != null) + try { - if (_forwardedPort.IsStarted) + if (_forwardedPort != null) { - _forwardedPort.Stop(); + if (_forwardedPort.IsStarted) + { + _forwardedPort.Stop(); + } + _forwardedPort.Exception -= ForwardedPortOnException; + _forwardedPort.Dispose(); + _forwardedPort = null; } - _forwardedPort.Dispose(); - _forwardedPort = null; } - } - catch - { - // Ignore errors on shutdown. - } + catch + { + // Ignore errors on shutdown. + } - try - { - if (_client != null) + try { - if (_client.IsConnected) + if (_client != null) { - _client.Disconnect(); + _client.ErrorOccurred -= ClientOnErrorOccurred; + if (_client.IsConnected) + { + _client.Disconnect(); + } + _client.Dispose(); + _client = null; } - _client.Dispose(); - _client = null; } - } - catch - { - // Ignore errors on shutdown. + catch + { + // Ignore errors on shutdown. + } } } @@ -235,7 +266,7 @@ namespace MonsterMonitor.Services _heartbeatCommand = _client.CreateCommand(heartbeatCommand); _heartbeatReadTokenSource = new CancellationTokenSource(); - _lastHeartbeatUtc = DateTime.UtcNow; + Interlocked.Exchange(ref _lastHeartbeatTicks, DateTime.UtcNow.Ticks); _heartbeatCommand.BeginExecute(); _heartbeatReadTask = Task.Run(() => @@ -287,7 +318,7 @@ namespace MonsterMonitor.Services continue; } - _lastHeartbeatUtc = DateTime.UtcNow; + Interlocked.Exchange(ref _lastHeartbeatTicks, DateTime.UtcNow.Ticks); _log.Debug("HB: " + line); } } @@ -305,9 +336,11 @@ namespace MonsterMonitor.Services { try { - var command = _client.CreateCommand("cmd /c ver"); - var output = command.Execute() ?? string.Empty; - return output.IndexOf("windows", StringComparison.OrdinalIgnoreCase) >= 0; + using (var command = _client.CreateCommand("cmd /c ver")) + { + var output = command.Execute() ?? string.Empty; + return output.IndexOf("windows", StringComparison.OrdinalIgnoreCase) >= 0; + } } catch { @@ -317,19 +350,35 @@ namespace MonsterMonitor.Services public void Stop() { - _monitorTokenSource?.Cancel(); - _monitorTokenSource?.Dispose(); + var monitorTokenSource = _monitorTokenSource; + var monitorTask = _monitorTask; _monitorTokenSource = null; - lock (_sync) + _monitorTask = null; + + try { - DisconnectCore(); + monitorTokenSource?.Cancel(); + // Дожидаемся завершения цикла монитора до освобождения CTS, + // иначе Task.Delay(token) может словить ObjectDisposedException. + monitorTask?.Wait(TimeSpan.FromSeconds(2)); } + catch + { + // Ignore shutdown errors. + } + finally + { + monitorTokenSource?.Dispose(); + } + + DisconnectCore(); } public void Dispose() { _disposed = true; Stop(); + _reconnectGate.Dispose(); } } } diff --git a/src/MonsterMonitor/UI/MainForm.cs b/src/MonsterMonitor/UI/MainForm.cs index 5d91d42..03ed1e0 100644 --- a/src/MonsterMonitor/UI/MainForm.cs +++ b/src/MonsterMonitor/UI/MainForm.cs @@ -1,11 +1,11 @@ using MonsterMonitor.Models; using MonsterMonitor.Services; using System; +using System.Collections.Concurrent; using System.Drawing; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows.Forms; -using static System.Net.Mime.MediaTypeNames; namespace MonsterMonitor.UI { @@ -13,12 +13,19 @@ namespace MonsterMonitor.UI { private const int EmGetFirstVisibleLine = 0x00CE; private const int EmLineScroll = 0x00B6; + private const int WmSetRedraw = 0x000B; + + // Ограничение размера буфера консоли, чтобы текст не рос бесконечно. + private const int ConsoleMaxChars = 100000; + private const int ConsoleTrimToChars = 80000; private readonly RichTextBox _console = new RichTextBox(); private readonly Button _btnSettings = new Button(); private readonly Button _btnExit = new Button(); private readonly NotifyIcon _notifyIcon = new NotifyIcon(); private readonly Timer _updateTimer = new Timer(); + private readonly Timer _logFlushTimer = new Timer(); + private readonly ConcurrentQueue _pendingLogs = new ConcurrentQueue(); private readonly Icon _trayIcon; private readonly LogService _log = new LogService(); private readonly PowerManagementService _power = new PowerManagementService(); @@ -30,11 +37,12 @@ namespace MonsterMonitor.UI public MainForm() { - Text = string.Format("Monster Monitor v{0}", System.Windows.Forms.Application.ProductVersion); + Text = string.Format("Monster Monitor v{0}", Application.ProductVersion); Width = 980; Height = 620; StartPosition = FormStartPosition.CenterScreen; _trayIcon = LoadTrayIcon(); + Icon = _trayIcon; BuildUi(); BindEvents(); @@ -43,12 +51,11 @@ namespace MonsterMonitor.UI protected override void OnLoad(EventArgs e) { base.OnLoad(e); - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); _settings = AppSettings.Load(); _controller = new AppController(_log, _power); _updateService = new GitHubUpdateService(_log, _settings); + ConfigureLogFlushTimer(); RestartServices(); ConfigureUpdateTimer(); _ = RunUpdateCheckAsync(true); @@ -182,44 +189,66 @@ namespace MonsterMonitor.UI return; } - _notifyIcon.Visible = false; + // Останавливаем таймеры заранее, чтобы не было тиков во время разрушения формы. + _logFlushTimer.Stop(); _updateTimer.Stop(); - _updateTimer.Dispose(); - _controller?.Dispose(); - _trayIcon?.Dispose(); + _notifyIcon.Visible = false; } + private void ConfigureLogFlushTimer() + { + // Логи копятся в очереди и выводятся пачкой ~10 раз в секунду, + // а не по одной строке за событие — это снимает нагрузку на CPU и убирает мерцание. + _logFlushTimer.Interval = 100; + _logFlushTimer.Tick += (_, __) => FlushLogs(); + _logFlushTimer.Start(); + } + + // Вызывается из фоновых потоков — только кладём запись в очередь, без обращения к UI. private void AppendLog(LogEntry entry) { - if (_console.Text.Length > 100000) - { - _console.ResetText(); - } - System.Windows.Forms.Application.DoEvents(); + _pendingLogs.Enqueue(entry); + } - if (InvokeRequired) + // Выполняется всегда в UI-потоке (таймер WinForms). + private void FlushLogs() + { + if (_pendingLogs.IsEmpty || !_console.IsHandleCreated) { - BeginInvoke(new Action(AppendLog), entry); return; } - System.Windows.Forms.Application.DoEvents(); var wasNearBottom = IsConsoleNearBottom(); var firstVisibleLineBeforeAppend = GetFirstVisibleLine(_console); - _console.SelectionStart = _console.TextLength; - _console.SelectionLength = 0; - _console.SelectionColor = GetColor(entry.Level); - _console.AppendText($"[{entry.Timestamp:HH:mm:ss}] [{entry.Level}] {entry.Message}{Environment.NewLine}"); - _console.SelectionColor = _console.ForeColor; - System.Windows.Forms.Application.DoEvents(); + // Замораживаем отрисовку на время пакетного добавления — одна перерисовка вместо десятков. + SendMessage(_console.Handle, WmSetRedraw, IntPtr.Zero, IntPtr.Zero); + try + { + while (_pendingLogs.TryDequeue(out var entry)) + { + _console.SelectionStart = _console.TextLength; + _console.SelectionLength = 0; + _console.SelectionColor = GetColor(entry.Level); + _console.AppendText($"[{entry.Timestamp:HH:mm:ss}] [{entry.Level}] {entry.Message}{Environment.NewLine}"); + _console.SelectionColor = _console.ForeColor; + } + + TrimConsole(); + } + finally + { + SendMessage(_console.Handle, WmSetRedraw, (IntPtr)1, IntPtr.Zero); + _console.Invalidate(); + } if (wasNearBottom) { + _console.SelectionStart = _console.TextLength; + _console.SelectionLength = 0; _console.ScrollToCaret(); return; } - System.Windows.Forms.Application.DoEvents(); var firstVisibleLineAfterAppend = GetFirstVisibleLine(_console); var linesToRestore = firstVisibleLineBeforeAppend - firstVisibleLineAfterAppend; @@ -227,7 +256,28 @@ namespace MonsterMonitor.UI { SendMessage(_console.Handle, EmLineScroll, IntPtr.Zero, (IntPtr)linesToRestore); } - System.Windows.Forms.Application.DoEvents(); + } + + // Обрезаем старые строки, а не сбрасываем весь буфер — плавно и без рывка скролла. + private void TrimConsole() + { + if (_console.TextLength <= ConsoleMaxChars) + { + return; + } + + var removeUpTo = _console.TextLength - ConsoleTrimToChars; + var line = _console.GetLineFromCharIndex(removeUpTo); + var cut = _console.GetFirstCharIndexFromLine(line + 1); + if (cut <= 0) + { + cut = removeUpTo; + } + + _console.Select(0, cut); + _console.SelectedText = string.Empty; + _console.SelectionStart = _console.TextLength; + _console.SelectionLength = 0; } private bool IsConsoleNearBottom() @@ -280,6 +330,11 @@ namespace MonsterMonitor.UI await _updateService.CheckAndPrepareUpdateAsync(); } + catch (Exception ex) + { + // async void (Timer.Tick) — необработанное исключение уронило бы приложение. + _log.Error("Ошибка проверки обновлений: " + ex.Message); + } finally { _isUpdateCheckRunning = false; @@ -307,18 +362,25 @@ namespace MonsterMonitor.UI return ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); } - private void InitializeComponent() + protected override void Dispose(bool disposing) { - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); - this.SuspendLayout(); - // - // MainForm - // - this.ClientSize = new System.Drawing.Size(284, 261); - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.Name = "MainForm"; - this.ResumeLayout(false); + if (disposing) + { + _log.LogReceived -= AppendLog; + _logFlushTimer.Stop(); + _logFlushTimer.Dispose(); + _updateTimer.Stop(); + _updateTimer.Dispose(); + + _controller?.Dispose(); + + _notifyIcon.Visible = false; + _notifyIcon.Dispose(); + _trayIcon?.Dispose(); + } + + base.Dispose(disposing); } } } diff --git a/src/MonsterMonitor/UI/SettingsForm.cs b/src/MonsterMonitor/UI/SettingsForm.cs index 57d8d70..7ac492e 100644 --- a/src/MonsterMonitor/UI/SettingsForm.cs +++ b/src/MonsterMonitor/UI/SettingsForm.cs @@ -194,14 +194,14 @@ namespace MonsterMonitor.UI private void LoadValues() { _txtHost.Text = _settings.SshHost; - _numSshPort.Value = _settings.SshPort; + _numSshPort.Value = Clamp(_numSshPort, _settings.SshPort); _txtUser.Text = _settings.SshUsername; _txtPassword.Text = _settings.GetPassword(); _chkSavePassword.Checked = _settings.SavePassword; - _numRemotePort.Value = _settings.RemotePort; - _numLocalPort.Value = _settings.LocalPort; - _numMaxFailures.Value = _settings.MaxPingFailures; - _numReconnectTimeout.Value = _settings.ReconnectTimeoutSec; + _numRemotePort.Value = Clamp(_numRemotePort, _settings.RemotePort); + _numLocalPort.Value = Clamp(_numLocalPort, _settings.LocalPort); + _numMaxFailures.Value = Clamp(_numMaxFailures, _settings.MaxPingFailures); + _numReconnectTimeout.Value = Clamp(_numReconnectTimeout, _settings.ReconnectTimeoutSec); _txtProxy.Text = _settings.Proxy; _txtSsPath.Text = _settings.SsProcessPath; _txtSsArgs.Text = _settings.SsArguments; @@ -209,6 +209,23 @@ namespace MonsterMonitor.UI _txtThreeProxyPassword.Text = _settings.GetThreeProxyPassword(); } + // Приводит значение из настроек к допустимому диапазону контрола, + // иначе NumericUpDown.Value бросит ArgumentOutOfRangeException и окно не откроется. + private static decimal Clamp(NumericUpDown control, int value) + { + if (value < control.Minimum) + { + return control.Minimum; + } + + if (value > control.Maximum) + { + return control.Maximum; + } + + return value; + } + private void SaveAndClose() { if (string.IsNullOrWhiteSpace(_txtHost.Text))