Реорганизация
.NET Core Desktop / build (Debug) (push) Has been cancelled

This commit is contained in:
kislovdm
2026-04-27 00:15:02 +03:00
parent 2fb673661f
commit 8467399001
23 changed files with 77 additions and 88 deletions
@@ -0,0 +1,52 @@
using System;
using MonsterMonitor.Models;
namespace MonsterMonitor.Services
{
public sealed class AppController : IDisposable
{
private readonly LogService _log;
private readonly PowerManagementService _power;
private readonly SsConfigService _ssConfig;
private ProcessMonitorService _processMonitor;
private SshTunnelService _sshTunnel;
public AppController(LogService log, PowerManagementService power)
{
_log = log;
_power = power;
_ssConfig = new SsConfigService(_log);
}
public void Start(AppSettings settings)
{
Stop();
_power.PreventSleep();
_processMonitor = new ProcessMonitorService(_log);
_sshTunnel = new SshTunnelService(_log);
_ssConfig.EnsureConfig(settings);
_processMonitor.Start(settings.SsProcessPath, settings.SsArguments);
_sshTunnel.Start(settings);
_log.Info("Сервисы приложения запущены.");
}
public void Stop()
{
_sshTunnel?.Dispose();
_sshTunnel = null;
_processMonitor?.Dispose();
_processMonitor = null;
_power.RestoreDefault();
}
public void Dispose()
{
Stop();
}
}
}
@@ -0,0 +1,82 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using MonsterMonitor.Models;
namespace MonsterMonitor.Services
{
public class AuthMonitor: IAuthMonitor
{
[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
private readonly AppSettings _settings;
public AuthMonitor(AppSettings settings)
{
_settings = settings;
}
private string GetActiveWindowTitle()
{
const int nChars = 256;
var buff = new StringBuilder(nChars);
var handle = GetForegroundWindow();
return GetWindowText(handle, buff, nChars) > 0 ? buff.ToString() : null;
}
public async Task StartMonitor()
{
_ = Task.Run(async () => await CheckProcess());
}
private async Task CheckProcess()
{
while (true)
{
try
{
Process[] processlist = Process.GetProcesses();
foreach (Process process in processlist)
{
if (!string.IsNullOrEmpty(process.MainWindowTitle) && process.MainWindowTitle.Contains("XAuth request"))
{
SetForegroundWindow(process.MainWindowHandle);
}
}
var activeWindowTitle = GetActiveWindowTitle();
if (activeWindowTitle?.Contains("XAuth request") == true)
{
var password = _settings.GetSystemPassword();
if (!string.IsNullOrEmpty(password))
{
SendKeys.SendWait(password);
SendKeys.SendWait("{ENTER}");
}
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message + ex.StackTrace);
}
await Task.Delay(TimeSpan.FromSeconds(3));
}
}
}
public interface IAuthMonitor
{
Task StartMonitor();
}
}
+39
View File
@@ -0,0 +1,39 @@
using System;
namespace MonsterMonitor.Services
{
public enum LogLevel
{
Debug,
Info,
Warn,
Error
}
public sealed class LogEntry
{
public DateTime Timestamp { get; set; }
public LogLevel Level { get; set; }
public string Message { get; set; }
}
public sealed class LogService
{
public event Action<LogEntry> LogReceived;
public void Log(LogLevel level, string message)
{
LogReceived?.Invoke(new LogEntry
{
Timestamp = DateTime.Now,
Level = level,
Message = message ?? string.Empty
});
}
public void Info(string message) => Log(LogLevel.Info, message);
public void Warn(string message) => Log(LogLevel.Warn, message);
public void Error(string message) => Log(LogLevel.Error, message);
public void Debug(string message) => Log(LogLevel.Debug, message);
}
}
@@ -0,0 +1,32 @@
using System;
using System.Runtime.InteropServices;
namespace MonsterMonitor.Services
{
public sealed class PowerManagementService
{
[Flags]
private enum ExecutionState : uint
{
ES_SYSTEM_REQUIRED = 0x00000001,
ES_DISPLAY_REQUIRED = 0x00000002,
ES_CONTINUOUS = 0x80000000
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern ExecutionState SetThreadExecutionState(ExecutionState flags);
public void PreventSleep()
{
SetThreadExecutionState(
ExecutionState.ES_CONTINUOUS |
ExecutionState.ES_SYSTEM_REQUIRED |
ExecutionState.ES_DISPLAY_REQUIRED);
}
public void RestoreDefault()
{
SetThreadExecutionState(ExecutionState.ES_CONTINUOUS);
}
}
}
@@ -0,0 +1,152 @@
using System;
using System.Diagnostics;
using System.Threading;
namespace MonsterMonitor.Services
{
public sealed class ProcessMonitorService : IDisposable
{
private readonly LogService _log;
private readonly object _sync = new object();
private readonly Timer _watchdogTimer;
private Process _process;
private string _path;
private string _arguments;
private bool _stopping;
public ProcessMonitorService(LogService log)
{
_log = log;
_watchdogTimer = new Timer(WatchdogTick, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
}
public void Start(string path, string arguments)
{
lock (_sync)
{
_path = path;
_arguments = arguments ?? string.Empty;
_stopping = false;
EnsureStarted();
_watchdogTimer.Change(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));
}
}
private void EnsureStarted()
{
if (IsProcessRunning(_process))
{
return;
}
try
{
if (_process != null)
{
_process.Exited -= ProcessOnExited;
_process.Dispose();
}
_process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = _path,
Arguments = _arguments,
UseShellExecute = false,
CreateNoWindow = true
},
EnableRaisingEvents = true
};
//_process.Exited += ProcessOnExited;
_process.Start();
_log.Info("Процесс ss запущен.");
}
catch (Exception ex)
{
_log.Error("Ошибка запуска ss: " + ex.Message);
}
}
private void ProcessOnExited(object sender, EventArgs e)
{
lock (_sync)
{
if (_stopping)
{
return;
}
_log.Warn("Процесс ss завершился, запускаю заново.");
EnsureStarted();
}
}
private void WatchdogTick(object state)
{
lock (_sync)
{
if (_stopping)
{
return;
}
EnsureStarted();
}
}
public void Stop()
{
lock (_sync)
{
_stopping = true;
_watchdogTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
try
{
if (IsProcessRunning(_process))
{
_process.Kill();
_process.WaitForExit(5000);
_log.Info("Процесс ss остановлен.");
}
}
catch (Exception ex)
{
_log.Warn("Не удалось корректно завершить ss: " + ex.Message);
}
finally
{
if (_process != null)
{
_process.Exited -= ProcessOnExited;
_process.Dispose();
}
_process = null;
}
}
}
private static bool IsProcessRunning(Process process)
{
if (process == null)
{
return false;
}
try
{
return !process.HasExited;
}
catch (InvalidOperationException)
{
return false;
}
}
public void Dispose()
{
Stop();
_watchdogTimer.Dispose();
}
}
}
@@ -0,0 +1,68 @@
using System;
using System.IO;
using System.Security.Cryptography;
using MonsterMonitor.Models;
namespace MonsterMonitor.Services
{
public sealed class SsConfigService
{
private readonly LogService _log;
public SsConfigService(LogService log)
{
_log = log;
}
public void EnsureConfig(AppSettings settings)
{
var password = settings.GetThreeProxyPassword();
if (string.IsNullOrWhiteSpace(password))
{
password = GeneratePassword();
settings.SetThreeProxyPassword(password);
settings.Save();
_log.Info("Сгенерирован пароль прокси и сохранен в настройках.");
}
var port = settings.LocalPort;
var config = "users admin:CL:" + password + "\r\n" +
"auth strong\r\n" +
$"socks -i127.0.0.1 -p{port}";
var configPath = GetConfigPath();
var directory = Path.GetDirectoryName(configPath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllText(configPath, config);
_log.Info("Сконфигурирован ss.cfg: " + configPath);
}
private static string GetConfigPath()
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "ss", "ss.cfg");
}
private static string GeneratePassword()
{
const string alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const int length = 20;
var bytes = new byte[length];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(bytes);
}
var chars = new char[length];
for (var i = 0; i < length; i++)
{
chars[i] = alphabet[bytes[i] % alphabet.Length];
}
return new string(chars);
}
}
}
@@ -0,0 +1,326 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MonsterMonitor.Models;
using Renci.SshNet;
using Renci.SshNet.Common;
namespace MonsterMonitor.Services
{
public sealed class SshTunnelService : IDisposable
{
private readonly LogService _log;
private readonly object _sync = new object();
private SshClient _client;
private ForwardedPortRemote _forwardedPort;
private SshCommand _heartbeatCommand;
private CancellationTokenSource _heartbeatReadTokenSource;
private Task _heartbeatReadTask;
private CancellationTokenSource _monitorTokenSource;
private AppSettings _settings;
private bool _disposed;
private bool _reconnectInProgress;
private DateTime _lastHeartbeatUtc = DateTime.MinValue;
private bool _remoteIsWindows;
public SshTunnelService(LogService log)
{
_log = log;
}
public void Start(AppSettings settings)
{
_settings = settings;
Connect();
StartMonitor();
}
private void Connect()
{
lock (_sync)
{
DisconnectCore();
var password = _settings.GetPassword();
var authMethod = new PasswordAuthenticationMethod(_settings.SshUsername, password);
var connectionInfo = new ConnectionInfo(_settings.SshHost, _settings.SshPort, _settings.SshUsername, authMethod);
_client = new SshClient(connectionInfo);
_client.ErrorOccurred += ClientOnErrorOccurred;
_client.Connect();
_forwardedPort = new ForwardedPortRemote((uint)_settings.RemotePort, "127.0.0.1", (uint)_settings.LocalPort);
_forwardedPort.Exception += ForwardedPortOnException;
_client.AddForwardedPort(_forwardedPort);
_forwardedPort.Start();
StartRemoteHeartbeatNoLock();
_log.Info(
$"SSH подключен. Туннель remote:{_settings.RemotePort} -> local:{_settings.LocalPort}");
}
}
private void ClientOnErrorOccurred(object sender, ExceptionEventArgs e)
{
_log.Warn("Ошибка SSH: " + e.Exception.Message);
ReconnectSoon();
}
private void ForwardedPortOnException(object sender, ExceptionEventArgs e)
{
_log.Warn("Ошибка перенаправления порта: " + e.Exception.Message);
ReconnectSoon();
}
private void StartMonitor()
{
_monitorTokenSource = new CancellationTokenSource();
Task.Run(() => MonitorLoop(_monitorTokenSource.Token));
}
private async Task MonitorLoop(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
var silenceThresholdSec = Math.Max(
5,
_settings.MaxPingFailures);
var lastHeartbeat = _lastHeartbeatUtc;
var isHeartbeatAlive = lastHeartbeat != DateTime.MinValue &&
(DateTime.UtcNow - lastHeartbeat).TotalSeconds <= silenceThresholdSec;
if (!IsConnected() || !isHeartbeatAlive)
{
_log.Warn("Нет живого вывода heartbeat-команды на удаленном сервере. Переподключаю SSH.");
await Reconnect().ConfigureAwait(false);
}
}
catch (Exception ex)
{
_log.Warn("Исключение мониторинга heartbeat: " + ex.Message);
}
try
{
await Task.Delay(TimeSpan.FromSeconds(2), token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
break;
}
}
}
private bool IsConnected()
{
lock (_sync)
{
return _client != null && _client.IsConnected;
}
}
private void ReconnectSoon()
{
Task.Run(async () =>
{
await Task.Delay(1000).ConfigureAwait(false);
await Reconnect().ConfigureAwait(false);
});
}
private async Task Reconnect()
{
if (_reconnectInProgress || _disposed)
{
return;
}
_reconnectInProgress = true;
try
{
var timeout = Math.Min(60, Math.Max(5, _settings.ReconnectTimeoutSec));
_log.Warn("Перезапуск SSH-соединения...");
var reconnectTask = Task.Run(() =>
{
DisconnectCore();
Connect();
});
var completed = await Task.WhenAny(reconnectTask, Task.Delay(TimeSpan.FromSeconds(timeout))).ConfigureAwait(false);
if (completed != reconnectTask)
{
_log.Error("Переподключение превысило таймаут " + timeout + "с.");
}
else
{
_log.Info("SSH-соединение восстановлено.");
}
}
catch (Exception ex)
{
_log.Error("Ошибка переподключения SSH: " + ex.Message);
}
finally
{
_reconnectInProgress = false;
}
}
private void DisconnectCore()
{
StopRemoteHeartbeatNoLock();
try
{
if (_forwardedPort != null)
{
if (_forwardedPort.IsStarted)
{
_forwardedPort.Stop();
}
_forwardedPort.Dispose();
_forwardedPort = null;
}
}
catch
{
// Ignore errors on shutdown.
}
try
{
if (_client != null)
{
if (_client.IsConnected)
{
_client.Disconnect();
}
_client.Dispose();
_client = null;
}
}
catch
{
// Ignore errors on shutdown.
}
}
private void StartRemoteHeartbeatNoLock()
{
if (_client == null || !_client.IsConnected)
{
return;
}
StopRemoteHeartbeatNoLock();
_remoteIsWindows = DetectRemoteWindowsNoLock();
var heartbeatCommand = _remoteIsWindows
? "ping ya.ru -t"
: "while :; do echo 'some type of ping response '$C; C=$((C+1)); sleep 1; done";
_heartbeatCommand = _client.CreateCommand(heartbeatCommand);
_heartbeatReadTokenSource = new CancellationTokenSource();
_lastHeartbeatUtc = DateTime.UtcNow;
_heartbeatCommand.BeginExecute();
_heartbeatReadTask = Task.Run(() =>
ReadHeartbeatOutputLoop(_heartbeatCommand, _heartbeatReadTokenSource.Token));
_log.Info("Запущен удаленный heartbeat через SSH (" + (_remoteIsWindows ? "Windows" : "Linux/Unix") + ").");
}
private void StopRemoteHeartbeatNoLock()
{
try
{
_heartbeatReadTokenSource?.Cancel();
_heartbeatReadTask?.Wait(1000);
}
catch
{
// Ignore errors on shutdown.
}
finally
{
_heartbeatReadTokenSource?.Dispose();
_heartbeatReadTokenSource = null;
_heartbeatReadTask = null;
}
try
{
_heartbeatCommand?.Dispose();
_heartbeatCommand = null;
}
catch
{
// Ignore errors on shutdown.
}
}
private void ReadHeartbeatOutputLoop(SshCommand command, CancellationToken token)
{
try
{
using (var reader = new StreamReader(command.OutputStream))
{
while (!token.IsCancellationRequested)
{
var line = reader.ReadLine();
if (line == null)
{
Thread.Sleep(200);
continue;
}
_lastHeartbeatUtc = DateTime.UtcNow;
_log.Debug("HB: " + line);
}
}
}
catch (Exception ex)
{
if (!token.IsCancellationRequested)
{
_log.Warn("Ошибка чтения heartbeat-вывода: " + ex.Message);
}
}
}
private bool DetectRemoteWindowsNoLock()
{
try
{
var command = _client.CreateCommand("cmd /c ver");
var output = command.Execute() ?? string.Empty;
return output.IndexOf("windows", StringComparison.OrdinalIgnoreCase) >= 0;
}
catch
{
return false;
}
}
public void Stop()
{
_monitorTokenSource?.Cancel();
_monitorTokenSource?.Dispose();
_monitorTokenSource = null;
lock (_sync)
{
DisconnectCore();
}
}
public void Dispose()
{
_disposed = true;
Stop();
}
}
}