惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
T
Threatpost
N
News and Events Feed by Topic
PCI Perspectives
PCI Perspectives
V2EX - 技术
V2EX - 技术
D
Docker
G
Google Developers Blog
Microsoft Security Blog
Microsoft Security Blog
N
News and Events Feed by Topic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
The GitHub Blog
The GitHub Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Y
Y Combinator Blog
M
MIT News - Artificial intelligence
Blog — PlanetScale
Blog — PlanetScale
博客园 - 司徒正美
T
Troy Hunt's Blog
Webroot Blog
Webroot Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
量子位
Apple Machine Learning Research
Apple Machine Learning Research
H
Help Net Security
F
Full Disclosure
B
Blog
O
OpenAI News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园_首页
Google DeepMind News
Google DeepMind News
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Engineering at Meta
Engineering at Meta
大猫的无限游戏
大猫的无限游戏
Forbes - Security
Forbes - Security
Know Your Adversary
Know Your Adversary
B
Blog RSS Feed
MongoDB | Blog
MongoDB | Blog
Scott Helme
Scott Helme
T
The Exploit Database - CXSecurity.com
博客园 - 聂微东
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
The Last Watchdog
The Last Watchdog
Recorded Future
Recorded Future
IT之家
IT之家
Project Zero
Project Zero
Stack Overflow Blog
Stack Overflow Blog
小众软件
小众软件
Attack and Defense Labs
Attack and Defense Labs
L
Lohrmann on Cybersecurity
SecWiki News
SecWiki News
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

晓雨杂记

【转载】舞萌 DX 报错 8103 / 8106 以及 0949 问题原因分析和解决方案 【转载】舞萌国服 1.53 补丁更新内容速览 站点之间,是生活的片段 EdgeOne 免费版,Yes! 2025 年,把一加 6T 作为主力机是什么体验?(上) 2024 年终总结 国行 iPhone 12 强开 Apple 智能 通过自定义主题重写我的博客 窗边浅谈:形同陌路的“朋友” 没有衡水命,一身衡水病——盲目复制衡水模式的意义何在? 在 Valaxy 中使用 Artalk 作为评论系统 窗边浅谈:序章 【转载】关于 HiPer 的安全性讨论,以及对于近期一些小事的简单介绍 2023 年终回顾 期中前半学期回顾 用起来还不错——国区 Apple Music 浅谈 万物皆可美化之 VSCode 博客二周年——我对博客的想法 我过不去的坎——数学 将 Waline 从 LeanCloud 迁移到 MongoDB Cloud 初入舞萌 DX 记一次重装 Windows 记一次被 DDoS 中考成绩! OMORI 初体验 中考之后 谈人际交往 我的音乐品味 自建 jsDelivr 镜像 渺软公益 CDN 2023 中考宣言 晓雨和林小雨的奇妙日常 为了纯粹的快——来看看我对我的博客做了什么 Valaxy 与 Cloudflare 的邂逅——将 Valaxy 博客部署到 Cloudflare Pages Minecraft 国际版玩家对近期事件的回应 对网易与 Minecraft 国际版的担忧 再见,Notepad++ 你的下一个启动器,何必是启动器 诈尸?—— 第三方网易云音乐客户端 LyricEase Valaxy —— 快到没边的全新静态博客框架 见证历史了——谷歌取消大陆地区翻译服务 在 iOS 桌面查看你的原神账号信息——原神披萨小助手 中秋节快乐!以及一些碎碎念 与朋友联机游玩 Minecraft 优化站点速度之见解 搭建 Hypixel 加速 IP 部署 VuePress v1 文档到 Vercel Hello World! 白嫖党福利——Backblaze B2 免费 10GB 对象存储 打开 Windows 传递优化,利人利己! 优化 Windows Search 搜索,加快搜索速度! 搭建一个属于你自己的 Minecraft 服务器 如何诊断 Minecraft Java 版客户端中的错误 手把手教你如何搭建属于自己的 Hexo 博客 关于我讲了节物理课这件事 我的 Minecraft 服务器 —— CakeMC Cake Launcher 开发日志 神级内存清理软件———Mem Reduct
为 PCL CE 添加 Sentry 集成
Big_Cake · 2026-03-29 · via 晓雨杂记

屎山嘛,不堆白不堆,反正总会有人堆的。

前言 ​

PCL CE 是一个 Minecraft 启动器,基于龙腾猫跃开发的 PCL 的开源代码进行二次开发。

