XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Buffers.Binary;

namespace XFEEDToolBox.Core.Session;

public sealed record StartupCrashEvidence(string Path,int ProcessId,uint ExceptionCode,DateTimeOffset At);
public sealed class StartupClientExitedException(int processId):InvalidOperationException($"新启动的游戏进程 {processId} 在完成载入前结束。");

/// <summary>Only an exception dump of this exact newly launched client authorizes another launch.</summary>
public static class StartupCrashRecovery
{
    private static bool ProtectedFailure(Exception error)=>error is InvalidDataException or NotSupportedException or Navigation.FlightSafetyPauseException
        ||error is AggregateException aggregate&&aggregate.InnerExceptions.Any(ProtectedFailure)
        ||error.InnerException is {} inner&&ProtectedFailure(inner);
    public static async Task RunAsync(Func<Action<GameSessionTarget>,CancellationToken,Task> attempt,
        Func<GameSessionTarget,CancellationToken,Task<bool>> exited,Func<GameSessionTarget,StartupCrashEvidence?> evidence,
        Func<StartupCrashEvidence,int,CancellationToken,Task>? recovering,CancellationToken token,int maximumRecoveries=2)
    {
        if(maximumRecoveries is <0 or >3)throw new ArgumentOutOfRangeException(nameof(maximumRecoveries));
        for(var retries=0;;retries++)
        {
            GameSessionTarget? owned=null;
            try{await attempt(target=>owned=target,token).ConfigureAwait(false);return;}
            catch(Exception error)when(!token.IsCancellationRequested&&!ProtectedFailure(error)&&retries<maximumRecoveries&&owned is not null)
            {
                token.ThrowIfCancellationRequested();
                if(!await exited(owned,token).ConfigureAwait(false)||evidence(owned) is not{} crash)throw;
                if(recovering is not null)await recovering(crash,retries+1,token).ConfigureAwait(false);
                await Task.Delay(TimeSpan.FromSeconds(1),token).ConfigureAwait(false);
            }
        }
    }
    public static StartupCrashEvidence? Find(string directory,GameSessionTarget target,DateTimeOffset now)
    {
        if(!Directory.Exists(directory))return null;
        foreach(var folder in Directory.EnumerateDirectories(directory).OrderByDescending(Directory.GetLastWriteTimeUtc).Take(4))
            foreach(var path in Directory.EnumerateFiles(folder,"*.dmp").OrderByDescending(File.GetLastWriteTimeUtc).Take(4))
                if(Read(path,target,now) is{} witness)return witness;
        return null;
    }
    public static StartupCrashEvidence? Read(string path,GameSessionTarget target,DateTimeOffset now)
    {
        try
        {
            using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
            Span<byte> header=stackalloc byte[32];stream.ReadExactly(header);
            static uint U32(ReadOnlySpan<byte> value,int offset)=>BinaryPrimitives.ReadUInt32LittleEndian(value[offset..]);
            if(U32(header,0)!=0x504d444d||target.ProcessId<=0||target.StartedUtcTicks<=0)return null;
            var count=U32(header,8);var table=U32(header,12);var at=DateTimeOffset.FromUnixTimeSeconds(U32(header,20));
            var began=new DateTimeOffset(target.StartedUtcTicks,TimeSpan.Zero);
            if(count is 0 or >128||at<began.AddSeconds(-1)||at>now.AddSeconds(5)||now-at>TimeSpan.FromMinutes(5)||table>stream.Length-count*12L)return null;
            uint pid=0,exception=0;Span<byte> entry=stackalloc byte[12];Span<byte> prefix=stackalloc byte[12];
            for(var i=0;i<count;i++)
            {
                stream.Position=table+i*12L;stream.ReadExactly(entry);var kind=U32(entry,0);var size=U32(entry,4);var offset=U32(entry,8);
                if(kind is not(6 or 15))continue;
                if(size<12||offset>stream.Length-size)return null;
                stream.Position=offset;stream.ReadExactly(prefix);
                if(kind==6)exception=U32(prefix,8);
                else if((U32(prefix,4)&1)!=0)pid=U32(prefix,8);
            }
            return pid==target.ProcessId&&exception!=0?new(Path.GetFullPath(path),(int)pid,exception,at):null;
        }
        catch(Exception error)when(error is IOException or UnauthorizedAccessException or ArgumentException){return null;}
    }
}