fix: ANSI-aware table column alignment using console::pad_str#57
Conversation
Two UX fixes: 1. Version update check is now non-blocking. The HTTP check runs as a fire-and-forget background task, and the cached result is always returned immediately. The exit notification picks up the refreshed state from disk if the background task completes during the session. 2. Interactive shell startup uses a two-phase approach: create container with `sleep infinity`, monitor bootstrap via `podman logs -f` behind a spinner, then `exec` into the shell. This replaces hundreds of lines of npm/rustup output with a clean "Setting up environment..." spinner. Explicit commands (`mino run -- cargo build`) keep the existing `start_attached` flow since they need the entrypoint's env. New runtime trait methods: `start_detached`, `logs_follow_until`. Bootstrap script redirects tool output to /tmp/mino-bootstrap.log (status lines still go to stderr for log following).
Replace Rust's format specifiers ({:<width}) with console::pad_str()
to properly handle invisible ANSI escape codes in colored/bold text.
Format specifiers count raw bytes including ANSI codes, causing
columns to misalign when styled text is present.
- Updated print_cache_table(): VOLUME, ECOSYSTEM, STATE, SIZE, CREATED
- Updated print_home_table(): VOLUME, PROJECT, CREATED
- Updated print_table() in list.rs: NAME, STATUS, STARTED, PROJECT
All padding now uses console::pad_str with proper width constants.
| } else { | ||
| vec![config.session.shell.clone()] | ||
| } | ||
| } else { |
There was a problem hiding this comment.
Duplicated shell command resolution (reported by: Architecture, Rust, Complexity, Consistency, Regression - 95% confidence)
Lines 263-272 duplicate the shell command logic from lines 200-208 in the same function. Both compute the same conditional: if using layers, use /bin/zsh, else use config.session.shell.
This creates a DRY violation that's a maintenance hazard — if one location is updated without the other, the command used for iptables wrapping (line 200) could diverge from the command used in the two-phase exec phase (line 263), causing subtle debugging issues.
Suggested fix: Compute the shell command once before the iptables wrapping, then reuse:
let shell_command = if is_shell_mode {
if using_layers {
vec!["/bin/zsh".to_string()]
} else {
vec![config.session.shell.clone()]
}
} else {
vec![]
};
let command = if is_shell_mode {
shell_command.clone()
} else {
args.command.clone()
};
// Then iptables wrapping only affects the container creation, not the shell_command stored in RunContext| &container_id[..12], | ||
| ctx.shell_command | ||
| ); | ||
| let exit_code = ctx |
There was a problem hiding this comment.
Security issue: CAP_NET_ADMIN not dropped in exec phase (Regression - 90% confidence)
The two-phase bootstrap applies iptables rules in phase 1 via generate_iptables_wrapper (line 511). However, the exec phase at line 562 calls the shell directly without wrapping with capsh --drop=cap_net_admin.
When NetworkMode::Allow is active, this is a regression: users with the container shell can run iptables -F to flush the firewall rules and bypass network restrictions. The original start_attached flow wraps the command with capsh to prevent this (line 213).
Suggested fix: Wrap the exec'd shell command with capsh if in network-allow mode:
let exec_command = if let NetworkMode::Allow(_) = ctx.network_mode {
vec![
"capsh".to_string(),
"--drop=cap_net_admin".to_string(),
"--".to_string(),
"-c".to_string(),
format!("exec {}", ctx.shell_command.join(" ")),
]
} else {
ctx.shell_command.clone()
};| tokio::select! { | ||
| _ = &mut sleep => { | ||
| // Timeout — kill the logs process | ||
| let _ = child.kill().await; |
There was a problem hiding this comment.
Resource leak: child process not waited after kill (Rust - 85% confidence)
After calling child.kill().await on line 119 (timeout path) and line 149 (marker found path), the code does not call child.wait().await to reap the child process. On Unix, kill() sends SIGKILL but the process becomes a zombie until its exit status is waited for. Relying on the Child destructor to reap is fragile.
Suggested fix: Add child.wait().await after each child.kill().await:
// Timeout path (line 119):
let _ = child.kill().await;
let _ = child.wait().await;
// Marker found path (line 149):
let _ = child.kill().await;
let _ = child.wait().await;| /// marker. | ||
| async fn run_interactive_shell( | ||
| ctx: &mut RunContext<'_>, | ||
| _cache_session: &CacheSession, |
There was a problem hiding this comment.
Unused parameter in function signature (reported by: Rust, Architecture, Performance, Consistency, Complexity - 92% confidence)
The _cache_session parameter is prefixed with underscore to suppress unused warnings, indicating it's not actually used inside run_interactive_shell. Cache finalization happens in the caller run_interactive (lines 425-427), so passing this parameter adds noise to the API and could mislead readers into thinking cache handling was delegated.
Suggested fix: Remove the parameter from both the function signature and the call site:
// Function signature:
async fn run_interactive_shell(ctx: &mut RunContext<'_>) -> MinoResult<i32> {
// Call site in run_interactive:
run_interactive_shell(ctx).await?| }); | ||
| } | ||
| return None; | ||
| // Background refresh if cache is stale (fire-and-forget) |
There was a problem hiding this comment.
Race condition: non-atomic state file write (Security - 82% confidence)
The fire-and-forget tokio::spawn block (lines 236-245) reads, mutates, and writes the version state file concurrently with the main thread. If two mino sessions start simultaneously (common in CI or multi-terminal workflows), both may read the same stale state before either writes back. The save_state_to function uses tokio::fs::write which is not atomic — a crash or concurrent write mid-operation could corrupt the file.
While this is low-risk for a local advisory-only state file, the pattern should be correct for production robustness.
Suggested fix: Use atomic write pattern (write to temp, then rename):
async fn save_state_to(path: &Path, state: &VersionState) {
// ... existing dir creation ...
let tmp = path.with_extension("json.tmp");
if let Err(e) = tokio::fs::write(&tmp, json).await {
warn!("Failed to write version state: {}", e);
return;
}
if let Err(e) = tokio::fs::rename(&tmp, path).await {
warn!("Failed to rename version state: {}", e);
}
}| println!( | ||
| "{:<40} {:<10} {:<10} {:<10} {:<16}", | ||
| "VOLUME", "ECOSYSTEM", "STATE", "SIZE", "CREATED" | ||
| "{} {} {} {} {}", |
There was a problem hiding this comment.
Table styling inconsistency (Consistency - 90% confidence)
The headers in this file ("VOLUME", "ECOSYSTEM", etc.) are plain strings, but list.rs wraps headers in style(...).bold() before calling pad_str. Both files were modified in this PR to adopt pad_str, but the styling convention diverged instead of being unified.
Since bold headers (as in list.rs) provide better visual hierarchy, apply bold styling consistently across both files:
pad_str(&style("VOLUME").bold().to_string(), W_VOLUME, Alignment::Left, None),
pad_str(&style("ECOSYSTEM").bold().to_string(), W_ECO, Alignment::Left, None),
// ... etc for all headers in both functions| INSTALL_REDIRECT="/dev/null" | ||
| else | ||
| INSTALL_REDIRECT="/dev/stderr" | ||
| INSTALL_REDIRECT="/tmp/mino-bootstrap.log" |
There was a problem hiding this comment.
Security: Predictable log file path in /tmp (Security - 85% confidence)
The bootstrap output is redirected to /tmp/mino-bootstrap.log, which is a predictable path. Inside a rootless container the risk is limited, but the path is not collision-proof. If multiple containers somehow shared /tmp (unlikely with Podman but possible with custom mounts), one container's bootstrap output could be read or overwritten.
Install output may contain URLs, version info, or error messages with sensitive path details.
Suggested fix: Use mktemp for a unique filename or write to the user's home directory:
# Option 1: Use mktemp
INSTALL_REDIRECT="$(mktemp /tmp/mino-bootstrap-XXXXXX.log)"
# Option 2: Use home directory (safer)
INSTALL_REDIRECT="$HOME/.mino-bootstrap.log"
Code Review Summary: feat/two-phase-bootstrapThis branch implements a well-designed two-phase bootstrap architecture for interactive shell mode. The refactoring correctly preserves existing behavior for explicit commands while eliminating terminal pollution during environment setup. Below is a consolidated overview of review findings. Review CoverageThe PR was reviewed across 8 dimensions: Security, Architecture, Performance, Complexity, Consistency, Regression, Tests, and Rust Code Quality. All 503 existing tests pass. Blocking Issues (7 High-Confidence Findings)All 7 items below appear as inline comments and require changes before merge:
Additional Findings (60-79% Confidence)These are noted for context but do not block merge if architectural decision is intentional:
RecommendationsBefore Merge:
Post-Merge Improvements (non-blocking):
Architecture AssessmentThe two-phase architecture is sound: phase 1 creates a container with All inline comments above include suggested fixes with code examples. Generated by Claude Code automated review | Review branch: feat/two-phase-bootstrap | Base: main | Confidence threshold: ≥80% |
Replace fixed /tmp/mino-bootstrap.log with mktemp-generated unique filename to avoid predictable temp file path. Co-Authored-By: Claude <noreply@anthropic.com>
…nd update check - Write version_state.json atomically via temp file + rename to prevent partial reads from concurrent mino sessions racing on the same file. - Add debug! logging to the fire-and-forget update check spawn so that persistent fetch failures or parse errors are diagnosable. Co-Authored-By: Claude <noreply@anthropic.com>
Add child.wait().await after child.kill().await in follow_until_marker to properly reap killed processes and prevent zombies. Add 4 unit tests for follow_until_marker covering: - marker found on stdout - marker found on stderr - timeout when marker not found - EOF without marker Co-Authored-By: Claude <noreply@anthropic.com>
…clean up - Wrap exec command with capsh --drop=cap_net_admin when NetworkMode::Allow is active in two-phase shell startup, preventing iptables -F bypass - Compute shell command once and reuse for both container start (iptables- wrapped) and exec phase (raw shell), eliminating duplicated logic - Remove unused _cache_session parameter from run_interactive_shell - Replace check_for_update with load_cached_update at exit to avoid redundant background HTTP request; new function reads cached state only - Use warn! consistently for both container stop and remove failures Co-Authored-By: Claude <noreply@anthropic.com>
Cover the two untested error branches in run_interactive_shell: - start_detached failure: verifies container cleanup and session marked as Failed - logs_follow_until error: verifies error propagation and that exec phase is never reached Co-Authored-By: Claude <noreply@anthropic.com>
…acted shared logic)
Summary
Fixes ANSI-aware table column alignment across cache, list, and home volume tables. Replaces Rust's format specifiers with
console::pad_str()to properly handle invisible ANSI escape codes in styled text.Changes
src/cli/commands/cache.rs: Updated
print_cache_table()andprint_home_table()pad_str()src/cli/commands/list.rs: Updated
print_table()pad_str()Why This Matters
Rust's
{:<width}format specifier counts all bytes including invisible ANSI escape codes. When text is styled (bold, colored), these codes inflate the byte count, causing columns to misalign. Theconsolecrate'spad_str()counts only visible characters, fixing alignment.Before: Columns shifted right when status/names were styled
After: Columns align properly regardless of ANSI styling
Testing
mino cache listandmino listwith styled outputRelated
Complements the two-phase bootstrap and update check infrastructure.