经过长期维护,PCL CE 的架构现在已经基本成熟,相较于主线版本而言维护难度与开发体验都要更上一层楼。毕竟谁也不想看着一大堆 goto 和到处都是的 VB.NET 代码尝试理解它的逻辑。

但是随着用户与反馈数量的增长,现在的 PCL CE 迫切需要一个错误收集与上报服务,用来调查可能存在的某些神秘 Bug。

于是我就提了个 PR……

Sentry 是一个很成熟的服务,提供了端到端的分布式错误追踪,允许开发者识别、追踪并调试系统与服务中潜在的性能问题与代码错误。它同时也是开源的。

它提供 SaaS 托管,但也允许开发者使用 Docker 自行部署。

对于 PCL CE 来说,自部署已经能满足我们的几乎所有需求了。

修改遥测服务 ​

如你所见,PCL CE 在此之前已经有了一个自行实现的遥测服务,旨在收集设备环境调查信息用来整一些类似 Steam 软硬件调查的东西。

csharp

// ./PCL.Core/App/Essentials/TelemetryService.cs
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
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.IO.Net.Http.Client.Request;
using PCL.Core.Utils.OS;
using STUN.Client;

namespace PCL.Core.App.Essentials;

[LifecycleScope("Telemetry", "遥测")]
[LifecycleService(LifecycleState.Running)]
public sealed partial class TelemetryService
{

    // 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;
        var telemetryKey = EnvironmentInterop.GetSecret("TELEMETRY_KEY");
        if (string.IsNullOrWhiteSpace(telemetryKey)) return;
        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 != 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()
        };
        using var response = await HttpRequest
            .CreatePost("https://pcl2ce.pysio.online/post")
            .WithHeader("Authorization", telemetryKey)
            .WithJsonContent(telemetry)
            .SendAsync()
            .ConfigureAwait(false);
        if (response.IsSuccess)
            Context.Info("已发送设备环境调查数据");
        else
            Context.Error("设备环境调查数据发送失败,请检查网络连接以及使用的版本");
        Context.DeclareStopped();
    }
}

初始化 Sentry SDK ​

首先,我们要把原本直接向服务器发送 POST 请求的代码(即上述代码块中的 HttpRequest 部分)干掉。因为用了 Sentry 也就没有必要再继续把这些调查数据发到自己搓的服务端上了。

然后跑一下指令安装 Sentry SDK。因为自部署的 Sentry 设置向导给的是 3.34.0,所以这里就装 3.34.0 了。

bash

dotnet add package Sentry -v 3.34.0

跑完以后,NuGet PM 会自动在 csproj 文件中添加引用,但它不知道我们在这里做了分类。所以为了符合 CE 的代码规范,我们还得手动改一下这里引用的位置。

xml

<!-- UI -->
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
<PackageReference Include="Sentry" Version="3.34.0" />
<PackageReference Include="Wacton.Unicolour" Version="6.4.0" />
<!-- 语言和系统特性 -->
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="System.Management" Version="10.0.3" />
<PackageReference Include="Polly" Version="8.6.5" />
<!-- 本地化 -->
<PackageReference Include="Humanizer.Core.zh-CN" Version="3.0.1" />
<!-- 日志与遥测 -->
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageReference Include="Sentry" Version="3.34.0" />

接下来就可以开始动手了。让我们先往 TelemetryService.cs 里加一下必要的引用:

csharp

// using ...
using PCL.Code.Logging;
using Sentry;

namespace PCL.Core.App.Essentials;
// ...

加入引用之后,在 TelemetryService 类里加一个 _InitSentry() 方法,代码片段如下:

csharp

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 = quot;{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.SetBeforeSend(@event =>
        {
            if (@event.Exception is TimeoutException) return null; // 过滤掉特定的一些 Exception(例如连接超时等等)
            return @event.Level is SentryLevel.Debug ? null : @event; // 过滤掉 Debug 等级的日志
        });
    });

    Context.Info("Sentry SDK 初始化完成");
}

然后把这个方法塞进 _StartAsync() 方法的最上面,这样 Sentry 就会在程序启动时自动初始化了。

让生命周期服务开心 ​

你注意到那个 [LifecycleStart] 了吗?生命周期服务会自动管理各个服务的启动与停止顺序,它会自动把它标记的这个 _StartAsync() 方法用于启动服务,也会自动调用另一个被 [LifecycleStop] 标记的方法停止这个服务。

