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