Skip to content

Process termination #49335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 16, 2025
Merged
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
1 change: 1 addition & 0 deletions src/BuiltInTools/dotnet-watch/DotNetWatchContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ internal sealed class DotNetWatchContext
public required GlobalOptions Options { get; init; }
public required EnvironmentOptions EnvironmentOptions { get; init; }
public required IReporter Reporter { get; init; }
public required ProcessRunner ProcessRunner { get; init; }

public required ProjectOptions RootProjectOptions { get; init; }
}
Expand Down
2 changes: 1 addition & 1 deletion src/BuiltInTools/dotnet-watch/DotNetWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke

fileSetWatcher.WatchContainingDirectories(evaluationResult.Files.Keys, includeSubdirectories: true);

var processTask = ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: true, launchResult: null, combinedCancellationSource.Token);
var processTask = Context.ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: true, launchResult: null, combinedCancellationSource.Token);

Task<ChangedFile?> fileSetTask;
Task finishedTask;
Expand Down
13 changes: 5 additions & 8 deletions src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal sealed class CompilationHandler : IDisposable
public readonly EnvironmentOptions EnvironmentOptions;
private readonly IReporter _reporter;
private readonly WatchHotReloadService _hotReloadService;
private readonly ProcessRunner _processRunner;

/// <summary>
/// Lock to synchronize:
Expand All @@ -36,17 +37,15 @@ internal sealed class CompilationHandler : IDisposable
/// </summary>
private ImmutableList<WatchHotReloadService.Update> _previousUpdates = [];

private readonly CancellationToken _shutdownCancellationToken;

private bool _isDisposed;

public CompilationHandler(IReporter reporter, EnvironmentOptions environmentOptions, CancellationToken shutdownCancellationToken)
public CompilationHandler(IReporter reporter, ProcessRunner processRunner, EnvironmentOptions environmentOptions)
{
_reporter = reporter;
_processRunner = processRunner;
EnvironmentOptions = environmentOptions;
Workspace = new IncrementalMSBuildWorkspace(reporter);
_hotReloadService = new WatchHotReloadService(Workspace.CurrentSolution.Services, () => ValueTask.FromResult(GetAggregateCapabilities()));
_shutdownCancellationToken = shutdownCancellationToken;
}

public void Dispose()
Expand Down Expand Up @@ -88,7 +87,6 @@ public void DiscardProjectBaselines(ImmutableDictionary<ProjectId, string> proje
public void UpdateProjectBaselines(ImmutableDictionary<ProjectId, string> projectsToBeRebuilt, CancellationToken cancellationToken)
{
_hotReloadService.UpdateBaselines(Workspace.CurrentSolution, projectsToBeRebuilt.Keys.ToImmutableArray());
_reporter.Report(MessageDescriptor.ProjectBaselinesUpdated);
}

public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
Expand Down Expand Up @@ -138,7 +136,7 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
};

var launchResult = new ProcessLaunchResult();
var runningProcess = ProcessRunner.RunAsync(processSpec, processReporter, isUserApplication: true, launchResult, processTerminationSource.Token);
var runningProcess = _processRunner.RunAsync(processSpec, processReporter, isUserApplication: true, launchResult, processTerminationSource.Token);
if (launchResult.ProcessId == null)
{
// error already reported
Expand All @@ -152,7 +150,6 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
var runningProject = new RunningProject(
projectNode,
projectOptions,
EnvironmentOptions,
deltaApplier,
processReporter,
browserRefreshServer,
Expand Down Expand Up @@ -659,7 +656,7 @@ public bool TryGetRunningProject(string projectPath, out ImmutableArray<RunningP
private async ValueTask<IReadOnlyList<int>> TerminateRunningProjects(IEnumerable<RunningProject> projects, CancellationToken cancellationToken)
{
// wait for all tasks to complete:
return await Task.WhenAll(projects.Select(p => p.TerminateAsync(_shutdownCancellationToken).AsTask())).WaitAsync(cancellationToken);
return await Task.WhenAll(projects.Select(p => p.TerminateAsync().AsTask())).WaitAsync(cancellationToken);
}

private static Task ForEachProjectAsync(ImmutableDictionary<string, ImmutableArray<RunningProject>> projects, Func<RunningProject, CancellationToken, Task> action, CancellationToken cancellationToken)
Expand Down
16 changes: 1 addition & 15 deletions src/BuiltInTools/dotnet-watch/HotReload/RunningProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ namespace Microsoft.DotNet.Watch
internal sealed class RunningProject(
ProjectGraphNode projectNode,
ProjectOptions options,
EnvironmentOptions environmentOptions,
DeltaApplier deltaApplier,
IReporter reporter,
BrowserRefreshServer? browserRefreshServer,
Expand Down Expand Up @@ -70,21 +69,8 @@ public async ValueTask WaitForProcessRunningAsync(CancellationToken cancellation
await DeltaApplier.WaitForProcessRunningAsync(cancellationToken);
}

public async ValueTask<int> TerminateAsync(CancellationToken shutdownCancellationToken)
public async ValueTask<int> TerminateAsync()
{
if (shutdownCancellationToken.IsCancellationRequested)
{
// Ctrl+C sent, wait for the process to exit
try
{
_ = await RunningProcess.WaitAsync(environmentOptions.ProcessCleanupTimeout, CancellationToken.None);
}
catch (TimeoutException)
{
// nop
}
}

ProcessTerminationSource.Cancel();
return await RunningProcess;
}
Expand Down
12 changes: 9 additions & 3 deletions src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
}

var projectMap = new ProjectNodeMap(evaluationResult.ProjectGraph, Context.Reporter);
compilationHandler = new CompilationHandler(Context.Reporter, Context.EnvironmentOptions, shutdownCancellationToken);
compilationHandler = new CompilationHandler(Context.Reporter, Context.ProcessRunner, Context.EnvironmentOptions);
var scopedCssFileHandler = new ScopedCssFileHandler(Context.Reporter, projectMap, browserConnector);
var projectLauncher = new ProjectLauncher(Context, projectMap, browserConnector, compilationHandler, iteration);
var outputDirectories = GetProjectOutputDirectories(evaluationResult.ProjectGraph);
Expand Down Expand Up @@ -369,6 +369,8 @@ void FileChangedCallback(ChangedPath change)

// Update project baselines to reflect changes to the restarted projects.
compilationHandler.UpdateProjectBaselines(projectsToRebuild, iterationCancellationToken);

Context.Reporter.Report(MessageDescriptor.ProjectsRebuilt, projectsToRebuild.Count);
}

if (projectsToRestart is not [])
Expand All @@ -392,6 +394,8 @@ await Task.WhenAll(
}
}))
.WaitAsync(shutdownCancellationToken);

