379379var metricsCollector = new MetricsCollector ( "sharpcoredb-server" ) ;
380380builder . Services . AddSingleton ( metricsCollector ) ;
381381builder . Services . AddSingleton < HealthCheckService > ( ) ;
382+ builder . Services . AddSingleton < StartupState > ( ) ;
382383
383384// Add health checks
384385builder . Services . AddHealthChecks ( ) ;
473474} ) ;
474475
475476// Map API health endpoint used by smoke tests and external tooling
476- app . MapGet ( "/api/v1/health" , ( HealthCheckService healthService ) =>
477- {
478- var health = healthService . GetDetailedHealth ( ) ;
479- return Results . Ok ( new
480- {
481- status = health . Status ,
482- version = health . Version ,
483- timestamp = health . Timestamp ,
484- } ) ;
485- } )
486- . WithName ( "Health" )
487- . AllowAnonymous ( )
488- . Produces ( StatusCodes . Status200OK ) ;
477+ // Health is served by DatabaseController so it can share the REST API surface
478+ // without duplicating routes.
489479
490480// Map detailed health endpoint
491- app . MapGet ( "/api/v1/health/detailed" , ( HealthCheckService healthService ) =>
481+ app . MapGet ( "/api/v1/health/detailed" , ( HealthCheckService healthService , StartupState startupState ) =>
492482{
483+ if ( ! startupState . IsReady )
484+ {
485+ return Results . Json ( new
486+ {
487+ status = startupState . ErrorMessage is null ? "starting" : "failed" ,
488+ error = startupState . ErrorMessage ,
489+ } , statusCode : StatusCodes . Status503ServiceUnavailable ) ;
490+ }
491+
493492 var health = healthService . GetDetailedHealth ( ) ;
494493 return Results . Ok ( health ) ;
495494} )
496495. WithName ( "DetailedHealth" )
497496. AllowAnonymous ( )
498- . Produces < ServerHealthInfo > ( StatusCodes . Status200OK ) ;
497+ . Produces < ServerHealthInfo > ( StatusCodes . Status200OK )
498+ . Produces ( StatusCodes . Status503ServiceUnavailable ) ;
499499
500500// Start the server
501501Log . Information ( "Starting SharpCoreDB Server v1.7.0" ) ;
536536
537537try
538538{
539- // Initialize database registry before constructing startup-dependent services.
539+ var startupState = app . Services . GetRequiredService < StartupState > ( ) ;
540+
541+ await app . StartAsync ( app . Lifetime . ApplicationStopping ) ;
542+
540543 var databaseRegistry = app . Services . GetRequiredService < DatabaseRegistry > ( ) ;
541544 await databaseRegistry . InitializeAsync ( app . Lifetime . ApplicationStopping ) ;
542545
543- // Start the network server
544546 var networkServer = app . Services . GetRequiredService < NetworkServer > ( ) ;
545547 await networkServer . StartAsync ( app . Lifetime . ApplicationStopping ) ;
546548
547- // Run the web host
548- await app . RunAsync ( ) ;
549+ startupState . MarkReady ( ) ;
550+ await app . WaitForShutdownAsync ( app . Lifetime . ApplicationStopping ) ;
549551}
550552catch ( InvalidOperationException ex )
551553{
554+ app . Services . GetRequiredService < StartupState > ( ) . MarkFailed ( ex . Message ) ;
552555 Log . Fatal ( ex , "SharpCoreDB Server failed startup validation" ) ;
553556}
554557finally
@@ -735,7 +738,7 @@ static void ConfigureHttpsEndpoint(ListenOptions listenOptions, SecurityConfigur
735738 throw new InvalidOperationException ( "--appsettings requires a non-empty path value." ) ;
736739 }
737740
738- return Path . GetFullPath ( args [ i + 1 ] ) ;
741+ return ResolveCustomAppSettingsPath ( args [ i + 1 ] ) ;
739742 }
740743
741744 const string Prefix = "--appsettings=" ;
@@ -747,9 +750,71 @@ static void ConfigureHttpsEndpoint(ListenOptions listenOptions, SecurityConfigur
747750 throw new InvalidOperationException ( "--appsettings requires a non-empty path value." ) ;
748751 }
749752
750- return Path . GetFullPath ( path ) ;
753+ return ResolveCustomAppSettingsPath ( path ) ;
751754 }
752755 }
753756
754757 return null ;
755758}
759+
760+ static string ResolveCustomAppSettingsPath ( string path )
761+ {
762+ ArgumentException . ThrowIfNullOrWhiteSpace ( path ) ;
763+
764+ if ( Path . IsPathRooted ( path ) )
765+ {
766+ return Path . GetFullPath ( path ) ;
767+ }
768+
769+ var visitedCandidates = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
770+ foreach ( var basePath in GetCandidateBaseDirectories ( ) )
771+ {
772+ string candidate ;
773+ try
774+ {
775+ candidate = Path . GetFullPath ( path , basePath ) ;
776+ }
777+ catch ( Exception )
778+ {
779+ continue ;
780+ }
781+
782+ if ( ! visitedCandidates . Add ( candidate ) )
783+ {
784+ continue ;
785+ }
786+
787+ if ( File . Exists ( candidate ) )
788+ {
789+ return candidate ;
790+ }
791+ }
792+
793+ return Path . GetFullPath ( path ) ;
794+ }
795+
796+ static IEnumerable < string > GetCandidateBaseDirectories ( )
797+ {
798+ foreach ( var baseDirectory in EnumerateBaseDirectories ( ) )
799+ {
800+ if ( string . IsNullOrWhiteSpace ( baseDirectory ) || ! Directory . Exists ( baseDirectory ) )
801+ {
802+ continue ;
803+ }
804+
805+ var directory = new DirectoryInfo ( baseDirectory ) ;
806+ while ( directory is not null )
807+ {
808+ yield return directory . FullName ;
809+ directory = directory . Parent ;
810+ }
811+ }
812+ }
813+
814+ static IEnumerable < string ? > EnumerateBaseDirectories ( )
815+ {
816+ yield return Environment . GetEnvironmentVariable ( "GITHUB_WORKSPACE" ) ;
817+ yield return Environment . GetEnvironmentVariable ( "PWD" ) ;
818+ yield return Environment . CurrentDirectory ;
819+ yield return AppContext . BaseDirectory ;
820+ }
0 commit comments