初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.EasyTier;
|
||||
|
||||
public class CliNetTest
|
||||
{
|
||||
public enum NatType
|
||||
{
|
||||
Unknown,
|
||||
OpenInternet,
|
||||
NoPat,
|
||||
FullCone,
|
||||
Restricted,
|
||||
PortRestricted,
|
||||
SymmetricEasy,
|
||||
Symmetric,
|
||||
SymmetricFirewall,
|
||||
UdpBlocked
|
||||
}
|
||||
public record NetStatus
|
||||
{
|
||||
public required NatType UdpNatType;
|
||||
public required NatType TcpNatType;
|
||||
public required bool SupportIPv6;
|
||||
}
|
||||
|
||||
public async static Task<NetStatus?> GetNetStatusAsync()
|
||||
{
|
||||
using var cliProcess = new Process();
|
||||
cliProcess.StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = $"{EasyTierMetadata.EasyTierFilePath}\\easytier-cli.exe",
|
||||
WorkingDirectory = EasyTierMetadata.EasyTierFilePath,
|
||||
Arguments = $"-o json stun",
|
||||
ErrorDialog = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8,
|
||||
StandardInputEncoding = Encoding.UTF8
|
||||
};
|
||||
cliProcess.EnableRaisingEvents = true;
|
||||
cliProcess.Start();
|
||||
var reader = PipeReader.Create(cliProcess.StandardOutput.BaseStream);
|
||||
|
||||
StunInfo? stunInfo = null;
|
||||
try
|
||||
{
|
||||
stunInfo = await JsonSerializer.DeserializeAsync<StunInfo>(reader, JsonCompat.SerializerOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Link", "Failed to do net test");
|
||||
}
|
||||
if (stunInfo is null) return null;
|
||||
|
||||
var supportIPv6 = false;
|
||||
foreach (var ip in stunInfo.Ips)
|
||||
{
|
||||
if (ip.Contains(":"))
|
||||
{
|
||||
supportIPv6 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new NetStatus { UdpNatType = GetNatTypeViaCode(stunInfo.UdpNatType), TcpNatType = GetNatTypeViaCode(stunInfo.TcpNatType), SupportIPv6 = supportIPv6 };
|
||||
}
|
||||
|
||||
public static NatType GetNatTypeViaCode(int type) => type switch
|
||||
{
|
||||
0 => NatType.OpenInternet,
|
||||
1 => NatType.NoPat,
|
||||
2 => NatType.FullCone,
|
||||
3 => NatType.Restricted,
|
||||
4 => NatType.PortRestricted,
|
||||
5 => NatType.SymmetricEasy,
|
||||
6 => NatType.Symmetric,
|
||||
7 => NatType.SymmetricFirewall,
|
||||
8 => NatType.UdpBlocked,
|
||||
_ => NatType.Unknown
|
||||
};
|
||||
|
||||
public static string GetNatTypeString(NatType type)
|
||||
{
|
||||
return Lang.Text(type switch
|
||||
{
|
||||
NatType.OpenInternet or NatType.NoPat => "Link.Nat.Type.Open",
|
||||
NatType.FullCone => "Link.Nat.Type.FullCone",
|
||||
NatType.PortRestricted => "Link.Nat.Type.PortRestricted",
|
||||
NatType.Restricted => "Link.Nat.Type.Restricted",
|
||||
NatType.SymmetricEasy => "Link.Nat.Type.SymmetricEasy",
|
||||
NatType.Symmetric => "Link.Nat.Type.Symmetric",
|
||||
NatType.SymmetricFirewall => "Link.Nat.Type.SymmetricFirewall",
|
||||
NatType.UdpBlocked => "Link.Nat.Type.UdpBlocked",
|
||||
_ => "Link.Nat.Type.Unknown"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace PCL.Core.Link.Scaffolding.EasyTier;
|
||||
|
||||
public enum ConnectionWay
|
||||
{
|
||||
Local,
|
||||
P2P,
|
||||
Relay,
|
||||
Unknown
|
||||
}
|
||||
|
||||
public record EasyPlayerInfo
|
||||
{
|
||||
public required bool IsHost { get; init; }
|
||||
public required string HostName { get; init; }
|
||||
public required string Ip { get; init; }
|
||||
public string UserName { get; init; } = string.Empty;
|
||||
public string MinecraftName { get; init; } = string.Empty;
|
||||
public ConnectionWay Way { get; init; } = ConnectionWay.Unknown;
|
||||
public double Ping { get; init; }
|
||||
public double Loss { get; init; }
|
||||
public string NatType { get; init; } = string.Empty;
|
||||
public string EasyTierVer { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,583 @@
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Link.EasyTier;
|
||||
using PCL.Core.Link.Scaffolding.Client.Models;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.IO.Net;
|
||||
using PCL.Core.IO.Net.Http;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.EasyTier;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates the state of EasyTier entity.
|
||||
/// </summary>
|
||||
public enum EtState
|
||||
{
|
||||
Stopped,
|
||||
Active,
|
||||
Ready
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An EasyTier entity that manages the EasyTier process and its interactions.
|
||||
/// </summary>
|
||||
public class EasyTierEntity
|
||||
{
|
||||
private readonly Process _etProcess;
|
||||
private readonly int _rpcPort;
|
||||
private readonly LobbyInfo _lobby;
|
||||
private readonly int _scfPort;
|
||||
|
||||
public int ForwardPort { get; private set; }
|
||||
public int MinecraftPort { get; init; }
|
||||
public EtState State { get; private set; }
|
||||
public LobbyInfo Lobby => _lobby;
|
||||
|
||||
public event Action? EasyTierProcessExisted;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor of EasyTierEntity
|
||||
/// </summary>
|
||||
/// <param name="lobby">The room information.</param>
|
||||
/// <param name="minecraftPort">Minecraft port.</param>
|
||||
/// <param name="scfPort">The server port.</param>
|
||||
/// <param name="asHost">Indicates whether the entity acts as a host.</param>
|
||||
/// <exception cref="FileNotFoundException">Thrown if EasyTier was broken.</exception>
|
||||
public EasyTierEntity(LobbyInfo lobby, int minecraftPort, int scfPort, bool asHost)
|
||||
{
|
||||
_lobby = lobby;
|
||||
MinecraftPort = minecraftPort;
|
||||
_scfPort = scfPort;
|
||||
State = EtState.Stopped;
|
||||
|
||||
var existEntities = Process.GetProcessesByName("easytier-core");
|
||||
foreach (var entity in existEntities)
|
||||
{
|
||||
LogWrapper.Warn("EasyTier", $"Find exist EasyTier Entity, may affect something: {entity.Id}");
|
||||
}
|
||||
|
||||
LogWrapper.Info("EasyTier", $"EasyTier folder path: {EasyTierMetadata.EasyTierFilePath}");
|
||||
|
||||
if (!(File.Exists($"{EasyTierMetadata.EasyTierFilePath}\\easytier-core.exe") &&
|
||||
File.Exists($"{EasyTierMetadata.EasyTierFilePath}\\easytier-cli.exe") &&
|
||||
File.Exists($"{EasyTierMetadata.EasyTierFilePath}\\Packet.dll")))
|
||||
{
|
||||
LogWrapper.Error("EasyTier", "EasyTier was broken.");
|
||||
|
||||
throw new FileNotFoundException("EasyTier was broken.");
|
||||
}
|
||||
|
||||
State = EtState.Ready;
|
||||
|
||||
_rpcPort = NetworkHelper.NewTcpPort();
|
||||
|
||||
ForwardPort = NetworkHelper.NewTcpPort();
|
||||
|
||||
_etProcess = _BuildProcessAsync(asHost).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Launches EasyTier process.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// - 1 means failed to launch EasyTier and can never launch again.<br/>
|
||||
/// - 0 means successful launch.
|
||||
/// </returns>
|
||||
public int Launch()
|
||||
{
|
||||
LogWrapper.Info("EasyTier", "Launch EasyTier Core.");
|
||||
|
||||
try
|
||||
{
|
||||
// LogWrapper.Info("Test", _etProcess.StartInfo.Arguments);
|
||||
_etProcess.Start();
|
||||
State = EtState.Active;
|
||||
|
||||
var cli = _GetCliOutputDebugAsync();
|
||||
|
||||
_etProcess.Exited += (_, _) => EasyTierProcessExisted?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "EasyTier", "Failed to launch EasyTier.");
|
||||
State = EtState.Stopped;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops EasyTier process.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// - 1 means failed to stop EasyTier.<br/>
|
||||
/// - 0 means successful stop.
|
||||
/// </returns>
|
||||
public Task<int> StopAsync()
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_etProcess.HasExited)
|
||||
{
|
||||
_etProcess.Kill(true);
|
||||
_etProcess.WaitForExit(5000);
|
||||
}
|
||||
|
||||
State = EtState.Stopped;
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "EasyTier", "Failed to stop EasyTier.");
|
||||
State = EtState.Stopped;
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<Process> _BuildProcessAsync(bool asHost)
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
EnableRaisingEvents = true,
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Path.Combine(EasyTierMetadata.EasyTierFilePath, "easytier-core.exe"),
|
||||
WorkingDirectory = EasyTierMetadata.EasyTierFilePath,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
}
|
||||
};
|
||||
|
||||
var args = new ArgumentsBuilder();
|
||||
|
||||
args.AddFlag("no-tun")
|
||||
.AddFlag("multi-thread")
|
||||
.AddFlag("enable-kcp-proxy")
|
||||
.AddFlag("enable-quic-proxy")
|
||||
.AddFlagIf(!Config.Link.TryPunchSym, "disable-sys-hole-punching")
|
||||
.AddFlagIf(!Config.Link.EnableIPv6, "disable-ipv6")
|
||||
.AddFlagIf(Config.Link.UseLatencyFirstMode, "latency-first")
|
||||
.Add("encryption-algorithm", "aes-gcm")
|
||||
.Add("compression", "zstd")
|
||||
.Add("default-protocol", Config.Link.ProtocolPreference.ToString().ToLowerInvariant())
|
||||
.Add("network-name", _lobby.NetworkName)
|
||||
.Add("network-secret", _lobby.NetworkSecret)
|
||||
//.Add("relay-network-whitelist", _lobby.NetworkName)
|
||||
.Add("machine-id", Utils.Secret.Identify.LauncherId)
|
||||
.Add("rpc-portal", _rpcPort.ToString())
|
||||
.Add("private-mode", "true")
|
||||
.AddFlag("p2p-only");
|
||||
|
||||
|
||||
if (asHost)
|
||||
{
|
||||
args.AddWithSpace("i", "10.114.51.41")
|
||||
.Add("hostname", $"scaffolding-mc-server-{_scfPort}")
|
||||
.Add("tcp-whitelist", _scfPort.ToString())
|
||||
.Add("udp-whitelist", _scfPort.ToString())
|
||||
.Add("tcp-whitelist", MinecraftPort.ToString())
|
||||
.Add("udp-whitelist", MinecraftPort.ToString())
|
||||
.Add("l", "tcp://0.0.0.0:0")
|
||||
.Add("l", "udp://0.0.0.0:0");
|
||||
}
|
||||
else
|
||||
{
|
||||
args.AddFlag("d")
|
||||
.Add("hostname", Guid.NewGuid().ToString())
|
||||
.Add("tcp-whitelist", "0")
|
||||
.Add("udp-whitelist", "0")
|
||||
.Add("l", "tcp://0.0.0.0:0")
|
||||
.Add("l", "udp://0.0.0.0:0");
|
||||
}
|
||||
|
||||
foreach (var address in ETRelay.RelayList
|
||||
.Select(static x => x.Url)
|
||||
.Concat(_fallbackNodeLinks))
|
||||
{
|
||||
args.Add("p", address);
|
||||
}
|
||||
|
||||
// foreach (var address in await _GetEtRelayListAsync().ConfigureAwait(false))
|
||||
// {
|
||||
// args.Add("p", address);
|
||||
// }
|
||||
|
||||
// if (Config.Link.RelayType == 1)
|
||||
// {
|
||||
// args.AddFlag("disable-p2p");
|
||||
// }
|
||||
|
||||
process.StartInfo.Arguments = args.GetResult();
|
||||
|
||||
LogWrapper.Debug("EasyTier", process.StartInfo.Arguments);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<string>> _GetEtRelayListAsync()
|
||||
{
|
||||
var relays = ETRelay.RelayList;
|
||||
var customedNodes = Config.Link.CustomRelayServer.Split(';', StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var node in customedNodes)
|
||||
{
|
||||
if (node.Contains("tcp://", StringComparison.OrdinalIgnoreCase) ||
|
||||
node.Contains("udp://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
relays.Add(new ETRelay
|
||||
{
|
||||
Url = node,
|
||||
Name = "Custom",
|
||||
Type = ETRelayType.Custom
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
LogWrapper.Warn("EasyTier", $"Invalid custom node URL: {node}.");
|
||||
}
|
||||
}
|
||||
|
||||
var setupRelayList = relays.Select(relay => new { relay, serverType = Config.Link.ServerType })
|
||||
.Where(rl =>
|
||||
(rl.relay.Type == ETRelayType.Selfhosted && rl.serverType != 2) ||
|
||||
(rl.relay.Type == ETRelayType.Community && rl.serverType == 1) ||
|
||||
rl.relay.Type == ETRelayType.Custom)
|
||||
.Select(rl => rl.relay.Url).ToImmutableList();
|
||||
|
||||
var pubNode = await _GetPublicNodeAsync().ConfigureAwait(false);
|
||||
|
||||
var result = setupRelayList
|
||||
.Concat(pubNode)
|
||||
.Take(6)
|
||||
.ToImmutableList();
|
||||
|
||||
LogWrapper.Debug($"Get public node:\n{string.Join("\n\t", result)}");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private readonly string[] _fallbackNodeLinks =
|
||||
[
|
||||
"tcp://public.easytier.top:11010",
|
||||
"tcp://public2.easytier.cn:54321",
|
||||
"https://etnode.zkitefly.eu.org/node1",
|
||||
"https://etnode.zkitefly.eu.org/node2",
|
||||
"https://etnode.zkitefly.eu.org/-node1",
|
||||
"https://etnode.zkitefly.eu.org/-node2"
|
||||
];
|
||||
|
||||
|
||||
private async Task<IReadOnlyList<string>> _GetPublicNodeAsync()
|
||||
{
|
||||
using var rep = await HttpRequest
|
||||
.Create("https://uptime.easytier.cn/api/nodes?page=1&per_page=50&is_active=true")
|
||||
.SendAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
rep.EnsureSuccessStatusCode();
|
||||
|
||||
var dto = await rep
|
||||
.AsJsonAsync<PublicNodeDto>()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(dto);
|
||||
|
||||
var result = dto.Data.Items
|
||||
.Where(it => it is { IsActive: true, IsAllowRelay: true })
|
||||
.Select(it => it.Host)
|
||||
.Union(_fallbackNodeLinks)
|
||||
.ToImmutableList();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
#region Information
|
||||
|
||||
/// <summary>
|
||||
/// Checks the status of EasyTier network until it is ready or time-out.
|
||||
/// </summary>
|
||||
/// <returns>Returns 0 when the network is ready, otherwise returns 1 for timeout.</returns>
|
||||
public async Task<(bool, EtPlayerList?)> CheckEasyTierStatusAsync(CancellationToken ct = default)
|
||||
{
|
||||
var retryCount = 0;
|
||||
|
||||
while (_etProcess is null && retryCount < 10)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
await Task.Delay(1000, ct).ConfigureAwait(false);
|
||||
retryCount++;
|
||||
}
|
||||
|
||||
if (_etProcess is null)
|
||||
{
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
retryCount = 0;
|
||||
while (State is EtState.Active && retryCount < 300)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var info = await _GetPlayersAsync().ConfigureAwait(false);
|
||||
if (info.Host is null)
|
||||
{
|
||||
LogWrapper.Debug("EasyTierEntity", "Retry to get EasyTier Info.");
|
||||
await Task.Delay(1000, ct).ConfigureAwait(false);
|
||||
retryCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
LogWrapper.Debug("EtEntity", "Successfully to get player info from EasyTier CLI.");
|
||||
|
||||
if (info.Host.Ping < 1000)
|
||||
{
|
||||
State = EtState.Ready;
|
||||
|
||||
return (true, info);
|
||||
}
|
||||
|
||||
await Task.Delay(1000, ct).ConfigureAwait(false);
|
||||
retryCount++;
|
||||
}
|
||||
|
||||
LogWrapper.Debug("EtEntity", "Failed to get player info from EasyTier CLI.");
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a port forward to the EasyTier instance.
|
||||
/// </summary>
|
||||
/// <param name="targetIp">Remote IP</param>
|
||||
/// <param name="targetPort">Remote Port</param>
|
||||
/// <returns>Forwarded local port</returns>
|
||||
public async Task<int> AddPortForwardAsync(string targetIp, int targetPort)
|
||||
{
|
||||
var localPort = NetworkHelper.NewTcpPort();
|
||||
using var cliProcess = new Process();
|
||||
cliProcess.StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = $"{EasyTierMetadata.EasyTierFilePath}\\easytier-cli.exe",
|
||||
WorkingDirectory = EasyTierMetadata.EasyTierFilePath,
|
||||
ErrorDialog = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8,
|
||||
StandardInputEncoding = Encoding.UTF8
|
||||
};
|
||||
cliProcess.EnableRaisingEvents = true;
|
||||
try
|
||||
{
|
||||
cliProcess.StartInfo.Arguments =
|
||||
$"--rpc-portal 127.0.0.1:{_rpcPort} port-forward add tcp 127.0.0.1:{localPort} {targetIp}:{targetPort}";
|
||||
cliProcess.Start();
|
||||
await cliProcess.WaitForExitAsync().ConfigureAwait(false);
|
||||
|
||||
cliProcess.StartInfo.Arguments =
|
||||
$"--rpc-portal 127.0.0.1:{_rpcPort} port-forward add udp 127.0.0.1:{localPort} {targetIp}:{targetPort}";
|
||||
cliProcess.Start();
|
||||
await cliProcess.WaitForExitAsync().ConfigureAwait(false);
|
||||
|
||||
cliProcess.StartInfo.Arguments =
|
||||
$"--rpc-portal 127.0.0.1:{_rpcPort} port-forward add tcp [::]:{localPort} {targetIp}:{targetPort}";
|
||||
cliProcess.Start();
|
||||
await cliProcess.WaitForExitAsync().ConfigureAwait(false);
|
||||
|
||||
cliProcess.StartInfo.Arguments =
|
||||
$"--rpc-portal 127.0.0.1:{_rpcPort} port-forward add udp [::]:{localPort} {targetIp}:{targetPort}";
|
||||
cliProcess.Start();
|
||||
await cliProcess.WaitForExitAsync().ConfigureAwait(false);
|
||||
|
||||
LogWrapper.Debug("ET Cli", await cliProcess.StandardOutput.ReadToEndAsync().ConfigureAwait(false) +
|
||||
await cliProcess.StandardError.ReadToEndAsync().ConfigureAwait(false));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogWrapper.Error(e, "ET Cli", "Failed to add port forward.");
|
||||
}
|
||||
return localPort;
|
||||
}
|
||||
|
||||
private async Task _GetCliOutputDebugAsync()
|
||||
{
|
||||
while (State != EtState.Stopped && Config.Link.EnableCliOutput)
|
||||
{
|
||||
using var cliProcess = new Process();
|
||||
cliProcess.StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = $"{EasyTierMetadata.EasyTierFilePath}\\easytier-cli.exe",
|
||||
WorkingDirectory = EasyTierMetadata.EasyTierFilePath,
|
||||
Arguments = $"--rpc-portal 127.0.0.1:{_rpcPort} peer",
|
||||
ErrorDialog = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8,
|
||||
StandardInputEncoding = Encoding.UTF8
|
||||
};
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(5000));
|
||||
|
||||
try
|
||||
{
|
||||
cliProcess.Start();
|
||||
cliProcess.StandardInput.Close();
|
||||
|
||||
var stdOut = await cliProcess.StandardOutput.ReadToEndAsync(cts.Token).ConfigureAwait(false);
|
||||
var stdErr = await cliProcess.StandardError.ReadToEndAsync(cts.Token).ConfigureAwait(false);
|
||||
|
||||
await cliProcess.WaitForExitAsync(cts.Token).ConfigureAwait(false);
|
||||
|
||||
var output = stdOut + stdErr;
|
||||
|
||||
LogWrapper.Info("EasyTier Cli Debug", "EasyTier Cli 抽样输出: \n" + output);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogWrapper.Error(e, "EasyTier Cli", "Failed to get EasyTier Cli info");
|
||||
}
|
||||
|
||||
await Task.Delay(30000);
|
||||
}
|
||||
}
|
||||
|
||||
/// <exception cref="ArgumentException">Thrown if host is duplicated.</exception>
|
||||
private async Task<EtPlayerList> _GetPlayersAsync()
|
||||
{
|
||||
using var cliProcess = new Process();
|
||||
cliProcess.StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = $"{EasyTierMetadata.EasyTierFilePath}\\easytier-cli.exe",
|
||||
WorkingDirectory = EasyTierMetadata.EasyTierFilePath,
|
||||
Arguments = $"--rpc-portal 127.0.0.1:{_rpcPort} -o json peer",
|
||||
ErrorDialog = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8,
|
||||
StandardInputEncoding = Encoding.UTF8
|
||||
};
|
||||
cliProcess.EnableRaisingEvents = true;
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(5000));
|
||||
|
||||
try
|
||||
{
|
||||
LogWrapper.Debug("Et Cli", "Trying to get player info.");
|
||||
|
||||
cliProcess.Start();
|
||||
cliProcess.StandardInput.Close();
|
||||
|
||||
var stdOut = await cliProcess.StandardOutput.ReadToEndAsync(cts.Token).ConfigureAwait(false);
|
||||
var stdErr = await cliProcess.StandardError.ReadToEndAsync(cts.Token).ConfigureAwait(false);
|
||||
|
||||
await cliProcess.WaitForExitAsync(cts.Token).ConfigureAwait(false);
|
||||
|
||||
var output = stdOut + stdErr;
|
||||
//LogWrapper.Debug("ET Cli", output);
|
||||
|
||||
if (JsonCompat.ParseNode(output) is not JsonArray jArray)
|
||||
{
|
||||
return new EtPlayerList(null, null);
|
||||
}
|
||||
|
||||
|
||||
List<EasyPlayerInfo> players = [];
|
||||
EasyPlayerInfo? host = null;
|
||||
foreach (var arr in jArray)
|
||||
{
|
||||
LogWrapper.Debug("Et Cli", "Getting player info.");
|
||||
|
||||
var info = arr.Deserialize<ETPeerInfo>(JsonCompat.SerializerOptions);
|
||||
if (info is null)
|
||||
{
|
||||
LogWrapper.Debug("Et Cli", "Player info is null.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (info.Hostname.StartsWith("scaffolding-mc-server-", StringComparison.Ordinal))
|
||||
{
|
||||
LogWrapper.Debug("Et Cli", $"Find host player: {info.Hostname}");
|
||||
|
||||
if (host is not null)
|
||||
{
|
||||
LogWrapper.Debug("Et Cli", "Duplicated host player.");
|
||||
throw new ArgumentException("Duplicated host.", nameof(host));
|
||||
}
|
||||
|
||||
host = _ConvertPeerToPlayer(info);
|
||||
continue;
|
||||
}
|
||||
|
||||
LogWrapper.Debug("Et Cli", $"Find player: {info.Hostname}");
|
||||
players.Add(_ConvertPeerToPlayer(info));
|
||||
}
|
||||
|
||||
LogWrapper.Debug("Et Cli", "Return from GetPlayersAsync().");
|
||||
|
||||
var result = host is null ? players : [host, .. players];
|
||||
|
||||
return new EtPlayerList(result, host);
|
||||
}
|
||||
catch (TaskCanceledException tce)
|
||||
{
|
||||
LogWrapper.Error(tce, "EasyTier", "Failed to read CLI output.");
|
||||
return new EtPlayerList(null, null);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "EasyTier", "Failed to get EasyTier player list info.");
|
||||
return new EtPlayerList(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static EasyPlayerInfo _ConvertPeerToPlayer(ETPeerInfo info)
|
||||
{
|
||||
var playerInfo = new EasyPlayerInfo
|
||||
{
|
||||
IsHost = info.Hostname.StartsWith("scaffolding-mc-server", StringComparison.Ordinal),
|
||||
HostName = info.Hostname,
|
||||
Ip = info.Ipv4,
|
||||
Ping = Math.Round(Convert.ToDouble(info.Ping != "-" ? info.Ping : "0")),
|
||||
Loss = Math.Round(Convert.ToDouble(info.Loss != "-" ? info.Loss.Replace("%", "") : "0")),
|
||||
NatType = info.NatType,
|
||||
EasyTierVer = info.ETVersion
|
||||
};
|
||||
|
||||
return playerInfo;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record EtPlayerList(IReadOnlyList<EasyPlayerInfo>? Players, EasyPlayerInfo? Host);
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using PCL.Core.App;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.EasyTier;
|
||||
|
||||
public static class EasyTierMetadata
|
||||
{
|
||||
public const string CurrentEasyTierVer = "2.6.4";
|
||||
|
||||
public static string EasyTierFilePath => Path.Combine(Paths.SharedLocalData, "EasyTier",
|
||||
CurrentEasyTierVer,
|
||||
$"easytier-windows-{(RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x86_64")}");
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.EasyTier;
|
||||
|
||||
internal record PublicNodeDto
|
||||
{
|
||||
[JsonPropertyName("success")] public bool IsSuccess { get; init; }
|
||||
[JsonPropertyName("data")] public required NodeDataDto Data { get; init; }
|
||||
}
|
||||
|
||||
internal record NodeDataDto
|
||||
{
|
||||
[JsonPropertyName("items")] public required IReadOnlyList<NodeItemDto> Items { get; init; }
|
||||
}
|
||||
|
||||
internal record NodeItemDto
|
||||
{
|
||||
[JsonPropertyName("address")] public required string Host { get; init; }
|
||||
[JsonPropertyName("allow_relay")] public bool IsAllowRelay { get; init; }
|
||||
[JsonPropertyName("is_active")] public bool IsActive { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.EasyTier;
|
||||
|
||||
public record StunInfo
|
||||
{
|
||||
[JsonPropertyName("udp_nat_type")] public required int UdpNatType { get; init; }
|
||||
[JsonPropertyName("tcp_nat_type")] public required int TcpNatType { get; init; }
|
||||
[JsonPropertyName("public_ip")] public required string[] Ips { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user