run_in_container_stream was sequentially awaiting wait_container before
reading the log stream. wait_container blocks until the container
exits, so by the time the log loop ran, all output was already buffered
in the attach stream and got drained near-instantly — appearing as a
one-shot dump instead of streaming.
Now uses tokio::select! to run log_reader and wait_with_timeout
concurrently:
- log_reader: drains the attach stream, pushing each chunk via tx
immediately as it arrives
- wait_with_timeout: waits for container exit (with timeout, kills on
timeout)
Whichever finishes first wins; the survivor is drained/handled. When
wait completes, any tail bytes still in the stream are flushed before
sending the Done chunk.
This is the root cause of the streaming not working — a sleep(1) loop
now streams line-by-line instead of dumping all at once after exit.
The output area was always visible (empty terminal container shown on
page load). Now it only appears after the user clicks Run, with three
states:
- Not run: output area hidden entirely (show_output = false)
- Running, no output yet: skeleton screen placeholder
(running && !has_output)
- Output received: skeleton gone, xterm renders streamed content
Two new signals drive this:
- show_output: set true on Run click, controls output area visibility
- has_output: set true on first stdout/stderr chunk (SSE) or writeAll
(polling fallback), dismisses the skeleton
xterm mount effect now reads show_output, so it retries after the
container div enters the DOM (when show_output flips true).
xterm.js terminal background was Catppuccin Base (#eff1f5 / #1e1e2e),
but the project's code-block background CSS variable uses Catppuccin
Surface0 (#dce0e8 light / #313244 dark). The mismatch left the output
area visibly lighter (light mode) / darker (dark mode) than the
surrounding container.
WASM path: start_exec_stream → EventSource SSE → xterm.js terminal.
stdout/stderr events write to the terminal in real time; done event
sets exit status and closes the connection. Falls back to polling
get_exec_result (writeAll) if EventSource creation fails.
Server path (placeholder, component runs client-side): retains the
original polling logic so both targets compile.
- xterm.js terminal mounted via xterm_bridge (mirrors CodeMirror mount
pattern: use_effect + TerminalHandle + use_drop)
- run_code split into #[cfg(wasm32)] (SSE) and #[cfg(not)] (polling)
- output area: <pre> replaced with xterm container div
- poll_result / start_sse helpers in WASM-only sse_consumer module
- term_handle declared at component scope (cfg-gated) so the WASM
run_code closure can capture it
Note: dx check reports use_resolved_theme-in-closure for the new xterm
effects, same as the pre-existing CodeMirror effects (lines 145, 177).
This is an existing project-wide pattern; cargo build on both targets
and cargo clippy pass clean.
run_in_container_stream: same container lifecycle + ContainerGuard
cleanup as run_in_container, but pushes output chunks to an mpsc
Sender as the log stream is read, instead of buffering until the
container exits. Also retains a full buffer for the caller to write
back to EXEC_TASKS (polling fallback path).
- OutputChunk enum: Stdout/Stderr/Done{exit_code,oom_killed,timed_out}
- client disconnect detection: tx.send fails → stops pushing but
continues draining the log stream so the container exits cleanly
- Done chunk carries terminal status for the SSE done event
- timeout/inspect/OOM logic identical to run_in_container
- run_in_container and its 4 tests left untouched
Mirrors codemirror_bridge.rs: Reflect::get + unchecked_into for the
IIFE object literal window.XtermTerminal, XtermOptions as wasm-bindgen
extern type (TS class survives erasure), TerminalHandle with Drop →
destroy(). WASM-only (pub mod wasm gated on target_arch = wasm32).
Dioxus.toml: register /xterm/terminal.{css,js} in both prod and dev
resource arrays. CSS is emitted as a separate file by vite lib mode
(cssCodeSplit:false still extracts one CSS file for IIFE).
- tokio-stream 0.1 (sync feature) wraps mpsc::Receiver into a Stream
for the upcoming axum SSE handler; axum::response::sse ships with
axum 0.8 already so no extra dep needed there
- web-sys features: add EventSource + MessageEvent so the WASM frontend
can consume SSE via the native EventSource API
Cross-compiling x86_64-unknown-linux-musl from an arm64 buildx leg breaks
ring: cc-rs emits -m64 for the x86_64 target but hands it to Debian's
arm64 musl-gcc (shipped for host arch only), whose cc1 has no -m64 →
'cc1: error: unrecognized command-line option "-m64"'.
Each buildx platform leg now builds its own native musl target
(amd64→x86_64, arm64→aarch64) so musl-gcc and the target always match —
no cross-compiler, no QEMU for the compile step. The built binary is
staged at an arch-independent path so the scratch runtime stage can COPY
it without knowing which target was used.
Makefile gains a docker-multiarch target (buildx --platform
linux/amd64,linux/arm64 --push); 'make docker' now uses buildx --load
for local single-arch testing.
A transparent HTTP proxy on the build host (http_proxy=127.0.0.1:10808)
truncates apt's plain-HTTP downloads to a 2578-byte HTML error page, which
apt rejects with 'Clearsigned file isn't valid, got NOSPLIT'. curl/wget
inside the same container fetch the full 151074-byte InRelease fine, and
HTTPS apt sources work — so switch the builder's Debian sources to HTTPS.
Proven by: apt-get update over HTTPS fetched 9264 kB successfully (4m27s
through the throttled proxy); plain HTTP failed with NOSPLIT every time.
CodeRunner 根 div 没有任何垂直外边距,而普通代码块经 .md-content pre
规则带 margin-bottom: var(--content-gap-paper)(20px)。可运行块根元素是
div 而非 pre,该规则不生效,导致可运行块紧贴其后正文——上方靠前段落
margin 折叠勉强可见,下方完全无间距。
给根 div 加 mb-[var(--content-gap-paper)](沿用组件既有的 var() 任意值
写法),与普通代码块、段落保持一致的 20px 间距。
Without a key, Dioxus 0.7 VDOM diffing may reuse hook slots from
the previous tab component on tab switch. This caused DelayedSkeleton's
visible signal to retain the old true value, bypassing the 200ms delay.
Adding key based on active_tab forces full unmount/remount on tab switch.