33
44using System . CommandLine ;
55using System . Diagnostics ;
6+ using System . Diagnostics . CodeAnalysis ;
67using System . Globalization ;
78using System . Text . Json ;
89using System . Text . Json . Serialization ;
@@ -82,6 +83,11 @@ internal sealed class RunCommand : BaseCommand
8283 {
8384 Description = RunCommandStrings . IsolatedArgumentDescription
8485 } ;
86+ private static readonly Option < string ? > s_logFileOption = new ( "--log-file" )
87+ {
88+ Description = "Path to write the log file (used internally by --detach)." ,
89+ Hidden = true
90+ } ;
8591 private readonly Option < bool > ? _startDebugSessionOption ;
8692
8793 public RunCommand (
@@ -121,6 +127,7 @@ public RunCommand(
121127 Options . Add ( s_detachOption ) ;
122128 Options . Add ( s_formatOption ) ;
123129 Options . Add ( s_isolatedOption ) ;
130+ Options . Add ( s_logFileOption ) ;
124131
125132 if ( ExtensionHelper . IsExtensionHost ( InteractionService , out _ , out _ ) )
126133 {
@@ -134,6 +141,7 @@ public RunCommand(
134141 TreatUnmatchedTokensAsErrors = false ;
135142 }
136143
144+ [ RequiresUnreferencedCode ( "Calls Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue<T>(String, T)" ) ]
137145 protected override async Task < int > ExecuteAsync ( ParseResult parseResult , CancellationToken cancellationToken )
138146 {
139147 var passedAppHostProjectFile = parseResult . GetValue ( s_projectOption ) ;
@@ -476,7 +484,7 @@ internal static int RenderAppHostSummary(
476484 new Align ( new Markup ( $ "[bold green]{ dashboardLabel } [/]:") , HorizontalAlignment . Right ) ,
477485 new Markup ( "[dim]N/A[/]" ) ) ;
478486 }
479- grid . AddRow ( Text . Empty , Text . Empty ) ;
487+ grid . AddRow ( Text . Empty , Text . Empty ) ;
480488 }
481489
482490 // Logs row
@@ -639,18 +647,23 @@ private async Task<int> ExecuteDetachedAsync(ParseResult parseResult, FileInfo?
639647 _logger . LogDebug ( "Found {Count} running instance(s) for this AppHost, stopping them first" , existingSockets . Length ) ;
640648 var manager = new RunningInstanceManager ( _logger , _interactionService , _timeProvider ) ;
641649 // Stop all running instances in parallel - don't block on failures
642- var stopTasks = existingSockets . Select ( socket =>
650+ var stopTasks = existingSockets . Select ( socket =>
643651 manager . StopRunningInstanceAsync ( socket , cancellationToken ) ) ;
644652 await Task . WhenAll ( stopTasks ) . ConfigureAwait ( false ) ;
645653 }
646654
647655 // Build the arguments for the child CLI process
656+ // Tell the child where to write its log so we can find it on failure.
657+ var childLogFile = GenerateChildLogFilePath ( ) ;
658+
648659 var args = new List < string >
649660 {
650661 "run" ,
651662 "--non-interactive" ,
652663 "--project" ,
653- effectiveAppHostFile . FullName
664+ effectiveAppHostFile . FullName ,
665+ "--log-file" ,
666+ childLogFile
654667 } ;
655668
656669 // Pass through global options that should be forwarded to child CLI
@@ -687,14 +700,17 @@ private async Task<int> ExecuteDetachedAsync(ParseResult parseResult, FileInfo?
687700 dotnetPath , isDotnetHost , string . Join ( " " , args ) ) ;
688701 _logger . LogDebug ( "Working directory: {WorkingDirectory}" , ExecutionContext . WorkingDirectory . FullName ) ;
689702
690- // Redirect stdout/stderr to suppress child output - it writes to log file anyway
703+ // Don't redirect stdout/stderr - child writes to log file anyway.
704+ // Redirecting creates pipe handles that get inherited by the AppHost grandchild,
705+ // which prevents callers using synchronous process APIs (e.g. execSync) from
706+ // detecting that the CLI has exited, since the pipe stays open until the AppHost dies.
691707 var startInfo = new ProcessStartInfo
692708 {
693709 FileName = dotnetPath ,
694710 UseShellExecute = false ,
695711 CreateNoWindow = true ,
696- RedirectStandardOutput = true ,
697- RedirectStandardError = true ,
712+ RedirectStandardOutput = false ,
713+ RedirectStandardError = false ,
698714 RedirectStandardInput = false ,
699715 WorkingDirectory = ExecutionContext . WorkingDirectory . FullName
700716 } ;
@@ -727,24 +743,6 @@ private async Task<int> ExecuteDetachedAsync(ParseResult parseResult, FileInfo?
727743 return null ;
728744 }
729745
730- // Start async reading of stdout/stderr to prevent buffer blocking
731- // Log output for debugging purposes
732- childProcess . OutputDataReceived += ( _ , e ) =>
733- {
734- if ( e . Data is not null )
735- {
736- _logger . LogDebug ( "Child stdout: {Line}" , e . Data ) ;
737- }
738- } ;
739- childProcess . ErrorDataReceived += ( _ , e ) =>
740- {
741- if ( e . Data is not null )
742- {
743- _logger . LogDebug ( "Child stderr: {Line}" , e . Data ) ;
744- }
745- } ;
746- childProcess . BeginOutputReadLine ( ) ;
747- childProcess . BeginErrorReadLine ( ) ;
748746 }
749747 catch ( Exception ex )
750748 {
@@ -823,10 +821,13 @@ private async Task<int> ExecuteDetachedAsync(ParseResult parseResult, FileInfo?
823821
824822 if ( childExitedEarly )
825823 {
826- _interactionService . DisplayError ( string . Format (
827- CultureInfo . CurrentCulture ,
828- RunCommandStrings . AppHostExitedWithCode ,
829- childExitCode ) ) ;
824+ // Show a friendly message based on well-known exit codes from the child
825+ var errorMessage = childExitCode switch
826+ {
827+ ExitCodeConstants . FailedToBuildArtifacts => RunCommandStrings . AppHostFailedToBuild ,
828+ _ => string . Format ( CultureInfo . CurrentCulture , RunCommandStrings . AppHostExitedWithCode , childExitCode )
829+ } ;
830+ _interactionService . DisplayError ( errorMessage ) ;
830831 }
831832 else
832833 {
@@ -846,11 +847,11 @@ private async Task<int> ExecuteDetachedAsync(ParseResult parseResult, FileInfo?
846847 }
847848 }
848849
849- // Always show log file path for troubleshooting
850+ // Point to the child's log file — it contains the actual build/runtime errors
850851 _interactionService . DisplayMessage ( "magnifying_glass_tilted_right" , string . Format (
851852 CultureInfo . CurrentCulture ,
852853 RunCommandStrings . CheckLogsForDetails ,
853- _fileLoggerProvider . LogFilePath . EscapeMarkup ( ) ) ) ;
854+ childLogFile . EscapeMarkup ( ) ) ) ;
854855
855856 return ExitCodeConstants . FailedToDotnetRunAppHost ;
856857 }
@@ -893,4 +894,12 @@ private async Task<int> ExecuteDetachedAsync(ParseResult parseResult, FileInfo?
893894
894895 return ExitCodeConstants . Success ;
895896 }
897+
898+ private string GenerateChildLogFilePath ( )
899+ {
900+ return Diagnostics . FileLoggerProvider . GenerateLogFilePath (
901+ ExecutionContext . LogsDirectory . FullName ,
902+ _timeProvider ,
903+ suffix : "detach-child" ) ;
904+ }
896905}
0 commit comments