Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/Aspire.Cli/Npm/NpmRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,11 @@ internal static ProcessStartInfo CreateNpmProcessStartInfo(string npmPath, strin
{
var startInfo = new ProcessStartInfo
{
// Redirect stdin so the child npm process (and any lifecycle scripts it invokes)
// does not inherit the CLI's TTY. The caller closes stdin immediately after Start()
// so any read surfaces as EOF instead of hanging waiting on the terminal. NpmRunner
// is intended to be fully non-interactive. See https://github.com/microsoft/aspire/issues/16791.
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
Expand Down Expand Up @@ -357,6 +362,17 @@ internal static bool TryExtractLastVersion(string npmOutput, [NotNullWhen(true)]
using var process = new Process { StartInfo = startInfo };
using var activity = profilingTelemetry.StartNpmCommand(npmPath, args, workingDirectory);
process.Start();
// Close stdin so any npm lifecycle script that tries to read terminal input
// sees EOF instead of blocking on the inherited TTY. See ProcessGuestLauncher
// and https://github.com/microsoft/aspire/issues/16791.
try
{
process.StandardInput.Close();
}
catch (IOException)
{
// The child may have already closed its stdin; ignore.
}
activity.SetProcessId(process.Id);

var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
Expand Down
13 changes: 13 additions & 0 deletions src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -518,10 +518,23 @@ public async Task<AppHostServerPrepareResult> PrepareAsync(
_logger.LogDebug("Enabling debug logging for AppHostServer");
}

startInfo.RedirectStandardInput = true;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;

var process = Process.Start(startInfo)!;
// Close the redirected stdin pipe immediately so the AppHost server process — and any
// child/library it loads — observes EOF rather than blocking on the parent CLI's TTY
// if it ever reads from stdin. The CLI communicates with the server over a Unix socket
// (REMOTE_APP_HOST_SOCKET_PATH), not stdin. See https://github.com/microsoft/aspire/issues/16791.
try
{
process.StandardInput.Close();
}
catch (IOException)
{
// The child may have already closed its stdin; ignore.
}

var outputCollector = new OutputCollector();
process.OutputDataReceived += (sender, e) =>
Expand Down
13 changes: 13 additions & 0 deletions src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,18 @@ private static string GetRestoreVersion(string packageName, string version, bool
var startInfo = CreateStartInfo(hostPid, environmentVariables, additionalArgs, debug);

var process = Process.Start(startInfo)!;
// Close the redirected stdin pipe immediately so the AppHost server process — and any
// child/library it loads — observes EOF rather than blocking on the parent CLI's TTY
// if it ever reads from stdin. The CLI communicates with the server over a Unix socket
// (REMOTE_APP_HOST_SOCKET_PATH), not stdin. See https://github.com/microsoft/aspire/issues/16791.
try
{
process.StandardInput.Close();
}
catch (IOException)
{
// The child may have already closed its stdin; ignore.
}

var outputCollector = new OutputCollector();
process.OutputDataReceived += (_, e) =>
Expand Down Expand Up @@ -980,6 +992,7 @@ internal ProcessStartInfo CreateStartInfo(
startInfo.Environment[KnownConfigNames.AspireLogLevel] = "Debug";
}

startInfo.RedirectStandardInput = true;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;

Expand Down
18 changes: 18 additions & 0 deletions src/Aspire.Cli/Projects/ProcessGuestLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ public ProcessGuestLauncher(string language, ILogger logger, FileLoggerProvider?
{
FileName = resolvedCommandPath,
WorkingDirectory = workingDirectory.FullName,
// Redirect stdin so the child does not inherit the CLI's TTY. Without this, on macOS/Linux
// any child (e.g. `npm install` postinstall scripts, husky, package-manager permission
// prompts) that reads from stdin will block forever waiting on the terminal, making
// `aspire new`/`init`/`add`/`restore` appear to stall with no output. We close stdin
// immediately after Start() below so a reader sees EOF instead of hanging.
// See https://github.com/microsoft/aspire/issues/16791.
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
Expand Down Expand Up @@ -123,6 +130,17 @@ public ProcessGuestLauncher(string language, ILogger logger, FileLoggerProvider?

AddEvent(activity, ProfilingTelemetry.Events.GuestProcessStart);
process.Start();
// Close the redirected stdin pipe immediately so any read attempt in the child surfaces
// as EOF rather than blocking on an empty pipe. We never write to the guest process
// stdin, so this is safe.
try
{
process.StandardInput.Close();
}
catch (IOException)
{
// The child may have already closed its stdin; ignore.
}
activity?.SetTag(TelemetryConstants.Tags.ProcessPid, process.Id);
AddEvent(activity, ProfilingTelemetry.Events.GuestProcessStarted, TelemetryConstants.Tags.ProcessPid, process.Id);
if (afterLaunchAsync is not null)
Expand Down
47 changes: 47 additions & 0 deletions tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,53 @@ public async Task ProcessGuestLauncher_AnnotatesAmbientGuestProfilingActivity()
Assert.Contains(activity.Events, @event => @event.Name == ProfilingTelemetry.Events.GuestProcessExited);
}

[Fact]
public async Task ProcessGuestLauncher_ClosesChildStdinSoReadsObserveEof()
{
// Regression coverage for https://github.com/microsoft/aspire/issues/16791.
// Before this fix, ProcessGuestLauncher did not redirect/close stdin, so a child
// process (e.g. `npm install` postinstall scripts on macOS) inherited the parent
// CLI's TTY and any stdin read blocked forever - making `aspire new` for the
// TypeScript starter appear to stall.
var launcher = new ProcessGuestLauncher(
"test",
_loggerFactory.CreateLogger<ProcessGuestLauncher>());

string command;
string[] args;
if (OperatingSystem.IsWindows())
{
// `set /p` reads a line from stdin. With redirected+closed stdin it sees EOF and
// exits immediately. With an inherited or open-empty stdin it would block.
command = "cmd.exe";
args = ["/c", "set /p line=<nul & echo eof"];
}
else
{
// `read` returns non-zero on EOF; the script prints `eof` and exits.
command = "sh";
args = ["-c", "if read line; then echo got-input; else echo eof; fi"];
}

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var stopwatch = Stopwatch.StartNew();

var (exitCode, output) = await launcher.LaunchAsync(
command,
args,
new DirectoryInfo(Path.GetTempPath()),
new Dictionary<string, string>(),
cts.Token);

stopwatch.Stop();

Assert.False(cts.IsCancellationRequested,
$"Child process did not exit on its own within 10s - stdin may not have been closed. Elapsed: {stopwatch.Elapsed}.");
Assert.Equal(0, exitCode);
var lines = output?.GetLines().Select(l => l.Line).ToArray() ?? [];
Assert.Contains(lines, l => l.Contains("eof", StringComparison.OrdinalIgnoreCase));
}

[Fact]
public async Task ProcessGuestLauncher_KillsProcessAndReturnsOnCancellation()
{
Expand Down
Loading