diff --git a/.idea/.idea.SuikaiLauncher.Core/.idea/encodings.xml b/.idea/.idea.SuikaiLauncher.Core/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.SuikaiLauncher.Core/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.SuikaiLauncher.Core/.idea/indexLayout.xml b/.idea/.idea.SuikaiLauncher.Core/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.SuikaiLauncher.Core/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.SuikaiLauncher.Core/.idea/projectSettingsUpdater.xml b/.idea/.idea.SuikaiLauncher.Core/.idea/projectSettingsUpdater.xml new file mode 100644 index 0000000..ef20cb0 --- /dev/null +++ b/.idea/.idea.SuikaiLauncher.Core/.idea/projectSettingsUpdater.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/.idea/.idea.SuikaiLauncher.Core/.idea/vcs.xml b/.idea/.idea.SuikaiLauncher.Core/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/.idea.SuikaiLauncher.Core/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/.idea.SuikaiLauncher.Core/.idea/workspace.xml b/.idea/.idea.SuikaiLauncher.Core/.idea/workspace.xml new file mode 100644 index 0000000..66c3b6e --- /dev/null +++ b/.idea/.idea.SuikaiLauncher.Core/.idea/workspace.xml @@ -0,0 +1,123 @@ + + + + SuikaiLauncher.Core.Modpack/SuikaiLauncher.Core.Modpack.csproj + SuikaiLauncher.Core.Modpack/SuikaiLauncher.Core.Modpack.csproj + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1749966088508 + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SuikaiLauncher.Core.Account/Modules/Microsoft.cs b/SuikaiLauncher.Core.Account/Modules/Microsoft.cs index 6dac874..52010be 100644 --- a/SuikaiLauncher.Core.Account/Modules/Microsoft.cs +++ b/SuikaiLauncher.Core.Account/Modules/Microsoft.cs @@ -1,10 +1,10 @@ -using SuikaiLauncher.Core.Account.JsonModel; +using SuikaiLauncher.Core.Account.JsonModel; using SuikaiLauncher.Core.Base; using SuikaiLauncher.Core.Override; using System.Runtime.CompilerServices; -namespace SuikaiLauncher.Core.Account.Modules +namespace SuikaiLauncher.Core.Account { public class Microsoft { diff --git a/SuikaiLauncher.Core.Account/Modules/Profile.cs b/SuikaiLauncher.Core.Account/Modules/Profile.cs new file mode 100644 index 0000000..8ea19fc --- /dev/null +++ b/SuikaiLauncher.Core.Account/Modules/Profile.cs @@ -0,0 +1,149 @@ +using System.Text; +using SuikaiLauncher.Core.Base; + +namespace SuikaiLauncher.Core.Account; +/// +/// 档案类 +/// +public class Profile +{ + private static StringBuilder StrBuilder = new("*",30); + /// + /// 用户名 + /// + public required string Name { get; set; } + /// + /// UUID + /// + public required string uuid { get; set; } + /// + /// 访问令牌 + /// + public required string accessToken { get; set; } + /// + /// 刷新令牌(可能为 null) + /// + public string? refreshToken { get; set; } + /// + /// 用户皮肤的下载地址,没有设置则为 null + /// + public string? Skin { get; set; } + /// + /// 披风下载地址,没有则为 null + /// + public string? Cape { get; set; } + /// + /// 账户类型 + /// + public McLoginType LoginType {get; set;} + /// + /// 档案过期时间(Access Token) + /// + public long ExpiresIn { get; set; } + /// + /// 完全过期时间(Refresh Token) + /// + public long ExpiredAt { get; set; } + /// + /// 档案创建时间 + /// + public long CreateAt { get; set; } + + public static string RemoveSecret(string SecretText) + { + return SecretText.Substring(0, 5) + StrBuilder + SecretText.Substring(SecretText.Length - 5,SecretText.Length -1); + } +} +/// +/// 档案管理器 +/// +public static class ProfileManager +{ + public static Profile CurrentProfile + { + private set + { + + } + get + { + + } + } + private static List? profiles; + /// + /// 初始化档案数据库 + /// + private static void InitializeProfilesDatabase() + { + + } + /// + /// 保存档案 + /// + public static void SaveProfile() + { + + } + /// + /// 清空档案数据库缓存(被移除的档案会在下次加载时恢复,除非数据库没有整这个档案) + /// + public static void Clear() + { + + } + /// + /// 获取某一个档案 + /// + /// 档案索引 + /// 代表这个档案的 Profile 类 + public static Profile GetProfile(int ProfileId) + { + + } + /// + /// 删除某个档案(这会导致档案永久丢失) + /// + /// 要删除的档案 + public static void DeleteProfile(int ProfileId) + { + + } + /// + /// 删除整个档案数据库(这会导致存储在数据库的全部档案丢失) + /// + public static void DeleteProfiles() + { + + } + /// + /// 刷新档案,如果档案过期,则会尝试重新登录 + /// + public static void RefreshProfile() + { + + } +} + +/// +/// 账户类型 +/// +public enum McLoginType +{ + /// + /// 离线 + /// + Offline = 0, + /// + /// 微软(正版) + /// + Microsoft = 1, + /// + /// Yggdrasil 第三方登录 + /// + Auth = 2, + /// + /// 统一通行证 + /// + Nide = 3 +} \ No newline at end of file diff --git a/SuikaiLauncher.Core.Base/Modules/FileIO.cs b/SuikaiLauncher.Core.Base/Modules/FileIO.cs index 075c67f..8988d9f 100644 --- a/SuikaiLauncher.Core.Base/Modules/FileIO.cs +++ b/SuikaiLauncher.Core.Base/Modules/FileIO.cs @@ -7,6 +7,7 @@ using System.IO.Compression; using System.Linq.Expressions; using System.Net; +using System.Formats.Tar; using System.Security.Cryptography; using System.Text; @@ -217,6 +218,7 @@ public class ArchiveFile : IDisposable { private bool _dispose; private string FilePath; + private dynamic? Handler; private GZipStream? DataStream; public bool disposed @@ -316,6 +318,5 @@ public async Task WriteFile(string ArchiveEntry,Stream FileReadStream) } } } - } } \ No newline at end of file diff --git a/SuikaiLauncher.Core.Base/Modules/Network.cs b/SuikaiLauncher.Core.Base/Modules/Network.cs index cd2ff19..332c315 100644 --- a/SuikaiLauncher.Core.Base/Modules/Network.cs +++ b/SuikaiLauncher.Core.Base/Modules/Network.cs @@ -1,611 +1,611 @@ -#pragma warning disable SYSLIB0014 -using SuikaiLauncher.Core.Base; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Cryptography.X509Certificates; -using SuikaiLauncher.Core.Override; -using System.Net.NetworkInformation; -using System.Runtime.CompilerServices; -using System.Reflection.Metadata; - -// 这里是一堆和网络有关系的工具,包括网络请求,代理,Ping,域名解析 -// 虽然看起来很乱然而我没空整理,就先这样吧(逃 - -namespace SuikaiLauncher.Core.Base -{ - public class SocketConnect - { - public required Socket Socket; - } - public class HttpRequestBuilder - { - public static readonly Dictionary ConnectionPool = new(); - public static readonly object ConnectionLock = new object(); - public class SslInfomation - { - public required object RequestMessage; - public required X509Certificate? Cert; - public required X509Chain? Chain; - public required SslPolicyErrors SslPolicyError; - } - private int timeout; - public required HttpRequestMessage Req; - public HttpResponseMessage? Resp; - private static readonly HttpProxy RequestProxyFactory = new(); - private string? ConnectAddress; - private int ConnectPort = 0; - private static bool CheckSsl = true; - public static readonly object HttpRequestBuilderPropertyChangeLock = new object(); - private static Func? CustomSslValidateCallback; - private static readonly SocketsHttpHandler SocketHandler = new() - { - UseProxy = true, - Proxy = RequestProxyFactory, - AllowAutoRedirect = false, - AutomaticDecompression = DecompressionMethods.All, - SslOptions = new SslClientAuthenticationOptions() - { - RemoteCertificateValidationCallback = (object httpRequestMessage, X509Certificate? cert, X509Chain? certChain, SslPolicyErrors sslPolicyError) => - { - if (CustomSslValidateCallback is not null) - { - return CustomSslValidateCallback(new SslInfomation() - { - RequestMessage = httpRequestMessage, - Cert = cert, - Chain = certChain, - SslPolicyError = sslPolicyError - }); - } - if (sslPolicyError != SslPolicyErrors.None && CheckSsl) - { - return false; - } - return true; - } - }, - // 长连接实现 - ConnectCallback = async (SocketsHttpConnectionContext context, CancellationToken token) => - { - var host = context.DnsEndPoint.Host; - var port = context.DnsEndPoint.Port; - SocketConnect? socketConnect = null; - lock (ConnectionLock) - { - if (ConnectionPool.TryGetValue($"{host}:{port}", out socketConnect)) - { - if (socketConnect.Socket != null && socketConnect.Socket.Connected) - { - // 检查 socket 是否可写 - try - { - bool poll = socketConnect.Socket.Poll(0, SelectMode.SelectWrite); - if (poll) - { - // 返回现有连接 - return new NetworkStream(socketConnect.Socket, ownsSocket: false); - } - } - catch - { - // 连接失效,移除 - ConnectionPool.Remove($"{host}:{port}"); - socketConnect = null; - } - } - else - { - ConnectionPool.Remove($"{host}:{port}"); - socketConnect = null; - } - } - } - - // 新建连接 - var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) - { - NoDelay = true - }; - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - await socket.ConnectAsync(host, port, token); - - lock (ConnectionLock) - { - ConnectionPool[$"{host}:{port}"] = new SocketConnect() { Socket = socket }; - } - - return new NetworkStream(socket, ownsSocket: false); - } - }; - private static readonly HttpClient Client = new(SocketHandler); - private static bool FirstRequest; - public static string UserAgent = "SuikaiLauncher.Core/0.0.1"; - /// - /// 创建 HttpRequestBuilder 的新实例 - /// - /// 目标服务器地址 - /// HttpRequestBuilder - public static HttpRequestBuilder Create(string url, HttpMethod method) - { - return new HttpRequestBuilder() { Req = new HttpRequestMessage(method,url) }; - } - /// - /// 设置默认证书验证函数 - /// - /// 证书验证函数 - public static void SetCustomSslValidateCallback(Func CustomCallback) - { - lock (HttpRequestBuilderPropertyChangeLock) - { - CustomSslValidateCallback = CustomCallback; - } - } - /// - /// 设置请求的 IP 地址,用于覆盖默认查询结果 - /// - /// IP 地址 - /// HttpRequestBuilder - public HttpRequestBuilder SetSourceAddress(string Address) - { - return this; - } - /// - /// 设置请求端口,用于覆盖默认端口 - /// - /// 端口号 - /// HttpRequestBuilder - public HttpRequestBuilder SetConnectPort(int Port) - { - return this; - } - /// - /// 是否在默认验证逻辑中忽略 SSL 证书错误 - /// - /// HttpRequestBuilder - public HttpRequestBuilder IgnoreSslError() - { - if (!CheckSsl) return this; - lock (HttpRequestBuilderPropertyChangeLock) - { - CheckSsl = true; - } - return this; - } - /// - /// 设置请求使用的代理 - /// - /// 代理服务器 - /// HttpRequestBuilder - public HttpRequestBuilder UseProxy(string? Proxy = null) - { - if (!string.IsNullOrWhiteSpace(Proxy)) - { - lock (RequestProxyFactory.ProxyChangeLock) - { - RequestProxyFactory.ProxyAddress = Proxy; - RequestProxyFactory.RequiredReloadProxyServer = true; - } - } - return this; - - } - public HttpRequestBuilder WithRequestData(MemoryStream Data) - { - this.Req.Content = new ByteArrayContent(Data.ToArray()); - return this; - } - /// - /// 设置标头 - /// - /// 标头 - /// 值 - /// HttpRequestBuilder - public HttpRequestBuilder SetHeader(string Name, string Value) - { - if (Name.ContainsF("Content") && Req.Content is not null) Req.Content.Headers.Add(Name, Value); - Req.Headers.Add(Name, Value); - return this; - } - /// - /// 发送网络请求并自动处理重定向 - /// - /// - public async Task Invoke() - { - await (await this.SendRequest()).ResolveHttpRedirect(); - return this; - } - /// - /// 获取响应 - /// - /// HttpResponseMessage - public HttpResponseMessage GetResponse(bool EnsureSuccessStatusCode = true) - { - if (this.Resp is null) throw new InvalidOperationException("尝试在服务器响应之前获取响应对象"); - if (EnsureSuccessStatusCode) this.Resp.EnsureSuccessStatusCode(); - return this.Resp; - } - /// - /// 发送单次网络请求,不处理重定向 - /// - /// HttpRequestBuilder - public async Task SendRequest() - { - using (CancellationTokenSource CTS = new(this.timeout)) - { - this.Resp = await Client.SendAsync(this.Req,HttpCompletionOption.ResponseHeadersRead,CTS.Token); - return this; - } - } - /// - /// 处理网络请求的重定向,直到响应码不处于 300~399 范围内(不包括 304) - /// - /// - public async Task ResolveHttpRedirect() - { - - if (this.Resp!.StatusCode is > (HttpStatusCode)300 and < (HttpStatusCode)400 && this.Resp.StatusCode != (HttpStatusCode)304) - { - HttpRequestMessage RedirectReq = new(); - foreach(var Header in this.Req.Headers) - { - Req.Headers.Add(Header.Key, Header.Value); - } - if (this.Req.Content is not null) - { - MemoryStream ReqStream = new(); - await (await this.Req.Content.ReadAsStreamAsync()).CopyToAsync(ReqStream); - if (ReqStream is null) goto SkipContent; - RedirectReq.Content = new ByteArrayContent(ReqStream.ToArray()); - ReqStream.Dispose(); - foreach (var Header in this.Req.Content.Headers) - { - RedirectReq.Content.Headers.Add(Header.Key, Header.Value); - } - } - SkipContent: - RedirectReq.RequestUri = this.Req.Headers.GetValues("location").First().ToURI(); - this.Req.Dispose(); - this.Resp.Dispose(); - this.Req = RedirectReq; - await this.Invoke(); - } - return this; - } - } - public class Localhost { - public class PingResult - { - public object? CustomResult; - public List Result { get { throw new InvalidOperationException("无法读取此属性"); } set - { - TotalSend = value.Count; - value.Select(k => - { - switch (k.Status) - { - case IPStatus.Success: - if (Fastest == -1) Fastest = k.RoundtripTime; - else if (Slowest == -1) Slowest = k.RoundtripTime; - else if (Slowest < k.RoundtripTime) Slowest = k.RoundtripTime; - else if (Fastest > k.RoundtripTime) Fastest = k.RoundtripTime; - TotalUsage += k.RoundtripTime; - return null; - case IPStatus.TimedOut: - Failed++; - Logger.Log($"[Network] Ping {k.Address} 失败:请求超时"); - return null; - case IPStatus.DestinationHostUnreachable: - Failed++; - Logger.Log($"[Network] Ping {k.Address} 失败:此远程地址不可达"); - return null; - case IPStatus.DestinationNetworkUnreachable: - Failed++; - Logger.Log($"[Network] Ping {k.Address} 失败:此远程地址所处的网络不可达"); - return null; - case IPStatus.DestinationPortUnreachable: - Failed++; - Logger.Log($"[Network] Ping {k.Address} 失败:远程地址所指定的端口不可达"); - return null; - case IPStatus.NoResources: - Failed++; - Logger.Log($"[Network] Ping {k.Address} 失败:网络资源不足"); - return null; - - } - if (k.Status == IPStatus.Success) Success++; - else - { - if (k.Status == IPStatus.TimedOut) - Failed++; - return null; - } - return null; - }); - Average = TotalUsage / TotalSend; - } - } - public long Fastest = -1; - public long Slowest = -1; - public long Average = -1; - public long TotalUsage = -1; - public long Success = 0; - public long Failed = 0; - public int TotalSend = 0; - } - private static bool SupportIPv6; - private static Ping ICMPClient = new(); - public class PingInfomation - { - public required string Address; - public int port = 25565; - public int MaxTry = 1; - public int Timeout = 2500; - } - /// - /// 并行发送多个 ICMP/TCP 包来测试本地网络到目标服务器的连通性 - /// - /// 目标服务器地址 - /// 端口号(仅 Tcping 模式下可用) - /// 是否使用 Tcping 模式 - /// 允许的最大超时 - /// 发送的 ICMP/TCP 包数量(并行发送过多包可能导致额外开销) - /// 自定义处理类 - public async static Task Ping(string Address, int port = 80, bool UseTcping = false, int MaxTimeout = 2500, int MaxTry = 4,Func? CustomResolver = null) - { - if (CustomResolver is not null) return new PingResult() { CustomResult = CustomResolver(new PingInfomation() { Address = Address, port = port, Timeout = MaxTimeout, MaxTry = MaxTry }) }; - Logger.Log($"[Network] 开始 Ping {Address}(0.0.0.0),具有 32 字节的数据。"); - var Operation = new List>(); - Operation.AddRange(Enumerable.Range(0, MaxTry).Select(avalue => ICMPClient.SendPingAsync(IPAddress.Parse(Address), MaxTimeout, new byte[32]))); - var ReplyResult = await Task.WhenAll(Operation); - - var Result = new PingResult() - { - Result = ReplyResult.ToList() - }; - Logger.Log($"[Network] {Address}(0.0.0.0)的 Ping 统计结果:\n\n已发送:{Result.TotalSend} 已接收:{Result.Success} 丢包率:{Math.Round((double)(Result.Success / Result.TotalSend), 0)}% \n\n最长:{Result.Slowest}ms 最短:{Result.Fastest} 平均:{Result.Average}ms"); - return Result; - } - public static bool CheckIPv6Support() - { - foreach(NetworkInterface adapter in NetworkInterface.GetAllNetworkInterfaces()) - { - if (adapter.OperationalStatus != OperationalStatus.Up) continue; - foreach (UnicastIPAddressInformation IP in adapter.GetIPProperties().UnicastAddresses) - { - if (IP.Address.AddressFamily is AddressFamily.InterNetworkV6) - { - SupportIPv6 = true; - break; - } - } - } - TestIPv6: - PingResult Result = Ping("[2400:3200:baba::1]").GetAwaiter().GetResult(); - Finally: - return SupportIPv6; - } - } - /// - /// 支持 DNS Over HTTPS 的域名解析查询类 - /// - public class DNSResolver { - private static readonly Dictionary DnsQueryCache = new(); - public class DNSResolveResult - { - public List? Address; - public List? IPAddress; - } - private static Dictionary DnsQueryResult = new(); - public static string? DOHServerAddress; - /// - /// - /// - /// - /// - /// - public async static Task GetResolveResultUsingLocalDns(string RequestUrl, int ResolveTimeout = 500) - { - try - { - // DNS 查询不像网络请求,过长的查询时间会让下载速度缓慢(尤其是从很多不同服务器下载文件,这种情况下 DNS 查询导致的缓慢会更加明显) - using (CancellationTokenSource CTS = new(ResolveTimeout)) - { - IPHostEntry ResolveResult = await Dns.GetHostEntryAsync(RequestUrl); - return new DNSResolveResult() - { - IPAddress = ResolveResult.AddressList.Select(ip => ip.ToString()).ToList(), - Address = ResolveResult.Aliases.ToList()! - }; - } - } - catch (TaskCanceledException ex) - { - throw new TimeoutException("操作超时", ex); - } - catch (SocketException ex) - { - throw new TaskCanceledException($"未能解析此远程名称 {RequestUrl}", ex); - } - catch (ArgumentException) - { - throw new ArgumentException("此 URI 格式不正确或为空字符串"); - } - } - public async static Task GetResolveResultUsingDOH(string RequestUrl, int ResolveTimeout = 500) - { - try - { - // DNS 查询不像网络请求,过长的查询时间会让下载速度缓慢(尤其是从很多不同服务器下载文件,这种情况下 DNS 查询导致的缓慢会更加明显) - using (CancellationTokenSource CTS = new(ResolveTimeout)) - { - await HttpRequestBuilder - .Create(DOHServerAddress! + "?name=" + RequestUrl + "&type=A", HttpMethod.Get) - .SetSourceAddress(DNSResolver.GetResolveResultUsingLocalDns(RequestUrl).Result.IPAddress![0]) - .SetConnectPort(443) - .SetHeader("Accept", "application/dns-json") - .UseProxy() - .Invoke(); - return null; - } - } - catch (TaskCanceledException ex) - { - throw new TimeoutException("操作超时", ex); - } - catch (SocketException ex) - { - throw new TaskCanceledException($"未能解析此远程名称 {RequestUrl}", ex); - } - catch (ArgumentException) - { - throw new ArgumentException("此 URI 格式不正确或为空字符串"); - } - } - } - public class HttpProxy : IWebProxy - { - public ICredentials? Credentials { get; set; } - private IWebProxy SystemProxy = HttpClient.DefaultProxy; - private WebProxy? CurrentProxy; - public object ProxyChangeLock = new object[1]; - public bool RequiredReloadProxyServer; - public bool UseSystemProxy = true; - public string? ProxyAddress; - public Uri? GetProxy(Uri RequestHost) - { - return GetProxy(RequestHost.AbsoluteUri)?.Address; - } - public WebProxy? GetProxy(string Host) - { - try - { - Logger.Log("Success!"); - WebProxy CurrentSystemProxy = new WebProxy(SystemProxy.GetProxy(new Uri(Host)), true); - if (CurrentProxy is not null && !RequiredReloadProxyServer) return CurrentProxy; - if (RequiredReloadProxyServer) - { - Logger.Log("[Network] 已要求刷新代理配置,开始重载代理配置"); - if (UseSystemProxy && ProxyAddress.IsNullOrWhiteSpaceF()) - { - Logger.Log("[Network] 当前代理配置:跟随系统代理设置"); - lock (ProxyChangeLock) - { - CurrentProxy = CurrentSystemProxy; - RequiredReloadProxyServer = false; - } - } - else if (!UseSystemProxy && !ProxyAddress.IsNullOrWhiteSpaceF()) - { - Logger.Log("[Network] 当前代理配置:自定义"); - lock (ProxyChangeLock) - { - CurrentProxy = new WebProxy(ProxyAddress, true); - RequiredReloadProxyServer = false; - } - } - else - { - // 直接返回 - Logger.Log("[Network] 当前代理配置:禁用"); - return null; - } - return CurrentProxy; - } - return null; - } - catch (UriFormatException) - { - Logger.Log("[Network] 检测到可能错误的配置,已清空自定义代理配置并使用默认值。"); - ProxyAddress = null; - return CurrentProxy; - } - } - public bool IsBypassed(Uri RequestUri) - { - return CurrentProxy?.GetProxy(RequestUri) == RequestUri; - } - } - - public class Download - { - internal static SemaphoreSlim MaxDownloadThread = new SemaphoreSlim(64); - - public static int MaxThread - { - set - { - if (value > 384) throw new ArgumentException("给定的线程数过多"); - var old = MaxDownloadThread; - MaxDownloadThread = new SemaphoreSlim(value); - old?.Dispose(); - } - get => MaxDownloadThread.CurrentCount; - } - - public static WebProxy? ProxyServer = null; - public static bool ParallelDownload = true; - - public class FileMetaData - { - public string? path { get; set; } - public string? hash { get; set; } - public string? algorithm { get; set; } - public long? size { get; set; } - public string? url { get; set; } - public long Start; - public bool ValidatePathContains(string path) - { - if (this.path.IsNullOrWhiteSpaceF() || path.IsNullOrWhiteSpaceF()) return false; - return Path.GetFullPath(this.path).StartsWith(path); - } - } - - public static readonly object FileListLock = new object[1]; - public static long TotalFileCount = 0; - public static long CompleteFileCount = 0; - - public static async Task NetCopyFileAsync(List DlTasks, CancellationToken? Token = null, int MaxThreadCount = 64) - { - var token = Token ?? CancellationToken.None; - SemaphoreSlim semaphore = MaxDownloadThread; - - lock (FileListLock) - { - TotalFileCount += DlTasks.Count; - } - - var tasks = DlTasks.Select(async t => - { - await semaphore.WaitAsync(token); - try - { - if (t.url is null || t.path is null) Logger.Crash(); - Logger.Log($"[Network] 直接下载文件:{t.url}"); - var data = await Network.NetworkRequest(t.url, Token: token); - await FileIO.WriteData(data, t.path, token); - lock (FileListLock) - { - CompleteFileCount++; - } - } - catch (OperationCanceledException ex) - { - Logger.Log(ex, "[Network] 下载已取消"); - } - catch (Exception ex) - { - Logger.Log(ex, "[Network] 下载文件失败"); - throw; - } - finally - { - semaphore.Release(); - } - }); - - await Task.WhenAll(tasks); - } - } +#pragma warning disable SYSLIB0014 +using SuikaiLauncher.Core.Base; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using SuikaiLauncher.Core.Override; +using System.Net.NetworkInformation; +using System.Runtime.CompilerServices; +using System.Reflection.Metadata; + +// 这里是一堆和网络有关系的工具,包括网络请求,代理,Ping,域名解析 +// 虽然看起来很乱然而我没空整理,就先这样吧(逃 + +namespace SuikaiLauncher.Core.Base +{ + public class SocketConnect + { + public required Socket Socket; + } + public class HttpRequestBuilder + { + public static readonly Dictionary ConnectionPool = new(); + public static readonly object ConnectionLock = new object(); + public class SslInfomation + { + public required object RequestMessage; + public required X509Certificate? Cert; + public required X509Chain? Chain; + public required SslPolicyErrors SslPolicyError; + } + private int timeout; + public required HttpRequestMessage Req; + public HttpResponseMessage? Resp; + private static readonly HttpProxy RequestProxyFactory = new(); + private string? ConnectAddress; + private int ConnectPort = 0; + private static bool CheckSsl = true; + public static readonly object HttpRequestBuilderPropertyChangeLock = new object(); + private static Func? CustomSslValidateCallback; + private static readonly SocketsHttpHandler SocketHandler = new() + { + UseProxy = true, + Proxy = RequestProxyFactory, + AllowAutoRedirect = false, + AutomaticDecompression = DecompressionMethods.All, + SslOptions = new SslClientAuthenticationOptions() + { + RemoteCertificateValidationCallback = (object httpRequestMessage, X509Certificate? cert, X509Chain? certChain, SslPolicyErrors sslPolicyError) => + { + if (CustomSslValidateCallback is not null) + { + return CustomSslValidateCallback(new SslInfomation() + { + RequestMessage = httpRequestMessage, + Cert = cert, + Chain = certChain, + SslPolicyError = sslPolicyError + }); + } + if (sslPolicyError != SslPolicyErrors.None && CheckSsl) + { + return false; + } + return true; + } + }, + // 长连接实现 + ConnectCallback = async (SocketsHttpConnectionContext context, CancellationToken token) => + { + var host = context.DnsEndPoint.Host; + var port = context.DnsEndPoint.Port; + SocketConnect? socketConnect = null; + lock (ConnectionLock) + { + if (ConnectionPool.TryGetValue($"{host}:{port}", out socketConnect)) + { + if (socketConnect.Socket != null && socketConnect.Socket.Connected) + { + // 检查 socket 是否可写 + try + { + bool poll = socketConnect.Socket.Poll(0, SelectMode.SelectWrite); + if (poll) + { + // 返回现有连接 + return new NetworkStream(socketConnect.Socket, ownsSocket: false); + } + } + catch + { + // 连接失效,移除 + ConnectionPool.Remove($"{host}:{port}"); + socketConnect = null; + } + } + else + { + ConnectionPool.Remove($"{host}:{port}"); + socketConnect = null; + } + } + } + + // 新建连接 + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) + { + NoDelay = true + }; + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + await socket.ConnectAsync(host, port, token); + + lock (ConnectionLock) + { + ConnectionPool[$"{host}:{port}"] = new SocketConnect() { Socket = socket }; + } + + return new NetworkStream(socket, ownsSocket: false); + } + }; + private static readonly HttpClient Client = new(SocketHandler); + private static bool FirstRequest; + public static string UserAgent = "SuikaiLauncher.Core/0.0.1"; + /// + /// 创建 HttpRequestBuilder 的新实例 + /// + /// 目标服务器地址 + /// HttpRequestBuilder + public static HttpRequestBuilder Create(string url, HttpMethod method) + { + return new HttpRequestBuilder() { Req = new HttpRequestMessage(method,url) }; + } + /// + /// 设置默认证书验证函数 + /// + /// 证书验证函数 + public static void SetCustomSslValidateCallback(Func CustomCallback) + { + lock (HttpRequestBuilderPropertyChangeLock) + { + CustomSslValidateCallback = CustomCallback; + } + } + /// + /// 设置请求的 IP 地址,用于覆盖默认查询结果 + /// + /// IP 地址 + /// HttpRequestBuilder + public HttpRequestBuilder SetSourceAddress(string Address) + { + return this; + } + /// + /// 设置请求端口,用于覆盖默认端口 + /// + /// 端口号 + /// HttpRequestBuilder + public HttpRequestBuilder SetConnectPort(int Port) + { + return this; + } + /// + /// 是否在默认验证逻辑中忽略 SSL 证书错误 + /// + /// HttpRequestBuilder + public HttpRequestBuilder IgnoreSslError() + { + if (!CheckSsl) return this; + lock (HttpRequestBuilderPropertyChangeLock) + { + CheckSsl = true; + } + return this; + } + /// + /// 设置请求使用的代理 + /// + /// 代理服务器 + /// HttpRequestBuilder + public HttpRequestBuilder UseProxy(string? Proxy = null) + { + if (!string.IsNullOrWhiteSpace(Proxy)) + { + lock (RequestProxyFactory.ProxyChangeLock) + { + RequestProxyFactory.ProxyAddress = Proxy; + RequestProxyFactory.RequiredReloadProxyServer = true; + } + } + return this; + + } + public HttpRequestBuilder WithRequestData(MemoryStream Data) + { + this.Req.Content = new ByteArrayContent(Data.ToArray()); + return this; + } + /// + /// 设置标头 + /// + /// 标头 + /// 值 + /// HttpRequestBuilder + public HttpRequestBuilder SetHeader(string Name, string Value) + { + if (Name.ContainsF("Content") && Req.Content is not null) Req.Content.Headers.Add(Name, Value); + Req.Headers.Add(Name, Value); + return this; + } + /// + /// 发送网络请求并自动处理重定向 + /// + /// + public async Task Invoke() + { + await (await this.SendRequest()).ResolveHttpRedirect(); + return this; + } + /// + /// 获取响应 + /// + /// HttpResponseMessage + public HttpResponseMessage GetResponse(bool EnsureSuccessStatusCode = true) + { + if (this.Resp is null) throw new InvalidOperationException("尝试在服务器响应之前获取响应对象"); + if (EnsureSuccessStatusCode) this.Resp.EnsureSuccessStatusCode(); + return this.Resp; + } + /// + /// 发送单次网络请求,不处理重定向 + /// + /// HttpRequestBuilder + public async Task SendRequest() + { + using (CancellationTokenSource CTS = new(this.timeout)) + { + this.Resp = await Client.SendAsync(this.Req,HttpCompletionOption.ResponseHeadersRead,CTS.Token); + return this; + } + } + /// + /// 处理网络请求的重定向,直到响应码不处于 300~399 范围内(不包括 304) + /// + /// + public async Task ResolveHttpRedirect() + { + + if (this.Resp!.StatusCode is > (HttpStatusCode)300 and < (HttpStatusCode)400 && this.Resp.StatusCode != (HttpStatusCode)304) + { + HttpRequestMessage RedirectReq = new(); + foreach(var Header in this.Req.Headers) + { + Req.Headers.Add(Header.Key, Header.Value); + } + if (this.Req.Content is not null) + { + MemoryStream ReqStream = new(); + await (await this.Req.Content.ReadAsStreamAsync()).CopyToAsync(ReqStream); + if (ReqStream is null) goto SkipContent; + RedirectReq.Content = new ByteArrayContent(ReqStream.ToArray()); + ReqStream.Dispose(); + foreach (var Header in this.Req.Content.Headers) + { + RedirectReq.Content.Headers.Add(Header.Key, Header.Value); + } + } + SkipContent: + RedirectReq.RequestUri = this.Req.Headers.GetValues("location").First().ToURI(); + this.Req.Dispose(); + this.Resp.Dispose(); + this.Req = RedirectReq; + await this.Invoke(); + } + return this; + } + } + public class Localhost { + public class PingResult + { + public object? CustomResult; + public List Result { get { throw new InvalidOperationException("无法读取此属性"); } set + { + TotalSend = value.Count; + value.Select(k => + { + switch (k.Status) + { + case IPStatus.Success: + if (Fastest == -1) Fastest = k.RoundtripTime; + else if (Slowest == -1) Slowest = k.RoundtripTime; + else if (Slowest < k.RoundtripTime) Slowest = k.RoundtripTime; + else if (Fastest > k.RoundtripTime) Fastest = k.RoundtripTime; + TotalUsage += k.RoundtripTime; + return null; + case IPStatus.TimedOut: + Failed++; + Logger.Log($"[Network] Ping {k.Address} 失败:请求超时"); + return null; + case IPStatus.DestinationHostUnreachable: + Failed++; + Logger.Log($"[Network] Ping {k.Address} 失败:此远程地址不可达"); + return null; + case IPStatus.DestinationNetworkUnreachable: + Failed++; + Logger.Log($"[Network] Ping {k.Address} 失败:此远程地址所处的网络不可达"); + return null; + case IPStatus.DestinationPortUnreachable: + Failed++; + Logger.Log($"[Network] Ping {k.Address} 失败:远程地址所指定的端口不可达"); + return null; + case IPStatus.NoResources: + Failed++; + Logger.Log($"[Network] Ping {k.Address} 失败:网络资源不足"); + return null; + + } + if (k.Status == IPStatus.Success) Success++; + else + { + if (k.Status == IPStatus.TimedOut) + Failed++; + return null; + } + return null; + }); + Average = TotalUsage / TotalSend; + } + } + public long Fastest = -1; + public long Slowest = -1; + public long Average = -1; + public long TotalUsage = -1; + public long Success = 0; + public long Failed = 0; + public int TotalSend = 0; + } + private static bool SupportIPv6; + private static Ping ICMPClient = new(); + public class PingInfomation + { + public required string Address; + public int port = 25565; + public int MaxTry = 1; + public int Timeout = 2500; + } + /// + /// 并行发送多个 ICMP/TCP 包来测试本地网络到目标服务器的连通性 + /// + /// 目标服务器地址 + /// 端口号(仅 Tcping 模式下可用) + /// 是否使用 Tcping 模式 + /// 允许的最大超时 + /// 发送的 ICMP/TCP 包数量(并行发送过多包可能导致额外开销) + /// 自定义处理类 + public async static Task Ping(string Address, int port = 80, bool UseTcping = false, int MaxTimeout = 2500, int MaxTry = 4,Func? CustomResolver = null) + { + if (CustomResolver is not null) return new PingResult() { CustomResult = CustomResolver(new PingInfomation() { Address = Address, port = port, Timeout = MaxTimeout, MaxTry = MaxTry }) }; + Logger.Log($"[Network] 开始 Ping {Address}(0.0.0.0),具有 32 字节的数据。"); + var Operation = new List>(); + Operation.AddRange(Enumerable.Range(0, MaxTry).Select(avalue => ICMPClient.SendPingAsync(IPAddress.Parse(Address), MaxTimeout, new byte[32]))); + var ReplyResult = await Task.WhenAll(Operation); + + var Result = new PingResult() + { + Result = ReplyResult.ToList() + }; + Logger.Log($"[Network] {Address}(0.0.0.0)的 Ping 统计结果:\n\n已发送:{Result.TotalSend} 已接收:{Result.Success} 丢包率:{Math.Round((double)(Result.Success / Result.TotalSend), 0)}% \n\n最长:{Result.Slowest}ms 最短:{Result.Fastest} 平均:{Result.Average}ms"); + return Result; + } + public static bool CheckIPv6Support() + { + foreach(NetworkInterface adapter in NetworkInterface.GetAllNetworkInterfaces()) + { + if (adapter.OperationalStatus != OperationalStatus.Up) continue; + foreach (UnicastIPAddressInformation IP in adapter.GetIPProperties().UnicastAddresses) + { + if (IP.Address.AddressFamily is AddressFamily.InterNetworkV6) + { + SupportIPv6 = true; + break; + } + } + } + TestIPv6: + PingResult Result = Ping("[2400:3200:baba::1]").GetAwaiter().GetResult(); + Finally: + return SupportIPv6; + } + } + /// + /// 支持 DNS Over HTTPS 的域名解析查询类 + /// + public class DNSResolver { + private static readonly Dictionary DnsQueryCache = new(); + public class DNSResolveResult + { + public List? Address; + public List? IPAddress; + } + private static Dictionary DnsQueryResult = new(); + public static string? DOHServerAddress; + /// + /// + /// + /// + /// + /// + public async static Task GetResolveResultUsingLocalDns(string RequestUrl, int ResolveTimeout = 500) + { + try + { + // DNS 查询不像网络请求,过长的查询时间会让下载速度缓慢(尤其是从很多不同服务器下载文件,这种情况下 DNS 查询导致的缓慢会更加明显) + using (CancellationTokenSource CTS = new(ResolveTimeout)) + { + IPHostEntry ResolveResult = await Dns.GetHostEntryAsync(RequestUrl); + return new DNSResolveResult() + { + IPAddress = ResolveResult.AddressList.Select(ip => ip.ToString()).ToList(), + Address = ResolveResult.Aliases.ToList()! + }; + } + } + catch (TaskCanceledException ex) + { + throw new TimeoutException("操作超时", ex); + } + catch (SocketException ex) + { + throw new TaskCanceledException($"未能解析此远程名称 {RequestUrl}", ex); + } + catch (ArgumentException) + { + throw new ArgumentException("此 URI 格式不正确或为空字符串"); + } + } + public async static Task GetResolveResultUsingDOH(string RequestUrl, int ResolveTimeout = 500) + { + try + { + // DNS 查询不像网络请求,过长的查询时间会让下载速度缓慢(尤其是从很多不同服务器下载文件,这种情况下 DNS 查询导致的缓慢会更加明显) + using (CancellationTokenSource CTS = new(ResolveTimeout)) + { + await HttpRequestBuilder + .Create(DOHServerAddress! + "?name=" + RequestUrl + "&type=A", HttpMethod.Get) + .SetSourceAddress(DNSResolver.GetResolveResultUsingLocalDns(RequestUrl).Result.IPAddress![0]) + .SetConnectPort(443) + .SetHeader("Accept", "application/dns-json") + .UseProxy() + .Invoke(); + return null; + } + } + catch (TaskCanceledException ex) + { + throw new TimeoutException("操作超时", ex); + } + catch (SocketException ex) + { + throw new TaskCanceledException($"未能解析此远程名称 {RequestUrl}", ex); + } + catch (ArgumentException) + { + throw new ArgumentException("此 URI 格式不正确或为空字符串"); + } + } + } + public class HttpProxy : IWebProxy + { + public ICredentials? Credentials { get; set; } + private IWebProxy SystemProxy = HttpClient.DefaultProxy; + private WebProxy? CurrentProxy; + public object ProxyChangeLock = new object[1]; + public bool RequiredReloadProxyServer; + public bool UseSystemProxy = true; + public string? ProxyAddress; + public Uri? GetProxy(Uri RequestHost) + { + return GetProxy(RequestHost.AbsoluteUri)?.Address; + } + public WebProxy? GetProxy(string Host) + { + try + { + Logger.Log("Success!"); + WebProxy CurrentSystemProxy = new WebProxy(SystemProxy.GetProxy(new Uri(Host)), true); + if (CurrentProxy is not null && !RequiredReloadProxyServer) return CurrentProxy; + if (RequiredReloadProxyServer) + { + Logger.Log("[Network] 已要求刷新代理配置,开始重载代理配置"); + if (UseSystemProxy && ProxyAddress.IsNullOrWhiteSpaceF()) + { + Logger.Log("[Network] 当前代理配置:跟随系统代理设置"); + lock (ProxyChangeLock) + { + CurrentProxy = CurrentSystemProxy; + RequiredReloadProxyServer = false; + } + } + else if (!UseSystemProxy && !ProxyAddress.IsNullOrWhiteSpaceF()) + { + Logger.Log("[Network] 当前代理配置:自定义"); + lock (ProxyChangeLock) + { + CurrentProxy = new WebProxy(ProxyAddress, true); + RequiredReloadProxyServer = false; + } + } + else + { + // 直接返回 + Logger.Log("[Network] 当前代理配置:禁用"); + return null; + } + return CurrentProxy; + } + return null; + } + catch (UriFormatException) + { + Logger.Log("[Network] 检测到可能错误的配置,已清空自定义代理配置并使用默认值。"); + ProxyAddress = null; + return CurrentProxy; + } + } + public bool IsBypassed(Uri RequestUri) + { + return CurrentProxy?.GetProxy(RequestUri) == RequestUri; + } + } + + public class Download + { + internal static SemaphoreSlim MaxDownloadThread = new SemaphoreSlim(64); + + public static int MaxThread + { + set + { + if (value > 384) throw new ArgumentException("给定的线程数过多"); + var old = MaxDownloadThread; + MaxDownloadThread = new SemaphoreSlim(value); + old?.Dispose(); + } + get => MaxDownloadThread.CurrentCount; + } + + public static WebProxy? ProxyServer = null; + public static bool ParallelDownload = true; + + public class FileMetaData + { + public string? path { get; set; } + public string? hash { get; set; } + public string? algorithm { get; set; } + public long? size { get; set; } + public string? url { get; set; } + public long Start; + public bool ValidatePathContains(string path) + { + if (this.path.IsNullOrWhiteSpaceF() || path.IsNullOrWhiteSpaceF()) return false; + return Path.GetFullPath(this.path).StartsWith(path); + } + } + + public static readonly object FileListLock = new object[1]; + public static long TotalFileCount = 0; + public static long CompleteFileCount = 0; + + public static async Task NetCopyFileAsync(List DlTasks, CancellationToken? Token = null, int MaxThreadCount = 64) + { + var token = Token ?? CancellationToken.None; + SemaphoreSlim semaphore = MaxDownloadThread; + + lock (FileListLock) + { + TotalFileCount += DlTasks.Count; + } + + var tasks = DlTasks.Select(async t => + { + await semaphore.WaitAsync(token); + try + { + if (t.url is null || t.path is null) Logger.Crash(); + Logger.Log($"[Network] 直接下载文件:{t.url}"); + var data = await Network.NetworkRequest(t.url, Token: token); + await FileIO.WriteData(data, t.path, token); + lock (FileListLock) + { + CompleteFileCount++; + } + } + catch (OperationCanceledException ex) + { + Logger.Log(ex, "[Network] 下载已取消"); + } + catch (Exception ex) + { + Logger.Log(ex, "[Network] 下载文件失败"); + throw; + } + finally + { + semaphore.Release(); + } + }); + + await Task.WhenAll(tasks); + } + } } \ No newline at end of file diff --git a/SuikaiLauncher.Core.Base/Modules/PEReader.cs b/SuikaiLauncher.Core.Base/Modules/PEReader.cs index 0eeb0fc..c0a5e64 100644 --- a/SuikaiLauncher.Core.Base/Modules/PEReader.cs +++ b/SuikaiLauncher.Core.Base/Modules/PEReader.cs @@ -1,39 +1,39 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.PortableExecutable; -using System.Text; -using System.Threading.Tasks; - - -namespace SuikaiLauncher.Core.Base -{ - // 通用 PE 头读取器 - public class PEReader - { - private PEHeaders? Headers; - private FileStream? FileReadStream; - public void OpenFile(string FilePath) - { - try - { - if (!File.Exists(FilePath)) throw new FileNotFoundException("未找到指定文件"); - using (this.FileReadStream = new(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - Headers = new PEHeaders(this.FileReadStream); - } - }catch (BadImageFormatException ex) - { - this.OnFailed(ex); - }catch (InvalidOperationException ex) - { - this.OnFailed(ex); - } - } - public void OnFailed(Exception ex) - { - Logger.Log(ex, "[Runtime] 读取文件 PE 头失败。"); - throw new TaskCanceledException("此 PE 文件的格式无效"); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Threading.Tasks; + + +namespace SuikaiLauncher.Core.Base +{ + // 通用 PE 头读取器 + public class PEReader + { + private PEHeaders? Headers; + private FileStream? FileReadStream; + public void OpenFile(string FilePath) + { + try + { + if (!File.Exists(FilePath)) throw new FileNotFoundException("未找到指定文件"); + using (this.FileReadStream = new(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + Headers = new PEHeaders(this.FileReadStream); + } + }catch (BadImageFormatException ex) + { + this.OnFailed(ex); + }catch (InvalidOperationException ex) + { + this.OnFailed(ex); + } + } + public void OnFailed(Exception ex) + { + Logger.Log(ex, "[Runtime] 读取文件 PE 头失败。"); + throw new TaskCanceledException("此 PE 文件的格式无效"); + } + } +} diff --git a/SuikaiLauncher.Core.Base/Modules/Process.cs b/SuikaiLauncher.Core.Base/Modules/Process.cs index 8123f48..98673d5 100644 --- a/SuikaiLauncher.Core.Base/Modules/Process.cs +++ b/SuikaiLauncher.Core.Base/Modules/Process.cs @@ -1,100 +1,100 @@ -using SuikaiLauncher.Core.Override; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using static System.Runtime.InteropServices.JavaScript.JSType; - -namespace SuikaiLauncher.Core.Base -{ - public class ProcessBuilder - { - private Action? CrashCallback; - private Action? ExitCallback; - private CancellationTokenSource CTS = new(); - private List Arguments = []; - private FileStream OutputStream = new(Environments.ApplicationDataPath + "SuikaiLauncher/Core/ProcessBuilder/Logs/" + DateTime.Now.ToString("yyyy-MM-dd") + ".log", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, 8192, true); - private Process process = new(); - private MemoryStream buffer = new(); - private readonly object StreamLock = new object[1]; - private bool HasStart = false; - public static ProcessBuilder Create() - { - return new ProcessBuilder(); - } - public ProcessBuilder Executable(string exec) - { - this.process.StartInfo.FileName = exec; - return this; - } - public ProcessBuilder RequireEncoding(Encoding? encoding = null) - { - this.process.StartInfo.StandardErrorEncoding = this.process.StartInfo.StandardOutputEncoding = encoding ?? Encoding.UTF8; - return this; - } - public ProcessBuilder WithArgument(string Argument) - { - this.process.StartInfo.ArgumentList.Add(Argument); - return this; - } - public ProcessBuilder RunAsAdmin() - { - this.process.StartInfo.Verb = "runas"; - return this; - } - public ProcessBuilder UseShell() - { - this.process.StartInfo.UseShellExecute = true; - return this; - } - public ProcessBuilder Invoke() - { - if (this.HasStart) throw new InvalidOperationException("不可启动已经启动的 ProcessBuilder 对象"); - this.HasStart = true; - process.OutputDataReceived += (sender, e) => - { - if (e.Data is null) return; - this.buffer.Write(e.Data.GetBytes()); - }; - process.Start(); - Task.Run(async () => - { - while (!process.HasExited) - { - await Task.Delay(TimeSpan.FromSeconds(10)); - lock (StreamLock) - { - - this.buffer.CopyToAsync(this.OutputStream); - - } - await this.OutputStream.FlushAsync(); - // 清空 Buffer 防止内存爆炸 - this.buffer.SetLength(0); - if (CTS.Token.IsCancellationRequested) throw new TaskCanceledException("进程监控已退出"); - } - TaskCanceledException TaskEX = new(); - TaskEX.Data["RawOutput"] = null; - if (process.ExitCode != 0 && this.CrashCallback is not null) this.CrashCallback(TaskEX); - else if(this.ExitCallback is not null) this.ExitCallback(); - }); - return this; - } - public void SetCustomProcessCrashCallback(Action Callback) - { - this.CrashCallback = Callback; - } - public void SetCustomProcessExitCallback(Action Callback) - { - this.ExitCallback = Callback; - } - public async Task StopProcess() - { - if (this.process.HasExited) return; - if (this.process.MainWindowTitle.IsNullOrWhiteSpaceF()) this.process.Kill(); - this.process.CloseMainWindow(); - this.process.WaitForExit(5000); - if (!this.process.HasExited) this.process.Kill(); - } - } -} +using SuikaiLauncher.Core.Override; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace SuikaiLauncher.Core.Base +{ + public class ProcessBuilder + { + private Action? CrashCallback; + private Action? ExitCallback; + private CancellationTokenSource CTS = new(); + private List Arguments = []; + private FileStream OutputStream = new(Environments.ApplicationDataPath + "SuikaiLauncher/Core/ProcessBuilder/Logs/" + DateTime.Now.ToString("yyyy-MM-dd") + ".log", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, 8192, true); + private Process process = new(); + private MemoryStream buffer = new(); + private readonly object StreamLock = new object[1]; + private bool HasStart = false; + public static ProcessBuilder Create() + { + return new ProcessBuilder(); + } + public ProcessBuilder Executable(string exec) + { + this.process.StartInfo.FileName = exec; + return this; + } + public ProcessBuilder RequireEncoding(Encoding? encoding = null) + { + this.process.StartInfo.StandardErrorEncoding = this.process.StartInfo.StandardOutputEncoding = encoding ?? Encoding.UTF8; + return this; + } + public ProcessBuilder WithArgument(string Argument) + { + this.process.StartInfo.ArgumentList.Add(Argument); + return this; + } + public ProcessBuilder RunAsAdmin() + { + this.process.StartInfo.Verb = "runas"; + return this; + } + public ProcessBuilder UseShell() + { + this.process.StartInfo.UseShellExecute = true; + return this; + } + public ProcessBuilder Invoke() + { + if (this.HasStart) throw new InvalidOperationException("不可启动已经启动的 ProcessBuilder 对象"); + this.HasStart = true; + process.OutputDataReceived += (sender, e) => + { + if (e.Data is null) return; + this.buffer.Write(e.Data.GetBytes()); + }; + process.Start(); + Task.Run(async () => + { + while (!process.HasExited) + { + await Task.Delay(TimeSpan.FromSeconds(10)); + lock (StreamLock) + { + + this.buffer.CopyToAsync(this.OutputStream); + + } + await this.OutputStream.FlushAsync(); + // 清空 Buffer 防止内存爆炸 + this.buffer.SetLength(0); + if (CTS.Token.IsCancellationRequested) throw new TaskCanceledException("进程监控已退出"); + } + TaskCanceledException TaskEX = new(); + TaskEX.Data["RawOutput"] = null; + if (process.ExitCode != 0 && this.CrashCallback is not null) this.CrashCallback(TaskEX); + else if(this.ExitCallback is not null) this.ExitCallback(); + }); + return this; + } + public void SetCustomProcessCrashCallback(Action Callback) + { + this.CrashCallback = Callback; + } + public void SetCustomProcessExitCallback(Action Callback) + { + this.ExitCallback = Callback; + } + public async Task StopProcess() + { + if (this.process.HasExited) return; + if (this.process.MainWindowTitle.IsNullOrWhiteSpaceF()) this.process.Kill(); + this.process.CloseMainWindow(); + this.process.WaitForExit(5000); + if (!this.process.HasExited) this.process.Kill(); + } + } +} diff --git a/SuikaiLauncher.Core.Base/Modules/Setup.cs b/SuikaiLauncher.Core.Base/Modules/Setup.cs index 9fba4af..83a0cc8 100644 --- a/SuikaiLauncher.Core.Base/Modules/Setup.cs +++ b/SuikaiLauncher.Core.Base/Modules/Setup.cs @@ -74,403 +74,8 @@ internal class ModData - public class Config + public static class Setup { - private static XDocument XmlConfig; - private static XDocument XmlBackupConfig; - private static object SetupChangeLock = new object[1]; - private static bool SetupChanged; - private static Thread SetupWatcher; - - public static void InitConfig() - { - if (File.Exists(Environments.ConfigPath)) - { - throw new FieldAccessException($"目标文件已存在: {Environments.ConfigPath}"); - } - - string? DirectoryPath = Path.GetDirectoryName(Environments.ConfigPath); - if (DirectoryPath.IsNullOrWhiteSpaceF() && !Directory.Exists(DirectoryPath)) - { - Directory.CreateDirectory(DirectoryPath); - } - - XDocument config = new XDocument(new XElement("Setup")); - config.Root?.Add(new XElement("System")); - config.Root?.Add(new XElement("Account")); - config.Root?.Add(new XElement("Versions")); - - try - { - config.Save(Environments.ConfigPath); - config.Save(Environments.ConfigPath.Replace(".xml", ".xml.backup")); - } - catch (Exception ex) - { - throw new Exception($"初始化配置文件时发生错误:",ex); - } - } - public static void ReleaseSetup() - { - lock (SetupChangeLock) - { - SetupWatcher.Interrupt(); - } - } - public static void LoadConfig(bool ForceReload = false) - { - // 必须包括全部代码,以避免同一时间有多个线程尝试加载和修改值导致线程冲突 - lock (SetupChangeLock) - { - // 确保单例 - if (!ForceReload && XmlConfig is not null) return; - if (File.Exists(Environments.ConfigPath)) - { - try - { - XmlConfig = XDocument.Load(Environments.ConfigPath); - SetupWatcher = new Thread(StartSetupWatcher); - SetupWatcher.Start(); - } - catch (Exception ex) - { - XmlConfig = null; - throw new Exception($"加载配置文件时发生错误", ex); - } - } - else - { - XmlConfig = null; - throw new FileNotFoundException($"配置文件不存在: {Environments.ConfigPath}"); - } - - if (File.Exists(Environments.ConfigPath.Replace(".xml", ".xml.backup"))) - { - try - { - XmlBackupConfig = XDocument.Load(Environments.ConfigPath.Replace(".xml", ".xml.backup")); - } - catch (Exception ex) - { - XmlBackupConfig = null; - throw new Exception($"加载备份配置文件时发生错误:", ex); - } - } - else - { - XmlBackupConfig = null; - } - } - } - - private static void StartSetupWatcher() - { - try - { - while (true) - { - // 减轻同步锁造成的性能影响 - Thread.Sleep(10000); - if (SetupChanged) - { - lock (SetupChangeLock) - { - XmlConfig.Save(Environments.ConfigPath); - } - } - } - }catch (ThreadInterruptedException) - { - if (SetupChanged) - { - lock (SetupChangeLock) - { - XmlConfig.Save(Environments.ConfigPath); - } - } - } - } - - /// - /// 设置命名空间内某个设置项的值 - /// - /// 设置项名称 - /// 值 - /// 主命名空间 - /// 子命名空间 - public static void Set(string key, string value, string XmlNameSpace = "System", string SubNameSpace = "") - { - if (XmlConfig is null) LoadConfig(); - if (XmlConfig?.Root == null) - { - throw new InvalidOperationException("配置文件未加载或根节点不存在。"); - } - - if (key.IsNullOrWhiteSpaceF()) - { - throw new ArgumentNullException(nameof(key), "设置项名称不能为空。"); - } - - if (XmlNameSpace.IsNullOrWhiteSpaceF()) - { - throw new ArgumentNullException("给定关键字不存在于设置项中", nameof(XmlNameSpace)); - } - - XElement namespaceElement = XmlConfig.Root.Element(XmlNameSpace); - if (namespaceElement == null) - { - throw new ArgumentException("给定关键字不存在于设置项中", nameof(XmlNameSpace)); - } - - if (SubNameSpace.IsNullOrWhiteSpaceF()) - { - XElement keyElement = namespaceElement.Element(key); - if (keyElement == null) - { - lock (SetupChangeLock) - { - namespaceElement.Add(new XElement(key, value)); - } - } - else - { - lock (SetupChangeLock) - { - keyElement.Value = value; - } - } - SaveConfig(); - return; - } - - XElement subNamespaceElement = namespaceElement.Element(SubNameSpace); - if (subNamespaceElement == null) - { - lock (SetupChangeLock) - { - namespaceElement.Add(new XElement(SubNameSpace, new XElement(key, value))); - } - } - else - { - XElement keyElement = subNamespaceElement.Element(key); - if (keyElement == null) - { - lock (SetupChangeLock) - { - subNamespaceElement.Add(new XElement(key, value)); - } - } - else - { - lock (SetupChangeLock) - { - keyElement.Value = value; - } - } - } - SaveConfig(); - } - - /// - /// 获取命名空间内某个设置项的值 - /// - /// 设置项名称 - /// 主命名空间 - /// 子命名空间 - /// object - public static object? Get(string key, string XmlNameSpace = "System", string SubNameSpace = "") - { - if (XmlConfig is null) LoadConfig(); - if (XmlConfig?.Root == null) - { - throw new InvalidOperationException("配置文件未加载或根节点不存在。"); - } - - if (key.IsNullOrWhiteSpaceF()) - { - throw new ArgumentNullException(nameof(key), "设置项名称不能为空。"); - } - - if (XmlNameSpace.IsNullOrWhiteSpaceF()) - { - throw new ArgumentNullException("给定关键字不存在于设置项中", nameof(XmlNameSpace)); - } - - XElement namespaceElement = XmlConfig.Root.Element(XmlNameSpace); - if (namespaceElement == null) - { - throw new ArgumentException("给定关键字不存在于设置项中", nameof(XmlNameSpace)); - } - - if (SubNameSpace.IsNullOrWhiteSpaceF()) - { - return namespaceElement.Element(key)?.Value; - } - - XElement subNamespaceElement = namespaceElement.Element(SubNameSpace); - if (subNamespaceElement == null) - { - throw new ArgumentException("给定关键字不存在于设置项中", nameof(SubNameSpace)); - } - - return subNamespaceElement.Element(key)?.Value; - } - - /// - /// 重置某个设置项 - /// - /// 设置项名称 - /// 主命名空间 - /// 子命名空间 - public static void Reset(string key, string XmlNameSpace = "System", string SubNameSpace = "") - { - if (XmlConfig is null) LoadConfig(); - if (XmlConfig?.Root == null) - { - throw new InvalidOperationException("配置文件未加载或根节点不存在。"); - } - - if (key.IsNullOrWhiteSpaceF()) - { - throw new ArgumentNullException(nameof(key), "设置项名称不能为空。"); - } - - if (XmlNameSpace.IsNullOrWhiteSpaceF()) - { - throw new ArgumentNullException("给定关键字不存在于设置项中", nameof(XmlNameSpace)); - } - - XElement namespaceElement = XmlConfig.Root.Element(XmlNameSpace); - if (namespaceElement == null) - { - throw new ArgumentException("给定关键字不存在于设置项中", nameof(XmlNameSpace)); - } - - if (SubNameSpace.IsNullOrWhiteSpaceF()) - { - if (namespaceElement.Element(key) == null) - { - throw new ArgumentException("给定关键字不存在于设置项中", nameof(key)); - } - namespaceElement.Element(key)?.Remove(); - SaveConfig(); - return; - } - - XElement subNamespaceElement = namespaceElement.Element(SubNameSpace); - if (subNamespaceElement == null) - { - throw new ArgumentException("给定关键字不存在于设置项中", nameof(SubNameSpace)); - } - if (subNamespaceElement.Element(key) == null) - { - throw new ArgumentException("给定关键字不存在于设置项中", nameof(key)); - } - subNamespaceElement.Element(key)?.Remove(); - SaveConfig(); - } - - - /// - /// 清空设置项 - /// - /// 设置项名称 - /// 设置项所处的命名空间 - /// 设置项所处的子命名空间 - public static void Clean(string key, string XmlNameSpace = "System", string SubNameSpace = "") - { - Set(key, string.Empty, XmlNameSpace, SubNameSpace); - } - - /// - /// 创建设置项命名空间 - /// - /// 主命名空间 - /// 子命名空间 - public static void CreateXmlNameSpace(string XmlNameSpace, string SubNameSpace = "") - { - if (XmlConfig is null) LoadConfig(); - if (XmlConfig?.Root == null) - { - throw new InvalidOperationException("配置文件未加载或根节点不存在。"); - } - - if (XmlNameSpace.IsNullOrWhiteSpaceF()) - { - throw new ArgumentNullException("给定关键字不存在于设置项中", nameof(XmlNameSpace)); - } - - XElement namespaceElement = XmlConfig.Root.Element(XmlNameSpace); - if (namespaceElement == null) - { - XmlConfig.Root.Add(new XElement(XmlNameSpace)); - namespaceElement = XmlConfig.Root.Element(XmlNameSpace); - } - - if (!SubNameSpace.IsNullOrWhiteSpaceF() && namespaceElement != null && namespaceElement.Element(SubNameSpace) == null) - { - namespaceElement.Add(new XElement(SubNameSpace)); - } - SaveConfig(); - } - - /// - /// 删除设置项命名空间 - /// - /// 主命名空间 - /// 子命名空间 - public static void DeleteXmlNameSpace(string XmlNameSpace, string SubNameSpace = "") - { - if (XmlConfig is null) LoadConfig(); - if (XmlConfig?.Root == null) - { - throw new InvalidOperationException("配置文件未加载或根节点不存在。"); - } - - if (XmlNameSpace.IsNullOrWhiteSpaceF()) - { - throw new ArgumentNullException("给定关键字不存在于设置项中", nameof(XmlNameSpace)); - } - - if (SubNameSpace.IsNullOrWhiteSpaceF()) - { - if (XmlConfig.Root.Element(XmlNameSpace) == null) - { - throw new ArgumentException("给定关键字不存在于设置项中", nameof(XmlNameSpace)); - } - XmlConfig.Root.Element(XmlNameSpace)?.Remove(); - } - else - { - XElement namespaceElement = XmlConfig.Root.Element(XmlNameSpace); - if (namespaceElement == null) - { - throw new ArgumentException("给定关键字不存在于设置项中", nameof(XmlNameSpace)); - } - if (namespaceElement.Element(SubNameSpace) == null) - { - throw new ArgumentException("给定关键字不存在于设置项中", nameof(SubNameSpace)); - } - namespaceElement.Element(SubNameSpace)?.Remove(); - } - SaveConfig(); - } - - private static void SaveConfig() - { - try - { - if (SetupChanged) return; - lock (SetupChangeLock) - { - SetupChanged = true; - } - } - catch (Exception ex) - { - throw new Exception($"保存配置文件时发生错误",ex); - } - } + } } diff --git a/SuikaiLauncher.Core.Base/SuikaiLauncher.Core.Base.csproj b/SuikaiLauncher.Core.Base/SuikaiLauncher.Core.Base.csproj index 589b982..6901f2e 100644 --- a/SuikaiLauncher.Core.Base/SuikaiLauncher.Core.Base.csproj +++ b/SuikaiLauncher.Core.Base/SuikaiLauncher.Core.Base.csproj @@ -4,6 +4,8 @@ net8.0 enable enable + SuikaiProject + SuikaiLauncher.Core diff --git a/SuikaiLauncher.Core.Minecraft/Modules/Launch.cs b/SuikaiLauncher.Core.Minecraft/Modules/Launch.cs new file mode 100644 index 0000000..ea73fc8 --- /dev/null +++ b/SuikaiLauncher.Core.Minecraft/Modules/Launch.cs @@ -0,0 +1,69 @@ +using System.Text; +using SuikaiLauncher.Core.Account; +using SuikaiLauncher.Core.Base; + +namespace SuikaiLauncher.Core.Minecraft.Modules; + +public class Launch +{ + public required McVersion? McVersion { get; set; } + + private Profile LaunchProfile = ProfileManager.CurrentProfile; + + public async Task LaunchGame() + { + Logger.Log("[Minecraft] 获取启动档案成功"); + Logger.Log("[Minecraft] 启动预检查阶段开始"); + this.PreCheck(); + Logger.Log("[Minecraft] 环境预检通过"); + Task[] LaunchTask = + [ + this.CheckFile(), + this.CheckRuntime() + ]; + await Task.WhenAll(LaunchTask); + Logger.Log("[Minecraft] ====== 启动信息 ======"); + Logger.Log($"[Minecraft] 游戏版本:{McVersion.Version}"); + Logger.Log($"[Minecraft] 游戏用户名:{LaunchProfile.Name}"); + Logger.Log($"[Minecraft] "); + ProcessBuilder PBuilder = ProcessBuilder + .Create() + .Executable("java.exe") + .RequireEncoding(Encoding.UTF8) + .WithArgument(await this.GetJvmArgument()) + + + + } + /// + /// 检查运行环境是否正确 + /// + public async Task CheckRuntime() + { + + } + /// + /// 预检运行环境和游戏信息 + /// + public void PreCheck() + { + + } + /// + /// 检查文件完整性 + /// + private async Task CheckFile() + { + + } + + private async Task GetJvmArgument() + { + return ""; + } + + private async Task CrashCallback(TaskCanceledException ex) + { + Logger.Log("[Minecraft] Minecraft 已崩溃"); + } +} \ No newline at end of file diff --git a/SuikaiLauncher.Core.Minecraft/SuikaiLauncher.Core.Minecraft.csproj b/SuikaiLauncher.Core.Minecraft/SuikaiLauncher.Core.Minecraft.csproj index ed07bda..8c1ba44 100644 --- a/SuikaiLauncher.Core.Minecraft/SuikaiLauncher.Core.Minecraft.csproj +++ b/SuikaiLauncher.Core.Minecraft/SuikaiLauncher.Core.Minecraft.csproj @@ -7,6 +7,7 @@ +