11using System . Net . Http . Json ;
22using System . Text . Json ;
3- using Daybreak . Linux . Services . Wine ;
43using Daybreak . Shared . Models . Api ;
5- using Daybreak . Shared . Services . MDns ;
4+ using Daybreak . Shared . Services . Api ;
65using Microsoft . Extensions . Hosting ;
76using Microsoft . Extensions . Logging ;
87using System . Extensions . Core ;
98
10- namespace Daybreak . Linux . Services . MDns ;
9+ namespace Daybreak . Services . Api ;
1110
1211/// <summary>
13- /// Linux-specific implementation of <see cref="IMDomainRegistrar"/>.
14- /// Since mDNS announcements from Wine don't propagate to the host,
15- /// this implementation scans a known port range (5080–5100) for running
16- /// Daybreak API instances and matches them to Guild Wars processes
17- /// by querying the API's health endpoint for its Wine PID, then
18- /// converting it to a Linux PID via <see cref="IWinePidMapper"/>.
12+ /// Scans a known port range (5080–5100) for running Daybreak API instances
13+ /// and matches them to Guild Wars processes by querying the API's health endpoint.
1914/// All probes fire in parallel with a 500ms total timeout.
2015/// </summary>
21- public sealed class PortScanningDomainRegistrar (
22- IWinePidMapper winePidMapper ,
23- ILogger < PortScanningDomainRegistrar > logger )
24- : IMDomainRegistrar , IHostedService , IDisposable
16+ public sealed class ApiScanningService (
17+ IPidProvider pidProvider ,
18+ ILogger < ApiScanningService > logger )
19+ : IApiScanningService , IHostedService , IDisposable
2520{
2621 private const int StartPort = 5080 ;
2722 private const int EndPort = 5100 ;
2823 private const string HealthPath = "/api/v1/rest/health" ;
29- private const string DaybreakApiServicePrefix = "daybreak-api- " ;
30- private static readonly TimeSpan ProbeTimeout = TimeSpan . FromMilliseconds ( 500 ) ;
31- private static readonly TimeSpan ScanInterval = TimeSpan . FromSeconds ( 15 ) ;
24+ private const string GuildWarsExecutable = "Gw.exe " ;
25+ private static readonly TimeSpan ProbeTimeout = TimeSpan . FromMilliseconds ( 2000 ) ;
26+ private static readonly TimeSpan ScanInterval = TimeSpan . FromSeconds ( 10 ) ;
3227 private static readonly JsonSerializerOptions JsonOptions = new ( ) { PropertyNameCaseInsensitive = true } ;
3328
34- private readonly IWinePidMapper winePidMapper = winePidMapper ;
35- private readonly ILogger < PortScanningDomainRegistrar > logger = logger ;
29+ private readonly IPidProvider pidProvider = pidProvider ;
30+ private readonly ILogger < ApiScanningService > logger = logger ;
3631 private readonly HttpClient httpClient = new ( ) { Timeout = ProbeTimeout } ;
3732
3833 // Rebuilt on each scan — survives Daybreak restarts since it's based on live port probing.
39- private volatile Dictionary < string , List < Uri > > discoveredServices = [ ] ;
34+ private volatile Dictionary < int , Uri > discoveredApis = [ ] ;
4035 private CancellationTokenSource ? backgroundCts ;
4136
4237 Task IHostedService . StartAsync ( CancellationToken cancellationToken )
4338 {
44- this . backgroundCts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
39+ this . backgroundCts = new CancellationTokenSource ( ) ;
4540 _ = Task . Factory . StartNew (
4641 ( ) => this . ScanPeriodically ( this . backgroundCts . Token ) ,
4742 this . backgroundCts . Token ,
@@ -62,31 +57,31 @@ public void Dispose()
6257 this . httpClient . Dispose ( ) ;
6358 }
6459
65- public IReadOnlyList < Uri > ? Resolve ( string service )
60+ public Uri ? GetApiUriByProcessId ( int processId )
6661 {
67- if ( this . discoveredServices . TryGetValue ( service , out var uris ) && uris . Count > 0 )
62+ if ( this . discoveredApis . TryGetValue ( processId , out var uri ) )
6863 {
69- return uris ;
64+ return uri ;
7065 }
7166
7267 return default ;
7368 }
7469
75- public IReadOnlyList < Uri > ? QueryByServiceName ( Func < string , bool > query )
70+ public IReadOnlyList < ( int ProcessId , Uri Uri ) > ? QueryByProcessId ( Func < int , bool > predicate )
7671 {
77- var results = new List < Uri > ( ) ;
78- foreach ( var ( serviceName , uris ) in this . discoveredServices )
72+ var results = new List < ( int ProcessId , Uri Uri ) > ( ) ;
73+ foreach ( var ( processId , uri ) in this . discoveredApis )
7974 {
80- if ( query ( serviceName ) )
75+ if ( predicate ( processId ) )
8176 {
82- results . AddRange ( uris ) ;
77+ results . Add ( ( processId , uri ) ) ;
8378 }
8479 }
8580
8681 return results . Count > 0 ? results : default ;
8782 }
8883
89- public void QueryAllServices ( )
84+ public void RequestScan ( )
9085 {
9186 _ = Task . Run ( ( ) => this . ScanPortsAsync ( CancellationToken . None ) ) ;
9287 }
@@ -103,7 +98,7 @@ private async Task ScanPeriodically(CancellationToken cancellationToken)
10398 private async Task ScanPortsAsync ( CancellationToken cancellationToken )
10499 {
105100 var scopedLogger = this . logger . CreateScopedLogger ( ) ;
106- var newServices = new Dictionary < string , List < Uri > > ( ) ;
101+ var newApis = new Dictionary < int , Uri > ( ) ;
107102
108103 using var timeoutCts = new CancellationTokenSource ( ProbeTimeout ) ;
109104 using var linkedCts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken , timeoutCts . Token ) ;
@@ -129,36 +124,27 @@ private async Task ScanPortsAsync(CancellationToken cancellationToken)
129124 continue ;
130125 }
131126
132- var ( port , processId ) = task . Result ;
133- if ( processId is null )
127+ var ( port , reportedPid ) = task . Result ;
128+ if ( reportedPid is null )
134129 {
135130 continue ;
136131 }
137132
138- // The API reports its Wine PID. Convert to Linux PID.
139- var linuxPid = this . winePidMapper . WinePidToLinuxPid ( processId . Value , "Gw.exe" ) ;
140- var effectivePid = linuxPid ?? processId . Value ;
141-
142- var serviceName = $ "{ DaybreakApiServicePrefix } { effectivePid } ";
133+ // Convert the reported PID (Wine PID on Linux) to system PID
134+ var systemPid = this . pidProvider . ResolveSystemPid ( reportedPid . Value , GuildWarsExecutable ) ;
143135 var uri = new Uri ( $ "http://localhost:{ port } ") ;
144136
145137 scopedLogger . LogDebug (
146- "Found Daybreak API on port {Port} with Wine PID {WinePid } (Linux PID {LinuxPid })" ,
138+ "Found Daybreak API on port {Port} with reported PID {ReportedPid } (system PID {SystemPid })" ,
147139 port ,
148- processId . Value ,
149- effectivePid ) ;
150-
151- if ( ! newServices . TryGetValue ( serviceName , out var uriList ) )
152- {
153- uriList = [ ] ;
154- newServices [ serviceName ] = uriList ;
155- }
140+ reportedPid . Value ,
141+ systemPid ) ;
156142
157- uriList . Add ( uri ) ;
143+ newApis [ systemPid ] = uri ;
158144 }
159145
160- this . discoveredServices = newServices ;
161- scopedLogger . LogDebug ( "Port scan complete. Found {Count} Daybreak API instance(s)" , newServices . Count ) ;
146+ this . discoveredApis = newApis ;
147+ scopedLogger . LogDebug ( "Port scan complete. Found {Count} Daybreak API instance(s)" , newApis . Count ) ;
162148 }
163149
164150 private async Task < ( int Port , int ? ProcessId ) > ProbePortAsync ( int port , CancellationToken cancellationToken )
0 commit comments