public void ValidateReentrancyPreventsReentrancyWithFuncVariant() { var reentrancyGuard = new ReentrancyGuard(); bool isInGuard = false; bool executedGuardedCode = false; using (ManualResetEvent nonReentrantCode = new ManualResetEvent(false)) using (ManualResetEvent nonReentrantCodeDone = new ManualResetEvent(false)) { reentrancyGuard.IsReentrancyPrevented.Should().BeFalse(); var allowedThread = new Thread(() => { ReentrancyGuard.CallReentrancySafe(reentrancyGuard, () => { nonReentrantCode.Set(); isInGuard = true; nonReentrantCodeDone.WaitOne(); isInGuard = false; executedGuardedCode = true; return(executedGuardedCode); }).Should().Be(ReentrancyGuard.ReentrancyCallResult.Success); executedGuardedCode.ShouldBeEquivalentTo(true); }); allowedThread.Start(); nonReentrantCode.WaitOne(TimeSpan.FromSeconds(1)).Should().BeTrue(); for (int i = 0; i < 1000; i++) { reentrancyGuard.IsReentrancyPrevented.Should().BeTrue(); ReentrancyGuard.CallReentrancySafe(reentrancyGuard, () => { throw new Exception("Reentered method despite guard being present"); #pragma warning disable 162 return(true); #pragma warning restore 162 }).ShouldBeEquivalentTo(ReentrancyGuard.ReentrancyCallResult.GuardBlocked); } nonReentrantCodeDone.Set(); allowedThread.Join(); reentrancyGuard.IsReentrancyPrevented.ShouldBeEquivalentTo(false); executedGuardedCode.ShouldBeEquivalentTo(true); } }
public CommandState GetCommandState <T>(Func <ITextView, ITextBuffer, T> argsFactory, Func <CommandState> nextCommandHandler) where T : EditorCommandArgs { if (!_factory.JoinableTaskContext.IsOnMainThread) { throw new InvalidOperationException($"{nameof(IEditorCommandHandlerService.GetCommandState)} method shoudl only be called on the UI thread."); } // In Razor scenario it's possible that EditorCommandHandlerService is called re-entrantly, // first by contained language command filter and then by editor command chain. // To preserve Razor commanding semantics, only execute handlers once. if (IsReentrantCall()) { return(nextCommandHandler?.Invoke() ?? CommandState.Unavailable); } using (var reentrancyGuard = new ReentrancyGuard(_textView)) { // Build up chain of handlers per buffer Func <CommandState> handlerChain = nextCommandHandler ?? UnavalableCommandFunc; foreach (var bufferAndHandler in GetOrderedBuffersAndCommandHandlers <T>().Reverse()) { T args = null; // Declare locals to ensure that we don't end up capturing the wrong thing var nextHandler = handlerChain; var handler = bufferAndHandler.handler; args = args ?? (args = argsFactory(_textView, bufferAndHandler.buffer)); if (args == null) { // Args factory failed, skip command handlers and just call next return(handlerChain()); } handlerChain = () => handler.GetCommandState(args, nextHandler); } // Kick off the first command handler return(handlerChain()); } }
public void Execute <T>(Func <ITextView, ITextBuffer, T> argsFactory, Action nextCommandHandler) where T : EditorCommandArgs { if (!_factory.JoinableTaskContext.IsOnMainThread) { throw new InvalidOperationException($"{nameof(IEditorCommandHandlerService.Execute)} method should only be called on the UI thread."); } // In contained languge (Razor) scenario it's possible that EditorCommandHandlerService is called re-entrantly // for the same command, first by contained language command filter and then by editor command chain. // To preserve Razor commanding semantics, only execute handlers once for the same command. if (IsReentrantCall <T>()) { nextCommandHandler?.Invoke(); return; } EditorCommandHandlerServiceState state = null; using (var reentrancyGuard = new ReentrancyGuard <T>(_textView)) { // Build up chain of handlers per buffer Action handlerChain = nextCommandHandler ?? EmptyAction; // TODO: realize the chain dynamically and without Reverse() foreach (var bufferAndHandler in GetOrderedBuffersAndCommandHandlers <T>().Reverse()) { T args = null; // Declare locals to ensure that we don't end up capturing the wrong thing var nextHandler = handlerChain; var handler = bufferAndHandler.handler; args = args ?? (args = argsFactory(_textView, bufferAndHandler.buffer)); if (args == null) { // Args factory failed, skip command handlers and just call next handlerChain(); return; } if (handler is IDynamicCommandHandler <T> dynamicCommandHandler && !dynamicCommandHandler.CanExecuteCommand(args)) { // Skip this one as it cannot execute the command. continue; } if (state == null) { state = InitializeExecutionState(args); } handlerChain = () => _factory.GuardedOperations.CallExtensionPoint(handler, () => { state.OnExecutingCommandHandlerBegin(handler); handler.ExecuteCommand(args, nextHandler, state.ExecutionContext); state.OnExecutingCommandHandlerEnd(handler); }, // Do not guard against cancellation exceptions, they are handled by ExecuteCommandHandlerChain exceptionGuardFilter: (e) => !IsOperationCancelledException(e)); } if (state == null) { // No matching command handlers, just call next handlerChain(); return; } _telemetrySession?.BeforeKeyProcessed(); ExecuteCommandHandlerChain(state, handlerChain, nextCommandHandler); _telemetrySession?.AfterKeyProcessed(); } }
private bool IsReentrantCall <T>() where T : EditorCommandArgs { return(ReentrancyGuard <T> .IsReentrantCall(_textView)); }