-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
unregister onExit event handler on disposing interactive host #24058
Conversation
@@ -61,6 +62,7 @@ internal void HookAutoRestartEvent() | |||
if (Interlocked.Exchange(ref processExitHandling, ProcessExitHandled) == ProcessExitHooked) | |||
{ | |||
Process.Exited -= localHandler; | |||
_currentLocalHandler = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the local method now vs. just a regular good old fashioned method? I realize it was nice when it was localized to this, but that ship has sailed. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think tossing the local method now makes this easier to follow.
@@ -115,6 +115,10 @@ internal void Dispose(bool joinThreads) | |||
{ | |||
// set _disposing so that we don't attempt restart the host anymore: | |||
_disposing = true; | |||
if (Interlocked.Exchange(ref _processExitHandling, ProcessExitHandled) == ProcessExitHooked) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if [](start = 16, length = 2)
This doesn't guarantee that ProcessExitedHandler does not call _host.OnProcesExited after we return from Dispose.
There is a race condition:
- ProcessExitedHandler has already been called and in it _disposing already tested, yet OnProcessExited has not been called yet
- Dispose is called
- Dispose returns
- ProcessExitedHandler is resumed and calls OnProcessExited #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🕐
} | ||
|
||
private void Dispose(bool joinThreads, bool disposing) | ||
{ | ||
if (disposing) | ||
using (_disposeLock.DisposableWait()) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the benefit of using DisposableWait, vs. just _disposeLock.Wait(); _disposeLock.Release();
? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, we should probably not wait when running in finalizer (disposing == false).
In reply to: 160765822 [](ancestors = 160765822)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more thing: Since we access _disposeLock in destructor we should guard against it being null, in case the constructor of this class doesn't run to completion.
var semaphore = _disposeSemaphore;
if (semaphore != null && disposing)
{
semaphore.Wait();
semaphore.Dispose();
}
In reply to: 160766053 [](ancestors = 160766053,160765822)
@@ -231,21 +232,24 @@ private bool CheckAlive(Process process) | |||
|
|||
public void Dispose() | |||
{ | |||
Dispose(joinThreads: false, disposing: true); | |||
Dispose(joinThreads: true, disposing: true); | |||
} | |||
|
|||
private void Dispose(bool joinThreads, bool disposing) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bool joinThreads, bool disposing [](start = 29, length = 32)
I think we should remove joinThreads and use disposing instead on line 250. We don't want to be waiting during finalization (on threads to join). #Resolved
} | ||
if (disposing) | ||
{ | ||
GC.SuppressFinalize(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GC.SuppressFinalize(this); [](start = 20, length = 26)
Let's move GC.SuppressFinalize(this)
to the end of Dispose()
to follow the common pattern. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What pattern do you mean? I see various position of GC.SuppressFinalize(this) within Dispose(): in the beginning, in the middle and in the end.
What is the purpose to move it to the end?
In reply to: 160767337 [](ancestors = 160767337)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the object is taken out of the finalizer queue and then dispose fails, it's never gonna be finalized. So the idea is that SuppressFinalize should be the last call of Dispose in order to avoid that. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -31,6 +31,7 @@ internal sealed partial class InteractiveHost : MarshalByRefObject | |||
// adjustable for testing purposes | |||
private readonly int _millisecondsTimeout; | |||
private const int MaxAttemptsToCreateProcess = 2; | |||
private readonly SemaphoreSlim _disposeLock = new SemaphoreSlim(initialCount: 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_disposeLock [](start = 39, length = 12)
Let's call this _disposeSemaphore, since it's not actually a lock. #Resolved
@tmat, could you please review? #Resolved |
if (!_disposing) | ||
if (!_disposing) | ||
{ | ||
using (await _disposeSemaphore.DisposableWaitAsync().ConfigureAwait(false)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using (await _disposeSemaphore.DisposableWaitAsync().ConfigureAwait(false)) [](start = 28, length = 75)
I'm not sure ho is this supposed to work. Should we just signal (call _disposeSemaphore.Release()
) when this event is finished? #Resolved
if (Interlocked.Exchange(ref _processExitHandling, ProcessExitHandled) == ProcessExitHooked) | ||
{ | ||
Process.Exited -= ProcessExitedHandler; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For maintain some consistency I'd also signal the semaphore here (call _disposeSemaphore.Release()
) and add a comment on that semaphore that it is signaled when the ProcessExited handler is removed and can no longer be executed. #Resolved
@@ -31,6 +31,7 @@ internal sealed partial class InteractiveHost : MarshalByRefObject | |||
// adjustable for testing purposes | |||
private readonly int _millisecondsTimeout; | |||
private const int MaxAttemptsToCreateProcess = 2; | |||
private readonly SemaphoreSlim _disposeSemaphore = new SemaphoreSlim(initialCount: 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1 [](start = 91, length = 1)
Shouldn't this start in non-signaled state (initialCount: 0)?
Actually, since we only need two states we should use ManualResetEventSlim instead of SemaphoreSlim. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more thing: i think this should be moved to the RemoteService instance and used in its Dispose method. The InteractiveHost itself doesn't really need to know about this -- it's all about disposing the RemoteService.
In reply to: 162140846 [](ancestors = 162140846)
{ | ||
semaphore.Wait(); | ||
semaphore.Dispose(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move this to the RemoteService.Dispose()
method.
#Resolved
{ | ||
await _host.OnProcessExited(Process).ConfigureAwait(false); | ||
} | ||
if (!_disposing && _host != null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_host != null [](start = 43, length = 13)
Good catch here, but there is still race. _host?.OnProcessExited(...)
would be better. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, since _disposing must be true before _host is set to null, _host can't be null at this point.
Make _disposing volatile field though, so that the JIT doesn't reorder.
In reply to: 162224558 [](ancestors = 162224558)
public readonly Process Process; | ||
public readonly Service Service; | ||
private readonly int _processId; | ||
// The semaphore is reset (closed) by default: | ||
private readonly ManualResetEventSlim _disposeSemaphore = new ManualResetEventSlim(initialState: false); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
object _disposeLock = new object(); #Resolved
{ | ||
try | ||
if (Interlocked.Exchange(ref _processExitHandling, ProcessExitHandled) == ProcessExitHooked) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (Interlocked.Exchange(ref _processExitHandling, ProcessExitHandled) == ProcessExitHooked) [](start = 20, length = 92)
bool invokeEvent = false;
lock (_disposeLock)
{
if (_processExitHandling == ProcessExitHooked)
{
Process.Exited -= ProcessExitedHandler;
invokeEvent =!_disposing;
_processExitHandling = ProcessExitHandled;
}
}
if (invokeEvent)
{
await _host.OnProcessExited(Process).ConfigureAwait(false);
}
``` #Resolved
// This semaphore should be set either in the block just above or by completion of another thread that was able to execute Interlocked.Exchange before. | ||
semaphore.Wait(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lock (_disposeLock)
{
_disposing = true;
if (_processExitHandling == ProcessExitHooked) {
Process.Exited -= ProcessExitedHandler;
_processExitHandling = ProcessExitHandled;
}
}
``` #Resolved
if (Interlocked.Exchange(ref _processExitHandling, ProcessExitHooked) == 0) | ||
{ | ||
Process.Exited += ProcessExitedHandler; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lock (_disposeLock)
{
if (_processExitHandling == 0)
{
_processExitHandling = ProcessExitHooked;
Process.Exited += ProcessExitedHandler;
}
}
#Resolved
@@ -116,34 +126,50 @@ internal void Dispose(bool joinThreads) | |||
// set _disposing so that we don't attempt restart the host anymore: | |||
_disposing = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_disposing = true; [](start = 16, length = 18)
This should also be inside the using. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually we don't need _disposing variable anymore.
In reply to: 162233741 [](ancestors = 162233741)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. For consistency, it is better to put it inside using.
In reply to: 162233741 [](ancestors = 162233741)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not just for consistency. Since this is now shared variable it would need to be under the lock. But we don't need it at all actually.
In reply to: 162234150 [](ancestors = 162234150,162233741)
{ | ||
Process.Exited -= localHandler; | ||
|
||
Process.Exited -= ProcessExitedHandler; | ||
if (!_disposing) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (!_disposing) [](start = 28, length = 16)
I think this check is not needed now. Once Dispose sets _processExitHandling to ProcessExitHandled we won't get here. #Resolved
3689744
to
ddf5bff
Compare
@tmat, @jinujoseph, @jasonmalinowski, please review. I would like to push this fix into 15.6 #Resolved |
@ivanbasov Does it meet the bar for escrow? I think moving it to 15.7 would be ok. #Resolved |
14f365e
to
68dfd2d
Compare
@tmat, could you please review? I hope I addressed all the comments. Thanks! #Resolved |
|
||
// output pumping threads (stream output from stdout/stderr of the host process to the output/errorOutput writers) | ||
private Thread _readOutputThread; // nulled on dispose | ||
private Thread _readErrorOutputThread; // nulled on dispose | ||
private InteractiveHost _host; // nulled on dispose | ||
private bool _disposing; // set to true on dispose | ||
private volatile int _processExitHandling; // set to ProcessExitHandled on dispose |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
int [](start = 29, length = 3)
Sorry, one more thing: now that we don't use Interlocked we could define the ProcessExitXXX values as an enum (ProcessExitHandlerStatus { Uninitialized = 0, Initialized = 1, Executed = 2 } ). Would make the code more readable.
#Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
Process.Exited -= ProcessExitedHandler; | ||
_processExitHandlerStatus = ProcessExitHandlerStatus.Handled; | ||
// Should set _processExitHandling before calling OnProcessExited to avoid deadlocks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_processExitHandling [](start = 42, length = 20)
nit: wrong name #Resolved
Tagging @MattGertz to approve merge into dev15.7.x |
@jasonmalinowski please unblock #Resolved |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ivanbasov I admit I still have some questions. I think there's some room for a better comment, maybe?
@@ -29,7 +29,7 @@ internal static string GetInteractiveHostPath() | |||
internal static void DisposeInteractiveHostProcess(InteractiveHost process) | |||
{ | |||
IpcServerChannel serverChannel = process._ServerChannel; | |||
process.Dispose(joinThreads: true); | |||
process.Dispose(disposing: true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just call the public overload? #Resolved
if (joinThreads) | ||
// There can be a call from host initiated from OnProcessExit. | ||
// This check on the beginning helps to avoid a reentrancy. | ||
if (_processExitHandlerStatus == ProcessExitHandlerStatus.Hooked) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the problem here that this can deadlock because if the _host.OnProcessExited() call is happening above and that's still holding the lock, and it tries to call back into Dispose(), that will deadlock? Why can't we call _host.OnProcessExited() outside the lock?
I guess a better comment here would be appreciated. The use of volatile
here is generally scary. I think it works, but that doesn't make it less scary. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(this question is best answered by just adding more comments so the code is clearer vs. answering my question in GitHub) #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, Jason! I added more comments to clarify the scenario.
Maybe there can be an option to avoid using volatile and removing the _host.OnProcessExited() outside of the lock. We could not see these options so far. The problem we fixed that the host is being disposed during _host.OnProcessExited() execution. So, the fix is to protect it from the dispose.
In reply to: 163994639 [](ancestors = 163994639)
Customer scenario
Customer shuts down Visual Studio. There is a race condition when closing the Interactive Window and the corresponding process.
When the process is closed before the window (within this or another scenario), it sends a message to display it in the interactive window. If the window was closed somewhere between checks the it is open and executing a command to output the message, an failure happens.
Bugs this fixes
VSO 514822 and corresponding: #24006
Workarounds, if any
None
Risk
Low
Performance impact
None
Is this a regression from a previous update?
No
How was the bug found?
Watson hits