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>,Task> 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{["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()??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()??actor["id"]!.GetValue(); 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{["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()),configuration.Workspace,StringComparison.OrdinalIgnoreCase))continue; try { using var process=System.Diagnostics.Process.GetProcessById(launch["ProcessId"]!.GetValue()); 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()!=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);