This commit is contained in:
kislovdm
2026-04-27 00:03:23 +03:00
parent e6147d9b83
commit 31a1650c7c
25 changed files with 3399 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
name: .NET Core Desktop
on:
push:
tags:
- '*'
jobs:
build:
strategy:
matrix:
configuration: [Debug]
runs-on: windows-latest # For a list of available runner types, refer to
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
steps:
- name: Checkout code.
uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.0.2
- name: Setup Nuget
uses: Nuget/setup-nuget@v1.0.5
- name: Restore nuget packages
run: nuget restore src/MonsterMonitor.sln
- name: Build
run: msbuild src/MonsterMonitor.sln /p:Configuration=${{ matrix.configuration }} /p:OutputPath=/app
- name: Pack
run: cd /app && 7z a ./MonsterMonitor.zip /app/*
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: /app/MonsterMonitor.zip
asset_name: MonsterMonitor.zip
asset_content_type: application/zip
+294
View File
@@ -0,0 +1,294 @@
# Huge kudos to all contributors to https://github.com/github/gitignore which is where
# this file came from.
syntax: glob
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
#settings
myentunnel.ini
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
#*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# Visual Studio design time assembly cache
*.cache
#Aeroclub lib catalog
[Ll]ib/
src/Aeroclub.WCF.CommonGateway/!Web.config
src/Aeroclub.WCF.CommonGateway/Web.config
src/Aeroclub.WCF.CommonGateway/!NLog.config
src/Aeroclub.WCF.CommonGateway/NLog.config
*.ncrunchproject
# WSCommon logs
src/Aeroclub.WCF.CommonGateway/logs/
build/nuget/*.exe
*.orig
tools/Confluence.dll*
.sonarqube/*
paket.lock
test/*
.paket/Paket/tools/paket.exe
coverage/*
src/WscommonDoc/Help/*.json
src/WscommonDoc/Help/*.json
src/WscommonDoc/Help/*
src/WscommonDoc/icons/Help.png
src/WscommonDoc/Doc/Working/*
src/WscommonDoc/Doc/*
.paket/*
*.env
.teamcity/target/*
docfx_project/_site/*
docfx_project/api/*.yml
+1
View File
@@ -0,0 +1 @@
1.0.0
+22
View File
@@ -0,0 +1,22 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.32929.386
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonsterMonitor", "src\MonsterMonitor.csproj", "{7D6104C6-6AEF-4D31-8463-FCEABDF1577A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7D6104C6-6AEF-4D31-8463-FCEABDF1577A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D6104C6-6AEF-4D31-8463-FCEABDF1577A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D6104C6-6AEF-4D31-8463-FCEABDF1577A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D6104C6-6AEF-4D31-8463-FCEABDF1577A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
+33
View File
@@ -0,0 +1,33 @@
@echo off
IF %1.==. GOTO NoCommitMessage
echo %~1
pause
for /f "tokens=1 delims=." %%G IN (.semver.txt) DO set MAJOR=%%G
for /f "tokens=2 delims=." %%G IN (.semver.txt) DO set MINOR=%%G
for /f "tokens=3 delims=." %%G IN (.semver.txt) DO set PATCH=%%G
set VERSION_CURRENT=%MAJOR:~0,1%.%MINOR:~0,1%.%PATCH%
echo Current: %VERSION_CURRENT%
SET /a PATCH1 = %PATCH%+1
set VERSION=%MAJOR:~0,1%.%MINOR:~0,1%.%PATCH1%
echo %VERSION%>.semver.txt
echo Updating to %VERSION%
call tools\SetVersion.cmd %VERSION%
git add .
git commit -m "%~1"
git tag -m "Version v%VERSION%: %~1" -a v%VERSION%
git push origin v%VERSION%
git push --tags
GOTO End
pause
:NoCommitMessage
echo Please specify commit message: %0 "commit message"
GOTO End
:End
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
+2
View File
@@ -0,0 +1,2 @@
auth none
socks -i127.0.0.1 -p7683
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura />
</Weavers>
+176
View File
@@ -0,0 +1,176 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX86Assemblies instead</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinX86Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX64Assemblies instead.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinX64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinArm64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCompression" type="xs:boolean">
<xs:annotation>
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCleanup" type="xs:boolean">
<xs:annotation>
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableEventSubscription" type="xs:boolean">
<xs:annotation>
<xs:documentation>The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
<xs:annotation>
<xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX86Assemblies instead</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UnmanagedWinX86Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X86 (32 bit) assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX64Assemblies instead</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UnmanagedWinX64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X64 (64 bit) assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UnmanagedWinArm64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>
+225
View File
@@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace SshTunnelMonitor.Models
{
public sealed class AppSettings
{
private const string ProtectedPrefix = "dpapi:";
public string SshHost { get; set; } = "127.0.0.1";
public int SshPort { get; set; } = 22;
public string SshUsername { get; set; } = "user";
public string SshPasswordProtected { get; set; } = string.Empty;
public string SystemPasswordProtected { get; set; } = string.Empty;
public string ThreeProxyPasswordProtected { get; set; } = string.Empty;
public bool SavePassword { get; set; } = false;
public int RemotePort { get; set; } = 3328;
public int LocalPort { get; set; } = 7829;
public int MaxPingFailures { get; set; } = 3;
public int ReconnectTimeoutSec { get; set; } = 45;
public string SsProcessPath { get; set; } = Path.Combine("App_Data", "ss", "ss.exe");
public string SsArguments { get; set; } = string.Empty;
public string GetPassword()
{
if (string.IsNullOrWhiteSpace(SshPasswordProtected))
{
return string.Empty;
}
if (!SshPasswordProtected.StartsWith(ProtectedPrefix, StringComparison.Ordinal))
{
return SshPasswordProtected;
}
var protectedData = Convert.FromBase64String(SshPasswordProtected.Substring(ProtectedPrefix.Length));
var unprotected = ProtectedData.Unprotect(protectedData, null, DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(unprotected);
}
public void SetPassword(string password)
{
if (!SavePassword || string.IsNullOrEmpty(password))
{
SshPasswordProtected = string.Empty;
return;
}
var bytes = Encoding.UTF8.GetBytes(password);
var protectedData = ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser);
SshPasswordProtected = ProtectedPrefix + Convert.ToBase64String(protectedData);
}
public string GetSystemPassword()
{
if (string.IsNullOrWhiteSpace(SystemPasswordProtected))
{
return "STerra";
}
if (!SystemPasswordProtected.StartsWith(ProtectedPrefix, StringComparison.Ordinal))
{
return SystemPasswordProtected;
}
var protectedData = Convert.FromBase64String(SystemPasswordProtected.Substring(ProtectedPrefix.Length));
var unprotected = ProtectedData.Unprotect(protectedData, null, DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(unprotected);
}
public void SetSystemPassword(string password)
{
if (string.IsNullOrEmpty(password))
{
SystemPasswordProtected = string.Empty;
return;
}
var bytes = Encoding.UTF8.GetBytes(password);
var protectedData = ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser);
SystemPasswordProtected = ProtectedPrefix + Convert.ToBase64String(protectedData);
}
public string GetThreeProxyPassword()
{
if (string.IsNullOrWhiteSpace(ThreeProxyPasswordProtected))
{
return string.Empty;
}
if (!ThreeProxyPasswordProtected.StartsWith(ProtectedPrefix, StringComparison.Ordinal))
{
return ThreeProxyPasswordProtected;
}
var protectedData = Convert.FromBase64String(ThreeProxyPasswordProtected.Substring(ProtectedPrefix.Length));
var unprotected = ProtectedData.Unprotect(protectedData, null, DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(unprotected);
}
public void SetThreeProxyPassword(string password)
{
if (string.IsNullOrEmpty(password))
{
ThreeProxyPasswordProtected = string.Empty;
return;
}
var bytes = Encoding.UTF8.GetBytes(password);
var protectedData = ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser);
ThreeProxyPasswordProtected = ProtectedPrefix + Convert.ToBase64String(protectedData);
}
public static string GetSettingsPath()
{
var directory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MonsterMonitor");
Directory.CreateDirectory(directory);
return Path.Combine(directory, "settings.ini");
}
public static AppSettings Load()
{
var path = GetSettingsPath();
if (!File.Exists(path))
{
return new AppSettings();
}
var result = new AppSettings();
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var line in File.ReadAllLines(path))
{
if (string.IsNullOrWhiteSpace(line) || line.TrimStart().StartsWith("#", StringComparison.Ordinal))
{
continue;
}
var separator = line.IndexOf('=');
if (separator <= 0)
{
continue;
}
var key = line.Substring(0, separator).Trim();
var value = line.Substring(separator + 1).Trim();
map[key] = value;
}
result.SshHost = Get(map, nameof(SshHost), result.SshHost);
result.SshPort = GetInt(map, nameof(SshPort), result.SshPort);
result.SshUsername = Get(map, nameof(SshUsername), result.SshUsername);
result.SshPasswordProtected = Get(map, nameof(SshPasswordProtected), result.SshPasswordProtected);
result.SystemPasswordProtected = Get(map, nameof(SystemPasswordProtected), result.SystemPasswordProtected);
result.ThreeProxyPasswordProtected = Get(map, nameof(ThreeProxyPasswordProtected), result.ThreeProxyPasswordProtected);
result.SavePassword = GetBool(map, nameof(SavePassword), result.SavePassword);
result.RemotePort = GetInt(map, nameof(RemotePort), result.RemotePort);
result.LocalPort = GetInt(map, nameof(LocalPort), result.LocalPort);
result.MaxPingFailures = GetInt(map, nameof(MaxPingFailures), result.MaxPingFailures);
result.ReconnectTimeoutSec = GetInt(map, nameof(ReconnectTimeoutSec), result.ReconnectTimeoutSec);
result.SsProcessPath = Get(map, nameof(SsProcessPath), result.SsProcessPath);
result.SsArguments = Get(map, nameof(SsArguments), result.SsArguments);
if (string.IsNullOrWhiteSpace(result.SystemPasswordProtected))
{
result.SetSystemPassword("STerra");
result.Save();
}
return result;
}
public void Save()
{
var path = GetSettingsPath();
var lines = new[]
{
$"{nameof(SshHost)}={SshHost}",
$"{nameof(SshPort)}={SshPort}",
$"{nameof(SshUsername)}={SshUsername}",
$"{nameof(SshPasswordProtected)}={SshPasswordProtected}",
$"{nameof(SystemPasswordProtected)}={SystemPasswordProtected}",
$"{nameof(ThreeProxyPasswordProtected)}={ThreeProxyPasswordProtected}",
$"{nameof(SavePassword)}={SavePassword}",
$"{nameof(RemotePort)}={RemotePort}",
$"{nameof(LocalPort)}={LocalPort}",
$"{nameof(MaxPingFailures)}={MaxPingFailures}",
$"{nameof(ReconnectTimeoutSec)}={ReconnectTimeoutSec}",
$"{nameof(SsProcessPath)}={SsProcessPath}",
$"{nameof(SsArguments)}={SsArguments}"
};
File.WriteAllLines(path, lines);
}
private static string Get(IDictionary<string, string> map, string key, string defaultValue)
{
return map.TryGetValue(key, out var value) ? value : defaultValue;
}
private static int GetInt(IDictionary<string, string> map, string key, int defaultValue)
{
if (map.TryGetValue(key, out var value) && int.TryParse(value, out var parsed))
{
return parsed;
}
return defaultValue;
}
private static bool GetBool(IDictionary<string, string> map, string key, bool defaultValue)
{
if (map.TryGetValue(key, out var value) && bool.TryParse(value, out var parsed))
{
return parsed;
}
return defaultValue;
}
}
}
+31
View File
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net472</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<LangVersion>7.3</LangVersion>
<RootNamespace>SshTunnelMonitor</RootNamespace>
<AssemblyName>MonsterMonitor</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Costura.Fody" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Fody" Version="6.9.3">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<Reference Include="System.Security" />
<PackageReference Include="SSH.NET" Version="2025.1.0" />
</ItemGroup>
<ItemGroup>
<None Update="App_Data\ss\ss.cfg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="App_Data\ss\ss.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="logistics_tunnel_icon_148216.ico">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+35
View File
@@ -0,0 +1,35 @@
using System;
using System.Windows.Forms;
using SshTunnelMonitor.UI;
namespace SshTunnelMonitor
{
internal static class Program
{
[STAThread]
private static void Main()
{
Application.ThreadException += (sender, args) =>
{
MessageBox.Show(
args.Exception.ToString(),
"Необработанное исключение UI-потока",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
};
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
MessageBox.Show(
args.ExceptionObject.ToString(),
"Критическое исключение",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
};
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}
+52
View File
@@ -0,0 +1,52 @@
using System;
using SshTunnelMonitor.Models;
namespace SshTunnelMonitor.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();
}
}
}
+78
View File
@@ -0,0 +1,78 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using SshTunnelMonitor.Models;
namespace SshTunnelMonitor.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)
{
SendKeys.SendWait(_settings.GetSystemPassword());
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 SshTunnelMonitor.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);
}
}
+32
View File
@@ -0,0 +1,32 @@
using System;
using System.Runtime.InteropServices;
namespace SshTunnelMonitor.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);
}
}
}
+152
View File
@@ -0,0 +1,152 @@
using System;
using System.Diagnostics;
using System.Threading;
namespace SshTunnelMonitor.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();
}
}
}
+68
View File
@@ -0,0 +1,68 @@
using System;
using System.IO;
using System.Security.Cryptography;
using SshTunnelMonitor.Models;
namespace SshTunnelMonitor.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);
}
}
}
+326
View File
@@ -0,0 +1,326 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Renci.SshNet;
using Renci.SshNet.Common;
using SshTunnelMonitor.Models;
namespace SshTunnelMonitor.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();
}
}
}
+202
View File
@@ -0,0 +1,202 @@
using SshTunnelMonitor.Models;
using SshTunnelMonitor.Services;
using System;
using System.Drawing;
using System.Windows.Forms;
namespace SshTunnelMonitor.UI
{
public sealed class MainForm : Form
{
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 Icon _trayIcon;
private readonly LogService _log = new LogService();
private readonly PowerManagementService _power = new PowerManagementService();
private AppController _controller;
private AppSettings _settings;
private bool _allowClose;
public MainForm()
{
Text = "Monster Monitor";
Width = 980;
Height = 620;
StartPosition = FormStartPosition.CenterScreen;
_trayIcon = LoadTrayIcon();
BuildUi();
BindEvents();
}
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);
RestartServices();
}
private void BuildUi()
{
var topPanel = new Panel { Dock = DockStyle.Top, Height = 42 };
_btnSettings.Text = "Настройки";
_btnSettings.Width = 110;
_btnSettings.Location = new Point(10, 8);
_btnExit.Text = "Выход";
_btnExit.Width = 110;
_btnExit.Location = new Point(130, 8);
topPanel.Controls.Add(_btnSettings);
topPanel.Controls.Add(_btnExit);
_console.Dock = DockStyle.Fill;
_console.ReadOnly = true;
_console.BackColor = Color.Black;
_console.ForeColor = Color.White;
_console.Font = new Font("Consolas", 10f);
Controls.Add(_console);
Controls.Add(topPanel);
_notifyIcon.Text = "Monster Monitor";
_notifyIcon.Icon = _trayIcon;
_notifyIcon.Visible = true;
var trayMenu = new ContextMenuStrip();
trayMenu.Items.Add("Открыть", null, (_, __) => RestoreFromTray());
trayMenu.Items.Add("Выход", null, (_, __) => ExitApplication());
_notifyIcon.ContextMenuStrip = trayMenu;
}
private void BindEvents()
{
Resize += MainFormOnResize;
FormClosing += MainFormOnFormClosing;
_notifyIcon.DoubleClick += (_, __) => RestoreFromTray();
_btnSettings.Click += (_, __) => OpenSettings();
_btnExit.Click += (_, __) => ExitApplication();
_log.LogReceived += AppendLog;
}
private void OpenSettings()
{
using (var form = new SettingsForm(_settings))
{
if (form.ShowDialog(this) == DialogResult.OK)
{
_log.Info("Настройки обновлены. Перезапускаю сервисы.");
RestartServices();
}
}
}
private void RestartServices()
{
try
{
_controller.Start(_settings);
}
catch (Exception ex)
{
_log.Error("Ошибка запуска сервисов: " + ex.Message);
}
}
private void MainFormOnResize(object sender, EventArgs e)
{
if (WindowState != FormWindowState.Minimized)
{
return;
}
Hide();
_notifyIcon.BalloonTipTitle = "Monster Monitor";
_notifyIcon.BalloonTipText = "Приложение свернуто в трей.";
_notifyIcon.ShowBalloonTip(1000);
}
private void RestoreFromTray()
{
Show();
WindowState = FormWindowState.Normal;
Activate();
}
private void ExitApplication()
{
_allowClose = true;
Close();
}
private void MainFormOnFormClosing(object sender, FormClosingEventArgs e)
{
if (!_allowClose && e.CloseReason == CloseReason.UserClosing)
{
e.Cancel = true;
WindowState = FormWindowState.Minimized;
return;
}
_notifyIcon.Visible = false;
_controller?.Dispose();
_trayIcon?.Dispose();
}
private void AppendLog(LogEntry entry)
{
if (InvokeRequired)
{
BeginInvoke(new Action<LogEntry>(AppendLog), entry);
return;
}
_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;
_console.ScrollToCaret();
}
private static Color GetColor(LogLevel level)
{
switch (level)
{
case LogLevel.Debug:
return Color.Silver;
case LogLevel.Warn:
return Color.Orange;
case LogLevel.Error:
return Color.Red;
default:
return Color.White;
}
}
private static Icon LoadTrayIcon()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
return ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
}
private void InitializeComponent()
{
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);
}
}
}
+1253
View File
File diff suppressed because it is too large Load Diff
+259
View File
@@ -0,0 +1,259 @@
using System;
using System.Drawing;
using System.Security.Cryptography;
using System.Windows.Forms;
using SshTunnelMonitor.Models;
namespace SshTunnelMonitor.UI
{
public sealed class SettingsForm : Form
{
private readonly AppSettings _settings;
private readonly TextBox _txtHost = new TextBox();
private readonly NumericUpDown _numSshPort = new NumericUpDown();
private readonly TextBox _txtUser = new TextBox();
private readonly TextBox _txtPassword = new TextBox();
private readonly CheckBox _chkSavePassword = new CheckBox();
private readonly NumericUpDown _numRemotePort = new NumericUpDown();
private readonly NumericUpDown _numLocalPort = new NumericUpDown();
private readonly NumericUpDown _numMaxFailures = new NumericUpDown();
private readonly NumericUpDown _numReconnectTimeout = new NumericUpDown();
private readonly TextBox _txtSsPath = new TextBox();
private readonly TextBox _txtSsArgs = new TextBox();
private readonly TextBox _txtSystemPassword = new TextBox();
private readonly CheckBox _chkShowSystemPassword = new CheckBox();
private readonly TextBox _txtThreeProxyPassword = new TextBox();
private readonly CheckBox _chkShowThreeProxyPassword = new CheckBox();
private readonly Button _btnGenerateThreeProxyPassword = new Button();
public SettingsForm(AppSettings settings)
{
_settings = settings;
Text = "Настройки";
Width = 560;
Height = 520;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
BuildLayout();
LoadValues();
}
private void BuildLayout()
{
var panel = new TableLayoutPanel
{
Dock = DockStyle.Fill,
Padding = new Padding(10),
ColumnCount = 2,
RowCount = 14
};
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 44f));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 56f));
Controls.Add(panel);
AddRow(panel, "Сервер:", _txtHost, 0);
AddRow(panel, "Порт:", _numSshPort, 1);
AddRow(panel, "Логин:", _txtUser, 2);
_txtPassword.PasswordChar = '*';
AddRow(panel, "Пароль:", _txtPassword, 3);
_chkSavePassword.Text = "Сохранить пароль";
AddRow(panel, string.Empty, _chkSavePassword, 4);
AddRow(panel, "Удаленный порт:", _numRemotePort, 5);
AddRow(panel, "Локальный порт:", _numLocalPort, 6);
AddRow(panel, "Макс. потерь:", _numMaxFailures, 7);
AddRow(panel, "Таймаут reconnect (сек):", _numReconnectTimeout, 8);
AddRow(panel, "Путь к ss:", _txtSsPath, 9);
AddRow(panel, "Аргументы ss:", _txtSsArgs, 10);
BuildSystemPasswordRow(panel, 11);
BuildThreeProxyPasswordRow(panel, 12);
foreach (var num in new[] { _numSshPort, _numRemotePort, _numLocalPort })
{
num.Minimum = 1;
num.Maximum = 65535;
}
_numMaxFailures.Minimum = 1;
_numMaxFailures.Maximum = 10;
_numReconnectTimeout.Minimum = 5;
_numReconnectTimeout.Maximum = 60;
var buttonsPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.RightToLeft
};
var btnSave = new Button { Text = "Сохранить", Width = 120, Height = 30 };
var btnCancel = new Button { Text = "Отмена", Width = 120, Height = 30 };
btnSave.Click += (_, __) => SaveAndClose();
btnCancel.Click += (_, __) => DialogResult = DialogResult.Cancel;
buttonsPanel.Controls.Add(btnSave);
buttonsPanel.Controls.Add(btnCancel);
panel.Controls.Add(buttonsPanel, 0, 13);
panel.SetColumnSpan(buttonsPanel, 2);
}
private void BuildSystemPasswordRow(TableLayoutPanel panel, int row)
{
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 34f));
panel.Controls.Add(new Label
{
Text = "Пароль Sterra:",
TextAlign = ContentAlignment.MiddleLeft,
Dock = DockStyle.Fill
}, 0, row);
var passPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
AutoSize = false
};
_txtSystemPassword.Width = 170;
_txtSystemPassword.PasswordChar = '*';
_chkShowSystemPassword.Text = "Показать";
_chkShowSystemPassword.AutoSize = true;
_chkShowSystemPassword.CheckedChanged += (_, __) =>
{
_txtSystemPassword.PasswordChar = _chkShowSystemPassword.Checked ? '\0' : '*';
};
passPanel.Controls.Add(_txtSystemPassword);
passPanel.Controls.Add(_chkShowSystemPassword);
panel.Controls.Add(passPanel, 1, row);
}
private void BuildThreeProxyPasswordRow(TableLayoutPanel panel, int row)
{
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 34f));
panel.Controls.Add(new Label
{
Text = "Пароль прокси:",
TextAlign = ContentAlignment.MiddleLeft,
Dock = DockStyle.Fill
}, 0, row);
var passPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
AutoSize = false
};
_txtThreeProxyPassword.Width = 170;
_txtThreeProxyPassword.PasswordChar = '*';
_chkShowThreeProxyPassword.Text = "Показать";
_chkShowThreeProxyPassword.AutoSize = true;
_chkShowThreeProxyPassword.CheckedChanged += (_, __) =>
{
_txtThreeProxyPassword.PasswordChar = _chkShowThreeProxyPassword.Checked ? '\0' : '*';
};
_btnGenerateThreeProxyPassword.Text = "Сгенерировать";
_btnGenerateThreeProxyPassword.AutoSize = true;
_btnGenerateThreeProxyPassword.Click += (_, __) =>
{
_txtThreeProxyPassword.Text = GeneratePassword();
};
passPanel.Controls.Add(_txtThreeProxyPassword);
passPanel.Controls.Add(_chkShowThreeProxyPassword);
passPanel.Controls.Add(_btnGenerateThreeProxyPassword);
panel.Controls.Add(passPanel, 1, row);
}
private static void AddRow(TableLayoutPanel panel, string label, Control control, int row)
{
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 34f));
if (!string.IsNullOrEmpty(label))
{
panel.Controls.Add(new Label
{
Text = label,
TextAlign = ContentAlignment.MiddleLeft,
Dock = DockStyle.Fill
}, 0, row);
}
control.Dock = DockStyle.Fill;
panel.Controls.Add(control, 1, row);
}
private void LoadValues()
{
_txtHost.Text = _settings.SshHost;
_numSshPort.Value = _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;
_txtSsPath.Text = _settings.SsProcessPath;
_txtSsArgs.Text = _settings.SsArguments;
_txtSystemPassword.Text = _settings.GetSystemPassword();
_txtThreeProxyPassword.Text = _settings.GetThreeProxyPassword();
}
private void SaveAndClose()
{
if (string.IsNullOrWhiteSpace(_txtHost.Text))
{
MessageBox.Show(this, "Поле сервера обязательно.", "Валидация", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
_settings.SshHost = _txtHost.Text.Trim();
_settings.SshPort = (int)_numSshPort.Value;
_settings.SshUsername = _txtUser.Text.Trim();
_settings.SavePassword = _chkSavePassword.Checked;
_settings.SetPassword(_txtPassword.Text);
_settings.RemotePort = (int)_numRemotePort.Value;
_settings.LocalPort = (int)_numLocalPort.Value;
_settings.MaxPingFailures = (int)_numMaxFailures.Value;
_settings.ReconnectTimeoutSec = (int)_numReconnectTimeout.Value;
_settings.SsProcessPath = string.IsNullOrWhiteSpace(_txtSsPath.Text)
? System.IO.Path.Combine("App_Data", "ss", "ss.exe")
: _txtSsPath.Text.Trim();
_settings.SsArguments = string.IsNullOrWhiteSpace(_txtSsArgs.Text)
? System.IO.Path.Combine("App_Data", "ss", "ss.cfg")
: _txtSsArgs.Text.Trim();
_settings.SetSystemPassword(_txtSystemPassword.Text.Trim());
_settings.SetThreeProxyPassword(_txtThreeProxyPassword.Text.Trim());
_settings.Save();
DialogResult = DialogResult.OK;
}
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);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

+47
View File
@@ -0,0 +1,47 @@
@echo off
setlocal enabledelayedexpansion
if [%1] == [] GOTO :USAGE
set version=%1
set fileversion=%1
set informationalversion=%1
if [%2] NEQ [] SET fileversion=%2
if [%3] NEQ [] SET informationalversion=%3
echo Setting assembly version information
for /F "delims=" %%f in ('dir /s /b Assemblyinfo.cs') do (
echo ^> %%f
pushd %%~dpf
for /F "usebackq delims=" %%g in ("AssemblyInfo.cs") do (
set ln=%%g
set skip=0
if "!ln:AssemblyVersion=!" NEQ "!ln!" set skip=1
if "!ln:AssemblyFileVersion=!" NEQ "!ln!" set skip=1
if "!ln:AssemblyInformationalVersion=!" NEQ "!ln!" set skip=1
if !skip!==0 echo !ln! >> AssemblyInfo.cs.versioned
)
echo [assembly: AssemblyVersion^("%version%"^)] >> AssemblyInfo.cs.versioned
echo [assembly: AssemblyFileVersion^("%fileversion%"^)] >> AssemblyInfo.cs.versioned
echo [assembly: AssemblyInformationalVersion^("%informationalversion%"^)] >> AssemblyInfo.cs.versioned
copy /y AssemblyInfo.cs AssemblyInfo.cs.orig
move /y AssemblyInfo.cs.versioned AssemblyInfo.cs
popd
)
echo Done^^!
GOTO:EOF
:USAGE
echo Usage:
echo.
echo SetVersion.bat Version [FileVersion] [InformationalVersion]
echo.