不过我们一开始的代码中是使用 Context.DeclareStopped(); 声明服务已停止的,这就会导致在初始化完成后服务自动停止。所以我们要把这个删了,然后重新写一个方法 _StopAsync() 以让生命周期服务能够顺利停止这个服务。很简单:

csharp

// 上面是 `_StartAsync()`

[LifecycleStop]
private static void _StopAsync()
{
    SentrySdk.Close();
}

// ...

这样就好了。

设备环境与错误收集上报 ​

接下来要在类里面再加两个方法:_ReportDeviceEnvironment()ReportException()3、2、1 上代码!

csharp

// 错误上报
public static void ReportException(Exception ex, string plain, LogLevel level)
{
    var sentryEvent = new SentryEvent(ex);
    
    sentryEvent.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,
    };

    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);
    }
}

这里上报错误用的是 SentrySdk.CaptureEvent() 而非 SentrySdk.CaptureException(),因为似乎用前者的性能相较而言会更好一点。这个就仁者见仁智者见智了。

_ReportDeviceEnvironment() 塞进 _StartAsync() 方法里最下面,这里的工作基本就完成了。

什么?你说如果这个服务没有初始化的话前面的错误全都拿不到?那就看你怎么解决了,反正我能力有限不知道怎么解决这个问题。或许搓个缓存队列会有点效果?

接入日志服务 ​

现在方法搓好了。因为我们不是直接向主窗口里塞代码,所以这里需要手动调用 ReportException() 方法才会收集错误。

CE 已经有了一套很完善的错误捕获与日志输出机制了。我们只需要简单改一下日志服务就好了。

老规矩,先引入 PCL.Core.Essentials 类:

csharp

// ./PCL.Core/Logging/LogService.cs
// using ...
using PCL.Core.Essentials;

然后再稍微改一下 _LogAction() 方法:

csharp

private static void _LogAction(ActionLevel level, string formatted, string plain, Exception? ex)
private static void _LogAction(LogLevel level, ActionLevel actionLevel, string formatted, string plain, Exception? ex)
{
    if (ex is not null) {
        TelemetryService.ReportException(ex, plain, level);
    }

    // 一定要记得同步修改这个方法中其他地方的签名!!!
}

大功告成!

修改 UI ​

因为现在这里的逻辑彻底变了,所以之前的 UI 提示已经不准确了。我们需要往涉及到的地方都加上新的文案。

xml

<local:MyCheckBox Grid.Row="0" Grid.Column="1" Text="启用遥测数据收集" x:Name="CheckSystemTelemetry" Tag="SystemTelemetry" 
                          ToolTip="启用此功能后,启动器将会收集并上报错误与设备环境信息,这可以帮助开发者修复潜在的问题、更好的进行规划和开发。&#xa;若启用此功能,我们将会收集以下信息:启动器内出现的错误,启动器版本信息与识别码,使用的 Windows 系统版本与架构,已安装的物理内存大小,NAT 与 IPv6 支持情况,是否使用过官方版 PCL、HMCL 或 BakaXL。&#xa;&#xa;这些数据均不与你关联,我们也绝不会向第三方出售数据。不启用此功能不会影响你使用其他功能,但可能会影响开发者修复潜在 Bug。&#xa;&#xa;此项更改将在重新启动启动器后生效。"/>

还有个 VB 里面控制的弹窗,懒得整了直接贴链接吧:https://github.com/PCL-Community/PCL-CE/commit/e264b2d2fea127c9aba31cdb90265f409b858720#diff-8f89f2cddd157060c43a539a835bc04f44227dac9409a2a961c2b6f58a830feb。

为什么我要提这个 PR? ​

正如开篇所言,CE 一直缺少一个错误收集与上报机制,这限制了社区成员调查问题的效率,并且也在一定程度上导致了大量无效反馈的产生。而隔壁 PCL 主线已经实现了这一特性,只不过龙腾猫跃使用的是腾讯云的服务。

以及,在 CE 2.14.0 第一个 Beta 版本发布后,因为“对底层代码进行了大量修改”,启动器经常出现因为严重错误而崩溃的情况。但修复 Bug 完全依靠用户报告,社区无法第一时间发现潜在的异常。

好在现在这个 PR 已经被合并了,或许等到下一个 Beta 版本,这个功能就会正式上线了。希望这套错误上报的代码自己不要出毛病。