Skip to content

feat(android): bootstrap rust android connection#2

Open
skulidropek wants to merge 2 commits into
mainfrom
release/bootstrap-android-connection
Open

feat(android): bootstrap rust android connection#2
skulidropek wants to merge 2 commits into
mainfrom
release/bootstrap-android-connection

Conversation

@skulidropek

@skulidropek skulidropek commented Jun 24, 2026

Copy link
Copy Markdown
Member

Bootstrap Android connection crate

This PR moves the docker-git Android connection codebase into the standalone rust-android-connection repository.

What changed

  • Replaced the template crate with the Android connection library, lifecycle CLI, MCP stdio server, and android-connection compatibility binary.
  • Added focused unit/integration coverage for deterministic runtime specs, CLI behavior, MCP framing, endpoint/path validation, and command construction.
  • Added Android-focused README, MIT license metadata, and a changelog fragment for release tooling.

Mathematical guarantees

  • Endpoint invariant: configured ADB endpoints are validated before use and cannot be empty or malformed.
  • Workspace confinement invariant: MCP file operations reject absolute, parent-directory, root, and prefix paths before producing filesystem effects.
  • Command construction invariant: Docker/ADB operations are represented as argv vectors rather than shell strings.
  • Release invariant: package metadata and Cargo.lock are committed, so binary installation with --locked is reproducible for this revision.

Proof of fix

  • Cause: the Android connection module previously lived inside docker-git, preventing independent ownership, release, and CI validation.
  • Solution: bootstrapped the standalone Rust crate from the docker-git module codebase and wired tests/docs/release metadata in this repository.
  • Proof: local fmt/test/doc-test/build/clippy/install smoke checks passed; GitHub Actions passed on Ubuntu, macOS, and Windows.

Verification

cargo fmt -- --check
cargo test --locked
cargo test --doc --locked
cargo build --locked --bins
cargo clippy --locked --all-targets --all-features -- -D warnings
cargo install --path . --locked --bins --root <tmp>

Docker-git integration PR: ProverCoderAI/docker-git#437

@skulidropek

skulidropek commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 2a204e7
Status: success
Files: 4 (2.24 MB)
Links: README | Manifest

git status

On branch release/bootstrap-android-connection
Your branch is up to date with 'origin/release/bootstrap-android-connection'.

nothing to commit, working tree clean

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a Docker-based Android lifecycle CLI with start, status, and stop commands.
    • Added an MCP stdio server for Android-related tools, including optional install and ADB probe controls.
    • Introduced support for noVNC publishing, resource limits, and dry-run output.
  • Bug Fixes

    • Improved Android project name handling and endpoint validation for safer, more consistent command generation.
  • Documentation

    • Replaced the project README with usage, install, smoke test, and development instructions.
    • Updated licensing information and changelog entries.

Walkthrough

The template crate is replaced with docker-git-android-connection, adding Android spec and Docker helpers, a lifecycle CLI, an MCP stdio server, updated tests, and project documentation.

Changes

Android MCP and Lifecycle Crate Bootstrap

