yggdrasil/components/code_runner/
runner.rs1use dioxus::prelude::*;
13
14#[cfg(target_arch = "wasm32")]
15use crate::api::code_runner::execute::start_exec_stream;
16#[cfg(not(target_arch = "wasm32"))]
17use crate::api::code_runner::execute::{get_exec_result, start_exec};
18use crate::api::code_runner::{ExecRequest, ExecStatus};
19use crate::components::ui::SPINNER_SVG;
20use crate::infra::runner_config::ResourceLimits;
21#[cfg(not(target_arch = "wasm32"))]
22use crate::utils::time::sleep_ms;
23
24#[cfg(not(target_arch = "wasm32"))]
26const POLL_INTERVAL_MS: u32 = 500;
27#[cfg(not(target_arch = "wasm32"))]
29const MAX_POLLS: u32 = 240; #[component]
46#[allow(unused_mut)]
47pub fn CodeRunner(
48 source: String,
49 language: String,
50 overrides: Option<ResourceLimits>,
51 instance_id: usize,
52) -> Element {
53 let mut running = use_signal(|| false);
54 let mut stage = use_signal(String::new);
55 let mut output = use_signal(String::new);
56 let mut exit_info = use_signal(String::new);
57 let mut error_msg = use_signal(String::new);
58 let mut show_output = use_signal(|| false);
60 let mut has_output = use_signal(|| false);
62
63 let mut source_signal = use_signal(|| source.clone());
70
71 let container_id = format!("code-runner-{instance_id}");
75
76 let output_container_id = format!("code-runner-output-{instance_id}");
78
79 #[cfg(target_arch = "wasm32")]
82 let mut term_handle: Signal<Option<crate::bridges::xterm::TerminalHandle>> =
83 use_signal(|| None);
84
85 let mut editor_ready = use_signal(|| false);
89
90 let mut vim_enabled = use_signal(|| {
92 #[cfg(target_arch = "wasm32")]
93 {
94 if let Some(window) = web_sys::window() {
95 if let Ok(Some(storage)) = window.local_storage() {
96 if let Ok(Some(val)) = storage.get_item("yggdrasil-code-runner-vim") {
97 return val == "true";
98 }
99 }
100 }
101 }
102 true
103 });
104
105 let toggle_vim = move |_| {
106 let next = !vim_enabled();
107 vim_enabled.set(next);
108 #[cfg(target_arch = "wasm32")]
109 {
110 if let Some(window) = web_sys::window() {
111 if let Ok(Some(storage)) = window.local_storage() {
112 let _ = storage.set_item("yggdrasil-code-runner-vim", &next.to_string());
113 }
114 }
115 }
116 };
117
118 #[cfg(target_arch = "wasm32")]
121 {
122 use crate::bridges::codemirror;
123 use crate::theme::{use_resolved_theme, ResolvedTheme};
124 use wasm_bindgen::closure::Closure;
125
126 let mut editor_handle: Signal<Option<codemirror::EditorHandle>> = use_signal(|| None);
127 let resolved_theme = use_resolved_theme();
131
132 let mount_language = language.clone();
135 let mount_container_id = container_id.clone();
136 use_effect(move || {
137 if editor_handle.read().is_some() {
138 return; }
140
141 let on_change = Closure::new({
143 let mut sig = source_signal;
144 move |v: String| sig.set(v)
145 });
146 let on_ready = Closure::new(|| {});
147 let on_run_shortcut = Closure::new(|| {});
150
151 let theme_name = if resolved_theme() == ResolvedTheme::Dark {
152 "dark"
153 } else {
154 "light"
155 };
156
157 let opts = codemirror::EditorOptions::new();
158 opts.set_language(&mount_language);
159 opts.set_theme(theme_name);
160 opts.set_vim(*vim_enabled.read());
161 opts.set_value(&source_signal.read());
162 opts.set_on_change(&on_change);
163 opts.set_on_ready(&on_ready);
164 opts.set_on_run_shortcut(&on_run_shortcut);
165
166 if let Ok(Some(inst)) = codemirror::get_module().create(&mount_container_id, &opts) {
167 let handle =
168 codemirror::EditorHandle::new(inst, on_change, on_ready, on_run_shortcut);
169 editor_handle.set(Some(handle));
170 editor_ready.set(true);
171 }
172 });
173
174 use_effect(move || {
184 #[cfg(target_arch = "wasm32")]
185 {
186 let transitioning = web_sys::window()
187 .and_then(|w| w.document())
188 .and_then(|d| d.document_element())
189 .map(|el| el.class_list().contains("is-theme-transitioning"))
190 .unwrap_or(false);
191 if transitioning {
192 return;
193 }
194 }
195 if let Some(h) = editor_handle.read().as_ref() {
196 h.instance()
197 .set_theme(if resolved_theme() == ResolvedTheme::Dark {
198 "dark"
199 } else {
200 "light"
201 });
202 }
203 });
204
205 use_effect(move || {
207 let enabled = vim_enabled();
208 if let Some(h) = editor_handle.read().as_ref() {
209 h.instance().set_vim(enabled);
210 }
211 });
212
213 use_drop(move || {
215 editor_handle.set(None);
216 editor_ready.set(false);
217 });
218 }
219
220 #[cfg(target_arch = "wasm32")]
223 {
224 use crate::bridges::xterm;
225 use crate::theme::{use_resolved_theme, ResolvedTheme};
226 use wasm_bindgen::closure::Closure;
227
228 let resolved_theme = use_resolved_theme();
231
232 let mount_container_id = output_container_id.clone();
236 use_effect(move || {
237 if term_handle.read().is_some() {
238 return; }
240 if !show_output() {
241 return; }
243
244 let on_ready = Closure::new(|| {});
245 let theme_name = if resolved_theme() == ResolvedTheme::Dark {
246 "dark"
247 } else {
248 "light"
249 };
250
251 let opts = xterm::XtermOptions::new();
252 opts.set_theme(theme_name);
253 opts.set_font_size(13);
254 opts.set_on_ready(&on_ready);
255
256 if let Ok(Some(inst)) = xterm::get_module().create(&mount_container_id, &opts) {
257 let handle = xterm::TerminalHandle::new(inst, on_ready);
258 term_handle.set(Some(handle));
259 }
260 });
261
262 use_effect(move || {
265 #[cfg(target_arch = "wasm32")]
266 {
267 let transitioning = web_sys::window()
268 .and_then(|w| w.document())
269 .and_then(|d| d.document_element())
270 .map(|el| el.class_list().contains("is-theme-transitioning"))
271 .unwrap_or(false);
272 if transitioning {
273 return;
274 }
275 }
276 if let Some(h) = term_handle.read().as_ref() {
277 h.instance()
278 .set_theme(if resolved_theme() == ResolvedTheme::Dark {
279 "dark"
280 } else {
281 "light"
282 });
283 }
284 });
285
286 use_drop(move || {
288 term_handle.set(None);
289 });
290 }
291
292 let run_language = language.clone();
294 let run_overrides = overrides.clone();
295
296 #[cfg(target_arch = "wasm32")]
299 let run_code = {
300 let mut running = running;
301 let mut stage = stage;
302 let mut output = output;
303 let mut exit_info = exit_info;
304 let mut error_msg = error_msg;
305 let mut source_signal = source_signal;
306 let mut term_handle = term_handle;
307 let mut show_output = show_output;
308 let mut has_output = has_output;
309 let run_language = run_language.clone();
310 let run_overrides = run_overrides.clone();
311 move |_| {
312 if running() {
313 return;
314 }
315 running.set(true);
316 show_output.set(true);
317 has_output.set(false);
318 stage.set("提交中...".to_string());
319 error_msg.set(String::new());
320
321 if let Some(h) = term_handle.read().as_ref() {
323 h.instance().clear();
324 }
325
326 let run_source = source_signal.read().clone();
327 let req = ExecRequest {
328 language: run_language.clone(),
329 source: run_source,
330 overrides: run_overrides.clone(),
331 };
332
333 spawn(async move {
334 let task_id = match start_exec_stream(req).await {
335 Ok(id) => id,
336 Err(e) => {
337 running.set(false);
338 let msg = e.to_string();
339 stage.set(msg.clone());
340 error_msg.set(msg);
341 return;
342 }
343 };
344 stage.set("运行中".to_string());
345
346 if sse_consumer::start_sse(
349 &task_id,
350 &term_handle,
351 &mut running,
352 &mut exit_info,
353 &mut error_msg,
354 &mut has_output,
355 )
356 .is_err()
357 {
358 sse_consumer::poll_result(
360 &task_id,
361 &mut running,
362 &mut stage,
363 &mut output,
364 &mut exit_info,
365 &mut error_msg,
366 &term_handle,
367 &mut has_output,
368 )
369 .await;
370 }
371 });
372 }
373 };
374
375 #[cfg(not(target_arch = "wasm32"))]
377 let run_code = {
378 let mut running = running;
379 let mut stage = stage;
380 let mut output = output;
381 let mut exit_info = exit_info;
382 let mut error_msg = error_msg;
383 let mut source_signal = source_signal;
384 let mut show_output = show_output;
385 let run_language = run_language.clone();
386 let run_overrides = run_overrides.clone();
387 move |_| {
388 if running() {
389 return;
390 }
391 running.set(true);
392 show_output.set(true);
393 stage.set("提交中...".to_string());
394 output.set(String::new());
395 exit_info.set(String::new());
396 error_msg.set(String::new());
397
398 let run_source = source_signal.read().clone();
399 let req = ExecRequest {
400 language: run_language.clone(),
401 source: run_source,
402 overrides: run_overrides.clone(),
403 };
404
405 spawn(async move {
406 match start_exec(req).await {
407 Ok(task_id) => {
408 let mut polls = 0u32;
409 loop {
410 polls += 1;
411 sleep_ms(POLL_INTERVAL_MS).await;
412 match get_exec_result(task_id.clone()).await {
413 Ok(task) => {
414 stage.set(task.stage.clone());
415 let terminal = task.status != ExecStatus::Queued
416 && task.status != ExecStatus::Running;
417 if terminal {
418 running.set(false);
419 if let Some(res) = task.result {
420 let out = format!(
421 "Stdout:\n{}\nStderr:\n{}",
422 res.stdout, res.stderr
423 );
424 output.set(out);
425 exit_info.set(format!(
426 "耗时: {}ms · 状态: {}",
427 res.duration_ms,
428 status_label(&res.status)
429 ));
430 if res.status == ExecStatus::Success {
431 error_msg.set(String::new());
432 } else {
433 error_msg.set(status_label(&res.status));
434 }
435 }
436 break;
437 }
438 if polls >= MAX_POLLS {
439 running.set(false);
440 stage.set("查询超时".to_string());
441 error_msg.set("轮询超时,请重试".to_string());
442 break;
443 }
444 }
445 Err(_) => {
446 running.set(false);
447 stage.set("结果获取异常".to_string());
448 error_msg.set("结果获取异常".to_string());
449 break;
450 }
451 }
452 }
453 }
454 Err(e) => {
455 running.set(false);
456 let msg = e.to_string();
457 stage.set(msg.clone());
458 error_msg.set(msg);
459 }
460 }
461 });
462 }
463 };
464
465 rsx! {
466 div { class: "rounded-2xl overflow-hidden border border-[var(--color-paper-border)] bg-[var(--color-paper-entry)] my-[var(--content-gap-paper)]",
467 div { class: "flex justify-between items-center px-4 py-2.5 border-b border-[var(--color-paper-border)] bg-[var(--color-paper-theme)]",
469 div { class: "flex items-center gap-3",
470 span { class: "w-2 h-2 rounded-full bg-[var(--color-paper-accent)]" }
471 span { class: "font-mono text-sm font-semibold text-[var(--color-paper-primary)]",
472 "{language}"
473 }
474 button {
475 class: format!(
476 "text-[10px] px-1.5 py-0.5 rounded border transition cursor-pointer {}",
477 if vim_enabled() {
478 "bg-[var(--color-paper-accent)]/15 text-[var(--color-paper-accent)] border-[var(--color-paper-accent)]/30 font-semibold"
479 } else {
480 "bg-transparent text-[var(--color-paper-tertiary)] border-[var(--color-paper-border)] hover:text-[var(--color-paper-primary)]"
481 },
482 ),
483 onclick: toggle_vim,
484 "Vim"
485 }
486 }
487 button {
488 class: "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium rounded-full text-[var(--color-paper-theme)] bg-[var(--color-paper-accent)] hover:brightness-110 active:scale-[0.98] transition disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer",
489 disabled: running(),
490 onclick: run_code,
491 if running() {
492 span {
493 class: "inline-block w-3.5 h-3.5 text-[var(--color-paper-theme)]",
494 dangerous_inner_html: SPINNER_SVG,
495 }
496 "{stage()}"
497 } else {
498 "运行"
499 }
500 }
501 }
502 div {
503 id: "{container_id}",
504 class: "code-runner-editor font-mono text-sm relative",
505
506 if !editor_ready() {
510 div { class: "absolute inset-0 flex flex-col justify-center gap-2.5 px-4 py-4 bg-[var(--color-paper-code-block)]",
511 div {
513 class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
514 style: "width: 90%",
515 }
516 div {
517 class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
518 style: "width: 70%",
519 }
520 div {
521 class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
522 style: "width: 55%",
523 }
524 div {
525 class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
526 style: "width: 85%",
527 }
528 div {
529 class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
530 style: "width: 40%",
531 }
532 }
533 }
534 }
535 if show_output() {
538 div { class: "border-t border-[var(--color-paper-border)]",
539 div { class: "flex justify-between items-center px-4 py-2 text-xs text-[var(--color-paper-tertiary)] border-b border-[var(--color-paper-border)] bg-[var(--color-paper-code-block)]",
540 span { class: "font-medium uppercase tracking-wide", "输出" }
541 span { "{exit_info()}" }
542 }
543 div {
544 id: "{output_container_id}",
545 class: "px-2 py-2 bg-[var(--color-paper-code-block)] max-h-80 overflow-hidden text-xs relative min-h-24",
546 if running() && !has_output() {
548 div { class: "absolute inset-0 flex flex-col justify-center gap-2.5 px-4 py-4 bg-[var(--color-paper-code-block)]",
549 div {
550 class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
551 style: "width: 70%",
552 }
553 div {
554 class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
555 style: "width: 55%",
556 }
557 div {
558 class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
559 style: "width: 85%",
560 }
561 }
562 }
563 }
564 }
565 }
566 if !error_msg().is_empty() {
568 div { class: "px-4 py-2.5 border-t border-[var(--color-paper-border)] text-xs text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-900/10",
569 {error_msg()}
570 }
571 }
572 }
573 }
574}
575
576fn status_label(status: &ExecStatus) -> String {
578 match status {
579 ExecStatus::Queued => "排队中".to_string(),
580 ExecStatus::Running => "运行中".to_string(),
581 ExecStatus::Success => "成功".to_string(),
582 ExecStatus::Timeout => "超时".to_string(),
583 ExecStatus::OomKilled => "内存超限".to_string(),
584 ExecStatus::Error => "运行错误".to_string(),
585 ExecStatus::Failed => "系统失败".to_string(),
586 ExecStatus::RateLimited => "请求过频".to_string(),
587 }
588}
589
590#[cfg(target_arch = "wasm32")]
594mod sse_consumer {
595 use dioxus::prelude::*;
596 use wasm_bindgen::closure::Closure;
597 use wasm_bindgen::prelude::*;
598 use wasm_bindgen::JsCast;
599 use web_sys::{EventSource, MessageEvent};
600
601 use crate::api::code_runner::execute::get_exec_result;
602 use crate::api::code_runner::ExecStatus;
603 use crate::bridges::xterm::TerminalHandle;
604 use crate::utils::time::sleep_ms;
605
606 #[derive(serde::Deserialize)]
608 struct DonePayload {
609 exit_code: Option<i64>,
610 oom_killed: bool,
611 timed_out: bool,
612 duration_ms: u64,
613 #[serde(default)]
615 error: Option<String>,
616 }
617
618 pub fn start_sse(
625 task_id: &str,
626 term_handle: &Signal<Option<TerminalHandle>>,
627 running: &mut Signal<bool>,
628 exit_info: &mut Signal<String>,
629 error_msg: &mut Signal<String>,
630 has_output: &mut Signal<bool>,
631 ) -> Result<(), JsValue> {
632 let url = format!("/api/exec/stream?task_id={task_id}");
633 let es = EventSource::new(&url)?;
634
635 let term_for_stdout = *term_handle;
637 let mut has_output_for_stdout = *has_output;
638 let on_stdout = Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
639 if let Some(s) = e.data().as_string() {
640 if let Some(h) = term_for_stdout.read().as_ref() {
641 h.instance().write_stdout(&s);
642 }
643 has_output_for_stdout.set(true);
644 }
645 });
646 es.add_event_listener_with_callback("stdout", on_stdout.as_ref().unchecked_ref())?;
647 on_stdout.forget();
648
649 let term_for_stderr = *term_handle;
651 let mut has_output_for_stderr = *has_output;
652 let on_stderr = Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
653 if let Some(s) = e.data().as_string() {
654 if let Some(h) = term_for_stderr.read().as_ref() {
655 h.instance().write_stderr(&s);
656 }
657 has_output_for_stderr.set(true);
658 }
659 });
660 es.add_event_listener_with_callback("stderr", on_stderr.as_ref().unchecked_ref())?;
661 on_stderr.forget();
662
663 let mut running_clone = *running;
665 let mut exit_info_clone = *exit_info;
666 let mut error_msg_clone = *error_msg;
667 let es_for_done = es.clone();
668 let on_done = Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
669 let payload: DonePayload = e
670 .data()
671 .as_string()
672 .and_then(|s| serde_json::from_str(&s).ok())
673 .unwrap_or(DonePayload {
674 exit_code: None,
675 oom_killed: false,
676 timed_out: false,
677 duration_ms: 0,
678 error: None,
679 });
680
681 let (info, err) = if let Some(msg) = payload.error {
684 (
685 format!("耗时: {}ms · 状态: 系统错误", payload.duration_ms),
686 msg,
687 )
688 } else if payload.timed_out {
689 (
690 format!("耗时: {}ms · 状态: 超时", payload.duration_ms),
691 "超时".to_string(),
692 )
693 } else if payload.oom_killed {
694 (
695 format!("耗时: {}ms · 状态: 内存超限", payload.duration_ms),
696 "内存超限".to_string(),
697 )
698 } else if payload.exit_code == Some(0) {
699 (
700 format!("耗时: {}ms · 状态: 成功", payload.duration_ms),
701 String::new(),
702 )
703 } else {
704 (
705 format!(
706 "耗时: {}ms · 退出码: {} · 运行错误",
707 payload.duration_ms,
708 payload.exit_code.unwrap_or(-1)
709 ),
710 "运行错误".to_string(),
711 )
712 };
713 exit_info_clone.set(info);
714 error_msg_clone.set(err);
715 running_clone.set(false);
716 es_for_done.close();
717 });
718 es.add_event_listener_with_callback("done", on_done.as_ref().unchecked_ref())?;
719 on_done.forget();
720
721 let mut running_clone = *running;
723 let mut error_msg_clone = *error_msg;
724 let es_for_error = es.clone();
725 let on_error = Closure::<dyn FnMut(web_sys::Event)>::new(move |_e: web_sys::Event| {
726 error_msg_clone.set("连接异常,请重试".to_string());
727 running_clone.set(false);
728 es_for_error.close();
729 });
730 es.add_event_listener_with_callback("error", on_error.as_ref().unchecked_ref())?;
731 on_error.forget();
732
733 Ok(())
734 }
735
736 pub async fn poll_result(
739 task_id: &str,
740 running: &mut Signal<bool>,
741 stage: &mut Signal<String>,
742 output: &mut Signal<String>,
743 exit_info: &mut Signal<String>,
744 error_msg: &mut Signal<String>,
745 term_handle: &Signal<Option<TerminalHandle>>,
746 has_output: &mut Signal<bool>,
747 ) {
748 let mut polls = 0u32;
749 loop {
750 polls += 1;
751 sleep_ms(500).await;
752 match get_exec_result(task_id.to_string()).await {
753 Ok(task) => {
754 stage.set(task.stage.clone());
755 let terminal =
756 task.status != ExecStatus::Queued && task.status != ExecStatus::Running;
757 if terminal {
758 running.set(false);
759 if let Some(res) = task.result {
760 let out = format!("Stdout:\n{}\nStderr:\n{}", res.stdout, res.stderr);
761 output.set(out);
762 exit_info.set(format!(
763 "耗时: {}ms · 状态: {}",
764 res.duration_ms,
765 super::status_label(&res.status)
766 ));
767 if res.status == ExecStatus::Success {
768 error_msg.set(String::new());
769 } else {
770 error_msg.set(super::status_label(&res.status));
771 }
772 if let Some(h) = term_handle.read().as_ref() {
774 h.instance().write_all(&res.stdout, &res.stderr);
775 }
776 has_output.set(true);
777 }
778 break;
779 }
780 if polls >= 240 {
781 running.set(false);
782 stage.set("查询超时".to_string());
783 error_msg.set("轮询超时,请重试".to_string());
784 break;
785 }
786 }
787 Err(_) => {
788 running.set(false);
789 stage.set("结果获取异常".to_string());
790 error_msg.set("结果获取异常".to_string());
791 break;
792 }
793 }
794 }
795 }
796}