diff --git a/MonacoRoslynCompletionProvider/Api/CodeRunner.cs b/MonacoRoslynCompletionProvider/Api/CodeRunner.cs index 5ed36a5..c779bc2 100644 --- a/MonacoRoslynCompletionProvider/Api/CodeRunner.cs +++ b/MonacoRoslynCompletionProvider/Api/CodeRunner.cs @@ -140,7 +140,8 @@ public static async Task RunMultiFileCodeAsync( Func onOutput, Func onError, string sessionId = null, - string projectType = null) + string projectType = null, + CancellationToken cancellationToken = default) { var result = new RunResult(); CustomAssemblyLoadContext loadContext = null; @@ -276,6 +277,9 @@ public static async Task RunMultiFileCodeAsync( } try { + // 检查取消令牌 + cancellationToken.ThrowIfCancellationRequested(); + if (parameters.Length == 1 && parameters[0].ParameterType == typeof(string[])) { entryPoint.Invoke(null, new object[] { new string[] { "sharpPad" } }); @@ -285,6 +289,10 @@ public static async Task RunMultiFileCodeAsync( entryPoint.Invoke(null, null); } } + catch (OperationCanceledException) + { + await onError("代码执行已被取消").ConfigureAwait(false); + } catch (Exception ex) { var errorMessage = "Execution error: " + (ex.InnerException?.Message ?? ex.Message); @@ -342,7 +350,8 @@ public static async Task RunProgramCodeAsync( Func onOutput, Func onError, string sessionId = null, - string projectType = null) + string projectType = null, + CancellationToken cancellationToken = default) { var result = new RunResult(); // 低内存版本:不使用 StringBuilder 缓存输出,只依赖回调传递实时数据 @@ -474,6 +483,9 @@ public static async Task RunProgramCodeAsync( } try { + // 检查取消令牌 + cancellationToken.ThrowIfCancellationRequested(); + if (parameters.Length == 1 && parameters[0].ParameterType == typeof(string[])) { // 兼容 Main(string[] args) @@ -484,6 +496,10 @@ public static async Task RunProgramCodeAsync( entryPoint.Invoke(null, null); } } + catch (OperationCanceledException) + { + await onError("代码执行已被取消").ConfigureAwait(false); + } catch (Exception ex) { var errorMessage = "Execution error: " + (ex.InnerException?.Message ?? ex.Message); @@ -497,7 +513,7 @@ public static async Task RunProgramCodeAsync( Console.SetError(originalError); Console.SetIn(originalIn); } - + // 清理会话 if (!string.IsNullOrEmpty(sessionId)) { diff --git a/SharpPad/Controllers/CodeRunController.cs b/SharpPad/Controllers/CodeRunController.cs index cbb1f05..9239f75 100644 --- a/SharpPad/Controllers/CodeRunController.cs +++ b/SharpPad/Controllers/CodeRunController.cs @@ -6,6 +6,7 @@ using System.Text.Json; using System.Threading.Channels; using System.IO.Compression; +using System.Collections.Concurrent; using static MonacoRoslynCompletionProvider.Api.CodeRunner; namespace SharpPad.Controllers @@ -14,6 +15,8 @@ namespace SharpPad.Controllers [Route("api/[controller]")] public class CodeRunController : ControllerBase { + // 会话管理:存储活跃的会话和对应的取消令牌 + private static readonly ConcurrentDictionary _activeSessions = new(); [HttpPost("run")] public async Task Run([FromBody] MultiFileCodeRunRequest request) { @@ -27,6 +30,16 @@ public async Task Run([FromBody] MultiFileCodeRunRequest request) var cts = new CancellationTokenSource(); HttpContext.RequestAborted.Register(() => cts.Cancel()); + // 注册会话,如果有SessionId的话(使用线程安全操作) + if (!string.IsNullOrEmpty(request?.SessionId)) + { + _activeSessions.AddOrUpdate(request.SessionId, cts, (key, existing) => { + existing?.Cancel(); + existing?.Dispose(); + return cts; + }); + } + // 创建一个无界的 Channel 以缓冲输出 var channel = Channel.CreateUnbounded(); @@ -40,7 +53,7 @@ public async Task Run([FromBody] MultiFileCodeRunRequest request) // Check if it's a multi-file request if (request?.IsMultiFile == true) { - // Execute multi-file code runner + // Execute multi-file code runner with cancellation token result = await CodeRunner.RunMultiFileCodeAsync( request.Files, nugetPackages, @@ -48,12 +61,13 @@ public async Task Run([FromBody] MultiFileCodeRunRequest request) message => OnOutputAsync(message, channel.Writer, cts.Token), error => OnErrorAsync(error, channel.Writer, cts.Token), sessionId: request?.SessionId, - projectType: request?.ProjectType + projectType: request?.ProjectType, + cancellationToken: cts.Token ); } else { - // Backward compatibility: single file execution + // Backward compatibility: single file execution with cancellation token result = await CodeRunner.RunProgramCodeAsync( request?.SourceCode, nugetPackages, @@ -61,7 +75,8 @@ public async Task Run([FromBody] MultiFileCodeRunRequest request) message => OnOutputAsync(message, channel.Writer, cts.Token), error => OnErrorAsync(error, channel.Writer, cts.Token), sessionId: request?.SessionId, - projectType: request?.ProjectType + projectType: request?.ProjectType, + cancellationToken: cts.Token ); } @@ -89,6 +104,18 @@ public async Task Run([FromBody] MultiFileCodeRunRequest request) } } } + finally + { + // 清理会话并释放资源 + if (!string.IsNullOrEmpty(request?.SessionId)) + { + if (_activeSessions.TryRemove(request.SessionId, out var removedCts)) + { + removedCts?.Dispose(); + } + } + cts?.Dispose(); + } } private async Task OnOutputAsync(string output, ChannelWriter writer, CancellationToken token) @@ -167,6 +194,32 @@ public IActionResult ProvideInput([FromBody] InputRequest request) return Ok(new { success }); } + [HttpPost("stop")] + public IActionResult StopExecution([FromBody] StopRequest request) + { + if (string.IsNullOrEmpty(request?.SessionId)) + { + return BadRequest(new { success = false, message = "SessionId is required" }); + } + + try + { + // 查找并取消对应的会话,使用线程安全操作 + if (_activeSessions.TryRemove(request.SessionId, out var cts)) + { + cts?.Cancel(); + cts?.Dispose(); + return Ok(new { success = true, message = "代码执行已停止" }); + } + + return Ok(new { success = false, message = "未找到活跃的执行会话" }); + } + catch (Exception ex) + { + return StatusCode(500, new { success = false, message = $"停止执行时发生错误: {ex.Message}" }); + } + } + [HttpPost("buildExe")] public async Task BuildExe([FromBody] ExeBuildRequest request) { @@ -238,4 +291,9 @@ public class InputRequest public string SessionId { get; set; } public string Input { get; set; } } + + public class StopRequest + { + public string SessionId { get; set; } + } } diff --git a/SharpPad/wwwroot/execution/runner.js b/SharpPad/wwwroot/execution/runner.js index 39cef2c..5adb7d7 100644 --- a/SharpPad/wwwroot/execution/runner.js +++ b/SharpPad/wwwroot/execution/runner.js @@ -58,18 +58,22 @@ function mergePackageLists(...groups) { export class CodeRunner { constructor() { this.runButton = document.getElementById('runButton'); + this.stopButton = document.getElementById('stopButton'); this.buildExeButton = document.getElementById('buildExeButton'); this.outputContent = document.getElementById('outputContent'); this.notification = document.getElementById('notification'); this.projectTypeSelect = document.getElementById('projectTypeSelect'); this.projectTypeStorageKey = 'sharpPad.projectType'; this.currentSessionId = null; + this.isRunning = false; + this.isStopping = false; this.initializeProjectTypeSelector(); this.initializeEventListeners(); } initializeEventListeners() { this.runButton.addEventListener('click', () => this.runCode(window.editor.getValue())); + this.stopButton.addEventListener('click', () => this.stopCode()); this.buildExeButton.addEventListener('click', () => this.buildExe(window.editor.getValue())); } @@ -97,6 +101,96 @@ export class CodeRunner { } }); } + + setRunningState(isRunning) { + this.isRunning = isRunning; + if (isRunning) { + this.runButton.style.display = 'none'; + this.stopButton.style.display = 'inline-block'; + this.stopButton.disabled = false; + } else { + this.runButton.style.display = 'inline-block'; + this.stopButton.style.display = 'none'; + this.stopButton.disabled = false; + } + + // 如果正在停止,禁用停止按钮 + if (this.isStopping) { + this.stopButton.disabled = true; + this.stopButton.textContent = '停止中...'; + } else { + this.stopButton.textContent = '停止'; + } + } + + async stopCode() { + if (!this.currentSessionId) { + this.appendOutput('没有正在运行的代码需要停止', 'error'); + return; + } + + if (this.isStopping) { + this.appendOutput('停止操作正在进行中...', 'info'); + return; + } + + this.isStopping = true; + + try { + const request = { + SessionId: this.currentSessionId + }; + + // 添加超时控制,防止长时间等待 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时 + + const response = await fetch('/api/coderun/stop', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request), + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + + if (result.success) { + this.appendOutput('✅ 代码执行已停止', 'info'); + this.notification.textContent = '执行已停止'; + this.notification.style.backgroundColor = 'rgba(255, 152, 0, 0.9)'; + this.notification.style.display = 'block'; + + // 3秒后隐藏通知 + setTimeout(() => { + this.notification.style.display = 'none'; + }, 3000); + } else { + this.appendOutput(`停止失败: ${result.message || '未知错误'}`, 'error'); + } + + } catch (error) { + if (error.name === 'AbortError') { + this.appendOutput('停止请求超时,但代码执行可能已被终止', 'error'); + } else { + this.appendOutput('停止代码执行失败: ' + error.message, 'error'); + } + } finally { + // 重置状态 + this.isStopping = false; + this.currentSessionId = null; + this.setRunningState(false); + this.outputContent.classList.remove("result-streaming"); + } + } + appendOutput(message, type = 'info') { const outputLine = document.createElement('div'); outputLine.className = `output-${type}`; @@ -250,6 +344,7 @@ export class CodeRunner { const csharpVersion = document.getElementById('csharpVersion')?.value || 2147483647; const projectType = (this.projectTypeSelect?.value || 'console').toLowerCase(); this.currentSessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + this.setRunningState(true); const { selectedFiles, autoIncludedNames, missingReferences, packages: contextPackages } = this.gatherMultiFileContext(code, fileId, file); const combinedPackages = mergePackageLists(basePackages, contextPackages); @@ -340,6 +435,7 @@ export class CodeRunner { this.notification.style.display = 'none'; this.outputContent.classList.remove("result-streaming"); this.currentSessionId = null; // 清空会话ID + this.setRunningState(false); // 重置运行状态 return; } } @@ -349,6 +445,7 @@ export class CodeRunner { this.notification.textContent = '运行失败'; this.notification.style.backgroundColor = 'rgba(244, 67, 54, 0.9)'; this.notification.style.display = 'block'; + this.setRunningState(false); // 重置运行状态 } } diff --git a/SharpPad/wwwroot/index.html b/SharpPad/wwwroot/index.html index 00ea2c5..15ecb7a 100644 --- a/SharpPad/wwwroot/index.html +++ b/SharpPad/wwwroot/index.html @@ -73,6 +73,7 @@ +