diff --git a/src/OneScript.DebugProtocol/TcpServer/DefaultMessageServer.cs b/src/OneScript.DebugProtocol/TcpServer/DefaultMessageServer.cs index ad86b4636..34b36e3da 100644 --- a/src/OneScript.DebugProtocol/TcpServer/DefaultMessageServer.cs +++ b/src/OneScript.DebugProtocol/TcpServer/DefaultMessageServer.cs @@ -59,12 +59,9 @@ private void RunCommandsLoop() } catch (ChannelException e) { - if (e.StopChannel) - { - // критичные исключения сразу должны завершать сервер - _serverStopped = true; - break; - } + // критичные исключения (например, соединение закрыто) завершают сервер, + // но подписчики узнают о них, чтобы закрыть свою сессию + var critical = e.StopChannel; var eventData = new CommunicationEventArgs { @@ -85,8 +82,10 @@ private void RunCommandsLoop() // Считаем, что факап подписчика - его проблемы. } - // свойство в исключении может быть уcтановлено в обработчике евента - _serverStopped = e.StopChannel; + // свойство в исключении может быть уcтановлено в обработчике евента, + // а обработчик мог и сам остановить сервер + if (critical || e.StopChannel) + _serverStopped = true; } catch (ObjectDisposedException) { @@ -147,7 +146,9 @@ public void Stop() _serverStopped = true; - if (_messageThread?.IsAlive == true) + // Из обработчика сообщения свой поток не прерываем: цикл и так завершится, + // а прерывание сорвало бы ближайшее ожидание в самом обработчике + if (_messageThread?.IsAlive == true && _messageThread != Thread.CurrentThread) { _messageThread.Interrupt(); } diff --git a/src/OneScript.DebugProtocol/TcpServer/JsonDtoChannel.cs b/src/OneScript.DebugProtocol/TcpServer/JsonDtoChannel.cs index 2b0b98d73..81a6b752b 100644 --- a/src/OneScript.DebugProtocol/TcpServer/JsonDtoChannel.cs +++ b/src/OneScript.DebugProtocol/TcpServer/JsonDtoChannel.cs @@ -18,7 +18,9 @@ public sealed class JsonDtoChannel : IMessageChannel private readonly IDebuggerClient _client; private readonly Stream _dataStream; - private bool _enabled = true; + // Пишут и поток сообщений (ответы), и потоки скриптов (события остановки) + private readonly object _writeLock = new object(); + private volatile bool _enabled = true; public JsonDtoChannel(IDebuggerClient client) { @@ -34,9 +36,10 @@ public JsonDtoChannel(Stream dataStream) public void Dispose() { + // Без блокировки записи: закрытие потока как раз прерывает зависшую запись + _enabled = false; _dataStream.Dispose(); _client?.Dispose(); - _enabled = false; } public void Write(object data) @@ -55,7 +58,11 @@ public void Write(object data) writer.Write(contentBytes, 0, contentBytes.Length); bufferedStream.Position = 0; - bufferedStream.CopyTo(_dataStream); + // Сообщение целиком, чтобы сообщения разных потоков не перемешались + lock (_writeLock) + { + bufferedStream.CopyTo(_dataStream); + } } } } @@ -85,6 +92,11 @@ public object Read() } } } + catch (Exception ex) when (ex is IOException or ObjectDisposedException) + { + // Соединение закрыто или оборвалось: дальше читать нечего + throw new ChannelException("Channel is closed", true, ex); + } catch (Exception ex) { throw new ChannelException("Channel read exception", ex); diff --git a/src/OneScript.DebugServices/DefaultDebugger.cs b/src/OneScript.DebugServices/DefaultDebugger.cs index 44471ac9d..9f61d9dee 100644 --- a/src/OneScript.DebugServices/DefaultDebugger.cs +++ b/src/OneScript.DebugServices/DefaultDebugger.cs @@ -5,6 +5,7 @@ This Source Code Form is subject to the terms of the at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ +using System.Threading; using OneScript.DebugProtocol.Abstractions; using OneScript.DebugProtocol.TcpServer; using OneScript.DebugServices.Internal; @@ -22,6 +23,9 @@ public class DefaultDebugger : IDebugger // NB! должен быть согласован с файлом ProtocolVersions в адаптере private const short SUPPORTED_FORMAT_VERSION = 4; + // Клиент, который подключился и молчит, не должен занимать прием подключений + private const int HANDSHAKE_TIMEOUT_MS = 5000; + private readonly IDebugServer _transport; private IDebugSession _session; @@ -60,7 +64,15 @@ private void TransportOnOnClientConnected(object sender, IDebuggerClient debugge } var dataStream = debuggerClient.GetDataStream(); - if (FormatReconcileUtils.CheckReconcileRequest(dataStream)) + if (dataStream.CanTimeout) + dataStream.ReadTimeout = HANDSHAKE_TIMEOUT_MS; + + var isReconcileRequest = FormatReconcileUtils.CheckReconcileRequest(dataStream); + + if (dataStream.CanTimeout) + dataStream.ReadTimeout = Timeout.Infinite; + + if (isReconcileRequest) { // Да, это наш фейковый заголовок FormatReconcileUtils.WriteReconcileResponse(dataStream, JSON_FORMAT_MARKER, SUPPORTED_FORMAT_VERSION); diff --git a/src/OneScript.DebugServices/Internal/DebugSession.cs b/src/OneScript.DebugServices/Internal/DebugSession.cs index 53e093397..96b1ce82a 100644 --- a/src/OneScript.DebugServices/Internal/DebugSession.cs +++ b/src/OneScript.DebugServices/Internal/DebugSession.cs @@ -18,6 +18,7 @@ namespace OneScript.DebugServices.Internal internal class DebugSession : IDebugSession { private bool _isStarted; + private int _disposed; private readonly ThreadManager _threadManager; private readonly TcpEventCallbackChannel _callbackChannel; private readonly DispatchingService _messageServer; @@ -39,6 +40,7 @@ public DebugSession(IDebuggerClient connectedClient, bool attachMode) }; ipcServer.OnError += CommunicationError; + ipcServer.DataReceived += ConnectionLost; BreakpointManager = new DefaultBreakpointManager(); _threadManager = new ThreadManager(); @@ -59,6 +61,13 @@ private void CommunicationError(object sender, CommunicationEventArgs e) Dispose(); } + private void ConnectionLost(object sender, CommunicationEventArgs e) + { + // IDE закрыла соединение без Disconnect или связь оборвалась + if (e.Exception?.StopChannel == true) + Dispose(); + } + private void ThreadManagerOnThreadStopped(object sender, ThreadStoppedEventArgs e) { MachineWaitToken token; @@ -71,19 +80,39 @@ private void ThreadManagerOnThreadStopped(object sender, ThreadStoppedEventArgs return; } + // Остановка внутри вычисления отладчика (watch вызвал метод с точкой останова): + // отладчик ждет результат и продолжить поток не сможет + if (token.IsRunningDebuggerWork) + return; + token.Reset(); + try + { + _callbackChannel.ThreadStoppedEx(e.ThreadId, ConvertStopReason(e.StopReason), e.ErrorMessage); + } + catch + { + // Остановку не сообщили - поток не должен числиться остановленным + token.Set(); + throw; + } - _callbackChannel.ThreadStoppedEx(e.ThreadId, ConvertStopReason(e.StopReason), e.ErrorMessage); token.Wait(); } public void Dispose() { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + return; + _threadManager.ThreadStopped -= ThreadManagerOnThreadStopped; _threadManager.Dispose(); _messageServer.Stop(); IsActive = false; + // IDE отключилась до команды запуска: выполняемся без отладчика, а не ждем вечно + _startEvent.Set(); + OnClose?.Invoke(this); } diff --git a/src/OneScript.DebugServices/Internal/DebuggerServiceImpl.cs b/src/OneScript.DebugServices/Internal/DebuggerServiceImpl.cs index cf284162f..535efb148 100644 --- a/src/OneScript.DebugServices/Internal/DebuggerServiceImpl.cs +++ b/src/OneScript.DebugServices/Internal/DebuggerServiceImpl.cs @@ -11,6 +11,7 @@ This Source Code Form is subject to the terms of the using OneScript.Contexts; using OneScript.DebugProtocol; using OneScript.Language; +using OneScript.Values; using ScriptEngine.Machine; using StackFrame = OneScript.DebugProtocol.StackFrame; using Variable = OneScript.DebugProtocol.Variable; @@ -101,7 +102,7 @@ public Breakpoint[] SetMachineBreakpoints(Breakpoint[] breaksToSet) public StackFrame[] GetStackFrames(int threadId) { - var machine = _threadManager.GetTokenForThread(threadId).Machine; + var machine = GetMachine(threadId); var frames = machine.GetExecutionFrames(); var result = new StackFrame[frames.Count]; int index = 0; @@ -121,7 +122,26 @@ public StackFrame[] GetStackFrames(int threadId) private MachineInstance GetMachine(int threadId) { - return _threadManager.GetTokenForThread(threadId).Machine; + return GetStoppedToken(threadId).Machine; + } + + // Состояние машины можно читать и менять, только пока ее поток стоит. Например, после + // «Продолжить» IDE еще запрашивает стек потока, событие остановки которого не успела обработать + private MachineWaitToken GetStoppedToken(int threadId) + { + var token = _threadManager.GetTokenForThread(threadId); + if (!token.IsStopped) + throw new InvalidOperationException($"Thread {threadId} is running"); + + return token; + } + + // Выражение может вызвать методы сценария: они выполняются в потоке машины, + // с его блокировками, а не в потоке отладчика + private BslValue EvaluateOnStoppedThread(int threadId, int frameIndex, string expression) + { + var token = GetStoppedToken(threadId); + return token.RunOnStoppedThread(() => token.Machine.EvaluateInFrame(expression, frameIndex)); } public Variable[] GetVariables(int threadId, int frameIndex, int[] path) @@ -160,7 +180,7 @@ public Variable[] GetEvaluatedVariables(string expression, int threadId, int fra try { - value = GetMachine(threadId).EvaluateInFrame(expression, frameIndex); + value = EvaluateOnStoppedThread(threadId, frameIndex, expression); } catch (Exception e) { @@ -182,8 +202,7 @@ public Variable Evaluate(int threadId, int contextFrame, string expression) { try { - var value = GetMachine(threadId) - .EvaluateInFrame(expression, contextFrame); + var value = EvaluateOnStoppedThread(threadId, contextFrame, expression); var variable = _visualizer.GetVariable(MachineVariable.Create(value, "$evalResult")); return variable; @@ -197,21 +216,21 @@ public Variable Evaluate(int threadId, int contextFrame, string expression) public void Next(int threadId) { - var t = _threadManager.GetTokenForThread(threadId); + var t = GetStoppedToken(threadId); t.Machine.StepOver(); t.Set(); } public void StepIn(int threadId) { - var t = _threadManager.GetTokenForThread(threadId); + var t = GetStoppedToken(threadId); t.Machine.StepIn(); t.Set(); } public void StepOut(int threadId) { - var t = _threadManager.GetTokenForThread(threadId); + var t = GetStoppedToken(threadId); t.Machine.StepOut(); t.Set(); } diff --git a/src/OneScript.DebugServices/Internal/TcpEventCallbackChannel.cs b/src/OneScript.DebugServices/Internal/TcpEventCallbackChannel.cs index e1aafea2e..a186c4896 100644 --- a/src/OneScript.DebugServices/Internal/TcpEventCallbackChannel.cs +++ b/src/OneScript.DebugServices/Internal/TcpEventCallbackChannel.cs @@ -5,6 +5,7 @@ This Source Code Form is subject to the terms of the at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ +using System; using System.IO; using OneScript.DebugProtocol; using OneScript.DebugProtocol.Abstractions; @@ -52,6 +53,10 @@ private void Write(RpcCall dto) { // Ignore } + catch (ObjectDisposedException) + { + // Канал закрыли, пока поток скрипта отправлял событие + } } } } \ No newline at end of file diff --git a/src/OneScript.DebugServices/MachineWaitToken.cs b/src/OneScript.DebugServices/MachineWaitToken.cs index ddb22b481..02d7ff41a 100644 --- a/src/OneScript.DebugServices/MachineWaitToken.cs +++ b/src/OneScript.DebugServices/MachineWaitToken.cs @@ -6,28 +6,152 @@ This Source Code Form is subject to the terms of the ----------------------------------------------------------*/ using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; using System.Threading; using ScriptEngine.Machine; namespace OneScript.DebugServices { + /// + /// Остановка потока машины в отладчике. Поток машины ждет здесь команды отладчика, + /// а отладчик может выполнить в остановленном потоке свое вычисление. + /// public class MachineWaitToken: IDisposable { - private ManualResetEventSlim _threadEvent = new ManualResetEventSlim(); + private readonly object _lock = new object(); + private readonly Queue _debuggerWork = new Queue(); + private bool _stopped; + private bool _released; public MachineInstance Machine { get; set; } - public void Wait() => _threadEvent.Wait(); + /// + /// Поток машины остановлен и ждет команды отладчика + /// + public bool IsStopped + { + get + { + lock (_lock) + { + return _stopped; + } + } + } + + /// + /// Поток машины сейчас выполняет вычисление отладчика. Только для потока машины. + /// + public bool IsRunningDebuggerWork { get; private set; } + + /// + /// Поток машины останавливается. Вызывается до отправки события остановки, + /// чтобы команда продолжения, пришедшая сразу за событием, не потерялась. + /// + public void Reset() + { + lock (_lock) + { + // Отладчик уже отключился - больше не останавливаемся + if (!_released) + _stopped = true; + } + } + + /// + /// Ожидание продолжения в потоке машины. Пока ждет, выполняет вычисления отладчика. + /// + public void Wait() + { + while (true) + { + Action work; + lock (_lock) + { + while (_stopped && _debuggerWork.Count == 0) + Monitor.Wait(_lock); + + if (_debuggerWork.Count == 0) + return; + + work = _debuggerWork.Dequeue(); + } - public void Set() => _threadEvent.Set(); + IsRunningDebuggerWork = true; + try + { + work(); + } + finally + { + IsRunningDebuggerWork = false; + } + } + } + + /// + /// Продолжить выполнение потока машины + /// + public void Set() + { + lock (_lock) + { + _stopped = false; + Monitor.PulseAll(_lock); + } + } - public void Reset() => _threadEvent.Reset(); + /// + /// Выполняет вычисление в остановленном потоке машины и ждет результата. + /// Так вычисление видит блокировки и состояние этого потока, а не потока отладчика. + /// + public T RunOnStoppedThread(Func func) + { + T result = default; + ExceptionDispatchInfo error = null; + using var completed = new ManualResetEventSlim(); + + lock (_lock) + { + if (!_stopped) + throw new InvalidOperationException("Thread is running"); + + _debuggerWork.Enqueue(() => + { + try + { + result = func(); + } + catch (Exception e) + { + error = ExceptionDispatchInfo.Capture(e); + } + finally + { + completed.Set(); + } + }); + Monitor.PulseAll(_lock); + } + + completed.Wait(); + error?.Throw(); + return result; + } + /// + /// Отладчик отключается: поток продолжает работу и больше не останавливается + /// public void Dispose() { - Machine.UnsetDebugMode(); - _threadEvent.Set(); - _threadEvent.Dispose(); + Machine?.UnsetDebugMode(); + lock (_lock) + { + _released = true; + _stopped = false; + Monitor.PulseAll(_lock); + } } } } \ No newline at end of file diff --git a/src/OneScript.DebugServices/TcpDebugServer.cs b/src/OneScript.DebugServices/TcpDebugServer.cs index 9bd585ab0..4ffe1fdb8 100644 --- a/src/OneScript.DebugServices/TcpDebugServer.cs +++ b/src/OneScript.DebugServices/TcpDebugServer.cs @@ -60,9 +60,24 @@ private void StartListenerThread() { try { - var client = _listener.AcceptTcpClient(); + // Stop() из другого потока обнуляет поле + var listener = _listener; + if (listener == null) + break; + + var client = listener.AcceptTcpClient(); OnLogEvent?.Invoke(LogLevel.Debug, "Client connected"); - OnClientConnected?.Invoke(this, new TcpDebuggerClient(client)); + try + { + OnClientConnected?.Invoke(this, new TcpDebuggerClient(client)); + } + catch (Exception e) + { + // Клиент закрылся до рукопожатия или прислал мусор (проверка порта и т.п.): + // отлаживаемый процесс из-за этого падать не должен + OnLogEvent?.Invoke(LogLevel.Error, $"Client connection failed: {e}"); + client.Dispose(); + } } catch (ObjectDisposedException) { diff --git a/src/OneScript.DebugServices/ThreadManager.cs b/src/OneScript.DebugServices/ThreadManager.cs index f93cae0c0..b0aa48e7d 100644 --- a/src/OneScript.DebugServices/ThreadManager.cs +++ b/src/OneScript.DebugServices/ThreadManager.cs @@ -37,7 +37,11 @@ public MachineWaitToken[] GetAllTokens() private void EmitThreadStopped(int threadId, MachineStopReason reason, string errMessage) { - var machine = GetTokenForThread(threadId).Machine; + // Поток мог уже отпустить отключившийся отладчик: тогда останавливаться некому + if (!_machinesOnThreads.TryGetValue(threadId, out var token)) + return; + + var machine = token.Machine; var args = new ThreadStoppedEventArgs { diff --git a/src/Tests/OneScript.DebugProtocol.Test/DebuggerConnectionTests.cs b/src/Tests/OneScript.DebugProtocol.Test/DebuggerConnectionTests.cs new file mode 100644 index 000000000..d709b7d28 --- /dev/null +++ b/src/Tests/OneScript.DebugProtocol.Test/DebuggerConnectionTests.cs @@ -0,0 +1,296 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using FluentAssertions; +using OneScript.DebugProtocol.TcpServer; +using OneScript.DebugProtocol.Test.Tools; +using OneScript.DebugServices; +using Xunit; + +namespace OneScript.DebugProtocol.Test +{ + public class DebuggerConnectionTests + { + [Fact] + public void ClientClosedBeforeHandshakeDoesNotStopDebugger() + { + var transport = new TcpDebugServer(0); + var debugger = new DefaultDebugger(transport) { AttachMode = true }; + debugger.Start(); + + // Проверка порта: подключились и сразу закрылись + using (var probe = new TcpClient()) + { + probe.Connect(new IPEndPoint(IPAddress.Loopback, transport.ActualPort())); + probe.GetStream().WriteByte(0); + } + + var client = new TestDebuggerClient(); + client.Connect(transport.ActualPort()); + + WaitEvent(() => debugger.GetSession().IsActive, 2000).Should().BeTrue(); + } + + [Fact] + public void SessionEndsWhenClientDropsConnection() + { + var transport = new TcpDebugServer(0); + var debugger = new DefaultDebugger(transport) { AttachMode = true }; + debugger.Start(); + + var client = new TestDebuggerClient(); + client.Connect(transport.ActualPort()); + var session = debugger.GetSession(); + WaitEvent(() => session.IsActive, 2000).Should().BeTrue(); + + client.Close(); + + WaitEvent(() => !session.IsActive, 2000).Should().BeTrue(); + + var client2 = new TestDebuggerClient(); + client2.Connect(transport.ActualPort()); + WaitEvent(() => debugger.GetSession().IsActive, 2000).Should().BeTrue(); + } + + [Fact] + public void LaunchContinuesWhenClientDropsBeforeExecute() + { + var transport = new TcpDebugServer(0); + var debugger = new DefaultDebugger(transport) { AttachMode = false }; + debugger.Start(); + + var client = new TestDebuggerClient(); + client.Connect(transport.ActualPort()); + WaitEvent(() => debugger.GetSession().IsActive, 2000).Should().BeTrue(); + + var started = new ManualResetEventSlim(); + ThreadPool.QueueUserWorkItem(_ => + { + debugger.GetSession().WaitReadyToRun(); + started.Set(); + }); + + client.Close(); + + started.Wait(2000).Should().BeTrue(); + } + + [Fact] + public void ContinueSentWithStopEventIsNotLost() + { + var token = new MachineWaitToken(); + token.Reset(); + token.Set(); + + RunWithTimeout(token.Wait).Should().BeTrue(); + token.IsStopped.Should().BeFalse(); + } + + [Fact] + public void ReleasedThreadDoesNotStopAgain() + { + var token = new MachineWaitToken(); + token.Dispose(); + + token.Reset(); + + token.IsStopped.Should().BeFalse(); + RunWithTimeout(token.Wait).Should().BeTrue(); + } + + [Fact] + public void DebuggerWorkRunsOnStoppedThread() + { + var token = new MachineWaitToken(); + var stoppedThreadId = 0; + var stoppedThread = new Thread(() => + { + stoppedThreadId = Environment.CurrentManagedThreadId; + token.Reset(); + token.Wait(); + }); + stoppedThread.Start(); + WaitEvent(() => token.IsStopped, 2000).Should().BeTrue(); + + var workThreadId = token.RunOnStoppedThread(() => Environment.CurrentManagedThreadId); + + workThreadId.Should().Be(stoppedThreadId); + token.IsStopped.Should().BeTrue(); + + token.Set(); + stoppedThread.Join(2000).Should().BeTrue(); + } + + [Fact] + public void DebuggerWorkOnRunningThreadIsRejected() + { + var token = new MachineWaitToken(); + + token.Invoking(t => t.RunOnStoppedThread(() => 1)) + .Should().Throw(); + } + + [Fact] + public void MessagesFromManyThreadsAreNotMixed() + { + const int threadsCount = 4; + const int messagesPerThread = 10; + var stream = new SlowStream(); + var channel = new JsonDtoChannel(stream); + + // Сообщение больше части, которую поток отправляет за раз + var payload = new string('x', 100_000); + var threads = Enumerable.Range(0, threadsCount).Select(number => new Thread(() => + { + for (var i = 0; i < messagesPerThread; i++) + { + channel.Write(RpcCall.Create("Test", "Message", number, payload)); + } + })).ToArray(); + + foreach (var thread in threads) + { + thread.Start(); + } + foreach (var thread in threads) + { + thread.Join(); + } + + var reader = new JsonDtoChannel(new MemoryStream(stream.ToArray())); + var messages = Enumerable.Range(0, threadsCount * messagesPerThread) + .Select(_ => reader.Read()) + .ToList(); + + messages.Should().OnlyContain(m => (string)m.Parameters[1] == payload); + } + + [Fact] + public void StopFromMessageHandlerDoesNotInterruptIt() + { + // Пустой поток: канал сразу сообщает об обрыве соединения + var server = new DefaultMessageServer(new JsonDtoChannel(new MemoryStream())); + var busy = new object(); + var holding = new ManualResetEventSlim(); + var release = new ManualResetEventSlim(); + var handled = new ManualResetEventSlim(); + Exception handlerError = null; + + // Блокировку держит другой поток, как OnSessionClose при новом подключении + var holder = new Thread(() => + { + lock (busy) + { + holding.Set(); + release.Wait(); + } + }); + holder.Start(); + holding.Wait(); + + server.DataReceived += (sender, e) => + { + // Как DebugSession.Dispose при обрыве соединения + server.Stop(); + try + { + var waiter = new Thread(() => { Thread.Sleep(200); release.Set(); }); + waiter.Start(); + lock (busy) + { + } + } + catch (Exception ex) + { + handlerError = ex; + } + handled.Set(); + }; + server.Start(); + + handled.Wait(2000).Should().BeTrue(); + release.Set(); + holder.Join(); + handlerError.Should().BeNull(); + } + + private static bool RunWithTimeout(Action action) + { + var thread = new Thread(() => action()) { IsBackground = true }; + thread.Start(); + return thread.Join(2000); + } + + private static bool WaitEvent(Func predicate, int timeout) + { + var start = Environment.TickCount; + while (!predicate()) + { + if (Environment.TickCount - start > timeout) + return false; + + Thread.Sleep(10); + } + + return true; + } + + /// + /// Поток, который, как сокет с заполненным буфером, отправляет большую запись частями + /// + private class SlowStream : Stream + { + private const int ChunkSize = 16 * 1024; + private readonly List _data = new List(); + + public override void Write(byte[] buffer, int offset, int count) + { + for (var sent = 0; sent < count; sent += ChunkSize) + { + lock (_data) + { + _data.AddRange(buffer.Skip(offset + sent).Take(Math.Min(ChunkSize, count - sent))); + } + Thread.Sleep(1); + } + } + + public byte[] ToArray() + { + lock (_data) + { + return _data.ToArray(); + } + } + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + } + } +} diff --git a/src/Tests/OneScript.DebugProtocol.Test/DebuggerEvaluationTests.cs b/src/Tests/OneScript.DebugProtocol.Test/DebuggerEvaluationTests.cs new file mode 100644 index 000000000..569b1804a --- /dev/null +++ b/src/Tests/OneScript.DebugProtocol.Test/DebuggerEvaluationTests.cs @@ -0,0 +1,127 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.IO; +using System.Threading; +using FluentAssertions; +using OneScript.DebugProtocol.TcpServer; +using OneScript.DebugProtocol.Test.Tools; +using OneScript.DebugServices; +using OneScript.StandardLibrary; +using ScriptEngine.Hosting; +using Xunit; + +namespace OneScript.DebugProtocol.Test +{ + public class DebuggerEvaluationTests + { + private const int StopLine = 11; + private const int LineInsideCalledMethod = 6; + + private const string Script = + "Перем Блок;\n" + + "\n" + + "Функция ВзятьБлокировку() Экспорт\n" + + " Блок.Заблокировать();\n" + + " Блок.Разблокировать();\n" + + " Возврат \"взял\";\n" + + "КонецФункции\n" + + "\n" + + "Блок = Новый БлокировкаРесурса;\n" + + "Блок.Заблокировать();\n" + + "Сообщение = \"стоп\";\n" + + "Блок.Разблокировать();\n"; + + [Fact] + public void WatchExpressionRunsOnStoppedThread() + { + var transport = new TcpDebugServer(0); + var debugger = new DefaultDebugger(transport) { AttachMode = true }; + var engine = DefaultEngineBuilder.Create() + .SetDefaultOptions() + .UseBinaryDataOptions() + .SetupEnvironment(e => e.AddStandardLibrary()) + .WithDebugger(debugger) + .Build(); + engine.Initialize(); + debugger.Start(); + + var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.os"); + File.WriteAllText(path, Script); + try + { + var client = new TestDebuggerClient(); + client.Connect(transport.ActualPort()); + SpinWait.SpinUntil(() => debugger.GetSession().IsActive, 2000).Should().BeTrue(); + + Call(client, nameof(IDebuggerService.SetMachineBreakpoints), (object)new[] + { + new Breakpoint { Source = path, Line = StopLine }, + new Breakpoint { Source = path, Line = LineInsideCalledMethod } + }); + + Exception scriptError = null; + var scriptThread = new Thread(() => + { + try + { + engine.AttachedScriptsFactory.LoadFromPath(engine.GetCompilerService(), path, engine.NewProcess()); + } + catch (Exception e) + { + scriptError = e; + } + }) { IsBackground = true }; + scriptThread.Start(); + + var stopped = WaitForEvent(client, nameof(IDebugEventListener.ThreadStoppedEx)); + var threadId = Convert.ToInt32(stopped.Parameters[0]); + Call(client, nameof(IDebuggerService.GetStackFrames), threadId); + + // Метод берет блокировку, которую держит остановленный поток, и в нем тоже точка останова. + // В потоке отладчика это зависало навсегда + var result = (Variable)Call(client, nameof(IDebuggerService.Evaluate), threadId, 0, "ВзятьБлокировку()"); + result.Presentation.Should().Be("взял"); + + // Execute ничего не возвращает, ответа на него нет + client.Send(RpcCall.Create(nameof(IDebuggerService), nameof(IDebuggerService.Execute), threadId)); + scriptThread.Join(5000).Should().BeTrue(); + scriptError.Should().BeNull(); + } + finally + { + debugger.NotifyProcessExit(0); + File.Delete(path); + } + } + + private static object Call(TestDebuggerClient client, string method, params object[] parameters) + { + client.Send(RpcCall.Create(nameof(IDebuggerService), method, parameters)); + while (true) + { + if (client.Read(5000) is RpcCallResult result && result.Id == method) + { + if (result.ReturnValue is RpcExceptionDto error) + throw new InvalidOperationException(error.Description); + + return result.ReturnValue; + } + } + } + + private static RpcCall WaitForEvent(TestDebuggerClient client, string eventName) + { + while (true) + { + if (client.Read(5000) is RpcCall call && call.Id == eventName) + return call; + } + } + } +} diff --git a/src/Tests/OneScript.DebugProtocol.Test/Tools/TestDebuggerClient.cs b/src/Tests/OneScript.DebugProtocol.Test/Tools/TestDebuggerClient.cs index 9fddb999e..bb84726d1 100644 --- a/src/Tests/OneScript.DebugProtocol.Test/Tools/TestDebuggerClient.cs +++ b/src/Tests/OneScript.DebugProtocol.Test/Tools/TestDebuggerClient.cs @@ -44,5 +44,17 @@ public void Send(RpcCall message) bufferedStream.CopyTo(_client.GetStream()); _client.GetStream().Flush(); } + + // Как IDE, которую закрыли без Disconnect + public void Close() + { + _client.Dispose(); + } + + public TcpProtocolDtoBase Read(int timeout) + { + _client.ReceiveTimeout = timeout; + return new JsonDtoChannel(_client.GetStream()).Read(); + } } } \ No newline at end of file