Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions MonacoRoslynCompletionProvider/Api/CodeRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ public static async Task<RunResult> RunMultiFileCodeAsync(
Func<string, Task> onOutput,
Func<string, Task> onError,
string sessionId = null,
string projectType = null)
string projectType = null,
CancellationToken cancellationToken = default)
{
var result = new RunResult();
CustomAssemblyLoadContext loadContext = null;
Expand Down Expand Up @@ -276,6 +277,9 @@ public static async Task<RunResult> RunMultiFileCodeAsync(
}
try
{
// 检查取消令牌
cancellationToken.ThrowIfCancellationRequested();

if (parameters.Length == 1 && parameters[0].ParameterType == typeof(string[]))
{
entryPoint.Invoke(null, new object[] { new string[] { "sharpPad" } });
Expand All @@ -285,6 +289,10 @@ public static async Task<RunResult> RunMultiFileCodeAsync(
entryPoint.Invoke(null, null);
}
}
catch (OperationCanceledException)
{
await onError("代码执行已被取消").ConfigureAwait(false);
}
catch (Exception ex)
{
var errorMessage = "Execution error: " + (ex.InnerException?.Message ?? ex.Message);
Expand Down Expand Up @@ -342,7 +350,8 @@ public static async Task<RunResult> RunProgramCodeAsync(
Func<string, Task> onOutput,
Func<string, Task> onError,
string sessionId = null,
string projectType = null)
string projectType = null,
CancellationToken cancellationToken = default)
{
var result = new RunResult();
// 低内存版本:不使用 StringBuilder 缓存输出,只依赖回调传递实时数据
Expand Down Expand Up @@ -474,6 +483,9 @@ public static async Task<RunResult> RunProgramCodeAsync(
}
try
{
// 检查取消令牌
cancellationToken.ThrowIfCancellationRequested();

if (parameters.Length == 1 && parameters[0].ParameterType == typeof(string[]))
{
// 兼容 Main(string[] args)
Expand All @@ -484,6 +496,10 @@ public static async Task<RunResult> RunProgramCodeAsync(
entryPoint.Invoke(null, null);
}
}
catch (OperationCanceledException)
{
await onError("代码执行已被取消").ConfigureAwait(false);
}
catch (Exception ex)
{
var errorMessage = "Execution error: " + (ex.InnerException?.Message ?? ex.Message);
Expand All @@ -497,7 +513,7 @@ public static async Task<RunResult> RunProgramCodeAsync(
Console.SetError(originalError);
Console.SetIn(originalIn);
}

// 清理会话
if (!string.IsNullOrEmpty(sessionId))
{
Expand Down
66 changes: 62 additions & 4 deletions SharpPad/Controllers/CodeRunController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,6 +15,8 @@ namespace SharpPad.Controllers
[Route("api/[controller]")]
public class CodeRunController : ControllerBase
{
// 会话管理:存储活跃的会话和对应的取消令牌
private static readonly ConcurrentDictionary<string, CancellationTokenSource> _activeSessions = new();
[HttpPost("run")]
public async Task Run([FromBody] MultiFileCodeRunRequest request)
{
Expand All @@ -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<string>();

Expand All @@ -40,28 +53,30 @@ 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,
request?.LanguageVersion ?? 2147483647,
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,
request?.LanguageVersion ?? 2147483647,
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
);
}

Expand Down Expand Up @@ -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<string> writer, CancellationToken token)
Expand Down Expand Up @@ -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<IActionResult> BuildExe([FromBody] ExeBuildRequest request)
{
Expand Down Expand Up @@ -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; }
}
}
97 changes: 97 additions & 0 deletions SharpPad/wwwroot/execution/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}

Expand Down Expand Up @@ -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}`;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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); // 重置运行状态
}
}

Expand Down
1 change: 1 addition & 0 deletions SharpPad/wwwroot/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
</select>
</div>
<button id="runButton" title="运行 (Ctrl+Enter)">运行</button>
<button id="stopButton" title="停止运行" style="display: none;">停止</button>
<button id="buildExeButton" title="生成发布包">发布包</button>
<div style="flex-grow: 0.1;"></div>
</div>
Expand Down