init
This commit is contained in:
@@ -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
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
@@ -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
@@ -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
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
auth none
|
||||
socks -i127.0.0.1 -p7683
|
||||
Binary file not shown.
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 |
@@ -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.
|
||||
Reference in New Issue
Block a user