Context.Reporter.Report(MessageDescriptor.ProjectsRestarted, projectsToRestart.Length);
}

async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDictionary<ProjectId, string>? rebuiltProjects)
Expand Down Expand Up @@ -455,6 +459,8 @@ async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDict
changedFiles = changedFiles
.Select(f => evaluationResult.Files.TryGetValue(f.Item.FilePath, out var evaluatedFile) ? f with { Item = evaluatedFile } : f)
.ToImmutableList();

Context.Reporter.Report(MessageDescriptor.ReEvaluationCompleted);
}

if (rebuiltProjects != null)
Expand Down Expand Up @@ -527,7 +533,7 @@ async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDict

if (rootRunningProject != null)
{
await rootRunningProject.TerminateAsync(shutdownCancellationToken);
await rootRunningProject.TerminateAsync();
}

if (runtimeProcessLauncher != null)
Expand Down Expand Up @@ -831,7 +837,7 @@ await FileWatcher.WaitForFileChangeAsync(

Context.Reporter.Output($"Building {projectPath} ...");

var exitCode = await ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: false, launchResult: null, cancellationToken);
var exitCode = await Context.ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: false, launchResult: null, cancellationToken);
return (exitCode == 0, buildOutput.ToImmutableArray(), projectPath);
}

Expand Down
5 changes: 3 additions & 2 deletions src/BuiltInTools/dotnet-watch/Internal/IReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ public MessageDescriptor ToErrorWhen(bool condition)
// predefined messages used for testing:
public static readonly MessageDescriptor HotReloadSessionStarting = new(Format: null, Emoji: null, MessageSeverity.None, s_id++);
public static readonly MessageDescriptor HotReloadSessionStarted = new("Hot reload session started.", HotReloadEmoji, MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor ProjectBaselinesUpdated = new("Project baselines updated.", HotReloadEmoji, MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor ProjectsRebuilt = new("Projects rebuilt ({0})", HotReloadEmoji, MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor ProjectsRestarted = new("Projects restarted ({0})", HotReloadEmoji, MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor FixBuildError = new("Fix the error to continue or press Ctrl+C to exit.", WatchEmoji, MessageSeverity.Warning, s_id++);
public static readonly MessageDescriptor WaitingForChanges = new("Waiting for changes", WatchEmoji, MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor LaunchedProcess = new("Launched '{0}' with arguments '{1}': process id {2}", LaunchEmoji, MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor KillingProcess = new("Killing process {0}", WatchEmoji, MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor HotReloadChangeHandled = new("Hot reload change handled in {0}ms.", HotReloadEmoji, MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor HotReloadSucceeded = new("Hot reload succeeded.", HotReloadEmoji, MessageSeverity.Output, s_id++);
public static readonly MessageDescriptor UpdatesApplied = new("Updates applied: {0} out of {1}.", HotReloadEmoji, MessageSeverity.Verbose, s_id++);
Expand All @@ -86,6 +86,7 @@ public MessageDescriptor ToErrorWhen(bool condition)
public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = new("Ignoring change in hidden directory '{0}': {1} '{2}'", WatchEmoji, MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = new("Ignoring change in output directory: {0} '{1}'", WatchEmoji, MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = new("File addition triggered re-evaluation.", WatchEmoji, MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor ReEvaluationCompleted = new("Re-evaluation completed.", WatchEmoji, MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor NoCSharpChangesToApply = new("No C# changes to apply.", WatchEmoji, MessageSeverity.Output, s_id++);
public static readonly MessageDescriptor Exited = new("Exited", WatchEmoji, MessageSeverity.Output, s_id++);
public static readonly MessageDescriptor ExitedWithUnknownErrorCode = new("Exited with unknown error code", ErrorEmoji, MessageSeverity.Error, s_id++);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal class MSBuildFileSetFactory(
string rootProjectFile,
IEnumerable<string> buildArguments,
EnvironmentOptions environmentOptions,
ProcessRunner processRunner,
IReporter reporter)
{
private const string TargetName = "GenerateWatchList";
Expand Down Expand Up @@ -53,7 +54,7 @@ internal class MSBuildFileSetFactory(

reporter.Verbose($"Running MSBuild target '{TargetName}' on '{rootProjectFile}'");

var exitCode = await ProcessRunner.RunAsync(processSpec, reporter, isUserApplication: false, launchResult: null, cancellationToken);
var exitCode = await processRunner.RunAsync(processSpec, reporter, isUserApplication: false, launchResult: null, cancellationToken);

var success = exitCode == 0 && File.Exists(watchList);

Expand Down
Loading
Loading