Layer / File(s) Summary
Project scaffolding and metadata
Cargo.toml, LICENSE, README.md, changelog.d/20260624_000000_bootstrap_android_connection.md
Package metadata, binary targets, dependencies, license text, README content, and changelog entry are updated for the Android connection crate.
Core library API
src/lib.rs
Exports Android naming constants, endpoint validation, Android spec construction, MCP tool metadata, noVNC endpoint helpers, resource limits, and Docker run/stop argument builders.
Lifecycle CLI entrypoints
src/main.rs, src/bin/android-connection.rs
Adds the start, status, and stop subcommands, lifecycle JSON output, noVNC and resource-limit parsing, Docker execution helpers, and the android-connection binary entrypoint wiring.
MCP server and Android tool handlers
src/mcp.rs
Adds framed stdio JSON-RPC parsing and response writing, tool listing and dispatch, Android tool handlers, ADB execution helpers, workspace path validation, and the McpState server state.
Tests and example updates
examples/basic_usage.rs, tests/unit/*, tests/integration/*
Unit and integration tests are updated for Android naming, endpoint validation, Docker args, noVNC parsing, CLI behavior, and MCP help output; the example now prints Android spec and Docker run details.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Main as src/main.rs
    participant Lib as src/lib.rs
    participant Docker

    User->>Main: start / status / stop
    Main->>Lib: android_spec(...)
    Main->>Lib: docker_run_args(...) or docker_stop_args(...)
    Main->>Docker: docker run / inspect / rm
    Docker-->>Main: stdout / exit status
    Main-->>User: lifecycle JSON or error
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~100 minutes

Poem

🐇 Hop hop, the old sum crate took a rest,
Android docks and MCP tools put the code to test.
JSON-RPC frames go bouncing through the wire,
Docker args and ADB paths now do conspire.
This bunny grins: “the crate is newly born!” 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: bootstrapping the Rust Android connection crate.
Description check ✅ Passed The description is directly related to the crate bootstrap, tests, docs, and release metadata changes in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/bootstrap-android-connection

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 markdownlint-cli2 (0.22.1)
README.md

markdownlint-cli2 v0.22.1 (markdownlint v0.40.0)
Error: Unable to use configuration file '/coderabbit-0.markdownlint-cli2.jsonc'; ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at throwForConfigurationFile (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:48:9)
at readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:169:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
[cause]: Error: ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at async open (node:internal/fs/promises:640:25)
at async Object.readFile (node:internal/fs/promises:1287:14)
at async readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:141:17)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/coderabbit-0.markdownlint-cli2.jsonc'
}
}

changelog.d/20260624_000000_bootstrap_android_connection.md

markdownlint-cli2 v0.22.1 (markdownlint v0.40.0)
Error: Unable to use configuration file '/coderabbit-1.markdownlint-cli2.jsonc'; ENOENT: no such file or directory, open '/coderabbit-1.markdownlint-cli2.jsonc'
at throwForConfigurationFile (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:48:9)
at readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:169:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
[cause]: Error: ENOENT: no such file or directory, open '/coderabbit-1.markdownlint-cli2.jsonc'
at async open (node:internal/fs/promises:640:25)
at async Object.readFile (node:internal/fs/promises:1287:14)
at async readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:141:17)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/coderabbit-1.markdownlint-cli2.jsonc'
}
}


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.

@skulidropek skulidropek force-pushed the release/bootstrap-android-connection branch from 2a204e7 to 7fd2c8f Compare June 24, 2026 06:42
@skulidropek

skulidropek commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 7fd2c8f
Status: success
Files: 4 (2.51 MB)
Links: README | Manifest

git status

On branch release/bootstrap-android-connection
Your branch is up to date with 'origin/release/bootstrap-android-connection'.

nothing to commit, working tree clean

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (1)
tests/integration/cli.rs (1)

17-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Parse CLI status output as JSON instead of substring matching.

This makes the test resilient to formatting changes and validates structure, not just text fragments.

Suggested patch
     assert!(output.status.success());
     let stdout = String::from_utf8_lossy(&output.stdout);
-    assert!(stdout.contains("\"project_id\": \"dg-test\""));
-    assert!(stdout.contains("\"android_container_name\": \"dg-test-android\""));
+    let value: serde_json::Value =
+        serde_json::from_str(&stdout).expect("status output should be valid JSON");
+    assert_eq!(value["project_id"], "dg-test");
+    assert_eq!(value["android_container_name"], "dg-test-android");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/cli.rs` around lines 17 - 19, The CLI integration test
currently checks status output with substring matching, which is brittle. Update
the test in the CLI status assertion block to parse stdout as JSON and assert on
the structured fields instead of raw text fragments, using the existing output
handling around stdout and the project/android container keys to locate the
assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 60-95: The smoke-test pipeline in the README is broken because the
second `python3 - <<'PY'` consumes stdin for its inline script instead of
reading from `android-connection`, so the response parser never sees the pipe
output. Update the example so the request generator and response reader are
separated correctly, using the existing `android-connection` handshake flow and
the inline Python blocks as distinct producer/consumer steps.

In `@src/lib.rs`:
- Around line 98-119: The endpoint check in is_safe_adb_endpoint() is too loose
because it only enforces a colon and a character whitelist, so malformed values
like missing host/port, nonnumeric ports, or multiple colons still pass through
validate_adb_endpoint() and later fail in run_adb_raw(). Tighten the validation
by parsing the value into exactly one host and one numeric port (and reject
empty parts or extra separators) before returning Ok in validate_adb_endpoint(),
keeping the existing EndpointError path for invalid shapes. Add regression tests
for android:, :5555, android:abc, and a:b:c alongside the current endpoint test
coverage.

In `@src/mcp.rs`:
- Around line 445-462: The adb subprocess invocations in the command handling
flow can block forever because they use plain .output() without any timeout.
Update the adb connect path and the later Command::new("adb").args(args)
execution in the same mcp.rs flow to run with a bounded timeout and terminate
the child process if it expires, returning a McpToolError::CommandFailed that
reflects the timeout condition.
- Around line 530-543: The current workspace_path helper only rejects absolute
paths and parent-dir segments, so symlinked subpaths can still escape the
workspace. Update workspace_path to resolve and validate the canonicalized
workspace plus the target or parent directory before any fs::write or APK
install path use, and ensure the resulting path stays under the workspace root.
Use the existing workspace_path function as the main guard and apply the same
resolved-path check at the call sites that write files or install APKs.
- Around line 129-134: The request routing in handle_request only treats id-less
methods starting with notifications/ as notifications, which still allows
id-less tools/list, tools/call, or unknown methods to emit a response with id:
null. Update handle_request so any request with request.id.is_none() is handled
as a notification and returns None before building the id, while keeping the
existing notification-specific behavior in the method dispatch.
- Around line 80-83: Cap the peer-controlled Content-Length before allocating
the payload in the MCP request path. In `read_message`, validate the parsed
length from `parse_content_length` against a reasonable maximum and return an
error before creating the `payload` Vec or calling `read_exact` if it is too
large. Keep the guard close to the `if let Some(length)` branch so the safety
check is enforced before any allocation or blocking read.
- Around line 445-461: The ADB invocation in run_adb_raw is only using
state.spec.adb_endpoint for the initial connect, but the follow-up
Command::new("adb") call still runs unpinned and can target the wrong device.
Update the shared ADB execution path in run_adb_raw so every adb command
includes the same state.spec.adb_endpoint selection, likely by threading it
through the helper that builds the command arguments and ensuring the final adb
call uses -s with the endpoint as well.
- Around line 302-378: Escape or strictly validate all user-controlled values
before passing them into adb shell commands in android_type_text,
android_launch_app, and android_open_url. Replace the current ad hoc space
substitution in android_type_text with proper shell-safe encoding/escaping for
free-form text, and validate package/activity as Android component identifiers
before building the -n target. Ensure url is encoded or shell-escaped before
being forwarded to am start.

---

Nitpick comments:
In `@tests/integration/cli.rs`:
- Around line 17-19: The CLI integration test currently checks status output
with substring matching, which is brittle. Update the test in the CLI status
assertion block to parse stdout as JSON and assert on the structured fields
instead of raw text fragments, using the existing output handling around stdout
and the project/android container keys to locate the assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 82cd0695-7ca3-4e9d-85be-0ce42de93168

📥 Commits

Reviewing files that changed from the base of the PR and between 62d0e9e and 2a204e7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • Cargo.toml
  • LICENSE
  • README.md
  • changelog.d/20260624_000000_bootstrap_android_connection.md
  • examples/basic_usage.rs
  • src/bin/android-connection.rs
  • src/lib.rs
  • src/main.rs
  • src/mcp.rs
  • src/sum.rs
  • tests/integration/cli.rs
  • tests/integration/mod.rs
  • tests/integration/sum.rs
  • tests/unit/android_connection.rs
  • tests/unit/mod.rs
  • tests/unit/sum.rs
💤 Files with no reviewable changes (3)
  • src/sum.rs
  • tests/integration/sum.rs
  • tests/unit/sum.rs

Comment thread README.md
Comment thread src/lib.rs
Comment on lines +98 to +119
#[must_use]
pub fn is_safe_adb_endpoint(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 255
&& value.contains(':')
&& value.bytes().all(|byte| {
matches!(
byte,
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_' | b':'
)
})
}

pub fn validate_adb_endpoint(value: &str) -> Result<String, EndpointError> {
if is_safe_adb_endpoint(value) {
Ok(value.to_string())
} else {
Err(EndpointError {
value: value.to_string(),
})
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Tighten endpoint validation beyond the character whitelist.

is_safe_adb_endpoint() still accepts malformed values like android:, :5555, android:abc, and a:b:c because it only checks for “contains :” plus allowed characters. android_spec() then blesses those values, and run_adb_raw() later forwards them to adb connect, so the promised upfront validation is bypassed and the failure moves to runtime.

Suggested fix
 pub fn is_safe_adb_endpoint(value: &str) -> bool {
-    !value.is_empty()
-        && value.len() <= 255
-        && value.contains(':')
-        && value.bytes().all(|byte| {
-            matches!(
-                byte,
-                b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_' | b':'
-            )
-        })
+    if value.is_empty() || value.len() > 255 {
+        return false;
+    }
+
+    let (host, port) = match value.split_once(':') {
+        Some((host, port)) if !host.is_empty() && !port.is_empty() && !port.contains(':') => {
+            (host, port)
+        }
+        _ => return false,
+    };
+
+    host.bytes().all(|byte| {
+        matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_')
+    }) && port.bytes().all(|byte| byte.is_ascii_digit())
 }

Please add regression cases for the malformed shapes above alongside the existing endpoint tests.

📝 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
#[must_use]
pub fn is_safe_adb_endpoint(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 255
&& value.contains(':')
&& value.bytes().all(|byte| {
matches!(
byte,
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_' | b':'
)
})
}
pub fn validate_adb_endpoint(value: &str) -> Result<String, EndpointError> {
if is_safe_adb_endpoint(value) {
Ok(value.to_string())
} else {
Err(EndpointError {
value: value.to_string(),
})
}
}
#[must_use]
pub fn is_safe_adb_endpoint(value: &str) -> bool {
if value.is_empty() || value.len() > 255 {
return false;
}
let (host, port) = match value.split_once(':') {
Some((host, port)) if !host.is_empty() && !port.is_empty() && !port.contains(':') => {
(host, port)
}
_ => return false,
};
host.bytes().all(|byte| {
matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_')
}) && port.bytes().all(|byte| byte.is_ascii_digit())
}
pub fn validate_adb_endpoint(value: &str) -> Result<String, EndpointError> {
if is_safe_adb_endpoint(value) {
Ok(value.to_string())
} else {
Err(EndpointError {
value: value.to_string(),
})
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib.rs` around lines 98 - 119, The endpoint check in
is_safe_adb_endpoint() is too loose because it only enforces a colon and a
character whitelist, so malformed values like missing host/port, nonnumeric
ports, or multiple colons still pass through validate_adb_endpoint() and later
fail in run_adb_raw(). Tighten the validation by parsing the value into exactly
one host and one numeric port (and reject empty parts or extra separators)
before returning Ok in validate_adb_endpoint(), keeping the existing
EndpointError path for invalid shapes. Add regression tests for android:, :5555,
android:abc, and a:b:c alongside the current endpoint test coverage.

Comment thread src/mcp.rs
Comment on lines +80 to +83
if let Some(length) = parse_content_length(first_line)? {
read_headers(reader)?;
let mut payload = vec![0_u8; length];
reader.read_exact(&mut payload)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cap Content-Length before allocating the payload.

Content-Length is peer-controlled; a huge value allocates an equally huge Vec and then blocks in read_exact, which can take down the stdio server.

Proposed guard
+const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
+
 fn read_next_message<R: BufRead>(reader: &mut R) -> io::Result<Option<String>> {
@@
         if let Some(length) = parse_content_length(first_line)? {
+            if length > MAX_FRAME_BYTES {
+                return Err(io::Error::new(
+                    io::ErrorKind::InvalidData,
+                    format!("Content-Length {length} exceeds limit {MAX_FRAME_BYTES}"),
+                ));
+            }
             read_headers(reader)?;
             let mut payload = vec![0_u8; length];
📝 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
if let Some(length) = parse_content_length(first_line)? {
read_headers(reader)?;
let mut payload = vec![0_u8; length];
reader.read_exact(&mut payload)?;
const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
if let Some(length) = parse_content_length(first_line)? {
if length > MAX_FRAME_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Content-Length {length} exceeds limit {MAX_FRAME_BYTES}"),
));
}
read_headers(reader)?;
let mut payload = vec![0_u8; length];
reader.read_exact(&mut payload)?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp.rs` around lines 80 - 83, Cap the peer-controlled Content-Length
before allocating the payload in the MCP request path. In `read_message`,
validate the parsed length from `parse_content_length` against a reasonable
maximum and return an error before creating the `payload` Vec or calling
`read_exact` if it is too large. Keep the guard close to the `if let
Some(length)` branch so the safety check is enforced before any allocation or
blocking read.

Comment thread src/mcp.rs
Comment on lines +129 to +134
fn handle_request(request: &JsonRpcRequest, state: &McpState) -> Option<Value> {
if request.id.is_none() && request.method.starts_with("notifications/") {
return None;
}

let id = request.id.clone().unwrap_or(Value::Null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat every request without an id as a notification.

JSON-RPC notifications are any request without id; currently id-less tools/list, tools/call, or unknown methods still emit responses with id: null, which can desynchronize strict clients.

Proposed fix
-    if request.id.is_none() && request.method.starts_with("notifications/") {
+    if request.id.is_none() {
         return None;
     }
📝 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
fn handle_request(request: &JsonRpcRequest, state: &McpState) -> Option<Value> {
if request.id.is_none() && request.method.starts_with("notifications/") {
return None;
}
let id = request.id.clone().unwrap_or(Value::Null);
fn handle_request(request: &JsonRpcRequest, state: &McpState) -> Option<Value> {
if request.id.is_none() {
return None;
}
let id = request.id.clone().unwrap_or(Value::Null);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp.rs` around lines 129 - 134, The request routing in handle_request
only treats id-less methods starting with notifications/ as notifications, which
still allows id-less tools/list, tools/call, or unknown methods to emit a
response with id: null. Update handle_request so any request with
request.id.is_none() is handled as a notification and returns None before
building the id, while keeping the existing notification-specific behavior in
the method dispatch.

Comment thread src/mcp.rs
Comment on lines +302 to +378
fn android_type_text(state: &McpState, arguments: &Value) -> Result<String, McpToolError> {
let text = string_argument(arguments, "text")?.replace(' ', "%s");
run_adb(
state,
&[
"shell".to_string(),
"input".to_string(),
"text".to_string(),
text,
],
)
}

fn android_press_key(state: &McpState, arguments: &Value) -> Result<String, McpToolError> {
let keycode = string_argument(arguments, "keycode")?;
if !keycode
.bytes()
.all(|byte| matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_'))
{
return Err(McpToolError::InvalidArgument(
"keycode may contain only ASCII letters, digits, and '_'".to_string(),
));
}
run_adb(
state,
&[
"shell".to_string(),
"input".to_string(),
"keyevent".to_string(),
keycode,
],
)
}

fn android_launch_app(state: &McpState, arguments: &Value) -> Result<String, McpToolError> {
let package_name = string_argument(arguments, "package")?;
let activity = optional_string_argument(arguments, "activity")?;
match activity {
Some(activity) if !activity.is_empty() => run_adb(
state,
&[
"shell".to_string(),
"am".to_string(),
"start".to_string(),
"-n".to_string(),
format!("{package_name}/{activity}"),
],
),
_ => run_adb(
state,
&[
"shell".to_string(),
"monkey".to_string(),
"-p".to_string(),
package_name,
"-c".to_string(),
"android.intent.category.LAUNCHER".to_string(),
"1".to_string(),
],
),
}
}

fn android_open_url(state: &McpState, arguments: &Value) -> Result<String, McpToolError> {
let url = string_argument(arguments, "url")?;
run_adb(
state,
&[
"shell".to_string(),
"am".to_string(),
"start".to_string(),
"-a".to_string(),
"android.intent.action.VIEW".to_string(),
"-d".to_string(),
url,
],
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

ast-grep outline src/mcp.rs --view expanded

Repository: ProverCoderAI/rust-android-connection

Length of output: 3391


🏁 Script executed:

#!/bin/bash
set -euo pipefail

wc -l src/mcp.rs
printf '\n--- outline ---\n'
ast-grep outline src/mcp.rs --view expanded | sed -n '1,220p'
printf '\n--- relevant lines ---\n'
sed -n '1,420p' src/mcp.rs | cat -n | sed -n '250,410p'

Repository: ProverCoderAI/rust-android-connection

Length of output: 9534


🏁 Script executed:

sed -n '435,520p' src/mcp.rs | cat -n

Repository: ProverCoderAI/rust-android-connection

Length of output: 3559


🏁 Script executed:

sed -n '435,520p' src/mcp.rs | cat -n

Repository: ProverCoderAI/rust-android-connection

Length of output: 3559


🏁 Script executed:

sed -n '435,520p' src/mcp.rs | cat -n

Repository: ProverCoderAI/rust-android-connection

Length of output: 3559


🏁 Script executed:

rg -n "adb shell|input text|am start|monkey|escaping|quote|shell metachar|adb connect" -S README* src

Repository: ProverCoderAI/rust-android-connection

Length of output: 463


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "adb shell|input text|am start|monkey|escaping|quote|shell metachar|adb connect" -S README* src || true

Repository: ProverCoderAI/rust-android-connection

Length of output: 463


🌐 Web query:

ADB shell argument quoting escaping remote shell metacharacters multiple arguments adb shell input text am start docs

💡 Result:

When running commands through adb shell, arguments must be quoted to survive two distinct layers of shell interpretation: your local (host) shell and the Android device's remote shell [1][2][3]. Because adb shell joins all arguments into a single string and executes them via the device's shell, improper quoting often results in arguments being split, special characters being consumed, or quotes being stripped before they reach the intended command [1][2]. For robust execution, follow these general principles: 1. Wrap the entire remote command in quotes: To ensure the host shell does not interpret characters like &, |, <, or >, pass the entire remote command string as a single argument to adb [1][3][4]. Example: adb shell "am start -n package/.Activity -e key 'value'" [2][3]. 2. Escape shell metacharacters: Special characters (', ", &, (, ), <, >, |, ;, \, backtick, *, ~) must be escaped to survive both the host and device shells [5][6]. If you find your quotes being stripped, you may need to use nested or triple-quoting, such as '''\"value\"''' [7][3]. 3. Handling adb shell input text: This command has specific, stricter requirements than general shell commands [5]. * Spaces: Must be encoded as %s [8][9][5]. * Metacharacters: Any shell metacharacter must be escaped (often with a backslash) [5][6]. * Example: To input "Hello World!", use adb shell input text 'Hello%sWorld\!' [8][5]. 4. Using am start (Activity Manager): When passing arguments like JSON or strings with spaces, the most reliable method is to wrap the full command in double quotes and use single quotes for the internal string values [2][10][3]. * For JSON strings: Use complex nested quoting to preserve inner double quotes [7][3]. * Pattern: adb shell "am start -n package/.Activity -e key '{\"json\":\"data\"}'" [3]. If you find that your local shell (like PowerShell or Bash) is consuming too many quotes, you can test by escaping the quotes with backslashes or using a different quoting style (e.g., switching from double quotes to single quotes for the outer wrapper) [7][2][10]. If execution remains unreliable, consider writing the command to a file, pushing it to the device, and executing the script directly with adb shell sh /path/to/script.sh [1][7]. Top results: [1][7][5][2][3]

Citations:


🌐 Web query:

adb shell multiple arguments remote shell quoting documentation "adb shell" "shell metacharacters" Android developers

💡 Result:

Since Android Platform Tools 23, adb handles arguments using ssh(1)-like semantics [1][2]. This means that when you run a command via adb shell, the arguments are joined with spaces into a single string and passed to the remote device, where they are interpreted by the remote shell [3][4][2]. Because the command string passes through both your local shell and the remote shell on the device, any shell metacharacters (such as quotes, backticks, or redirection operators) risk being interpreted prematurely by the local shell [4][1][2]. To ensure commands reach the remote device as intended, you generally need to quote or escape the arguments twice: once for the local shell and once for the remote shell [1][2]. Key strategies for handling arguments: 1. Double Quoting: Wrap the entire remote command in quotes, and use inner quotes for arguments that require them [1][2]. Example: adb shell setprop key "'two words'" In this case, the local shell consumes the outer quotes, and the remote shell receives setprop key 'two words' [1][2]. 2. Escaping: Alternatively, you can use backslashes to escape metacharacters, though nesting quotes is often more readable [1][2]. Example: adb shell ping `cat /data/my_address.pst` 3. Complexity Handling: For highly complex commands involving nested quotes, JSON, or multiple metacharacters, it is often safer to avoid the complexity of escaping entirely by writing the command to a script file, pushing that file to the device, and executing the script via adb shell [5]. Example: adb push myscript.sh /data/local/tmp/ adb shell sh /data/local/tmp/myscript.sh This approach bypasses the pitfalls of multi-stage shell interpretation [5].

Citations:


🌐 Web query:

site:developer.android.com adb shell quoting escaping arguments input text am start url

💡 Result:

When passing arguments containing special characters, spaces, or shell metacharacters through adb shell, you must account for the local shell (the one on your computer) and the remote shell (the one on the Android device) [1][2]. Since Android Platform Tools 23, adb handles arguments similarly to ssh, meaning command arguments are passed through the local shell and then interpreted by the remote shell [1][2]. Key Quoting and Escaping Rules: 1. Double Quoting (The "Quote Twice" Method): If you need to pass a string containing spaces or shell-sensitive characters (like ampersands or wildcards) so that the remote shell receives them intact, wrap the entire command argument in an outer layer of quotes (for the local shell) and an inner layer of quotes (for the remote shell) [1]. Example: adb shell setprop key "'two words'" [1] 2. Escaping Special Characters: If a URL or argument contains characters that have special meaning to the shell (such as & or *), you can escape them with a backslash () [3][4]. Example: adb shell am start -a android.intent.action.VIEW -d "example://path?param=red&color=blue" [3] 3. URI Encoding for URLs: For URLs or intent data strings, ensure the content itself is properly URI-encoded before passing it to adb [4][5]. Characters like spaces, ampersands, and commas should be replaced by their percent-encoded equivalents (e.g., %20 for space, %26 for &) [5]. Example: adb shell am start -a android.intent.action.VIEW -d 'geo:0,0?q=restaurants+nearby' [4] 4. JSON Arguments: When passing JSON strings (common for certain commands), wrap the entire JSON object in single quotes to ensure the shell does not attempt to interpret its internal braces or quotes [6]. Example: adb shell cmd app_function execute-app-function --parameters '{"key": "value"}' [6] If you encounter issues, verify if your local shell (e.g., bash, zsh, or Windows CMD/PowerShell) is stripping quotes or escaping characters before they reach adb. Using the "quote twice" approach or wrapping complex strings in single quotes is generally the most reliable way to preserve the argument structure [1][6].

Citations:


Escape user-controlled values passed to adb shell. android_type_text only replaces spaces, and package, activity, and url are still shell-interpreted on the device side. Strictly validate component names and shell-escape or encode free-form text and URLs before forwarding them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp.rs` around lines 302 - 378, Escape or strictly validate all
user-controlled values before passing them into adb shell commands in
android_type_text, android_launch_app, and android_open_url. Replace the current
ad hoc space substitution in android_type_text with proper shell-safe
encoding/escaping for free-form text, and validate package/activity as Android
component identifiers before building the -n target. Ensure url is encoded or
shell-escaped before being forwarded to am start.

Comment thread src/mcp.rs
Comment on lines +445 to +461
let connect_output = Command::new("adb")
.arg("connect")
.arg(&state.spec.adb_endpoint)
.output()
.map_err(|error| {
McpToolError::CommandFailed(format!("failed to execute adb connect: {error}"))
})?;
if !connect_output.status.success() {
return Err(McpToolError::CommandFailed(command_failure(
"adb connect",
&connect_output,
)));
}

Command::new("adb")
.args(args)
.output()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

sed -n '400,520p' src/mcp.rs

Repository: ProverCoderAI/rust-android-connection

Length of output: 4107


🏁 Script executed:

sed -n '400,520p' src/mcp.rs

Repository: ProverCoderAI/rust-android-connection

Length of output: 4107


🏁 Script executed:

rg -n "run_adb_raw|adb_endpoint|adb connect|Command::new\\(\"adb\"\\)" src/mcp.rs

Repository: ProverCoderAI/rust-android-connection

Length of output: 756


🏁 Script executed:

sed -n '1,220p' src/mcp.rs

Repository: ProverCoderAI/rust-android-connection

Length of output: 6616


🏁 Script executed:

rg -n "run_adb_raw|adb_endpoint|adb connect|Command::new\\(\"adb\"\\)" src/mcp.rs

Repository: ProverCoderAI/rust-android-connection

Length of output: 756


🏁 Script executed:

rg -n "run_adb_raw|adb_endpoint|adb connect|Command::new\\(\"adb\"\\)" src/mcp.rs

Repository: ProverCoderAI/rust-android-connection

Length of output: 756


🏁 Script executed:

sed -n '1,240p' src/mcp.rs

Repository: ProverCoderAI/rust-android-connection

Length of output: 7534


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
text = Path("src/mcp.rs").read_text()
for needle in ["run_adb_raw", "adb_endpoint", "Command::new(\"adb\")", "adb connect"]:
    print(f"--- {needle} ---")
    for i, line in enumerate(text.splitlines(), 1):
        if needle in line:
            start = max(1, i-5)
            end = min(len(text.splitlines()), i+10)
            for j in range(start, end+1):
                print(f"{j}: {text.splitlines()[j-1]}")
            break
PY

Repository: ProverCoderAI/rust-android-connection

Length of output: 2827


🏁 Script executed:

rg -n "adb_endpoint|allow_install|adb_probe|McpState|connect" -S .

Repository: ProverCoderAI/rust-android-connection

Length of output: 7637


🏁 Script executed:

rg -n "struct McpState|adb_probe|adb_endpoint|spec:" src -S

Repository: ProverCoderAI/rust-android-connection

Length of output: 1690


Pin all ADB commands to state.spec.adb_endpoint.
run_adb_raw connects first, but then executes plain adb ...; in multi-device setups that can select the wrong target or fail. Pass -s <endpoint> through the shared helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp.rs` around lines 445 - 461, The ADB invocation in run_adb_raw is only
using state.spec.adb_endpoint for the initial connect, but the follow-up
Command::new("adb") call still runs unpinned and can target the wrong device.
Update the shared ADB execution path in run_adb_raw so every adb command
includes the same state.spec.adb_endpoint selection, likely by threading it
through the helper that builds the command arguments and ensuring the final adb
call uses -s with the endpoint as well.

Comment thread src/mcp.rs
Comment on lines +445 to +462
let connect_output = Command::new("adb")
.arg("connect")
.arg(&state.spec.adb_endpoint)
.output()
.map_err(|error| {
McpToolError::CommandFailed(format!("failed to execute adb connect: {error}"))
})?;
if !connect_output.status.success() {
return Err(McpToolError::CommandFailed(command_failure(
"adb connect",
&connect_output,
)));
}

Command::new("adb")
.args(args)
.output()
.map_err(|error| McpToolError::CommandFailed(format!("failed to execute adb: {error}")))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Add timeouts around ADB subprocesses.

adb connect and tool commands use blocking .output() calls; if ADB hangs, the single stdio request loop is stuck indefinitely. Wrap these subprocesses with a bounded timeout and kill on expiry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp.rs` around lines 445 - 462, The adb subprocess invocations in the
command handling flow can block forever because they use plain .output() without
any timeout. Update the adb connect path and the later
Command::new("adb").args(args) execution in the same mcp.rs flow to run with a
bounded timeout and terminate the child process if it expires, returning a
McpToolError::CommandFailed that reflects the timeout condition.

Comment thread src/mcp.rs
Comment on lines +530 to +543
fn workspace_path(workspace: &Path, value: &str) -> Result<PathBuf, McpToolError> {
let candidate = PathBuf::from(value);
if value.is_empty()
|| candidate.is_absolute()
|| candidate
.components()
.any(|component| component == Component::ParentDir)
{
return Err(McpToolError::InvalidArgument(
"path must be relative, non-empty, and must not contain '..'".to_string(),
));
}

Ok(workspace.join(candidate))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Resolve symlinks before trusting workspace paths.

The helper blocks absolute paths and .., but workspace/link/out.png can still escape if link is a symlink outside the workspace. Canonicalize the workspace plus the target/parent path before fs::write and APK install use the result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp.rs` around lines 530 - 543, The current workspace_path helper only
rejects absolute paths and parent-dir segments, so symlinked subpaths can still
escape the workspace. Update workspace_path to resolve and validate the
canonicalized workspace plus the target or parent directory before any fs::write
or APK install path use, and ensure the resulting path stays under the workspace
root. Use the existing workspace_path function as the main guard and apply the
same resolved-path check at the call sites that write files or install APKs.

@skulidropek

skulidropek commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 3660e4c
Status: success
Files: 6 (5.73 MB)
Links: README | Manifest

git status

On branch release/bootstrap-android-connection
Your branch is up to date with 'origin/release/bootstrap-android-connection'.

nothing to commit, working tree clean

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib.rs`:
- Around line 307-308: The default ADB endpoint is mismatched with the Docker
DNS name used by the Android container, so `DEFAULT_ADB_ENDPOINT` should resolve
to the container actually joined by `docker_run_args()`. Update the Docker setup
in `docker_run_args()` / `spec.android_container_name` handling so the Android
container is reachable as `android` (for example via a network alias), or change
the default endpoint derivation to use the generated container name consistently
across the `DEFAULT_ADB_ENDPOINT` and Docker networking logic.

In `@src/main.rs`:
- Around line 352-360: The copy_to_stderr helper currently holds the stderr lock
for the entire io::copy call, which can block the other Docker pipe reader and
cause hangs. Update copy_to_stderr so it reads from the provided Read in chunks
first, then acquires io::stderr().lock() only long enough to write each chunk
and flush as needed, keeping the lock scope minimal within the thread::spawn
closure.

In `@tests/integration/cli.rs`:
- Around line 4-48: The lifecycle CLI `status` path still shells out to Docker
to resolve `noVncUrl`, which makes the `lifecycle_cli_renders_status_json` and
`lifecycle_cli_can_disable_no_vnc_publication` tests depend on Docker. Update
the `status` implementation to avoid calling `docker port` during status
rendering, or add an explicit opt-out/override for test runs so
`parse_stdout_json` can validate `noVncUrl` and `noVncPublished` offline. Focus
the change in the `status` command handling and the `noVncUrl` resolution logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c3bc73e-4d26-42f3-87d3-4837f21b61a3

📥 Commits

Reviewing files that changed from the base of the PR and between 2a204e7 and 3660e4c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • Cargo.toml
  • LICENSE
  • README.md
  • changelog.d/20260624_000000_bootstrap_android_connection.md
  • examples/basic_usage.rs
  • src/bin/android-connection.rs
  • src/lib.rs
  • src/main.rs
  • src/mcp.rs
  • src/sum.rs
  • tests/integration/cli.rs
  • tests/integration/mod.rs
  • tests/integration/sum.rs
  • tests/unit/android_connection.rs
  • tests/unit/mod.rs
  • tests/unit/sum.rs
💤 Files with no reviewable changes (3)
  • tests/unit/sum.rs
  • tests/integration/sum.rs
  • src/sum.rs
✅ Files skipped from review due to trivial changes (5)
  • changelog.d/20260624_000000_bootstrap_android_connection.md
  • tests/integration/mod.rs
  • LICENSE
  • tests/unit/mod.rs
  • README.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • examples/basic_usage.rs
  • src/bin/android-connection.rs
  • src/mcp.rs
  • Cargo.toml

Comment thread src/lib.rs
Comment on lines +307 to +308
"--network".to_string(),
spec.docker_network.clone(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file list ==\n'
git ls-files | rg '^(src/lib\.rs|src/|README|.*adb.*|.*docker.*)$' || true

printf '\n== search symbols ==\n'
rg -n "DEFAULT_ADB_ENDPOINT|adb_endpoint|docker_run_args|docker_network|network-alias|android:5555|5555" src README.md

printf '\n== src/lib.rs excerpt ==\n'
sed -n '250,360p' src/lib.rs

Repository: ProverCoderAI/rust-android-connection

Length of output: 5761


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file list ==\n'
git ls-files | rg '^(src/lib\.rs|src/|README|.*adb.*|.*docker.*)$' || true

printf '\n== search symbols ==\n'
rg -n "DEFAULT_ADB_ENDPOINT|adb_endpoint|docker_run_args|docker_network|network-alias|android:5555|5555" src README.md

printf '\n== src/lib.rs excerpt ==\n'
sed -n '250,360p' src/lib.rs

Repository: ProverCoderAI/rust-android-connection

Length of output: 5761


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' src/lib.rs
printf '\n== main.rs excerpt ==\n'
sed -n '1,120p' src/main.rs
printf '\n== README excerpt ==\n'
sed -n '35,60p' README.md

Repository: ProverCoderAI/rust-android-connection

Length of output: 12023


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' src/lib.rs
printf '\n== main.rs excerpt ==\n'
sed -n '1,120p' src/main.rs
printf '\n== README excerpt ==\n'
sed -n '35,60p' README.md

Repository: ProverCoderAI/rust-android-connection

Length of output: 12023


Align the default ADB host with the Docker container name.
DEFAULT_ADB_ENDPOINT is android:5555, but docker_run_args() only joins the network under spec.android_container_name (for example dg-my-project-android), so the default endpoint does not resolve on the Docker network. Add a --network-alias android or derive the default endpoint from the generated Android container name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib.rs` around lines 307 - 308, The default ADB endpoint is mismatched
with the Docker DNS name used by the Android container, so
`DEFAULT_ADB_ENDPOINT` should resolve to the container actually joined by
`docker_run_args()`. Update the Docker setup in `docker_run_args()` /
`spec.android_container_name` handling so the Android container is reachable as
`android` (for example via a network alias), or change the default endpoint
derivation to use the generated container name consistently across the
`DEFAULT_ADB_ENDPOINT` and Docker networking logic.

Comment thread src/main.rs
Comment on lines +352 to +360
fn copy_to_stderr<R>(mut reader: R) -> thread::JoinHandle<io::Result<()>>
where
R: Read + Send + 'static,
{
thread::spawn(move || {
let mut stderr = io::stderr().lock();
io::copy(&mut reader, &mut stderr)?;
stderr.flush()
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid holding stderr locked while draining Docker pipes.

Each copy thread locks stderr for the full io::copy; if one stream holds the lock while reading and Docker fills the other pipe, docker pull can hang. Read chunks first, then lock only for each write.

Suggested fix
 fn copy_to_stderr<R>(mut reader: R) -> thread::JoinHandle<io::Result<()>>
 where
     R: Read + Send + 'static,
 {
     thread::spawn(move || {
-        let mut stderr = io::stderr().lock();
-        io::copy(&mut reader, &mut stderr)?;
-        stderr.flush()
+        let mut buffer = [0_u8; 8192];
+        loop {
+            let bytes_read = reader.read(&mut buffer)?;
+            if bytes_read == 0 {
+                return Ok(());
+            }
+
+            let mut stderr = io::stderr().lock();
+            stderr.write_all(&buffer[..bytes_read])?;
+            stderr.flush()?;
+        }
     })
 }
📝 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
fn copy_to_stderr<R>(mut reader: R) -> thread::JoinHandle<io::Result<()>>
where
R: Read + Send + 'static,
{
thread::spawn(move || {
let mut stderr = io::stderr().lock();
io::copy(&mut reader, &mut stderr)?;
stderr.flush()
})
fn copy_to_stderr<R>(mut reader: R) -> thread::JoinHandle<io::Result<()>>
where
R: Read + Send + 'static,
{
thread::spawn(move || {
let mut buffer = [0_u8; 8192];
loop {
let bytes_read = reader.read(&mut buffer)?;
if bytes_read == 0 {
return Ok(());
}
let mut stderr = io::stderr().lock();
stderr.write_all(&buffer[..bytes_read])?;
stderr.flush()?;
}
})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.rs` around lines 352 - 360, The copy_to_stderr helper currently
holds the stderr lock for the entire io::copy call, which can block the other
Docker pipe reader and cause hangs. Update copy_to_stderr so it reads from the
provided Read in chunks first, then acquires io::stderr().lock() only long
enough to write each chunk and flush as needed, keeping the lock scope minimal
within the thread::spawn closure.

Comment thread tests/integration/cli.rs
Comment on lines +4 to +48
#[test]
fn lifecycle_cli_renders_status_json() {
let output = lifecycle_output(&[
"status",
"--project",
"dg-test",
"--endpoint",
"dg-test-android:5555",
"--novnc-port",
"16080",
]);

assert!(output.status.success());
let json = parse_stdout_json(&output);

assert_eq!(json["projectId"], "dg-test");
assert_eq!(json["androidContainerName"], "dg-test-android");
assert_eq!(
json["noVncUrl"],
"http://127.0.0.1:16080/?autoconnect=true&resize=remote"
);
assert_eq!(json["noVncPublished"], true);
assert_eq!(json["resourceLimits"]["memory"], "3g");
assert_eq!(json["resourceLimits"]["memorySwap"], "3g");
assert_eq!(json["resourceLimits"]["cpus"], "1.0");
}

#[test]
fn lifecycle_cli_can_disable_no_vnc_publication() {
let output = lifecycle_output(&[
"status",
"--project",
"dg-test",
"--endpoint",
"dg-test-android:5555",
"--no-novnc-publish",
]);

assert!(output.status.success());
let json = parse_stdout_json(&output);

assert_eq!(json["projectId"], "dg-test");
assert_eq!(json["noVncPublished"], false);
assert!(json["noVncUrl"].is_null());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the lifecycle status subcommand for any docker/adb invocation.
fd -e rs main.rs src | xargs -I{} sh -c 'echo "== {} =="; sed -n "1,400p" {}'
rg -nP '\b(adb|docker)\b|Command::new|probe' src/main.rs -C2

Repository: ProverCoderAI/rust-android-connection

Length of output: 14300


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the integration tests and any helper setup that could stub docker/adb.
fd -e rs tests src | sort
echo "---- tests/integration/cli.rs ----"
sed -n '1,260p' tests/integration/cli.rs
echo "---- search for DOCKER_GIT_ANDROID_DOCKER / adb stubs ----"
rg -n "DOCKER_GIT_ANDROID_DOCKER|adb|docker" tests src -C 2

Repository: ProverCoderAI/rust-android-connection

Length of output: 41668


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the integration tests and any helper setup that could stub docker/adb.
fd -e rs tests src | sort
echo "---- tests/integration/cli.rs ----"
sed -n '1,260p' tests/integration/cli.rs
echo "---- search for DOCKER_GIT_ANDROID_DOCKER / adb stubs ----"
rg -n "DOCKER_GIT_ANDROID_DOCKER|adb|docker" tests src -C 2

Repository: ProverCoderAI/rust-android-connection

Length of output: 41668


status still shells out to Docker for noVncUrl
status calls docker port when --no-novnc-publish isn’t set, so these tests aren’t fully offline. Consider skipping that lookup in status or making it opt-out for tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/cli.rs` around lines 4 - 48, The lifecycle CLI `status`
path still shells out to Docker to resolve `noVncUrl`, which makes the
`lifecycle_cli_renders_status_json` and
`lifecycle_cli_can_disable_no_vnc_publication` tests depend on Docker. Update
the `status` implementation to avoid calling `docker port` during status
rendering, or add an explicit opt-out/override for test runs so
`parse_stdout_json` can validate `noVncUrl` and `noVncPublished` offline. Focus
the change in the `status` command handling and the `noVncUrl` resolution logic.

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