XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
UTF-8
using System.Net;
using System.Text.Json;
using System.Text.Json.Nodes;
using XFEEliteDangerous.Core;
using XFEEliteDangerous.Client;
using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Implements.CoreService;

namespace XFEEliteDangerous.Server;

public partial class ManagementService : ServerCoreStandardServiceBase
{
    private static OfflineWorkspace workspace=null!;
    private static ManagementCommands commands=null!;
    private static ServerConfiguration configuration=null!;
    private static string localKey="";
    private static Action stop=()=>{};
    internal static Func<string,Func<Task<JsonNode>>,Task<JsonNode>> EditOnlineIdle=(_,edit)=>edit();
    public static void Configure(OfflineWorkspace data,ServerConfiguration options,string key,Action shutdown)
    {workspace=data;commands=new(data);configuration=options;localKey=key;stop=shutdown;OfflineDiagnostics.Current?.RegisterSecret(key);}
    [EntryPoint("health"),NoLog]
    public async Task Health(){await Reply(new JsonObject{["status"]="ready",["framework"]="XFEExtension.NetCore.ServerInteractive",["version"]=1,["workspace"]=configuration.Workspace,["pid"]=Environment.ProcessId},200);}
    [EntryPoint("shutdown"),NoLog]
    public async Task Shutdown()
    {
        using var language = AppText.UseLanguage(Args.RequestHeaders["Accept-Language"]);
        var header=Args.RequestHeaders["Authorization"];
        if(Args.RequestMethod!="POST"||!Wire.Equal(header,"Bearer "+localKey)){await Reply(new JsonObject{["error"]=AppText.Get("ManagementService.Text427")},401);return;}
        await Reply(new JsonObject{["stopping"]=true},200);stop();
    }
    [EntryPoint("control"),NoLog]
    public async Task Control()
    {
        using var language = AppText.UseLanguage(Args.RequestHeaders["Accept-Language"]);
        var operation="unparsed";var status=200;
        using var diagnostic=OfflineDiagnostics.Current?.Begin("management","request",new Dictionary<string,string?>{["correlationId"]=Args.CorrelationId});
        try
        {
            if(Args.RequestMethod!="POST"){await Reply(new JsonObject{["error"]=AppText.Get("ManagementService.Text428")},405);return;}
            var body=JsonNode.Parse(Args.RequestBody??"",documentOptions:new(){MaxDepth=64})?.AsObject()??throw new InvalidDataException(AppText.Get("ManagementService.Text429"));
            if(body.Count!=2||body["arguments"] is not JsonObject input)throw new InvalidDataException(AppText.Get("ManagementService.Text430"));
            operation=body["operation"]?.GetValue<string>()??throw new InvalidDataException(AppText.Get("ManagementService.Text431"));
            var header=Args.RequestHeaders["Authorization"];
            var token=header?.StartsWith("Bearer ",StringComparison.Ordinal)==true?header[7..]:null;
            if(token is not null)OfflineDiagnostics.Current?.RegisterSecret(token);
            JsonNode result;
            if(operation=="auth/local-owner")
            {
                if(!Wire.Equal(token,localKey))throw new LocalSessionException(AppText.Get("ManagementService.Text427"));
                result=await workspace.OpenLocalOwnerSessionAsync();
            }
            else
            {
                if(operation is "inventory/edit" or "fleet/edit" or "materials/prepare" or "native/import" or "native/initialize" or "journal/import")
                {
                    var actor=await workspace.ProfileAsync(token);
                    var account=input["accountId"]?.GetValue<string>()??actor["id"]!.GetValue<string>();
                    CheckNativeGameIdle();
                    result=await EditOnlineIdle(account,()=>commands.ExecuteAsync(operation,token,input));
                }
                else result=await commands.ExecuteAsync(operation,token,input);
                if(operation=="status")
                {
                    result["networkTransport"]=true;result["managementTransport"]="loopback-http";
                    result["gameTransport"]=configuration.Online is {Enabled:true}?"launcher-https":"in-process";
                    result["nativeMultiplayerValidated"]=OnlineGameService.NativeMultiplayerValidated;
                    result["nativeMultiplayerValidationScope"]=OnlineGameService.NativeValidationScope;
                }
            }
            await Reply(result,200);diagnostic?.Complete("completed");
        }
        catch(Exception error)
        {
            status=error switch{LocalSessionException=>401,UnauthorizedAccessException=>403,SaveConflictException or ManagementBusyException=>409,
                JsonException or InvalidDataException or ArgumentException or OverflowException or InvalidOperationException=>400,_=>500};
            OfflineDiagnostics.Current?.Write(status==500?"error":"warning","management","request.rejected",error.Message,new Dictionary<string,string?>{["operation"]=operation,["status"]=status.ToString()},error);
            await Reply(new JsonObject{["error"]=status==500?AppText.Get("ManagementService.Text432"):error.Message,
                ["code"]=error is SaveConflictException?"save_conflict":"request_rejected",["correlationId"]=Args.CorrelationId},status);
            diagnostic?.Complete("rejected");
        }
    }
    private async Task Reply(JsonNode data,int status)
    {
        Handled=true;await Args.ReplyAndClose(data.ToJsonString(),(HttpStatusCode)status,"application/json; charset=utf-8");
    }
    private static void CheckNativeGameIdle()
    {
        if(configuration.ArtifactsRoot is not {} root||!Directory.Exists(root))return;
        foreach(var directory in Directory.EnumerateDirectories(root,"offline-game-*"))
        {
            var receipt=Path.Combine(directory,"launch.json");if(!File.Exists(receipt))continue;
            JsonObject launch;try{launch=JsonNode.Parse(File.ReadAllText(receipt))!.AsObject();}catch(IOException){continue;}
            if(!string.Equals(Path.GetFullPath(launch["workspace"]!.GetValue<string>()),configuration.Workspace,StringComparison.OrdinalIgnoreCase))continue;
            try
            {
                using var process=System.Diagnostics.Process.GetProcessById(launch["ProcessId"]!.GetValue<int>());
                if(process.HasExited||Math.Abs((process.StartTime.ToUniversalTime()-File.GetLastWriteTimeUtc(receipt)).TotalSeconds)>15
                    ||!string.Equals(process.MainModule?.FileName,@"E:\Elite Dangerous\Products\elite-dangerous-odyssey-64\EliteDangerous64.exe",StringComparison.OrdinalIgnoreCase))continue;
                if(launch["nativeManagementSchema"]?.GetValue<int>()!=1)throw new ManagementBusyException(AppText.Get("ManagementService.Text433"));
                var flow=Path.Combine(directory,"logs","native-game-flow.tsv");
                if(!File.Exists(flow))throw new ManagementBusyException(AppText.Get("ManagementService.Text434"));
                using var stream=new FileStream(flow,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
                using var reader=new StreamReader(stream);var last=reader.ReadToEnd().Split('\n',StringSplitOptions.RemoveEmptyEntries).LastOrDefault(value=>value.Contains("\tFLOW\t",StringComparison.Ordinal));
                if(last is null||!last.Trim().EndsWith("\tMainMenu\tMainMenu",StringComparison.Ordinal))throw new ManagementBusyException(AppText.Get("ManagementService.Text435"));
            }
            catch(ArgumentException){continue;}
        }
    }
}
internal sealed class ManagementBusyException(string message):InvalidOperationException(message);