初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32;
|
||||
using PCL.Core.App.IoC;
|
||||
using PCL.Core.IO.Net;
|
||||
using PCL.Core.IO.Net.Dns;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils.OS;
|
||||
using STUN.Client;
|
||||
using Sentry;
|
||||
using Sentry.Extensibility;
|
||||
|
||||
namespace PCL.Core.App.Essentials;
|
||||
|
||||
[LifecycleScope("telemetry", "遥测")]
|
||||
[LifecycleService(LifecycleState.Running)]
|
||||
public sealed partial class TelemetryService
|
||||
{
|
||||
private static void _InitSentry()
|
||||
{
|
||||
Context.Info("开始初始化 Sentry SDK");
|
||||
var dsn = EnvironmentInterop.GetSecret("SENTRY_DSN");
|
||||
if (dsn is null)
|
||||
{
|
||||
Context.Warn("未找到 Sentry DSN");
|
||||
return;
|
||||
}
|
||||
|
||||
var release = $"{Basics.VersionName}";
|
||||
|
||||
#if DEBUG
|
||||
var environment = "Debug";
|
||||
#else
|
||||
var environment = "Production";
|
||||
#endif
|
||||
|
||||
SentrySdk.Init(options =>
|
||||
{
|
||||
options.Dsn = dsn;
|
||||
#if DEBUG
|
||||
options.Debug = true;
|
||||
#else
|
||||
options.Debug = false;
|
||||
#endif
|
||||
options.SendDefaultPii = false;
|
||||
options.IsGlobalModeEnabled = true;
|
||||
options.AutoSessionTracking = true;
|
||||
options.Release = release;
|
||||
options.Environment = environment;
|
||||
|
||||
// 应该被直接丢弃不上报的异常类型
|
||||
options.AddExceptionFilterForType<TimeoutException>();
|
||||
options.AddExceptionFilterForType<HttpRequestException>();
|
||||
options.AddExceptionFilterForType<WebException>();
|
||||
options.AddExceptionFilterForType<TaskCanceledException>();
|
||||
options.AddExceptionFilterForType<DirectoryNotFoundException>();
|
||||
options.AddExceptionFilterForType<UnauthorizedAccessException>();
|
||||
options.AddExceptionFilterForType<FileNotFoundException>();
|
||||
|
||||
// 细分类型的过滤器
|
||||
options.AddExceptionFilter(new SocketExceptionFilter());
|
||||
|
||||
options.SetBeforeSend(@event => @event.Level is SentryLevel.Debug ? null : @event);
|
||||
});
|
||||
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
scope.User = new SentryUser
|
||||
{
|
||||
Id = Utils.Secret.Identify.LauncherId
|
||||
};
|
||||
});
|
||||
|
||||
Context.Info("Sentry SDK 初始化完成");
|
||||
}
|
||||
|
||||
// 错误上报
|
||||
public static void ReportException(Exception ex, string plain, LogLevel level)
|
||||
{
|
||||
var sentryEvent = new SentryEvent(ex)
|
||||
{
|
||||
Level = level.RealLevel() switch
|
||||
{
|
||||
LogLevel.Fatal => SentryLevel.Fatal,
|
||||
LogLevel.Error => SentryLevel.Error,
|
||||
LogLevel.Warning => SentryLevel.Warning,
|
||||
LogLevel.Info => SentryLevel.Info,
|
||||
LogLevel.Debug or LogLevel.Trace => SentryLevel.Debug,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(level))
|
||||
}
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(plain))
|
||||
{
|
||||
sentryEvent.Message = new SentryMessage { Formatted = plain };
|
||||
}
|
||||
|
||||
SentrySdk.CaptureEvent(sentryEvent);
|
||||
}
|
||||
|
||||
// 设备环境上报
|
||||
private static void _ReportDeviceEnvironment(TelemetryDeviceEnvironment content)
|
||||
{
|
||||
Context.Info("正在上报设备环境调查数据");
|
||||
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
scope.Contexts["Telemetry"] = content;
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
SentrySdk.CaptureMessage("设备环境调查");
|
||||
Context.Info("已发送设备环境调查数据");
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Context.Error("设备环境调查数据发送失败,请检查网络连接以及使用的版本", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Local
|
||||
|
||||
private class TelemetryDeviceEnvironment
|
||||
{
|
||||
public required string Tag { get; set; }
|
||||
public required string Id { get; set; }
|
||||
[JsonPropertyName("OS")] public required int Os { get; set; }
|
||||
public required bool Is64Bit { get; set; }
|
||||
[JsonPropertyName("IsARM64")] public required bool IsArm64 { get; set; }
|
||||
public required string Launcher { get; set; }
|
||||
public required string LauncherBranch {get; set; }
|
||||
[JsonPropertyName("UsedOfficialPCL")] public required bool UsedOfficialPcl { get; set; }
|
||||
[JsonPropertyName("UsedHMCL")] public required bool UsedHmcl { get; set; }
|
||||
[JsonPropertyName("UsedBakaXL")] public required bool UsedBakaXl { get; set; }
|
||||
public required ulong Memory { get; set; }
|
||||
public required string? NatMapBehaviour { get; set; }
|
||||
public required string? NatFilterBehaviour { get; set; }
|
||||
[JsonPropertyName("IPv6Status")] public required string Ipv6Status { get; set; }
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private const string STUN_SERVER_ADDR = "stun.miwifi.com";
|
||||
|
||||
// ReSharper restore UnusedAutoPropertyAccessor.Local
|
||||
[LifecycleStart]
|
||||
private static async Task _StartAsync()
|
||||
{
|
||||
if (!Config.System.Telemetry) return;
|
||||
_InitSentry();
|
||||
|
||||
var appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
|
||||
// stun test
|
||||
StunClient5389UDP? natTest = null;
|
||||
var miWifiIps = await DnsQuery.Instance.QueryForIpAsync(STUN_SERVER_ADDR).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
miWifiIps ??= await Dns.GetHostAddressesAsync(STUN_SERVER_ADDR).ConfigureAwait(false);
|
||||
} catch(Exception) { /* Ignore dns error */ }
|
||||
|
||||
if (miWifiIps is not null && miWifiIps.Length != 0)
|
||||
{
|
||||
natTest = new StunClient5389UDP(new IPEndPoint(miWifiIps.First(), 3478),
|
||||
new IPEndPoint(IPAddress.Any, 0));
|
||||
await natTest.QueryAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var telemetry = new TelemetryDeviceEnvironment
|
||||
{
|
||||
Tag = "Telemetry",
|
||||
Id = Utils.Secret.Identify.LauncherId,
|
||||
Os = Environment.OSVersion.Version.Build,
|
||||
Is64Bit = Environment.Is64BitOperatingSystem,
|
||||
IsArm64 = RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64),
|
||||
Launcher = Basics.VersionName,
|
||||
LauncherBranch = Config.Update.UpdateChannel switch
|
||||
{
|
||||
UpdateChannel.Release => "Release",
|
||||
UpdateChannel.Beta => "Beta",
|
||||
UpdateChannel.Dev => "Dev",
|
||||
_ => "Unknown"
|
||||
},
|
||||
UsedOfficialPcl =
|
||||
bool.TryParse(Registry.GetValue(@"HKEY_CURRENT_USER\Software\PCL", "SystemEula", "false") as string,
|
||||
out var officialPcl) && officialPcl,
|
||||
UsedHmcl = Directory.Exists(Path.Combine(appDataFolder, ".hmcl")),
|
||||
UsedBakaXl = Directory.Exists(Path.Combine(appDataFolder, "BakaXL")),
|
||||
Memory = KernelInterop.GetPhysicalMemoryBytes().Total,
|
||||
NatMapBehaviour = natTest?.State.MappingBehavior.ToString(),
|
||||
NatFilterBehaviour = natTest?.State.FilteringBehavior.ToString(),
|
||||
Ipv6Status = NetworkInterfaceUtils.GetIPv6Status().ToString()
|
||||
};
|
||||
|
||||
_ReportDeviceEnvironment(telemetry);
|
||||
}
|
||||
|
||||
// 用来细分过滤 SocketException 的过滤器,我觉得应该除了遥测服务之外没有其他东西会用到这破玩意儿
|
||||
private sealed class SocketExceptionFilter : IExceptionFilter
|
||||
{
|
||||
public bool Filter(Exception ex)
|
||||
{
|
||||
if (ex is SocketException socketEx)
|
||||
{
|
||||
return socketEx.SocketErrorCode is
|
||||
SocketError.ConnectionRefused or
|
||||
SocketError.TimedOut or
|
||||
SocketError.HostNotFound or
|
||||
SocketError.NetworkUnreachable or
|
||||
SocketError.ConnectionReset;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[LifecycleStop]
|
||||
private static void _StopAsync()
|
||||
{
|
||||
SentrySdk.Close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user