Refactor Guard.cs

This commit is contained in:
ChsBuffer
2021-06-26 06:14:08 +08:00
parent e7d04e36ac
commit 62dc9166ce
10 changed files with 195 additions and 332 deletions

View File

@@ -1,6 +1,3 @@
using Netch.Models;
using Netch.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
@@ -9,83 +6,157 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Netch.Models;
using Netch.Utils;
using Serilog;
using Timer = System.Timers.Timer;
namespace Netch.Controllers
{
public abstract class Guard
{
private readonly Timer _flushFileStreamTimer = new(300) { AutoReset = true };
private FileStream? _logFileStream;
private StreamWriter? _logStreamWriter;
private bool _redirectToFile = true;
/// <summary>
/// 日志文件(重定向输出文件)
/// </summary>
/// <param name="mainFile">application path relative of Netch\bin</param>
/// <param name="redirectOutput"></param>
/// <param name="encoding">application output encode</param>
protected Guard(string mainFile, bool redirectOutput = true, Encoding? encoding = null)
{
RedirectOutput = redirectOutput;
var fileName = Path.GetFullPath($"bin\\{mainFile}");
if (!File.Exists(fileName))
throw new MessageException(i18N.Translate($"bin\\{mainFile} file not found!"));
Instance = new Process
{
StartInfo =
{
FileName = fileName,
WorkingDirectory = $"{Global.NetchDir}\\bin",
CreateNoWindow = true,
UseShellExecute = !RedirectOutput,
RedirectStandardOutput = RedirectOutput,
StandardOutputEncoding = RedirectOutput ? encoding : null,
RedirectStandardError = RedirectOutput,
StandardErrorEncoding = RedirectOutput ? encoding : null,
WindowStyle = ProcessWindowStyle.Hidden
}
};
}
protected string LogPath => Path.Combine(Global.NetchDir, $"logging\\{Name}.log");
/// <summary>
/// 成功启动关键词
/// </summary>
protected virtual IEnumerable<string> StartedKeywords { get; set; } = new List<string>();
protected virtual IEnumerable<string> StartedKeywords { get; } = new List<string>();
/// <summary>
/// 启动失败关键词
/// </summary>
protected virtual IEnumerable<string> StoppedKeywords { get; set; } = new List<string>();
protected virtual IEnumerable<string> FailedKeywords { get; } = new List<string>();
public abstract string Name { get; }
/// <summary>
/// 主程序名
/// </summary>
public abstract string MainFile { get; protected set; }
private State State { get; set; } = State.Waiting;
protected State State { get; set; } = State.Waiting;
private bool RedirectOutput { get; }
/// <summary>
/// 进程是否可以重定向输出
/// </summary>
protected bool RedirectStd { get; set; } = true;
public Process Instance { get; }
protected bool RedirectToFile
~Guard()
{
get => RedirectStd && _redirectToFile;
set => _redirectToFile = value;
_logFileStream?.Dispose();
_logStreamWriter?.Dispose();
Instance.Dispose();
}
/// <summary>
/// 进程实例
/// </summary>
public Process? Instance { get; private set; }
/// <summary>
/// 程序输出的编码,
/// </summary>
protected virtual Encoding? InstanceOutputEncoding { get; } = null;
public abstract void Stop();
/// <summary>
/// 停止进程
/// </summary>
protected void StopInstance()
protected void StartGuard(string argument, ProcessPriorityClass priority = ProcessPriorityClass.Normal)
{
State = State.Starting;
_logFileStream = File.Open(LogPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
_logStreamWriter = new StreamWriter(_logFileStream) { AutoFlush = true };
Instance.StartInfo.Arguments = argument;
Instance.Start();
if (priority != ProcessPriorityClass.Normal)
Instance.PriorityClass = priority;
if (RedirectOutput)
{
Task.Run(() => ReadOutput(Instance.StandardOutput));
Task.Run(() => ReadOutput(Instance.StandardError));
if (!StartedKeywords.Any())
{
// Skip, No started keyword
State = State.Started;
return;
}
// wait ReadOutput change State
for (var i = 0; i < 1000; i++)
{
Thread.Sleep(10);
switch (State)
{
case State.Started:
OnStarted();
return;
case State.Stopped:
StopGuard();
OnStartFailed();
throw new MessageException($"{Name} 控制器启动失败");
}
}
StopGuard();
throw new MessageException($"{Name} 控制器启动超时");
}
}
private void ReadOutput(TextReader reader)
{
string? line;
while ((line = reader.ReadLine()) != null)
{
_logStreamWriter!.WriteLine(line);
OnReadNewLine(line);
if (State == State.Starting)
{
if (StartedKeywords.Any(s => line.Contains(s)))
State = State.Started;
else if (FailedKeywords.Any(s => line.Contains(s)))
{
OnStartFailed();
State = State.Stopped;
}
}
}
State = State.Stopped;
}
public virtual void Stop()
{
StopGuard();
}
protected void StopGuard()
{
_logStreamWriter?.Close();
_logFileStream?.Close();
try
{
if (Instance == null || Instance.HasExited)
return;
Instance.Kill();
Instance.WaitForExit();
if (Instance is { HasExited: false })
{
Instance.Kill();
Instance.WaitForExit();
}
}
catch (Win32Exception e)
{
Log.Error(e, "停止 {MainFile} 异常", MainFile);
Log.Error(e, "停止 {Name} 异常", Instance.ProcessName);
}
catch
{
@@ -93,191 +164,17 @@ namespace Netch.Controllers
}
}
/// <summary>
/// 仅初始化 <see cref="Instance" />,不设定事件处理方法
/// </summary>
/// <param name="argument"></param>
protected virtual void InitInstance(string argument)
protected virtual void OnStarted()
{
Instance = new Process
{
StartInfo =
{
FileName = Path.GetFullPath($"bin\\{MainFile}"),
WorkingDirectory = $"{Global.NetchDir}\\bin",
Arguments = argument,
CreateNoWindow = true,
UseShellExecute = !RedirectStd,
RedirectStandardOutput = RedirectStd,
StandardOutputEncoding = RedirectStd ? InstanceOutputEncoding : null,
RedirectStandardError = RedirectStd,
StandardErrorEncoding = RedirectStd ? InstanceOutputEncoding : null,
WindowStyle = ProcessWindowStyle.Hidden
}
};
if (!File.Exists(Instance.StartInfo.FileName))
throw new MessageException(i18N.Translate($"bin\\{MainFile} file not found!"));
}
/// <summary>
/// 默认行为启动主程序
/// </summary>
/// <param name="argument">主程序启动参数</param>
/// <param name="priority">进程优先级</param>
/// <returns>是否成功启动</returns>
protected void StartInstanceAuto(string argument, ProcessPriorityClass priority = ProcessPriorityClass.Normal)
{
State = State.Starting;
// 初始化程序
InitInstance(argument);
if (RedirectToFile)
OpenLogFile();
// 启动程序
Instance!.Start();
if (priority != ProcessPriorityClass.Normal)
Instance.PriorityClass = priority;
if (RedirectStd)
{
Task.Run(() => ReadOutput(Instance.StandardOutput));
Task.Run(() => ReadOutput(Instance.StandardError));
if (!StartedKeywords.Any())
{
State = State.Started;
return;
}
}
else
{
return;
}
// 等待启动
for (var i = 0; i < 1000; i++)
{
Thread.Sleep(10);
switch (State)
{
case State.Started:
Task.Run(OnKeywordStarted);
return;
case State.Stopped:
Stop();
CloseLogFile();
OnKeywordStopped();
throw new MessageException($"{Name} 控制器启动失败");
}
}
Stop();
OnKeywordTimeout();
throw new MessageException($"{Name} 控制器启动超时");
}
#region FileStream
private void OpenLogFile()
{
if (!RedirectToFile)
return;
_logFileStream = File.Open(LogPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
_logStreamWriter = new StreamWriter(_logFileStream);
_flushFileStreamTimer.Elapsed += FlushFileStreamTimerEvent;
_flushFileStreamTimer.Enabled = true;
}
private void WriteLog(string line)
{
if (!RedirectToFile)
return;
_logStreamWriter!.WriteLine(line);
}
private readonly object _logStreamLock = new();
private void CloseLogFile()
{
if (!RedirectToFile)
return;
lock (_logStreamLock)
{
if (_logFileStream == null)
return;
_flushFileStreamTimer.Enabled = false;
_logStreamWriter?.Close();
_logFileStream?.Close();
_logStreamWriter = _logStreamWriter = null;
}
}
#endregion
#region virtual
protected virtual void OnReadNewLine(string line)
{
}
protected virtual void OnKeywordStarted()
{
}
protected virtual void OnKeywordStopped()
protected virtual void OnStartFailed()
{
Utils.Utils.Open(LogPath);
}
protected virtual void OnKeywordTimeout()
{
}
#endregion
protected void ReadOutput(TextReader reader)
{
string? line;
while ((line = reader.ReadLine()) != null)
{
WriteLog(line);
OnReadNewLine(line);
// State == State.Started if !StartedKeywords.Any()
if (State == State.Starting)
{
if (StartedKeywords.Any(s => line.Contains(s)))
State = State.Started;
else if (StoppedKeywords.Any(s => line.Contains(s)))
State = State.Stopped;
}
}
CloseLogFile();
State = State.Stopped;
}
/// <summary>
/// 计时器存储日志
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FlushFileStreamTimerEvent(object sender, EventArgs e)
{
try
{
_logStreamWriter!.Flush();
}
catch (Exception ex)
{
Log.Warning(ex, "写入 {Name} 日志异常", Name);
}
}
}
}

View File

@@ -10,15 +10,12 @@ namespace Netch.Controllers
{
public class NTTController : Guard, IController
{
public override string MainFile { get; protected set; } = "NTT.exe";
public override string Name { get; } = "NTT";
public override void Stop()
public NTTController() : base("NTT.exe")
{
StopInstance();
}
public override string Name => "NTT";
/// <summary>
/// 启动 NatTypeTester
/// </summary>
@@ -29,8 +26,8 @@ namespace Netch.Controllers
try
{
InitInstance($" {Global.Settings.STUN_Server} {Global.Settings.STUN_Server_Port}");
Instance!.Start();
Instance.StartInfo.Arguments = $" {Global.Settings.STUN_Server} {Global.Settings.STUN_Server_Port}";
Instance.Start();
var output = await Instance.StandardOutput.ReadToEndAsync();
var error = await Instance.StandardError.ReadToEndAsync();
@@ -47,8 +44,7 @@ namespace Netch.Controllers
if (output.IsNullOrWhiteSpace())
if (!error.IsNullOrWhiteSpace())
{
error = error.Trim();
var errorFirst = error.Substring(0, error.IndexOf('\n')).Trim();
var errorFirst = error.GetLines().First();
return (errorFirst.SplitTrimEntries(':').Last(), null, null);
}

View File

@@ -15,23 +15,27 @@ namespace Netch.Controllers
{
public class PcapController : Guard, IModeController
{
public override string Name { get; } = "pcap2socks";
public PcapController() : base("pcap2socks.exe", encoding: Encoding.UTF8)
{
_form = new LogForm(Global.MainForm);
_form.CreateControl();
}
public override string MainFile { get; protected set; } = "pcap2socks.exe";
~PcapController()
{
_form.Dispose();
}
protected override IEnumerable<string> StartedKeywords { get; set; } = new[] { "└" };
public override string Name => "pcap2socks";
protected override Encoding? InstanceOutputEncoding { get; } = Encoding.UTF8;
protected override IEnumerable<string> StartedKeywords { get; } = new[] { "└" };
private LogForm? _form;
private readonly LogForm _form;
public void Start(in Mode mode)
{
var server = MainController.Server!;
_form = new LogForm(Global.MainForm);
_form.CreateControl();
var outboundNetworkInterface = NetworkInterfaceUtils.GetBest();
var argument = new StringBuilder($@"-i \Device\NPF_{outboundNetworkInterface.Id}");
@@ -41,26 +45,22 @@ namespace Netch.Controllers
argument.Append($" --destination 127.0.0.1:{Global.Settings.Socks5LocalPort}");
argument.Append($" {mode.GetRules().FirstOrDefault() ?? "-P n"}");
StartInstanceAuto(argument.ToString());
StartGuard(argument.ToString());
}
protected override void OnReadNewLine(string line)
{
Global.MainForm.BeginInvoke(new Action(() =>
{
if (!_form!.IsDisposed)
_form.richTextBox1.AppendText(line + "\n");
}));
Global.MainForm.BeginInvoke(new Action(() => _form.richTextBox1.AppendText(line + "\n")));
}
protected override void OnKeywordStarted()
protected override void OnStarted()
{
Global.MainForm.BeginInvoke(new Action(() => { _form!.Show(); }));
Global.MainForm.BeginInvoke(new Action(() => _form.Show()));
}
protected override void OnKeywordStopped()
protected override void OnStartFailed()
{
if (File.ReadAllText(LogPath).Length == 0)
if (new FileInfo(LogPath).Length == 0)
{
Task.Run(() =>
{
@@ -76,8 +76,8 @@ namespace Netch.Controllers
public override void Stop()
{
_form!.Close();
StopInstance();
_form.Close();
StopGuard();
}
}
}

View File

@@ -2,14 +2,8 @@
{
public interface IController
{
/// <summary>
/// 控制器名
/// </summary>
public string Name { get; }
/// <summary>
/// 停止
/// </summary>
public void Stop();
}
}

View File

@@ -4,11 +4,6 @@ namespace Netch.Interfaces
{
public interface IModeController : IController
{
/// <summary>
/// 启动
/// </summary>
/// <param name="mode">模式</param>
/// <returns>是否成功</returns>
public abstract void Start(in Mode mode);
public void Start(in Mode mode);
}
}

View File

@@ -7,13 +7,15 @@ namespace Netch.Servers.Shadowsocks
{
public class SSController : Guard, IServerController
{
public override string MainFile { get; protected set; } = "Shadowsocks.exe";
public SSController() : base("Shadowsocks.exe")
{
}
protected override IEnumerable<string> StartedKeywords { get; set; } = new[] { "listening at" };
protected override IEnumerable<string> StartedKeywords => new[] { "listening at" };
protected override IEnumerable<string> StoppedKeywords { get; set; } = new[] { "Invalid config path", "usage", "plugin service exit unexpectedly" };
protected override IEnumerable<string> FailedKeywords => new[] { "Invalid config path", "usage", "plugin service exit unexpectedly" };
public override string Name { get; } = "Shadowsocks";
public override string Name => "Shadowsocks";
public ushort? Socks5LocalPort { get; set; }
@@ -36,7 +38,7 @@ namespace Netch.Servers.Shadowsocks
plugin_opts = server.PluginOption
};
StartInstanceAuto(command.ToString());
StartGuard(command.ToString());
}
[Verb]
@@ -70,10 +72,5 @@ namespace Netch.Servers.Shadowsocks
[Optional]
public string? acl { get; set; }
}
public override void Stop()
{
StopInstance();
}
}
}

View File

@@ -7,13 +7,15 @@ namespace Netch.Servers.ShadowsocksR
{
public class SSRController : Guard, IServerController
{
public override string MainFile { get; protected set; } = "ShadowsocksR.exe";
public SSRController() : base("ShadowsocksR.exe")
{
}
protected override IEnumerable<string> StartedKeywords { get; set; } = new[] { "listening at" };
protected override IEnumerable<string> StartedKeywords => new[] { "listening at" };
protected override IEnumerable<string> StoppedKeywords { get; set; } = new[] { "Invalid config path", "usage" };
protected override IEnumerable<string> FailedKeywords => new[] { "Invalid config path", "usage" };
public override string Name { get; } = "ShadowsocksR";
public override string Name => "ShadowsocksR";
public ushort? Socks5LocalPort { get; set; }
@@ -39,7 +41,7 @@ namespace Netch.Servers.ShadowsocksR
u = true
};
StartInstanceAuto(command.ToString());
StartGuard(command.ToString());
}
[Verb]
@@ -79,10 +81,5 @@ namespace Netch.Servers.ShadowsocksR
[Optional]
public string? acl { get; set; }
}
public override void Stop()
{
StopInstance();
}
}
}

View File

@@ -10,13 +10,15 @@ namespace Netch.Servers.Trojan
{
public class TrojanController : Guard, IServerController
{
public override string MainFile { get; protected set; } = "Trojan.exe";
public TrojanController() : base("Trojan.exe")
{
}
protected override IEnumerable<string> StartedKeywords { get; set; } = new[] { "started" };
protected override IEnumerable<string> StartedKeywords => new[] { "started" };
protected override IEnumerable<string> StoppedKeywords { get; set; } = new[] { "exiting" };
protected override IEnumerable<string> FailedKeywords => new[] { "exiting" };
public override string Name { get; } = "Trojan";
public override string Name => "Trojan";
public ushort? Socks5LocalPort { get; set; }
@@ -45,12 +47,7 @@ namespace Netch.Servers.Trojan
File.WriteAllBytes(Constants.TempConfig, JsonSerializer.SerializeToUtf8Bytes(trojanConfig, Global.NewDefaultJsonSerializerOptions));
StartInstanceAuto("-c ..\\data\\last.json");
}
public override void Stop()
{
StopInstance();
StartGuard("-c ..\\data\\last.json");
}
}
}

View File

@@ -9,13 +9,17 @@ namespace Netch.Servers.V2ray
{
public class V2rayController : Guard, IServerController
{
public override string MainFile { get; protected set; } = "xray.exe";
public V2rayController() : base("xray.exe")
{
if (!Global.Settings.V2RayConfig.XrayCone)
Instance.StartInfo.Environment["XRAY_CONE_DISABLED"] = "true";
}
protected override IEnumerable<string> StartedKeywords { get; set; } = new[] { "started" };
protected override IEnumerable<string> StartedKeywords => new[] { "started" };
protected override IEnumerable<string> StoppedKeywords { get; set; } = new[] { "config file not readable", "failed to" };
protected override IEnumerable<string> FailedKeywords => new[] { "config file not readable", "failed to" };
public override string Name { get; } = "Xray";
public override string Name => "Xray";
public ushort? Socks5LocalPort { get; set; }
@@ -24,19 +28,7 @@ namespace Netch.Servers.V2ray
public virtual void Start(in Server s, in Mode mode)
{
File.WriteAllText(Constants.TempConfig, V2rayConfigUtils.GenerateClientConfig(s, mode));
StartInstanceAuto("-config ..\\data\\last.json");
}
public override void Stop()
{
StopInstance();
}
protected override void InitInstance(string argument)
{
base.InitInstance(argument);
if (!Global.Settings.V2RayConfig.XrayCone)
Instance!.StartInfo.Environment["XRAY_CONE_DISABLED"] = "true";
StartGuard("-config ..\\data\\last.json");
}
}
}

View File

@@ -1,12 +1,12 @@
using Microsoft.Diagnostics.Tracing.Parsers;
using Microsoft.Diagnostics.Tracing.Session;
using Netch.Controllers;
using Netch.Models;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Diagnostics.Tracing.Parsers;
using Microsoft.Diagnostics.Tracing.Session;
using Netch.Controllers;
using Netch.Models;
using Serilog;
namespace Netch.Utils
@@ -58,9 +58,8 @@ namespace Netch.Utils
{
case null:
break;
case Guard instanceController:
if (instanceController.Instance != null)
instances.Add(instanceController.Instance);
case Guard guard:
instances.Add(guard.Instance);
break;
}
@@ -70,18 +69,17 @@ namespace Netch.Utils
{
case null:
break;
case NFController _:
case NFController:
instances.Add(Process.GetCurrentProcess());
break;
case Guard instanceController:
instances.Add(instanceController.Instance!);
case Guard guard:
instances.Add(guard.Instance);
break;
}
var processList = instances.Select(instance => instance.Id).ToList();
var processList = instances.Select(instance => instance.Id).ToHashSet();
Log.Information("流量统计进程: {Processes}",
$"{string.Join(",", instances.Select(instance => $"({instance.Id})" + instance.ProcessName).ToArray())}");
Log.Information("流量统计进程: {Processes}", string.Join(',', instances.Select(v => $"({v.Id}){v.ProcessName}")));
received = 0;