Skip to content
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

Console.Unix: also reset terminal state when process terminates due to an unhandled exception. #111272

Merged
merged 2 commits into from
Jan 13, 2025

Conversation

tmds
Copy link
Member

@tmds tmds commented Jan 10, 2025

Fixes #110502.

@adamsitnik @jkotas ptal.

@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Jan 10, 2025
@tmds
Copy link
Member Author

tmds commented Jan 10, 2025

I manually checked the behavior with this program:

if (args.Length > 0 && args[0] == "throw")
{
    Console.ReadLine();

    // Intentional crash
    int[] arr = new int[1];
    arr[100] = 10;
}
else
{
    Process.Start(Environment.ProcessPath!, [ Assembly.GetExecutingAssembly().Location, "throw"]).WaitForExit();
}

I don't think there's a good way to test this in System.Console.Manual.Tests because the .NET process will re-configure the terminal when the child exits and when using Console.ReadLine after that.

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the changes LGTM, but I would prefer @jkotas to sign of the part that registers for the AppDomain.CurrentDomain.UnhandledException event as I don't know all the pros and cons of doing so.

src/native/libs/System.Native/pal_console_wasi.c Outdated Show resolved Hide resolved

// InitializeTerminalAndSignalHandling will reset the terminal on a normal exit.
// This also resets it for termination due to an unhandled exception.
AppDomain.CurrentDomain.UnhandledException += delegate { Interop.Sys.UninitializeTerminal(); };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we should do that before the s_initialized is set to true (so setting s_initialized to true is the last thing that is part of the initialization logic)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put it here intentionally because we won't start modifying the terminal until we've reached this point.
At this location we're sure to only add the delegate once.

I'm fine with moving it higher up, though this location should also be fine.

@adamsitnik can you confirm you'd still like me to move this higher up?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We won't start modifying because there is a lock, but in theory once the flag is set to true some other thread can assume everything is initialized and it can be used.

internal static void EnsureConsoleInitialized()
{
if (!s_initialized)
{
EnsureInitializedCore(); // factored out for inlinability
}
}

I just like to have all initialization flags set to true when all the initialization logic is finished. I won't block the PR without it, but I think that in the future somebody can add some new initialization logic after your new logic and then we are going to be in trouble if we miss that in a code review.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theory once the flag is set to true some other thread can assume everything is initialized and it can be used.

Does it mean that registering after s_initialized is set increases chances that the app will terminate without resetting the terminal?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it mean that registering after s_initialized is set increases chances that the app will terminate without resetting the terminal?

Based on the logic for AppDomain.CurrentDomain I believe it's possible to be so (it seems to be non-trivial and can take some time, so in meantime something can use the console and then somehow crash the app).

public static AppDomain CurrentDomain
{
get
{
// Create AppDomain instance only once external code asks for it. AppDomain brings lots of unnecessary
// dependencies into minimal trimmed app via ToString method.
if (s_domain == null)
{
Interlocked.CompareExchange(ref s_domain, new AppDomain(), null);
}
return s_domain;
}
}

But the chances are very low.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW this comment made me realize that simplest "Hello World" app size after trimming may grow with this change. I am still not against it, just something to keep in mind.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the chances are very low.

I agree that the chances are low, but this ordering is needlessly increasing them.

simplest "Hello World" app size after trimming may grow with this change

This is true for a lot of PRs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true for a lot of PRs.

I know, but in case of Hello World there are some benchmarks that some people look at when comparing tech stacks. I just wanted to point it out since IIRC we may be having some automation that tracks such regressions. So then the person looking at it will find my comment and close it as "by design".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi, I explored what the implementation would need to implement it in a native signal handler. The challenge is that runtime will reset to the default SIGABRT handler (in PROCAbort by calling SEHCleanupSignals) and a handler that was added later for Console that chains back to the runtime handler is not called.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tmds big thanks for explaining why we have not relied on SIGABRT!

@adamsitnik adamsitnik added this to the 10.0.0 milestone Jan 10, 2025
@adamsitnik adamsitnik requested a review from jkotas January 10, 2025 16:32
@jkotas
Copy link
Member

jkotas commented Jan 10, 2025

LGTM modulo feedback

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you for your help @tmds !

@adamsitnik
Copy link
Member

/ba-g the timeout in the Windows job is unrelated

@adamsitnik adamsitnik merged commit 2f51a56 into dotnet:main Jan 13, 2025
100 of 104 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Console community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

dotnet run leaves Linux terminal in broken state after unhandled exception following Console.ReadLine
4 participants