Skip to content

feat: Add monitor server#102

Merged
junyu0312 merged 2 commits intomainfrom
refine
Mar 15, 2026
Merged

feat: Add monitor server#102
junyu0312 merged 2 commits intomainfrom
refine

Conversation

@junyu0312
Copy link
Owner

@junyu0312 junyu0312 commented Mar 13, 2026

Summary by CodeRabbit

  • New Features

    • Adds an in-VM monitor server that starts during boot, accepting diagnostic commands and returning JSON responses.
    • Exposes balloon device controls via the monitor (info and update_num_pages commands).
  • Improvements

    • Async runtime integration for non-blocking monitor and device interactions.
    • Improved monitor error handling and more robust IPC behavior.
  • Chores

    • Added JSON serialization support for monitor payloads.

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds an async Unix-domain socket monitor module to vm-core with a MonitorServer/Builder/MonitorCommand API, integrates it into Vm (started during boot), and implements a virtio-balloon device plus a balloon monitor that registers with the server; workspace serde/serde_json/async-trait/tokio dependencies were added.

Changes

Cohort / File(s) Summary
Workspace deps
Cargo.toml, crates/vm-core/Cargo.toml, crates/vm-device/Cargo.toml, crates/vm-machine/Cargo.toml
Added workspace dependencies: serde (+derive), serde_json, async-trait, and tokio (workspace = true) across crates.
vm-core monitor
crates/vm-core/src/lib.rs, crates/vm-core/src/monitor.rs
New public monitor module: Error enum, MonitorCommand trait, MonitorServer, MonitorServerBuilder, per-connection handling using Tokio and Unix-domain socket at /tmp/vm.sock.
Vm wiring
crates/vm-machine/src/vm.rs, crates/vm-machine/src/vm_builder.rs
Vm gains monitor: MonitorServer (pub(crate)); builder constructs MonitorServer via MonitorServerBuilder; Vm::run calls monitor.start() after boot.
Device init / monitor registration
crates/vm-machine/src/device.rs
InitDevice::init_devices signature updated to accept &mut MonitorServerBuilder; VM device initialization registers a balloon monitor handler instead of spawning previous paging thread.
Error plumbing
crates/vm-machine/src/error.rs
Added Monitor(#[from] vm_core::monitor::Error) variant to propagate monitor errors into vm-machine Result type.
Virtio transport accessor
crates/vm-virtio/src/transport/mmio.rs
Added pub fn dev(&self) -> Arc<Mutex<VirtioDev<C, D>>> to expose underlying device Arc<Mutex<...>>.
Virtio balloon device: restructure
crates/vm-device/src/device/virtio/virtio_balloon_traditional.rs, .../device.rs, .../monitor.rs
Replaced large inline implementation with module split: new device.rs implements VirtioBalloonTranditional device, APIs, virtqueue handlers and type aliases; new monitor.rs implements VirtioBalloonMonitor implementing MonitorCommand and BalloonInfo (serde Serialize). Top-level file now exposes pub mod device; pub mod monitor;.
Monitor client CLI
crates/vm-monitor/src/main.rs
Minor CLI adjustments: deny(warnings), PATH type change, dynamic buffer, and explicit read-loop error handling.
vm-core Cargo metadata
crates/vm-core/Cargo.toml
Added async-trait, serde_json, and tokio to dependencies.

Sequence Diagram(s)

sequenceDiagram
    participant Builder as MonitorServerBuilder
    participant Server as MonitorServer
    participant Listener as UnixListener
    participant Conn as MonitorConnection
    participant Registry as Handler Registry
    participant Cmd as MonitorCommand Handler

    Builder->>Builder: register_command_handler(name, handler)
    Builder->>Server: build()
    Server->>Listener: bind(/tmp/vm.sock)
    Listener-->>Server: socket ready
    loop accept connections
        Listener-->>Server: accept stream
        Server->>Conn: spawn per-connection task
        loop read lines
            Conn->>Conn: parse (cmd, subcommands)
            Conn->>Registry: lookup handler
            alt handler found
                Registry-->>Conn: handler ref
                Conn->>Cmd: handle_command(subcommands)
                Cmd-->>Conn: response
                Conn->>Client: write response
            else unknown
                Conn->>Client: write "ERR unknown command"
            end
        end
    end
Loading
sequenceDiagram
    participant Vm
    participant Builder as MonitorServerBuilder
    participant Server as MonitorServer

    Vm->>Builder: MonitorServerBuilder::default()
    Vm->>Builder: register balloon handler
    Builder->>Server: build()
    Vm->>Vm: set monitor field
    Vm->>Server: start()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • refine: Refine virtio-pci #96 — touches virtio balloon device constants/types and virtio transport code; likely overlapping changes to device definitions.
  • refine: Refine to prepare for userspace irq_chip #75 — modifies device initialization API (InitDevice::init_devices); strongly related to the monitor_builder parameter change.
  • Refine #97 — modifies virtio balloon device internals and APIs (state/config handling); related to the new balloon device/monitor implementation.

Poem

🐰 I dug a socket where the temp fools walk,
I listen for commands and answer with a talk.
Balloon pages hop, monitors hum and cheer,
Async whiskers twitch — the VM's ears hear! 🎩✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Add monitor server' directly and clearly describes the main change: introducing a new monitor server infrastructure with Unix-domain socket IPC support across the VM core and machine crates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refine
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
crates/vm-core/src/monitor.rs (4)

10-10: Hardcoded socket path reduces flexibility.

The socket path /tmp/vm.sock is hardcoded and duplicated across monitor.rs and vm-monitor/src/main.rs. Consider making this configurable via the builder or exposing it as a public constant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-core/src/monitor.rs` at line 10, The hardcoded PATH constant (const
PATH: &str = "/tmp/vm.sock") reduces flexibility and is duplicated; replace it
with a configurable value by either (a) exposing a shared public constant and
using that from both monitor.rs and vm-monitor/src/main.rs, or (b) adding a
socket_path field to the Monitor builder / Monitor::new API so callers can pass
the path (e.g., add socket_path: String to the MonitorBuilder/Monitor struct and
use that instead of PATH). Update all usages (references to PATH in monitor.rs
and vm-monitor/src/main.rs) to read the new builder/struct field or the shared
pub const so the path is no longer hardcoded or duplicated.

34-99: Spawned task errors are silently dropped.

The JoinHandle returned by tokio::spawn is not stored or awaited, so any errors from the connection handler task (returned via Ok::<(), Error>(()) on line 96) are silently discarded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-core/src/monitor.rs` around lines 34 - 99, The spawned task inside
start (the async block that uses stream and components) returns a Result and its
JoinHandle is currently discarded, so any panics or returned Err values are
lost; capture the JoinHandle returned by tokio::spawn and arrange to await it
(or spawn a short-lived supervisor task that awaits the handle) and log any
JoinError or the inner Err value from the connection handler (include context
like the command connection or component name), so errors from the async block
are surfaced instead of silently dropped.

58-58: Remove debug print statement.

This println! appears to be debug output that should be removed or converted to proper tracing/logging.

♻️ Proposed fix
-                            println!("line: {line}");
+                            tracing::debug!("line: {line}");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-core/src/monitor.rs` at line 58, Remove the stray debug
println!("line: {line}"); — either delete it or convert it to a proper logger
call (e.g., tracing::trace!("line: {}", line) or log::debug!("line: {}", line))
and add the corresponding import if necessary; ensure you reference the existing
logging/tracing setup used in monitor.rs so the replacement uses the project’s
logging facility rather than stdout.

42-43: Fixed buffer size may truncate long commands.

Commands longer than 1024 bytes will be silently truncated. Consider either documenting this limit, handling partial reads, or using a growable buffer approach.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-core/src/monitor.rs` around lines 42 - 43, The fixed 1024-byte
buffer (buf) used with stream.try_read can truncate long commands; replace it
with a growable buffer and read in a loop appending each chunk until the full
command is received. Specifically, stop using let mut buf = vec![0u8; 1024] and
instead create let mut buf = Vec::new(), then repeatedly call stream.try_read
(or stream.read) into a temporary small stack buffer (e.g., tmp [u8; 4096]) and
extend buf with the bytes read, breaking on 0 bytes (EOF) or
WouldBlock/WouldBlock-equivalent for nonblocking streams; finally process the
complete buf (or parse by newline/delimiter) so no command is silently
truncated. Ensure error handling preserves and logs read errors and that loop
termination conditions handle partial reads correctly.
crates/vm-machine/src/vm.rs (1)

35-36: Monitor start failure is not propagated.

MonitorServer::start() returns () and spawns an async task that silently fails if binding fails (see review comment in monitor.rs). The VM will continue running without an operational monitor if the socket bind fails.

Consider returning a Result from start() or at minimum logging the failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-machine/src/vm.rs` around lines 35 - 36, The call to
self.monitor.start() in VM ignores bind failures because MonitorServer::start()
currently returns () and spawns a background task that can fail silently; change
MonitorServer::start to return Result<(), MonitorError> (or similar) that
reports binding/accept errors, update the implementation in monitor.rs to
propagate the listener bind error instead of swallowing it, and then update the
call site in vm.rs (the self.monitor.start() invocation) to handle the Result
(propagate it from VM::start or log and abort VM startup) so monitor start
failures are not silently ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/vm-core/Cargo.toml`:
- Line 15: Remove the unused rustyline dependency from the vm-core crate's
Cargo.toml: delete the line that declares rustyline = { workspace = true } so
vm-core no longer lists or pulls in the rustyline crate (it's only needed by
vm-monitor's REPL code).

In `@crates/vm-core/src/monitor.rs`:
- Around line 17-18: Rename the typo'd error enum variant
CommandHandlerConflicat to CommandHandlerConflict in the monitor.rs definition
and update every usage/constructor and match arm that references
CommandHandlerConflicat (e.g., places creating the error or matching on it) to
use CommandHandlerConflict instead so symbol names remain consistent across the
codebase.
- Around line 110-113: The UnixListener::bind call inside the tokio::spawn
currently silently returns on error; update the block around
UnixListener::bind(PATH) so that on Err you log the bind error (including the
error value) and, if the error looks like a stale socket (e.g., file exists /
AddrInUse), attempt to remove PATH via std::fs::remove_file and then retry
UnixListener::bind once, logging any removal or retry failures; ensure you still
return/exit the task if the retry fails. Refer to UnixListener::bind, PATH, and
the spawned tokio::spawn listener block when making the changes.
- Line 78: Remove the stray debug print by deleting the println!("write aaa")
statement found in monitor.rs (the debug println! call); simply remove that line
so no debug output is emitted from the monitor code (ensure no other leftover
debug println! calls remain in the surrounding function).

In `@crates/vm-machine/src/vm_builder.rs`:
- Around line 42-43: The MonitorServerBuilder is instantiated with
MonitorServerBuilder::default() but never has any command handlers registered
before being built and started, so add at least placeholder handlers via
MonitorServerBuilder::register_command_handler(...) for expected commands (or a
generic noop/help handler) before calling build(), or change Vm::run() to defer
calling monitor.start() until after handlers are registered; locate the
MonitorServerBuilder usage and the monitor.start() call in Vm::run(), and either
register handlers on the builder (using register_command_handler) prior to
build() or document/defer startup to ensure the monitor can respond to commands.

In `@crates/vm-monitor/src/main.rs`:
- Around line 33-34: The buffer is created empty so stream.read_buf(&mut
buf).await always reads 0 bytes; pre-allocate spare capacity before calling
read_buf. Replace vec![] with a capacity-backed buffer (e.g., let mut buf =
Vec::with_capacity(SERVER_BUFFER_SIZE) or use
BytesMut::with_capacity(SERVER_BUFFER_SIZE)) so stream.read_buf(&mut buf).await
can write into the spare capacity; adjust SERVER_BUFFER_SIZE (or a constant) as
appropriate and keep the read call using buf and stream.read_buf.

---

Nitpick comments:
In `@crates/vm-core/src/monitor.rs`:
- Line 10: The hardcoded PATH constant (const PATH: &str = "/tmp/vm.sock")
reduces flexibility and is duplicated; replace it with a configurable value by
either (a) exposing a shared public constant and using that from both monitor.rs
and vm-monitor/src/main.rs, or (b) adding a socket_path field to the Monitor
builder / Monitor::new API so callers can pass the path (e.g., add socket_path:
String to the MonitorBuilder/Monitor struct and use that instead of PATH).
Update all usages (references to PATH in monitor.rs and vm-monitor/src/main.rs)
to read the new builder/struct field or the shared pub const so the path is no
longer hardcoded or duplicated.
- Around line 34-99: The spawned task inside start (the async block that uses
stream and components) returns a Result and its JoinHandle is currently
discarded, so any panics or returned Err values are lost; capture the JoinHandle
returned by tokio::spawn and arrange to await it (or spawn a short-lived
supervisor task that awaits the handle) and log any JoinError or the inner Err
value from the connection handler (include context like the command connection
or component name), so errors from the async block are surfaced instead of
silently dropped.
- Line 58: Remove the stray debug println!("line: {line}"); — either delete it
or convert it to a proper logger call (e.g., tracing::trace!("line: {}", line)
or log::debug!("line: {}", line)) and add the corresponding import if necessary;
ensure you reference the existing logging/tracing setup used in monitor.rs so
the replacement uses the project’s logging facility rather than stdout.
- Around line 42-43: The fixed 1024-byte buffer (buf) used with stream.try_read
can truncate long commands; replace it with a growable buffer and read in a loop
appending each chunk until the full command is received. Specifically, stop
using let mut buf = vec![0u8; 1024] and instead create let mut buf = Vec::new(),
then repeatedly call stream.try_read (or stream.read) into a temporary small
stack buffer (e.g., tmp [u8; 4096]) and extend buf with the bytes read, breaking
on 0 bytes (EOF) or WouldBlock/WouldBlock-equivalent for nonblocking streams;
finally process the complete buf (or parse by newline/delimiter) so no command
is silently truncated. Ensure error handling preserves and logs read errors and
that loop termination conditions handle partial reads correctly.

In `@crates/vm-machine/src/vm.rs`:
- Around line 35-36: The call to self.monitor.start() in VM ignores bind
failures because MonitorServer::start() currently returns () and spawns a
background task that can fail silently; change MonitorServer::start to return
Result<(), MonitorError> (or similar) that reports binding/accept errors, update
the implementation in monitor.rs to propagate the listener bind error instead of
swallowing it, and then update the call site in vm.rs (the self.monitor.start()
invocation) to handle the Result (propagate it from VM::start or log and abort
VM startup) so monitor start failures are not silently ignored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 80aaceac-3907-49fc-9b47-87de3be618ba

📥 Commits

Reviewing files that changed from the base of the PR and between 7c50898 and d4d19be.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • crates/vm-core/Cargo.toml
  • crates/vm-core/src/lib.rs
  • crates/vm-core/src/monitor.rs
  • crates/vm-machine/Cargo.toml
  • crates/vm-machine/src/vm.rs
  • crates/vm-machine/src/vm_builder.rs
  • crates/vm-monitor/src/main.rs

kvm-bindings = { workspace = true, optional = true }
kvm-ioctls = { workspace = true, optional = true }
memmap2 = { workspace = true }
rustyline = { workspace = true }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if rustyline is actually used anywhere in vm-core
rg -n 'rustyline' crates/vm-core/

Repository: junyu0312/rust-vm

Length of output: 122


🏁 Script executed:

#!/bin/bash
# Check all Rust files in vm-core for any rustyline usage (not just declarations)
rg 'rustyline' crates/vm-core/src/ crates/vm-core/tests/ 2>/dev/null || echo "No rustyline usage in vm-core src/tests"

# Check if rustyline is used in vm-monitor
rg 'rustyline' crates/vm-monitor/src/ 2>/dev/null | head -20

# Check workspace dependency definition
rg -A 2 'rustyline' Cargo.toml | head -10

Repository: junyu0312/rust-vm

Length of output: 300


Remove unnecessary rustyline dependency from vm-core.

The rustyline crate is not used anywhere in vm-core but is actively used in crates/vm-monitor/src/main.rs for REPL functionality. This dependency should be removed from crates/vm-core/Cargo.toml (line 15).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-core/Cargo.toml` at line 15, Remove the unused rustyline dependency
from the vm-core crate's Cargo.toml: delete the line that declares rustyline = {
workspace = true } so vm-core no longer lists or pulls in the rustyline crate
(it's only needed by vm-monitor's REPL code).

Comment on lines +17 to +18
#[error("{0}")]
CommandHandlerConflicat(String),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in error variant name.

CommandHandlerConflicat should be CommandHandlerConflict.

✏️ Proposed fix
     #[error("{0}")]
-    CommandHandlerConflicat(String),
+    CommandHandlerConflict(String),

Also update the usage on line 147:

-            return Err(Error::CommandHandlerConflicat(name));
+            return Err(Error::CommandHandlerConflict(name));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[error("{0}")]
CommandHandlerConflicat(String),
#[error("{0}")]
CommandHandlerConflict(String),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-core/src/monitor.rs` around lines 17 - 18, Rename the typo'd error
enum variant CommandHandlerConflicat to CommandHandlerConflict in the monitor.rs
definition and update every usage/constructor and match arm that references
CommandHandlerConflicat (e.g., places creating the error or matching on it) to
use CommandHandlerConflict instead so symbol names remain consistent across the
codebase.

Comment on lines +110 to +113
tokio::spawn(async move {
let Ok(listener) = UnixListener::bind(PATH) else {
return;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Silent failure when socket bind fails.

If a stale socket file exists from a previous crashed run, UnixListener::bind will fail and the server silently exits without any indication to the caller or logs. This makes debugging difficult.

Consider removing the stale socket before binding and logging/returning the error.

🛠️ Proposed fix
         tokio::spawn(async move {
+            // Remove stale socket file if it exists
+            let _ = std::fs::remove_file(PATH);
+
-            let Ok(listener) = UnixListener::bind(PATH) else {
-                return;
-            };
+            let listener = match UnixListener::bind(PATH) {
+                Ok(l) => l,
+                Err(e) => {
+                    tracing::error!("Failed to bind monitor socket at {PATH}: {e}");
+                    return;
+                }
+            };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tokio::spawn(async move {
let Ok(listener) = UnixListener::bind(PATH) else {
return;
};
tokio::spawn(async move {
// Remove stale socket file if it exists
let _ = std::fs::remove_file(PATH);
let listener = match UnixListener::bind(PATH) {
Ok(l) => l,
Err(e) => {
tracing::error!("Failed to bind monitor socket at {PATH}: {e}");
return;
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-core/src/monitor.rs` around lines 110 - 113, The UnixListener::bind
call inside the tokio::spawn currently silently returns on error; update the
block around UnixListener::bind(PATH) so that on Err you log the bind error
(including the error value) and, if the error looks like a stale socket (e.g.,
file exists / AddrInUse), attempt to remove PATH via std::fs::remove_file and
then retry UnixListener::bind once, logging any removal or retry failures;
ensure you still return/exit the task if the retry fails. Refer to
UnixListener::bind, PATH, and the spawned tokio::spawn listener block when
making the changes.

Comment on lines +42 to +43
let monitor_server_builder = MonitorServerBuilder::default();

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are any MonitorCommand implementations that should be registered
ast-grep --pattern $'impl MonitorCommand for $_ {
  $$$
}'

Repository: junyu0312/rust-vm

Length of output: 43


🏁 Script executed:

# View the vm_builder.rs file to understand the context
cat -n crates/vm-machine/src/vm_builder.rs | head -100

Repository: junyu0312/rust-vm

Length of output: 2471


🏁 Script executed:

# Search for MonitorServer definitions
fd -e rs -e toml | xargs grep -l "MonitorServer" | head -20

Repository: junyu0312/rust-vm

Length of output: 154


🏁 Script executed:

# Check if there are any command handler registrations in the codebase
rg "register_command_handler|register_handler" -A 3 -B 1

Repository: junyu0312/rust-vm

Length of output: 1060


🏁 Script executed:

# View the monitor.rs file to understand MonitorServerBuilder
cat -n crates/vm-core/src/monitor.rs

Repository: junyu0312/rust-vm

Length of output: 6077


🏁 Script executed:

# Check if monitor.start() is called anywhere
rg "monitor.*\.start\(|\.start\(\)" --type rs

Repository: junyu0312/rust-vm

Length of output: 87


🏁 Script executed:

# Search for MonitorCommand trait implementations
rg "impl.*MonitorCommand" --type rs -A 5

Repository: junyu0312/rust-vm

Length of output: 87


🏁 Script executed:

# Check where the monitor field is used in Vm
rg "monitor" crates/vm-machine/src/vm.rs

Repository: junyu0312/rust-vm

Length of output: 166


🏁 Script executed:

# Search for MonitorCommand implementations without file type restrictions
rg "impl.*MonitorCommand" -A 5

Repository: junyu0312/rust-vm

Length of output: 43


🏁 Script executed:

# Check where monitor.start() is called in vm.rs
cat -n crates/vm-machine/src/vm.rs | grep -A 5 -B 5 "monitor"

Repository: junyu0312/rust-vm

Length of output: 1266


Monitor server is built without any command handlers.

The MonitorServerBuilder is created with default() on line 42 and built on line 68 without calling register_command_handler(). Since monitor.start() is invoked in Vm::run(), the server runs but cannot process any commands—all client requests will receive "ERR unknown command" responses. No MonitorCommand implementations exist in the codebase, leaving the monitoring infrastructure non-functional.

If command handlers are planned as future work, consider either:

  • Adding placeholder handlers for expected monitor commands
  • Deferring the monitor.start() call until handlers are registered
  • Documenting this as intentional scaffolding
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-machine/src/vm_builder.rs` around lines 42 - 43, The
MonitorServerBuilder is instantiated with MonitorServerBuilder::default() but
never has any command handlers registered before being built and started, so add
at least placeholder handlers via
MonitorServerBuilder::register_command_handler(...) for expected commands (or a
generic noop/help handler) before calling build(), or change Vm::run() to defer
calling monitor.start() until after handlers are registered; locate the
MonitorServerBuilder usage and the monitor.start() call in Vm::run(), and either
register handlers on the builder (using register_command_handler) prior to
build() or document/defer startup to ensure the monitor can respond to commands.

Comment on lines +33 to 34
let mut buf = vec![];
let n = stream.read_buf(&mut buf).await?;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Empty buffer will read zero bytes.

read_buf reads into the spare capacity of a Vec. An empty Vec created with vec![] has zero capacity, so read_buf will always return Ok(0) without reading any data from the socket.

Pre-allocate the buffer to match the server's buffer size.

🐛 Proposed fix
-                let mut buf = vec![];
-                let n = stream.read_buf(&mut buf).await?;
+                let mut buf = vec![0u8; 1024];
+                let n = stream.read(&mut buf).await?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-monitor/src/main.rs` around lines 33 - 34, The buffer is created
empty so stream.read_buf(&mut buf).await always reads 0 bytes; pre-allocate
spare capacity before calling read_buf. Replace vec![] with a capacity-backed
buffer (e.g., let mut buf = Vec::with_capacity(SERVER_BUFFER_SIZE) or use
BytesMut::with_capacity(SERVER_BUFFER_SIZE)) so stream.read_buf(&mut buf).await
can write into the spare capacity; adjust SERVER_BUFFER_SIZE (or a constant) as
appropriate and keep the read call using buf and stream.read_buf.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
crates/vm-core/src/monitor.rs (3)

17-18: ⚠️ Potential issue | 🟡 Minor

Rename typoed error variant for consistency.

CommandHandlerConflicat should be CommandHandlerConflict in both declaration and constructor usage.

✏️ Proposed fix
 pub enum Error {
@@
-    CommandHandlerConflicat(String),
+    CommandHandlerConflict(String),
@@
-            return Err(Error::CommandHandlerConflicat(name));
+            return Err(Error::CommandHandlerConflict(name));

Also applies to: 147-147

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-core/src/monitor.rs` around lines 17 - 18, Rename the misspelled
error enum variant CommandHandlerConflicat to CommandHandlerConflict in the
error enum declaration and update all usages/constructors that construct this
variant (e.g., places referencing CommandHandlerConflicat) so they use
CommandHandlerConflict instead; ensure the #[error("{0}")] annotation remains on
the corrected variant and compile to catch any remaining references (search for
CommandHandlerConflicat to update all occurrences).

110-113: ⚠️ Potential issue | 🟠 Major

Do not silently fail monitor startup on socket bind errors.

Current behavior returns without logs/retry, so monitor can be disabled with no signal (common case: stale /tmp/vm.sock).

🛠️ Proposed fix (log + stale-socket retry once)
         tokio::spawn(async move {
-            let Ok(listener) = UnixListener::bind(PATH) else {
-                return;
-            };
+            let listener = match UnixListener::bind(PATH) {
+                Ok(listener) => listener,
+                Err(bind_err) => {
+                    eprintln!("monitor bind failed at {PATH}: {bind_err}");
+                    if bind_err.kind() == io::ErrorKind::AddrInUse {
+                        match std::fs::remove_file(PATH) {
+                            Ok(_) => {}
+                            Err(remove_err) => {
+                                eprintln!("failed to remove stale monitor socket {PATH}: {remove_err}");
+                                return;
+                            }
+                        }
+                        match UnixListener::bind(PATH) {
+                            Ok(listener) => listener,
+                            Err(retry_err) => {
+                                eprintln!("monitor bind retry failed at {PATH}: {retry_err}");
+                                return;
+                            }
+                        }
+                    } else {
+                        return;
+                    }
+                }
+            };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-core/src/monitor.rs` around lines 110 - 113, The monitor currently
silently returns when UnixListener::bind(PATH) fails; change the tokio::spawn
block that contains UnixListener::bind(PATH) to handle Err instead of returning
silently: on bind Err, log the error (using the crate's logging/tracing
facility) and if the error suggests a stale socket (e.g., EADDRINUSE or file
exists), unlink/remove PATH and retry bind once; if the retry still fails, log
the final failure and return. Ensure logs include the PATH and the error details
and keep the rest of the tokio::spawn flow intact.

58-58: ⚠️ Potential issue | 🟡 Minor

Remove debug println! statements from monitor I/O path.

These produce noisy output in normal runs and should not be in the hot path.

🧹 Proposed fix
-                            println!("line: {line}");
@@
-                                    println!("write aaa");

Also applies to: 78-78

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-core/src/monitor.rs` at line 58, Remove the stray debug println!
calls from the monitor I/O hot path in monitor.rs (the println! occurrences
found in the I/O handling code), they should be deleted or replaced with a
proper low-level logging call (trace! or debug!) using the crate's logging
facility instead of println!; remove both duplicates (the one shown and the
similar occurrence) so the hot path no longer emits noisy stdout.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/vm-core/src/monitor.rs`:
- Around line 39-53: The current loop using stream.try_read and treating each
chunk as a single command mis-parses when reads are fragmented or coalesced;
replace chunk-based parsing with a line-oriented reader: buffer and accumulate
bytes until newline(s) and then split into complete commands (or wrap the stream
with an AsyncBufRead + lines() iterator) before calling the existing command
handling and respond via stream.write_all; apply the same change to the second
occurrence around the 59-65 block so both places use line-based framing instead
of one-read-per-command parsing (refer to the variables/functions stream,
try_read, buf, and the utf8 conversion/stream.write_all calls to locate the
code).
- Around line 116-120: The accept branch currently discards errors; change the
Err(_err) arm that handles listener.accept().await to capture the error (e.g.,
Err(err)), emit an observable log/metric (using your project's logger/tracing)
including err and context, and implement a backoff before retrying (use
tokio::time::sleep with an exponential/backoff delay capped at a max and reset
the delay on successful accept). Ensure you reference listener.accept().await
and the stream handling so the code resets backoff on Ok((stream, _)) and sleeps
on Err(err) rather than immediately continuing.

---

Duplicate comments:
In `@crates/vm-core/src/monitor.rs`:
- Around line 17-18: Rename the misspelled error enum variant
CommandHandlerConflicat to CommandHandlerConflict in the error enum declaration
and update all usages/constructors that construct this variant (e.g., places
referencing CommandHandlerConflicat) so they use CommandHandlerConflict instead;
ensure the #[error("{0}")] annotation remains on the corrected variant and
compile to catch any remaining references (search for CommandHandlerConflicat to
update all occurrences).
- Around line 110-113: The monitor currently silently returns when
UnixListener::bind(PATH) fails; change the tokio::spawn block that contains
UnixListener::bind(PATH) to handle Err instead of returning silently: on bind
Err, log the error (using the crate's logging/tracing facility) and if the error
suggests a stale socket (e.g., EADDRINUSE or file exists), unlink/remove PATH
and retry bind once; if the retry still fails, log the final failure and return.
Ensure logs include the PATH and the error details and keep the rest of the
tokio::spawn flow intact.
- Line 58: Remove the stray debug println! calls from the monitor I/O hot path
in monitor.rs (the println! occurrences found in the I/O handling code), they
should be deleted or replaced with a proper low-level logging call (trace! or
debug!) using the crate's logging facility instead of println!; remove both
duplicates (the one shown and the similar occurrence) so the hot path no longer
emits noisy stdout.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 097b1e16-a7cb-4555-8cb5-43777804b81a

📥 Commits

Reviewing files that changed from the base of the PR and between d4d19be and 4a0d767.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • crates/vm-core/Cargo.toml
  • crates/vm-core/src/lib.rs
  • crates/vm-core/src/monitor.rs
  • crates/vm-machine/Cargo.toml
  • crates/vm-machine/src/vm.rs
  • crates/vm-machine/src/vm_builder.rs
  • crates/vm-monitor/src/main.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/vm-machine/src/vm_builder.rs
  • crates/vm-core/src/lib.rs
  • crates/vm-core/Cargo.toml
  • crates/vm-monitor/src/main.rs
  • crates/vm-machine/Cargo.toml

Comment on lines +39 to +53
loop {
stream.readable().await?;

let mut buf = vec![0u8; 1024];
match stream.try_read(&mut buf) {
Ok(0) => break,
Ok(n) => {
let line = match str::from_utf8(&buf[..n]) {
Ok(line) => line.trim(),
Err(err) => {
stream.write_all(format!("ERR {err}\n").as_bytes()).await?;

continue;
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix command parsing to handle stream framing correctly.

A Unix stream is byte-oriented; one try_read chunk is not guaranteed to be exactly one command. Fragmented/coalesced reads will mis-parse commands.

🛠️ Proposed fix (line-based reader)
+use tokio::io::AsyncBufReadExt;
+use tokio::io::BufReader;
@@
-    fn start(&self, mut stream: UnixStream) {
+    fn start(&self, stream: UnixStream) {
         tokio::spawn({
             let components = self.components.clone();
 
             async move {
+                let (read_half, mut write_half) = stream.into_split();
+                let mut reader = BufReader::new(read_half);
+                let mut line = String::new();
+
                 loop {
-                    stream.readable().await?;
-
-                    let mut buf = vec![0u8; 1024];
-                    match stream.try_read(&mut buf) {
-                        Ok(0) => break,
-                        Ok(n) => {
-                            let line = match str::from_utf8(&buf[..n]) {
-                                Ok(line) => line.trim(),
-                                Err(err) => {
-                                    stream.write_all(format!("ERR {err}\n").as_bytes()).await?;
-
-                                    continue;
-                                }
-                            };
-                            if line.is_empty() {
-                                continue;
-                            }
+                    line.clear();
+                    let n = reader.read_line(&mut line).await?;
+                    if n == 0 {
+                        break;
+                    }
+                    let line = line.trim();
+                    if line.is_empty() {
+                        continue;
+                    }
@@
-                                        stream.writable().await?;
-
-                                        stream.write_all(resp.as_bytes()).await?;
+                                        write_half.write_all(resp.as_bytes()).await?;
                                     }
                                     Err(e) => {
-                                        stream.write_all(format!("ERR {e}\n").as_bytes()).await?;
+                                        write_half
+                                            .write_all(format!("ERR {e}\n").as_bytes())
+                                            .await?;
                                     }
                                 },
                                 None => {
-                                    stream
+                                    write_half
                                         .write_all(
                                             format!("ERR unknown command {command}\n").as_bytes(),
                                         )
                                         .await?;
                                 }
                             }
-                        }
-                        Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
-                            continue;
-                        }
-                        Err(e) => {
-                            return Err(e.into());
-                        }
-                    }
                 }
 
                 Ok::<(), Error>(())
             }
         });

Also applies to: 59-65

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-core/src/monitor.rs` around lines 39 - 53, The current loop using
stream.try_read and treating each chunk as a single command mis-parses when
reads are fragmented or coalesced; replace chunk-based parsing with a
line-oriented reader: buffer and accumulate bytes until newline(s) and then
split into complete commands (or wrap the stream with an AsyncBufRead + lines()
iterator) before calling the existing command handling and respond via
stream.write_all; apply the same change to the second occurrence around the
59-65 block so both places use line-based framing instead of
one-read-per-command parsing (refer to the variables/functions stream, try_read,
buf, and the utf8 conversion/stream.write_all calls to locate the code).

Comment on lines +116 to +120
let stream = match listener.accept().await {
Ok((stream, _)) => stream,
Err(_err) => {
continue;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle repeated accept failures with observability/backoff.

Blind continue on accept errors can create a noisy tight loop and hides operational issues.

🛠️ Proposed fix
                     Ok((stream, _)) => stream,
-                    Err(_err) => {
+                    Err(err) => {
+                        eprintln!("monitor accept failed: {err}");
+                        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
                         continue;
                     }
                 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let stream = match listener.accept().await {
Ok((stream, _)) => stream,
Err(_err) => {
continue;
}
let stream = match listener.accept().await {
Ok((stream, _)) => stream,
Err(err) => {
eprintln!("monitor accept failed: {err}");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
continue;
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm-core/src/monitor.rs` around lines 116 - 120, The accept branch
currently discards errors; change the Err(_err) arm that handles
listener.accept().await to capture the error (e.g., Err(err)), emit an
observable log/metric (using your project's logger/tracing) including err and
context, and implement a backoff before retrying (use tokio::time::sleep with an
exponential/backoff delay capped at a max and reset the delay on successful
accept). Ensure you reference listener.accept().await and the stream handling so
the code resets backoff on Ok((stream, _)) and sleeps on Err(err) rather than
immediately continuing.

@junyu0312 junyu0312 merged commit 9b57748 into main Mar 15, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant