diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6833c92..c0f716a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -14,27 +14,87 @@ use crate::utils::logger::{ clear_logs, get_log_directory, get_log_files, reset_log_directory, set_log_directory, }; use config::{get_app_config, get_config_path, init_config, update_app_config}; +use std::collections::HashMap; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; use plugins::{CodeExecutionRequest, ExecutionResult, LanguageInfo, PluginManager}; use std::fs; use std::process::{Command, Stdio}; use std::time::{SystemTime, UNIX_EPOCH}; -use tauri::State; +use tauri::{AppHandle, Emitter, State}; use tokio::sync::Mutex; use uuid::Uuid; +use std::io::{BufRead, BufReader}; +use std::sync::OnceLock; +use std::sync::{Arc, mpsc}; +use std::thread; + +// 执行任务结构 +#[derive(Debug)] +struct ExecutionTask { + #[allow(dead_code)] + pub language: String, + #[allow(dead_code)] + pub process_id: u32, + pub stop_flag: Arc>, +} + type ExecutionHistory = Mutex>; type PluginManagerState = Mutex; +// 全局任务管理器 +type TaskManager = Arc>>; +static TASK_MANAGER: OnceLock = OnceLock::new(); + +// 初始化任务管理器 +fn init_task_manager() -> TaskManager { + TASK_MANAGER + .get_or_init(|| Arc::new(tokio::sync::Mutex::new(HashMap::new()))) + .clone() +} + +// 停止执行命令 +#[tauri::command] +async fn stop_execution(language: String) -> Result { + let task_manager = init_task_manager(); + let mut guard = task_manager.lock().await; + + if let Some(task) = guard.remove(&language) { + // 设置停止标志 + { + let mut stop_flag = task.stop_flag.lock().await; + *stop_flag = true; + } + info!("停止执行 -> 成功设置停止标志给语言 [ {} ]", language); + Ok(true) + } else { + warn!("停止执行 -> 语言 [ {} ] 没有正在运行的任务", language); + Ok(false) + } +} + +// 检查是否有正在运行的任务 +#[tauri::command] +async fn is_execution_running(language: String) -> Result { + let task_manager = init_task_manager(); + let guard = task_manager.lock().await; + Ok(guard.contains_key(&language)) +} + // 通用的代码执行函数 #[tauri::command] async fn execute_code( request: CodeExecutionRequest, history: State<'_, ExecutionHistory>, plugin_manager: State<'_, PluginManagerState>, + app: AppHandle, ) -> Result { info!("执行代码 -> 调用插件 [ {} ] 开始", request.language); + + // 先停止之前可能正在运行的任务 + let _ = stop_execution(request.language.clone()).await; + let manager = plugin_manager.lock().await; let plugin = manager .get_plugin(&request.language) @@ -62,7 +122,6 @@ async fn execute_code( .map_err(|e| format!("Failed to write temporary file: {}", e))?; let start_time = std::time::Instant::now(); - let mut _last_error: String = String::new(); let cmd = plugin.get_command(None); let args = plugin.get_execute_args(file_path.to_str().unwrap()); @@ -73,83 +132,278 @@ async fn execute_code( args.join(" ") ); - let output = Command::new(&cmd) - .args(args) + // 发送执行开始事件 + let _ = app.emit( + "code-execution-start", + serde_json::json!({ + "language": request.language + }), + ); + + // 启动子进程 + let mut child = match Command::new(&cmd) + .args(&args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .output(); - - match output { - Ok(output) => { + .spawn() + { + Ok(child) => child, + Err(e) => { let execution_time = start_time.elapsed().as_millis(); let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); - // 清理临时文件 let _ = fs::remove_file(&file_path); - - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - let mut result = ExecutionResult { - success: output.status.success(), - stdout, - stderr, + let _ = app.emit( + "code-execution-complete", + serde_json::json!({ + "language": request.language, + "success": false + }), + ); + + error!("执行代码 -> 调用插件 [ {} ] 失败: {}", request.language, e); + return Ok(ExecutionResult { + success: false, + stdout: String::new(), + stderr: format!( + "{} interpreter not found. Please install {} and ensure it's in your PATH.\n\nError: {}", + request.language, request.language, e + ), execution_time, timestamp, + language: request.language, + }); + } + }; + + // 创建停止标志 + let stop_flag = Arc::new(tokio::sync::Mutex::new(false)); + + // 将任务添加到管理器 + let task_manager = init_task_manager(); + { + let mut guard = task_manager.lock().await; + guard.insert( + request.language.clone(), + ExecutionTask { language: request.language.clone(), - }; + process_id: child.id(), + stop_flag: stop_flag.clone(), + }, + ); + } + + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + + let (stdout_tx, stdout_rx) = mpsc::channel::(); + let (stderr_tx, stderr_rx) = mpsc::channel::(); + + // 读取 stdout + thread::spawn(move || { + let reader = BufReader::new(stdout); + for line in reader.lines().map_while(Result::ok) { + if stdout_tx.send(line).is_err() { + break; + } + } + }); + + // 读取 stderr + thread::spawn(move || { + let reader = BufReader::new(stderr); + for line in reader.lines().map_while(Result::ok) { + if stderr_tx.send(line).is_err() { + break; + } + } + }); + + let mut stdout_lines = Vec::new(); + let mut stderr_lines = Vec::new(); + let timeout = std::time::Duration::from_secs(30); + + // 主执行循环 + loop { + // 检查停止标志 + { + let stop_guard = stop_flag.lock().await; + if *stop_guard { + info!( + "执行代码 -> 收到停止信号,终止语言 [ {} ] 的执行", + request.language + ); + let _ = child.kill(); + let _ = child.wait(); + let _ = fs::remove_file(&file_path); + + // 从任务管理器中移除 + { + let mut guard = task_manager.lock().await; + guard.remove(&request.language); + } + + let _ = app.emit( + "code-execution-stopped", + serde_json::json!({ + "language": request.language + }), + ); - // 后处理 - let _ = plugin.post_execute_hook(&mut result); + return Err("代码执行被用户停止".to_string()); + } + } - // 添加到执行历史 - drop(manager); // 释放插件管理器锁 - let mut history_guard = history.lock().await; - history_guard.push(result.clone()); + // 检查超时 + if start_time.elapsed() > timeout { + let _ = child.kill(); + let _ = child.wait(); + let _ = fs::remove_file(&file_path); - // 保持历史记录不超过100条 - if history_guard.len() > 100 { - history_guard.remove(0); + // 从任务管理器中移除 + { + let mut guard = task_manager.lock().await; + guard.remove(&request.language); } - info!("执行代码 -> 调用插件 [ {} ] 完成", request.language); - return Ok(result); + let _ = app.emit( + "code-execution-timeout", + serde_json::json!({ + "language": request.language + }), + ); + + error!("执行代码 -> 超时,终止语言 [ {} ] 的执行", request.language); + return Err("代码执行超时(30秒)".to_string()); } - Err(e) => { - _last_error = format!("Failed to execute {} - {}", cmd, e); + + // 读取并发送 stdout + while let Ok(line) = stdout_rx.try_recv() { + stdout_lines.push(line.clone()); + let _ = app.emit( + "code-output", + serde_json::json!({ + "type": "stdout", + "content": line, + "language": request.language + }), + ); } - } - // 如果所有命令都失败了 - let execution_time = start_time.elapsed().as_millis(); - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - // 清理临时文件 - let _ = fs::remove_file(&file_path); - - error!("执行代码 -> 调用插件 [ {} ] 失败", request.language); - Ok(ExecutionResult { - success: false, - stdout: String::new(), - stderr: format!( - "{} interpreter not found. Please install {} and ensure it's in your PATH.\n\nLast error: {}\n\nTried commands: {:?}", - request.language, - request.language, - _last_error, - plugin - .get_command(Some(file_path.to_str().unwrap())) - .to_string() - ), - execution_time, - timestamp, - language: request.language, - }) + // 读取并发送 stderr + while let Ok(line) = stderr_rx.try_recv() { + stderr_lines.push(line.clone()); + let _ = app.emit( + "code-output", + serde_json::json!({ + "type": "stderr", + "content": line, + "language": request.language + }), + ); + } + + // 检查进程是否结束 + match child.try_wait() { + Ok(Some(status)) => { + // 进程已结束,读取剩余输出 + while let Ok(line) = stdout_rx.try_recv() { + stdout_lines.push(line.clone()); + let _ = app.emit( + "code-output", + serde_json::json!({ + "type": "stdout", + "content": line, + "language": request.language + }), + ); + } + while let Ok(line) = stderr_rx.try_recv() { + stderr_lines.push(line.clone()); + let _ = app.emit( + "code-output", + serde_json::json!({ + "type": "stderr", + "content": line, + "language": request.language + }), + ); + } + + let execution_time = start_time.elapsed().as_millis(); + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let _ = fs::remove_file(&file_path); + + // 从任务管理器中移除 + { + let mut guard = task_manager.lock().await; + guard.remove(&request.language); + } + + let mut result = ExecutionResult { + success: status.success(), + stdout: stdout_lines.join("\n"), + stderr: stderr_lines.join("\n"), + execution_time, + timestamp, + language: request.language.clone(), + }; + + let _ = plugin.post_execute_hook(&mut result); + + let _ = app.emit( + "code-execution-complete", + serde_json::json!({ + "language": request.language, + "success": result.success, + "execution_time": result.execution_time + }), + ); + + drop(manager); + let mut history_guard = history.lock().await; + history_guard.push(result.clone()); + + if history_guard.len() > 100 { + history_guard.remove(0); + } + + info!("执行代码 -> 调用插件 [ {} ] 完成", request.language); + return Ok(result); + } + Ok(None) => { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + Err(e) => { + let _ = child.kill(); + let _ = child.wait(); + let _ = fs::remove_file(&file_path); + + // 从任务管理器中移除 + { + let mut guard = task_manager.lock().await; + guard.remove(&request.language); + } + + let _ = app.emit( + "code-execution-error", + serde_json::json!({ + "language": request.language, + "error": e.to_string() + }), + ); + + return Err(format!("检查进程状态失败: {}", e)); + } + } + } } // 通用的环境信息获取函数 @@ -273,6 +527,8 @@ fn main() { get_supported_languages, get_execution_history, clear_execution_history, + stop_execution, + is_execution_running, get_app_info, // 日志相关命令 get_log_directory, diff --git a/src/App.vue b/src/App.vue index 4e69a3c..9cae388 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,6 +5,7 @@ :supported-languages="supportedLanguages" :current-language="currentLanguage" @run-code="runCode" + @stop-code="stopCode" @clear-output="clearOutput" @language-change="handleLanguageChange" @show-settings="showSettings = true"> @@ -95,6 +96,13 @@ interface Language value: string } +interface CodeOutputEvent +{ + type: 'stdout' | 'stderr' + content: string + language: string +} + // 代码模板 const codeTemplates: Record = { python: `# Welcome to CodeForge! @@ -159,9 +167,21 @@ const lastExecutionTime = ref(0) const activeTab = ref('output') const supportedLanguages = ref([]) const showAbout = ref(false) -let unlistenAboutFn: UnlistenFn | null = null const showSettings = ref(false) + +// 实时输出相关 +const realTimeOutput = ref('') +const realTimeStderr = ref('') + +// 事件监听器 +let unlistenAboutFn: UnlistenFn | null = null let unlistenSettingsFn: UnlistenFn | null = null +let unlistenOutputFn: UnlistenFn | null = null +let unlistenExecutionStartFn: UnlistenFn | null = null +let unlistenExecutionCompleteFn: UnlistenFn | null = null +let unlistenExecutionStoppedFn: UnlistenFn | null = null +let unlistenExecutionTimeoutFn: UnlistenFn | null = null +let unlistenExecutionErrorFn: UnlistenFn | null = null const closeAbout = () => { showAbout.value = false @@ -236,7 +256,7 @@ const handleLanguageChange = async (newLanguage: string) => { print("Hello from ${ getLanguageDisplayName(newLanguage) }!")` // 清空输出 - output.value = '' + clearOutput() // 刷新环境信息 await refreshEnvInfo() @@ -251,7 +271,13 @@ const runCode = async () => { } isRunning.value = true + + // 清空所有输出 output.value = '' + realTimeOutput.value = '' + realTimeStderr.value = '' + isSuccess.value = false + lastExecutionTime.value = 0 try { const result: ExecutionResult = await invoke('execute_code', { @@ -261,35 +287,134 @@ const runCode = async () => { } }) + // 注意:这里不需要手动设置 output,因为实时输出已经通过事件处理了 lastExecutionTime.value = result.execution_time isSuccess.value = result.success if (result.success) { - output.value = result.stdout || '代码执行成功 (无输出)' - if (result.stderr) { - output.value += '\n' + result.stderr - } toast.success(`代码执行成功,用时 ${ result.execution_time } 毫秒`) } else { - output.value = result.stderr || '代码执行失败 (无输出)' toast.error('代码执行失败,查看输出的错误信息') } } catch (error) { output.value = `代码执行失败: ${ error }` toast.error('代码执行失败,请检查日志') - } - finally { isRunning.value = false } } +const stopCode = async () => { + if (!isRunning.value) { + return + } + + try { + const result = await invoke('stop_execution', { + language: currentLanguage.value + }) + + if (result) { + toast.info('正在停止代码执行...') + } + else { + toast.warning('没有找到正在运行的任务') + } + } + catch (error) { + console.error('Error stopping execution:', error) + toast.error('停止执行失败') + } +} + const clearOutput = () => { output.value = '' + realTimeOutput.value = '' + realTimeStderr.value = '' toast.info('输出已清空') } +// 处理实时输出 +const handleRealtimeOutput = (event: any) => { + const data: CodeOutputEvent = event.payload + + // 只处理当前语言的输出 + if (data.language !== currentLanguage.value) { + return + } + + if (data.type === 'stdout') { + realTimeOutput.value += data.content + '\n' + } + else if (data.type === 'stderr') { + realTimeStderr.value += data.content + '\n' + } + + // 合并输出显示 + let combinedOutput = '' + if (realTimeOutput.value) { + combinedOutput += realTimeOutput.value + } + if (realTimeStderr.value) { + if (combinedOutput) { + combinedOutput += '\n' + } + combinedOutput += realTimeStderr.value + } + + output.value = combinedOutput +} + +// 处理执行状态事件 +const handleExecutionStart = (event: any) => { + const data = event.payload + if (data.language === currentLanguage.value) { + console.log('代码开始执行') + } +} + +const handleExecutionComplete = (event: any) => { + const data = event.payload + if (data.language === currentLanguage.value) { + isRunning.value = false + isSuccess.value = data.success + if (data.execution_time) { + lastExecutionTime.value = data.execution_time + } + console.log('代码执行完成') + } +} + +const handleExecutionStopped = (event: any) => { + const data = event.payload + if (data.language === currentLanguage.value) { + isRunning.value = false + output.value += '\n\n🛑 代码执行已被用户停止' + toast.warning('代码执行已停止') + console.log('代码执行已停止') + } +} + +const handleExecutionTimeout = (event: any) => { + const data = event.payload + if (data.language === currentLanguage.value) { + isRunning.value = false + output.value += '\n\n⚠️ 代码执行超时(30秒)' + toast.error('代码执行超时') + } +} + +const handleExecutionError = (event: any) => { + const data = event.payload + if (data.language === currentLanguage.value) { + isRunning.value = false + output.value += `\n\n❌ 执行错误: ${ data.error }` + toast.error('代码执行出错') + } +} + +// 禁用右键菜单 window.addEventListener('contextmenu', (e) => e.preventDefault(), false) onMounted(async () => { @@ -305,24 +430,43 @@ onMounted(async () => { code.value = codeTemplates.python } - // 监听来自 Rust 的 show-about 事件 + // 监听来自 Rust 的各种事件 unlistenAboutFn = await listen('show-about', () => { showAbout.value = true }) - // 监听来自 Rust 的 show-settings 事件 unlistenSettingsFn = await listen('show-settings', () => { showSettings.value = true }) + + // 监听实时输出事件 + unlistenOutputFn = await listen('code-output', handleRealtimeOutput) + + // 监听执行状态事件 + unlistenExecutionStartFn = await listen('code-execution-start', handleExecutionStart) + unlistenExecutionCompleteFn = await listen('code-execution-complete', handleExecutionComplete) + unlistenExecutionStoppedFn = await listen('code-execution-stopped', handleExecutionStopped) + unlistenExecutionTimeoutFn = await listen('code-execution-timeout', handleExecutionTimeout) + unlistenExecutionErrorFn = await listen('code-execution-error', handleExecutionError) }) onUnmounted(() => { - if (unlistenAboutFn) { - unlistenAboutFn() - } - - if (unlistenSettingsFn) { - unlistenSettingsFn() - } + // 清理所有事件监听器 + const listeners = [ + unlistenAboutFn, + unlistenSettingsFn, + unlistenOutputFn, + unlistenExecutionStartFn, + unlistenExecutionCompleteFn, + unlistenExecutionStoppedFn, + unlistenExecutionTimeoutFn, + unlistenExecutionErrorFn + ] + + listeners.forEach(listener => { + if (listener) { + listener() + } + }) }) diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 0dcbab6..91be1e8 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -18,15 +18,25 @@
- - + + + @@ -55,6 +65,7 @@ const props = defineProps<{ const emit = defineEmits<{ 'run-code': [] + 'stop-code': [] 'clear-output': [] 'show-settings': [] 'language-change': [language: string] diff --git a/src/components/OutputPanel.vue b/src/components/OutputPanel.vue index b40ccea..9e3552d 100644 --- a/src/components/OutputPanel.vue +++ b/src/components/OutputPanel.vue @@ -4,6 +4,10 @@
控制台 +
+ + 执行中 +
{{ isCopied ? '已复制' : '复制失败' }} @@ -23,14 +27,20 @@
-
-
+
+
执行代码中...
-
{{ output }}
+
{{ output }}
+ + +
+ + 程序正在运行... +
@@ -43,7 +53,7 @@ \ No newline at end of file + +// 自动滚动到底部(实时输出时) +watch(() => props.output, async (newOutput, oldOutput) => { + // 只有在输出增加时才滚动(避免清空时滚动) + if (newOutput.length > (oldOutput?.length || 0)) { + await nextTick() + if (outputContainer.value) { + outputContainer.value.scrollTop = outputContainer.value.scrollHeight + } + } +}, { flush: 'post' }) + +// 监听运行状态变化,运行开始时也滚动到底部 +watch(() => props.isRunning, async (isRunning) => { + if (isRunning) { + await nextTick() + if (outputContainer.value) { + outputContainer.value.scrollTop = outputContainer.value.scrollHeight + } + } +}) +