fix(tauri): resolve relative paths and prepare for IPC merge - #12

Merged
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths
Feb 23, 2026
Merged

fix(tauri): resolve relative paths and prepare for IPC merge#12
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths

Conversation

@wilcorrea

@wilcorreawilcorrea commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#11 - Relative paths lead to a blank screen

  • Canonicalize file paths in read_file command to correctly resolve relative paths before reading files
  • Fix macOS Opened event to canonicalize paths when opening files via file associations (critical bug fix)
  • Add comprehensive debug logging at all file loading entry points (CLI args, single instance, Opened event, read_file)
  • Display full path in toolbar instead of just filename for better debugging visibility
  • Restructure builder for merge compatibility with feat/unix-socket-ipc branch to prevent future conflicts
  • Add merge guide documentation with step-by-step instructions for integrating IPC changes

Technical Details

The root cause was that relative paths (e.g., ./README.md) were not being canonicalized at all entry points:

  • ✅ CLI args: Already canonicalized
  • ✅ Single instance: Already canonicalized
  • Opened event (macOS): Missing canonicalization → FIXED
  • read_file command: No path resolution → FIXED

When a relative path reached read_file, it would try to read relative to the process working directory (not the file's directory), causing file not found errors and blank screens.

Merge Compatibility

This PR restructures the builder to match feat/unix-socket-ipc branch structure:

  • Separates builder initialization from setup
  • Adds placeholder comments showing exactly where to add IPC socket code
  • Includes MERGE_GUIDE.md with complete merge instructions

This ensures clean merges between branches with zero conflicts.

Test plan

  • Build and install: cd apps/tauri && make build-dev && make install
  • Test with relative path: cd ~/Documents && arandu ./README.md
  • Verify file loads correctly (no blank screen)
  • Test with absolute path: arandu /full/path/to/file.md
  • Check toolbar shows full path (not just filename)
  • Open Console.app and filter by "arandu" to see debug logs showing path resolution
  • Test file associations (double-click .md file) works correctly
  • Verify compilation: cargo check --manifest-path src-tauri/Cargo.toml

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a TCP-based IPC endpoint to allow external tooling to send open-file and command requests.
    • Toolbar now displays the full file path with hover tooltip.
  • Improvements

    • Expanded debug logging for file loading, CLI/opened-file and URL handling for better troubleshooting.
    • File path resolution now prefers canonical paths and falls back reliably; socket state is cleaned up on exit.

- Canonicalize paths in read_file command to handle relative paths
- Canonicalize paths in macOS Opened event (fixes#11)
- Add debug logging at all file loading entry points
- Restructure builder to allow conditional IPC state management
- Add placeholders for feat/unix-socket-ipc merge compatibility
- Display full path in toolbar for better debugging
@coderabbitai

coderabbitaiBot commented Feb 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • apps/tauri/src-tauri/icons/icon.icns
⛔ Files ignored due to path filters (5)
  • apps/tauri/src-tauri/icons/128x128.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/128x128@2x.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/32x32.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/64x64.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/icon.png is excluded by !**/*.png

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a TCP IPC server and state to the Tauri backend; makes IPC structs/functions public; adds path canonicalization and extensive [DEBUG] logging for CLI / opened-file flows; updates frontend to show full file paths and add debug logs.

Changes

Cohort / File(s)Summary
TCP IPC server & state
apps/tauri/src-tauri/src/tcp_ipc.rs
New TCP IPC implementation: binds listener, accepts connections, parses newline-delimited JSON IpcCommand, calls process_command, responds with IpcResponse; exposes TcpSocketState, setup, and cleanup.
Backend integration & path handling
apps/tauri/src-tauri/src/lib.rs
Integrated TCP IPC setup/cleanup and TcpSocketState into app init/teardown; added path canonicalization in read_file and all open-file / CLI / macOS opened-url handling; added many [DEBUG] logs and fallback behavior when canonicalization fails.
IPC types & API visibility
apps/tauri/src-tauri/src/ipc.rs
Made IpcCommand and IpcResponse public with exported fields; made process_command public for reuse by tcp_ipc.
Frontend: toolbar & logging
apps/tauri/src/main.js
Show full file path in toolbar (with hover title) instead of only last segment; added debug logs around loadFile and open-file event handling.

Sequence Diagram(s)

sequenceDiagram
participant Client as TCP Client
participant TCP as Tcp Listener (tcp_ipc)
participant App as Tauri App / process_command
participant FS as Filesystem
participant UI as Frontend (webview)
Client->>TCP: connect, send JSON line (IpcCommand)
TCP->>App: parse JSON -> call process_command(cmd)
App->>FS: read_file / resolve path (canonicalize)
alt command produces response
FS-->>App: file contents / error
App-->>TCP: IpcResponse (JSON)
App->>UI: emit "open-file" with resolved path (if applicable)
UI-->>UI: loadFile(resolved path), update toolbar
else error
App-->>TCP: IpcResponse (error)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nudged a socket, sniffed a path so neat,
I chased the canonical footprints down the street,
I told the app and sang with glee,
"Full paths now shine for you and me!"
— a rabbit, debugging with tiny feet 🌿✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main changes: fixing relative path resolution and preparing code structure for IPC integration.
Linked Issues check✅ PassedThe PR successfully addresses issue #11 by canonicalizing file paths at all entry points (CLI args, single-instance handler, macOS Opened event, and read_file command) to resolve relative paths correctly.
Out of Scope Changes check✅ PassedAll changes directly support the PR objectives: path canonicalization fixes, debug logging, toolbar display improvements, and TCP IPC setup for future merge compatibility.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/relative-paths

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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

366-387: Consider adding fallback behavior for consistency.

The single-instance handler silently drops the file-open request when canonicalize fails (lines 383-385 only log, no open-file event emitted). In contrast:

  • CLI args handler (line 462) uses unwrap_or_else to fall back to the original path
  • macOS Opened event (line 535) uses unwrap_or to fall back

This inconsistency could cause confusing behavior where files open in some scenarios but not others.

♻️ Suggested fix to add fallback
 if !file_path.is_empty() && !file_path.starts_with('-') {
- if let Ok(abs_path) = std::fs::canonicalize(file_path) {- let path_str = abs_path.to_string_lossy().to_string();- eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);- let _ = app.emit("open-file", &path_str);- } else {- eprintln!("[DEBUG] Failed to canonicalize path: {:?}", file_path);- }+ let abs_path = std::fs::canonicalize(file_path).unwrap_or_else(|e| {+ eprintln!("[DEBUG] Canonicalize failed ({}), using as-is", e);+ PathBuf::from(file_path)+ });+ let path_str = abs_path.to_string_lossy().to_string();+ eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);+ let _ = app.emit("open-file", &path_str);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 366 - 387, The single-instance
handler in the tauri_plugin_single_instance::init closure drops the open-file
event when std::fs::canonicalize(file_path) fails; change the logic so that on
canonicalize failure you fall back to the original file_path (or its
to_string_lossy) and still call app.emit("open-file", &path_str) so behavior
matches the CLI and macOS handlers; update the block handling args[1] to compute
a path_str variable from canonicalize if Ok or else from the original file_path,
log the fallback, and always emit "open-file" with that path_str.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 366-387: The single-instance handler in the
tauri_plugin_single_instance::init closure drops the open-file event when
std::fs::canonicalize(file_path) fails; change the logic so that on canonicalize
failure you fall back to the original file_path (or its to_string_lossy) and
still call app.emit("open-file", &path_str) so behavior matches the CLI and
macOS handlers; update the block handling args[1] to compute a path_str variable
from canonicalize if Ok or else from the original file_path, log the fallback,
and always emit "open-file" with that path_str.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51408db and a075094.

📒 Files selected for processing (2)
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src/main.js

@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed the CodeRabbit nitpick on the single-instance handler canonicalize fallback.

lib.rs lines 378-385: Changed from if let Ok(abs_path) = ... (which silently dropped the open-file event on failure) to unwrap_or_else with fallback to the original path. This now matches the behavior of both the CLI args handler (line 462) and the macOS Opened event handler (line 535).

Fixed in 53dbe11.

@coderabbitaicoderabbitaiBot 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: 1

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

66-66: Consider structured logging for production.

The [DEBUG] prefix in eprintln! calls works for development, but for production builds you might want to use the log or tracing crate with configurable log levels. This would allow filtering debug output without code changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 66, The debug print in read_file
currently uses eprintln!("[DEBUG] ...", path); switch this to a structured
logging call (e.g. log::debug! or tracing::debug!) so debug output can be
filtered at runtime, and ensure the application initializes the chosen
logger/subscriber (e.g. env_logger::init() or tracing_subscriber) in
main/startup; replace the eprintln! invocation inside read_file with the
appropriate debug macro and remove the hardcoded "[DEBUG]" prefix so the logging
framework controls level and formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 533-537: The opened-event handler uses
std::fs::canonicalize(&path).unwrap_or(path) which silently falls back; change
it to use unwrap_or_else so you can log the canonicalize error (matching
single-instance and CLI handlers). In the opened event block, replace
unwrap_or(path) with unwrap_or_else(|err| { eprintln!("[DEBUG] canonicalize
failed: {:?}", err); path.clone() }) (ensure abs_path and path_str keep the same
names) so failures are logged consistently.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Line 66: The debug print in read_file currently uses eprintln!("[DEBUG] ...",
path); switch this to a structured logging call (e.g. log::debug! or
tracing::debug!) so debug output can be filtered at runtime, and ensure the
application initializes the chosen logger/subscriber (e.g. env_logger::init() or
tracing_subscriber) in main/startup; replace the eprintln! invocation inside
read_file with the appropriate debug macro and remove the hardcoded "[DEBUG]"
prefix so the logging framework controls level and formatting.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a075094 and 53dbe11.

📒 Files selected for processing (1)
  • apps/tauri/src-tauri/src/lib.rs

Comment threadapps/tauri/src-tauri/src/lib.rs
…onicalize
Align the macOS Opened event handler with the single-instance and CLI
handlers by replacing silent unwrap_or fallback with unwrap_or_else
that logs the canonicalize failure before falling back.
@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed all remaining CodeRabbit review comments from both review rounds:

1. Inconsistent canonicalization fallback (Opened event, line 535) -- Fixed in 6fb6ca2. Replaced unwrap_or(path) with unwrap_or_else that logs the error, matching the single-instance and CLI handlers.

2. Single-instance handler dropping file-open on canonicalize failure (lines 366-387) -- Already addressed in 53dbe11 (previous commit).

3. Structured logging suggestion (line 66) -- Acknowledged as valid but deferred. Switching from eprintln! to log/tracing is a broader change that affects the entire codebase and is better handled in a dedicated PR. All debug prints in this PR are consistent with the existing eprintln!("[DEBUG] ...") pattern used throughout the project.

All three canonicalization entry points now use the same unwrap_or_else + debug logging pattern for consistent diagnostics.

- Add tcp_ipc.rs module with TCP listener on 127.0.0.1:7474
- Refactor ipc.rs to make IpcCommand/Response and process_command public
- TCP socket works in parallel with Unix socket for compatibility
- Supports same JSON protocol: open, ping, show commands
- Fixes Unix socket limitation with Docker/Podman containers
- Both sockets share same command processing logic
Resolves container accessibility issue where Unix sockets from macOS
are not accessible within Linux containers due to different kernel stacks.

@coderabbitaicoderabbitaiBot 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: 6

🧹 Nitpick comments (2)
apps/tauri/src-tauri/src/tcp_ipc.rs (1)

45-62: No connection limits or rate-limiting on the accept loop.

Any local process can open unlimited connections to this TCP server. Without a maximum connection count or per-connection timeout, a misbehaving local process could exhaust file descriptors or memory by opening many connections and sending data slowly.

Consider adding:

  • A maximum concurrent connection semaphore (tokio::sync::Semaphore)
  • A read timeout per connection (tokio::time::timeout)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 45 - 62, The accept loop in
tcp_listener_loop currently allows unlimited concurrent connections; add a
tokio::sync::Semaphore to limit max concurrent clients (acquire a permit before
spawning handle_client and release when that client's task finishes) and wrap
per-connection read/write operations inside tokio::time::timeout in
handle_client to enforce a per-connection inactivity/read timeout; ensure
tcp_listener_loop attempts to accept new connections even when semaphore is
exhausted (e.g., reject/close the stream immediately with a log) and
propagate/handle timeout errors from handle_client so they are logged.
apps/tauri/src-tauri/src/lib.rs (1)

529-533: TCP cleanup only clears state, doesn't stop the listener (see tcp_ipc.rs comment).

As noted in the tcp_ipc.rs review, cleanup() only nulls out the stored address. The accept loop and active connections continue until the process exits. For the ExitRequested + ExplicitQuit path this is likely fine since the process terminates shortly after, but it's worth noting the asymmetry with the Unix socket cleanup which actually removes the socket file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 529 - 533, The current call to
tcp_ipc::cleanup(tcp_socket_state) only clears the stored address but does not
stop the accept loop or close active connections; update the shutdown so the TCP
listener and any active connections are closed and the accept loop signalled to
exit. Either call a dedicated function (e.g., tcp_ipc::stop_listener or
tcp_ipc::shutdown_listener) from this location, or extend tcp_ipc::cleanup to
close the listener socket stored in tcp_ipc::TcpSocketState, close active
connections and signal/join the accept loop so it terminates cleanly rather than
leaving it running until process exit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 416-419: The tcp_ipc::setup() function currently masks bind
failures by always returning Ok(()), so the error branch here
(tcp_ipc::setup(app)) is effectively unreachable; update tcp_ipc::setup in
tcp_ipc.rs to propagate and return an Err when socket bind/listen fails (return
a Result with the real io::Error or a boxed error), and keep this call site
as-is so the Err path (eprintln!("Failed to setup TCP IPC: {}", e)) is reachable
and logs the actual failure; ensure the tcp_ipc::setup signature and any callers
are updated to handle the Result change.
- Line 15: The tcp_ipc module is currently compiled unconditionally, exposing an
unauthenticated TCP listener (127.0.0.1:7474); gate this to be opt-in and add
simple auth: protect the module with a cfg feature (e.g., #[cfg(feature =
"tcp-ipc")] on the mod tcp_ipc declaration and corresponding Cargo.toml feature)
and/or add token-based auth inside the tcp_ipc listener (generate a random token
stored in a file with 0o600 perms and require clients to present it when
connecting, validating in the tcp listener accept/handle path); update any
public APIs that assume tcp_ipc exists to compile without it (use
#[cfg(any(...))] or conditional stubs) and document the feature and token file
location so consumers can opt in securely.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 12-35: The setup() function currently spawns an async task and
returns Ok(()) before TcpListener::bind runs, so binding failures are never
propagated; fix by performing the bind synchronously (await the bind) or by
using a oneshot channel to relay the bind result back to the caller before
returning. Concretely, call TcpListener::bind(&addr).await (or bind on the sync
thread) and handle Err by returning Err(String) from setup(), or if you must
spawn first, create a tokio::sync::oneshot sender/receiver, have the spawned
task send Ok(()) or Err(e.to_string()) after attempting TcpListener::bind, and
have setup() block/await on the receiver and return the corresponding Result;
update references in this flow around setup, TcpListener::bind,
tcp_listener_loop, and the app_handle state assignment so errors are returned to
the caller instead of always returning Ok(()).
- Around line 37-43: cleanup() only removes the stored address string and does
not stop the spawned accept loop or active connections; update TcpSocketState to
hold a cancellation mechanism (e.g., a tokio::sync::watch,
tokio_util::sync::CancellationToken, or the JoinHandle of the spawned task) and
modify the accept loop to listen for that cancellation token or be abortable,
then in cleanup() signal cancellation or abort the JoinHandle and await/cleanup
resources before dropping the address; reference TcpSocketState, cleanup(), the
spawned accept loop (the task that owns the TcpListener) and whichever chosen
token/handle so you can set and trigger it from cleanup().
- Around line 64-94: The handle_client function currently logs raw,
attacker-controlled TCP input and command strings (the `line` contents and
`cmd.command`) which can contain newlines and inject fake log entries; sanitize
or escape/newline-normalize and/or truncate these values before logging (e.g.,
replace newlines with escaped sequences and limit length) when using the
eprintln! calls and when logging inside the match branch that processes
`process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.
- Around line 7-8: The current setup() spawns the listener asynchronously and
always returns Ok(()), and the hardcoded DEFAULT_PORT = 7474 can silently
conflict with Neo4j; change setup() to synchronously bind with
TcpListener::bind((DEFAULT_HOST, port)) (propagating any bind Err back to the
caller) before spawning the listener loop, and make the port configurable (read
from an env var or config with DEFAULT_PORT as fallback) so callers see bind
failures immediately and the port can be changed without code edits.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 529-533: The current call to tcp_ipc::cleanup(tcp_socket_state)
only clears the stored address but does not stop the accept loop or close active
connections; update the shutdown so the TCP listener and any active connections
are closed and the accept loop signalled to exit. Either call a dedicated
function (e.g., tcp_ipc::stop_listener or tcp_ipc::shutdown_listener) from this
location, or extend tcp_ipc::cleanup to close the listener socket stored in
tcp_ipc::TcpSocketState, close active connections and signal/join the accept
loop so it terminates cleanly rather than leaving it running until process exit.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 45-62: The accept loop in tcp_listener_loop currently allows
unlimited concurrent connections; add a tokio::sync::Semaphore to limit max
concurrent clients (acquire a permit before spawning handle_client and release
when that client's task finishes) and wrap per-connection read/write operations
inside tokio::time::timeout in handle_client to enforce a per-connection
inactivity/read timeout; ensure tcp_listener_loop attempts to accept new
connections even when semaphore is exhausted (e.g., reject/close the stream
immediately with a log) and propagate/handle timeout errors from handle_client
so they are logged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53dbe11 and 29db134.

📒 Files selected for processing (3)
  • apps/tauri/src-tauri/src/ipc.rs
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src-tauri/src/tcp_ipc.rs

mod cli_installer;
#[cfg(unix)]
mod ipc;
mod tcp_ipc;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TCP IPC module is unconditionally compiled — security implications of an unauthenticated local TCP listener.

Unlike the Unix socket (gated behind #[cfg(unix)] with 0o600 permissions), the TCP listener on 127.0.0.1:7474 has no authentication or authorization. Any local process (including those in containers, sandboxes, or running as different users) can connect and issue commands like open (to open arbitrary files the app user can read) or show (to manipulate window focus).

On multi-user systems or shared hosts this is a local attack surface. Consider adding at minimum:

  • A shared secret / token-based auth (e.g., random token written to a file with restrictive permissions, client must present it)
  • Or restricting TCP IPC behind a feature flag / cfg gate so it's opt-in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 15, The tcp_ipc module is currently
compiled unconditionally, exposing an unauthenticated TCP listener
(127.0.0.1:7474); gate this to be opt-in and add simple auth: protect the module
with a cfg feature (e.g., #[cfg(feature = "tcp-ipc")] on the mod tcp_ipc
declaration and corresponding Cargo.toml feature) and/or add token-based auth
inside the tcp_ipc listener (generate a random token stored in a file with 0o600
perms and require clients to present it when connecting, validating in the tcp
listener accept/handle path); update any public APIs that assume tcp_ipc exists
to compile without it (use #[cfg(any(...))] or conditional stubs) and document
the feature and token file location so consumers can opt in securely.

Comment on lines +416 to +419
// TCP IPC setup (works on all platforms, including from containers)
if let Err(e) = tcp_ipc::setup(app) {
eprintln!("Failed to setup TCP IPC: {}", e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TCP IPC setup error is logged but doesn't prevent app startup — acceptable.

This mirrors the Unix socket setup pattern. Since the TCP IPC is supplementary, failing gracefully is reasonable. However, note that as discussed in tcp_ipc.rs, setup() currently always returns Ok(()) even on bind failure, making this error branch unreachable for the most important failure case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 416 - 419, The tcp_ipc::setup()
function currently masks bind failures by always returning Ok(()), so the error
branch here (tcp_ipc::setup(app)) is effectively unreachable; update
tcp_ipc::setup in tcp_ipc.rs to propagate and return an Err when socket
bind/listen fails (return a Result with the real io::Error or a boxed error),
and keep this call site as-is so the Err path (eprintln!("Failed to setup TCP
IPC: {}", e)) is reachable and logs the actual failure; ensure the
tcp_ipc::setup signature and any callers are updated to handle the Result
change.

Comment on lines +7 to +8
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 7474;

Copy link
Copy Markdown

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

🌐 Web query:

Neo4j default port database

💡 Result:

Neo4j’s default ports depend on how you connect:

  • Bolt (drivers / neo4j:// / bolt://): 7687 [1][3]
  • HTTP (Neo4j Browser / HTTP API): 7474 [1][3]
  • HTTPS: 7473 [1][3]

(Other common defaults include Backup: 6362.) [1]


🏁 Script executed:

# First, let's find and examine the tcp_ipc.rs file
find . -name "tcp_ipc.rs" -type f | head -5

Repository: devitools/arandu

Length of output: 97


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/tcp_ipc.rs

Repository: devitools/arandu

Length of output: 3886


🏁 Script executed:

# Now let's check lib.rs to see how setup() is called and if it handles the result
find . -name "lib.rs" -path "*/src-tauri/src/lib.rs" -type f

Repository: devitools/arandu

Length of output: 93


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/lib.rs

Repository: devitools/arandu

Length of output: 23262


Hardcoded port 7474 conflicts with Neo4j and fails silently.

Port 7474 is Neo4j's default HTTP port. The TCP bind happens asynchronously inside a spawned task (lines 17–30), but setup() returns Ok(()) immediately (line 34) before the bind attempt completes. If another process holds the port, the error is only logged to stderr; the caller in lib.rs (lines 417–419) cannot distinguish success from failure since setup() always returns Ok(()).

Consider either:

  • Synchronously binding before spawning the listener loop, or
  • Using a oneshot channel to propagate bind errors back to the caller, or
  • Making the port configurable (via config file or environment variable) with a fallback port range.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 7 - 8, The current setup()
spawns the listener asynchronously and always returns Ok(()), and the hardcoded
DEFAULT_PORT = 7474 can silently conflict with Neo4j; change setup() to
synchronously bind with TcpListener::bind((DEFAULT_HOST, port)) (propagating any
bind Err back to the caller) before spawning the listener loop, and make the
port configurable (read from an env var or config with DEFAULT_PORT as fallback)
so callers see bind failures immediately and the port can be changed without
code edits.

Comment on lines +12 to +35
pub fn setup(app: &tauri::App) -> Result<(), String> {
let addr = format!("{}:{}", DEFAULT_HOST, DEFAULT_PORT);

let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
match TcpListener::bind(&addr).await {
Ok(listener) => {
eprintln!("[TCP IPC] Listening on {}", addr);

let state = app_handle.state::<TcpSocketState>();
if let Ok(mut guard) = state.0.lock() {
*guard = Some(addr.clone());
}

tcp_listener_loop(listener, app_handle).await;
}
Err(e) => {
eprintln!("[TCP IPC] Failed to bind to {}: {}", addr, e);
}
}
});

Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

setup() always returns Ok(()) regardless of bind outcome.

Because TcpListener::bind happens inside the spawned async task, setup() returns Ok(()) before the bind attempt completes. The caller in lib.rs (line 417) checks for Err but will never see one from a bind failure. This makes the error handling at the call site dead code for the most critical failure mode.

Compare with the Unix socket ipc::setup() which also has this pattern — but at minimum, consider a oneshot channel to relay the bind result, or bind synchronously before spawning the accept loop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 12 - 35, The setup()
function currently spawns an async task and returns Ok(()) before
TcpListener::bind runs, so binding failures are never propagated; fix by
performing the bind synchronously (await the bind) or by using a oneshot channel
to relay the bind result back to the caller before returning. Concretely, call
TcpListener::bind(&addr).await (or bind on the sync thread) and handle Err by
returning Err(String) from setup(), or if you must spawn first, create a
tokio::sync::oneshot sender/receiver, have the spawned task send Ok(()) or
Err(e.to_string()) after attempting TcpListener::bind, and have setup()
block/await on the receiver and return the corresponding Result; update
references in this flow around setup, TcpListener::bind, tcp_listener_loop, and
the app_handle state assignment so errors are returned to the caller instead of
always returning Ok(()).

Comment on lines +37 to +43
pub fn cleanup(state: tauri::State<TcpSocketState>) {
if let Ok(mut guard) = state.0.lock() {
if let Some(addr) = guard.take() {
eprintln!("[TCP IPC] Shutting down listener on {}", addr);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cleanup() does not actually stop the TCP listener.

cleanup() only clears the stored address string from state. The TcpListener and its accept loop (and any active client connections) continue running because the listener is owned by the spawned task, not by TcpSocketState. Unlike the Unix socket ipc::cleanup() which removes the socket file (preventing new connections), this cleanup is purely cosmetic.

To actually shut down, you'd need a cancellation mechanism (e.g., a tokio::sync::watch or CancellationToken) that the accept loop checks, or store a JoinHandle to abort the task.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 37 - 43, cleanup() only
removes the stored address string and does not stop the spawned accept loop or
active connections; update TcpSocketState to hold a cancellation mechanism
(e.g., a tokio::sync::watch, tokio_util::sync::CancellationToken, or the
JoinHandle of the spawned task) and modify the accept loop to listen for that
cancellation token or be abortable, then in cleanup() signal cancellation or
abort the JoinHandle and await/cleanup resources before dropping the address;
reference TcpSocketState, cleanup(), the spawned accept loop (the task that owns
the TcpListener) and whichever chosen token/handle so you can set and trigger it
from cleanup().

Comment on lines +64 to +94
async fn handle_client(stream: TcpStream, app: tauri::AppHandle) -> Result<(), String> {
let peer_addr = stream.peer_addr().map_err(|e| e.to_string())?;
let (reader, mut writer) = stream.into_split();
let reader = BufReader::new(reader);
let mut lines = reader.lines();

while let Some(line) = lines.next_line().await.map_err(|e| e.to_string())? {
eprintln!("[TCP IPC] Received from {}: {}", peer_addr, line);

let response = match serde_json::from_str::<IpcCommand>(&line) {
Ok(cmd) => {
eprintln!("[TCP IPC] Processing command: {}", cmd.command);
process_command(cmd, &app)
}
Err(e) => IpcResponse {
success: false,
error: Some(format!("Invalid JSON: {}", e)),
},
};

let json = serde_json::to_string(&response).unwrap_or_default();
eprintln!("[TCP IPC] Sending response to {}: {}", peer_addr, json);

writer
.write_all(format!("{}\n", json).as_bytes())
.await
.map_err(|e| e.to_string())?;
}

eprintln!("[TCP IPC] Connection closed: {}", peer_addr);
Ok(())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential log injection via unsanitized TCP input.

Line 71 logs the raw line received from the TCP socket, and line 75 logs cmd.command which is attacker-controlled input from any local process. While eprintln! is less exploitable than structured logging frameworks, newlines within the JSON string could forge log entries. Consider sanitizing or escaping logged values, or at minimum truncating them.

Also, Line 84: serde_json::to_string(&response).unwrap_or_default() will send an empty line "\n" to the client if serialization fails. Since IpcResponse is a simple struct this is unlikely, but sending an empty line could confuse a client expecting valid JSON. Consider sending a hardcoded error JSON instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 64 - 94, The handle_client
function currently logs raw, attacker-controlled TCP input and command strings
(the `line` contents and `cmd.command`) which can contain newlines and inject
fake log entries; sanitize or escape/newline-normalize and/or truncate these
values before logging (e.g., replace newlines with escaped sequences and limit
length) when using the eprintln! calls and when logging inside the match branch
that processes `process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.

The app icon had a solid background that extended to the edges, preventing
macOS from properly applying its automatic corner rounding. This resulted in
a square icon appearance instead of the expected rounded corners.
Changes:
- Applied ~12% corner radius to all icon assets (matching macOS guidelines)
- Regenerated icon.icns with proper alpha channel masks
- Updated all PNG sizes (32x32, 64x64, 128x128, 256x256, 512x512, 1024x1024)
The icon now displays with proper rounded corners in Dock, Finder, and app switcher.
@wilcorrea
wilcorrea merged commit 31f187b into mainFeb 23, 2026
1 check passed
@wilcorrea
wilcorrea deleted the fix/relative-paths branch February 23, 2026 20:42
Sign up for freeto 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.

Relative paths lead to a blank screen

1 participant

@wilcorrea
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(tauri): resolve relative paths and prepare for IPC merge - #12

Merged
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths
Feb 23, 2026
Merged

fix(tauri): resolve relative paths and prepare for IPC merge#12
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths

Conversation

@wilcorrea

@wilcorreawilcorrea commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#11 - Relative paths lead to a blank screen

  • Canonicalize file paths in read_file command to correctly resolve relative paths before reading files
  • Fix macOS Opened event to canonicalize paths when opening files via file associations (critical bug fix)
  • Add comprehensive debug logging at all file loading entry points (CLI args, single instance, Opened event, read_file)
  • Display full path in toolbar instead of just filename for better debugging visibility
  • Restructure builder for merge compatibility with feat/unix-socket-ipc branch to prevent future conflicts
  • Add merge guide documentation with step-by-step instructions for integrating IPC changes

Technical Details

The root cause was that relative paths (e.g., ./README.md) were not being canonicalized at all entry points:

  • ✅ CLI args: Already canonicalized
  • ✅ Single instance: Already canonicalized
  • Opened event (macOS): Missing canonicalization → FIXED
  • read_file command: No path resolution → FIXED

When a relative path reached read_file, it would try to read relative to the process working directory (not the file's directory), causing file not found errors and blank screens.

Merge Compatibility

This PR restructures the builder to match feat/unix-socket-ipc branch structure:

  • Separates builder initialization from setup
  • Adds placeholder comments showing exactly where to add IPC socket code
  • Includes MERGE_GUIDE.md with complete merge instructions

This ensures clean merges between branches with zero conflicts.

Test plan

  • Build and install: cd apps/tauri && make build-dev && make install
  • Test with relative path: cd ~/Documents && arandu ./README.md
  • Verify file loads correctly (no blank screen)
  • Test with absolute path: arandu /full/path/to/file.md
  • Check toolbar shows full path (not just filename)
  • Open Console.app and filter by "arandu" to see debug logs showing path resolution
  • Test file associations (double-click .md file) works correctly
  • Verify compilation: cargo check --manifest-path src-tauri/Cargo.toml

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a TCP-based IPC endpoint to allow external tooling to send open-file and command requests.
    • Toolbar now displays the full file path with hover tooltip.
  • Improvements

    • Expanded debug logging for file loading, CLI/opened-file and URL handling for better troubleshooting.
    • File path resolution now prefers canonical paths and falls back reliably; socket state is cleaned up on exit.

- Canonicalize paths in read_file command to handle relative paths
- Canonicalize paths in macOS Opened event (fixes#11)
- Add debug logging at all file loading entry points
- Restructure builder to allow conditional IPC state management
- Add placeholders for feat/unix-socket-ipc merge compatibility
- Display full path in toolbar for better debugging
@coderabbitai

coderabbitaiBot commented Feb 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • apps/tauri/src-tauri/icons/icon.icns
⛔ Files ignored due to path filters (5)
  • apps/tauri/src-tauri/icons/128x128.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/128x128@2x.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/32x32.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/64x64.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/icon.png is excluded by !**/*.png

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a TCP IPC server and state to the Tauri backend; makes IPC structs/functions public; adds path canonicalization and extensive [DEBUG] logging for CLI / opened-file flows; updates frontend to show full file paths and add debug logs.

Changes

Cohort / File(s)Summary
TCP IPC server & state
apps/tauri/src-tauri/src/tcp_ipc.rs
New TCP IPC implementation: binds listener, accepts connections, parses newline-delimited JSON IpcCommand, calls process_command, responds with IpcResponse; exposes TcpSocketState, setup, and cleanup.
Backend integration & path handling
apps/tauri/src-tauri/src/lib.rs
Integrated TCP IPC setup/cleanup and TcpSocketState into app init/teardown; added path canonicalization in read_file and all open-file / CLI / macOS opened-url handling; added many [DEBUG] logs and fallback behavior when canonicalization fails.
IPC types & API visibility
apps/tauri/src-tauri/src/ipc.rs
Made IpcCommand and IpcResponse public with exported fields; made process_command public for reuse by tcp_ipc.
Frontend: toolbar & logging
apps/tauri/src/main.js
Show full file path in toolbar (with hover title) instead of only last segment; added debug logs around loadFile and open-file event handling.

Sequence Diagram(s)

sequenceDiagram
participant Client as TCP Client
participant TCP as Tcp Listener (tcp_ipc)
participant App as Tauri App / process_command
participant FS as Filesystem
participant UI as Frontend (webview)
Client->>TCP: connect, send JSON line (IpcCommand)
TCP->>App: parse JSON -> call process_command(cmd)
App->>FS: read_file / resolve path (canonicalize)
alt command produces response
FS-->>App: file contents / error
App-->>TCP: IpcResponse (JSON)
App->>UI: emit "open-file" with resolved path (if applicable)
UI-->>UI: loadFile(resolved path), update toolbar
else error
App-->>TCP: IpcResponse (error)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nudged a socket, sniffed a path so neat,
I chased the canonical footprints down the street,
I told the app and sang with glee,
"Full paths now shine for you and me!"
— a rabbit, debugging with tiny feet 🌿✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main changes: fixing relative path resolution and preparing code structure for IPC integration.
Linked Issues check✅ PassedThe PR successfully addresses issue #11 by canonicalizing file paths at all entry points (CLI args, single-instance handler, macOS Opened event, and read_file command) to resolve relative paths correctly.
Out of Scope Changes check✅ PassedAll changes directly support the PR objectives: path canonicalization fixes, debug logging, toolbar display improvements, and TCP IPC setup for future merge compatibility.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/relative-paths

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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

366-387: Consider adding fallback behavior for consistency.

The single-instance handler silently drops the file-open request when canonicalize fails (lines 383-385 only log, no open-file event emitted). In contrast:

  • CLI args handler (line 462) uses unwrap_or_else to fall back to the original path
  • macOS Opened event (line 535) uses unwrap_or to fall back

This inconsistency could cause confusing behavior where files open in some scenarios but not others.

♻️ Suggested fix to add fallback
 if !file_path.is_empty() && !file_path.starts_with('-') {
- if let Ok(abs_path) = std::fs::canonicalize(file_path) {- let path_str = abs_path.to_string_lossy().to_string();- eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);- let _ = app.emit("open-file", &path_str);- } else {- eprintln!("[DEBUG] Failed to canonicalize path: {:?}", file_path);- }+ let abs_path = std::fs::canonicalize(file_path).unwrap_or_else(|e| {+ eprintln!("[DEBUG] Canonicalize failed ({}), using as-is", e);+ PathBuf::from(file_path)+ });+ let path_str = abs_path.to_string_lossy().to_string();+ eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);+ let _ = app.emit("open-file", &path_str);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 366 - 387, The single-instance
handler in the tauri_plugin_single_instance::init closure drops the open-file
event when std::fs::canonicalize(file_path) fails; change the logic so that on
canonicalize failure you fall back to the original file_path (or its
to_string_lossy) and still call app.emit("open-file", &path_str) so behavior
matches the CLI and macOS handlers; update the block handling args[1] to compute
a path_str variable from canonicalize if Ok or else from the original file_path,
log the fallback, and always emit "open-file" with that path_str.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 366-387: The single-instance handler in the
tauri_plugin_single_instance::init closure drops the open-file event when
std::fs::canonicalize(file_path) fails; change the logic so that on canonicalize
failure you fall back to the original file_path (or its to_string_lossy) and
still call app.emit("open-file", &path_str) so behavior matches the CLI and
macOS handlers; update the block handling args[1] to compute a path_str variable
from canonicalize if Ok or else from the original file_path, log the fallback,
and always emit "open-file" with that path_str.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51408db and a075094.

📒 Files selected for processing (2)
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src/main.js

@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed the CodeRabbit nitpick on the single-instance handler canonicalize fallback.

lib.rs lines 378-385: Changed from if let Ok(abs_path) = ... (which silently dropped the open-file event on failure) to unwrap_or_else with fallback to the original path. This now matches the behavior of both the CLI args handler (line 462) and the macOS Opened event handler (line 535).

Fixed in 53dbe11.

@coderabbitaicoderabbitaiBot 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: 1

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

66-66: Consider structured logging for production.

The [DEBUG] prefix in eprintln! calls works for development, but for production builds you might want to use the log or tracing crate with configurable log levels. This would allow filtering debug output without code changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 66, The debug print in read_file
currently uses eprintln!("[DEBUG] ...", path); switch this to a structured
logging call (e.g. log::debug! or tracing::debug!) so debug output can be
filtered at runtime, and ensure the application initializes the chosen
logger/subscriber (e.g. env_logger::init() or tracing_subscriber) in
main/startup; replace the eprintln! invocation inside read_file with the
appropriate debug macro and remove the hardcoded "[DEBUG]" prefix so the logging
framework controls level and formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 533-537: The opened-event handler uses
std::fs::canonicalize(&path).unwrap_or(path) which silently falls back; change
it to use unwrap_or_else so you can log the canonicalize error (matching
single-instance and CLI handlers). In the opened event block, replace
unwrap_or(path) with unwrap_or_else(|err| { eprintln!("[DEBUG] canonicalize
failed: {:?}", err); path.clone() }) (ensure abs_path and path_str keep the same
names) so failures are logged consistently.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Line 66: The debug print in read_file currently uses eprintln!("[DEBUG] ...",
path); switch this to a structured logging call (e.g. log::debug! or
tracing::debug!) so debug output can be filtered at runtime, and ensure the
application initializes the chosen logger/subscriber (e.g. env_logger::init() or
tracing_subscriber) in main/startup; replace the eprintln! invocation inside
read_file with the appropriate debug macro and remove the hardcoded "[DEBUG]"
prefix so the logging framework controls level and formatting.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a075094 and 53dbe11.

📒 Files selected for processing (1)
  • apps/tauri/src-tauri/src/lib.rs

Comment threadapps/tauri/src-tauri/src/lib.rs
…onicalize
Align the macOS Opened event handler with the single-instance and CLI
handlers by replacing silent unwrap_or fallback with unwrap_or_else
that logs the canonicalize failure before falling back.
@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed all remaining CodeRabbit review comments from both review rounds:

1. Inconsistent canonicalization fallback (Opened event, line 535) -- Fixed in 6fb6ca2. Replaced unwrap_or(path) with unwrap_or_else that logs the error, matching the single-instance and CLI handlers.

2. Single-instance handler dropping file-open on canonicalize failure (lines 366-387) -- Already addressed in 53dbe11 (previous commit).

3. Structured logging suggestion (line 66) -- Acknowledged as valid but deferred. Switching from eprintln! to log/tracing is a broader change that affects the entire codebase and is better handled in a dedicated PR. All debug prints in this PR are consistent with the existing eprintln!("[DEBUG] ...") pattern used throughout the project.

All three canonicalization entry points now use the same unwrap_or_else + debug logging pattern for consistent diagnostics.

- Add tcp_ipc.rs module with TCP listener on 127.0.0.1:7474
- Refactor ipc.rs to make IpcCommand/Response and process_command public
- TCP socket works in parallel with Unix socket for compatibility
- Supports same JSON protocol: open, ping, show commands
- Fixes Unix socket limitation with Docker/Podman containers
- Both sockets share same command processing logic
Resolves container accessibility issue where Unix sockets from macOS
are not accessible within Linux containers due to different kernel stacks.

@coderabbitaicoderabbitaiBot 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: 6

🧹 Nitpick comments (2)
apps/tauri/src-tauri/src/tcp_ipc.rs (1)

45-62: No connection limits or rate-limiting on the accept loop.

Any local process can open unlimited connections to this TCP server. Without a maximum connection count or per-connection timeout, a misbehaving local process could exhaust file descriptors or memory by opening many connections and sending data slowly.

Consider adding:

  • A maximum concurrent connection semaphore (tokio::sync::Semaphore)
  • A read timeout per connection (tokio::time::timeout)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 45 - 62, The accept loop in
tcp_listener_loop currently allows unlimited concurrent connections; add a
tokio::sync::Semaphore to limit max concurrent clients (acquire a permit before
spawning handle_client and release when that client's task finishes) and wrap
per-connection read/write operations inside tokio::time::timeout in
handle_client to enforce a per-connection inactivity/read timeout; ensure
tcp_listener_loop attempts to accept new connections even when semaphore is
exhausted (e.g., reject/close the stream immediately with a log) and
propagate/handle timeout errors from handle_client so they are logged.
apps/tauri/src-tauri/src/lib.rs (1)

529-533: TCP cleanup only clears state, doesn't stop the listener (see tcp_ipc.rs comment).

As noted in the tcp_ipc.rs review, cleanup() only nulls out the stored address. The accept loop and active connections continue until the process exits. For the ExitRequested + ExplicitQuit path this is likely fine since the process terminates shortly after, but it's worth noting the asymmetry with the Unix socket cleanup which actually removes the socket file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 529 - 533, The current call to
tcp_ipc::cleanup(tcp_socket_state) only clears the stored address but does not
stop the accept loop or close active connections; update the shutdown so the TCP
listener and any active connections are closed and the accept loop signalled to
exit. Either call a dedicated function (e.g., tcp_ipc::stop_listener or
tcp_ipc::shutdown_listener) from this location, or extend tcp_ipc::cleanup to
close the listener socket stored in tcp_ipc::TcpSocketState, close active
connections and signal/join the accept loop so it terminates cleanly rather than
leaving it running until process exit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 416-419: The tcp_ipc::setup() function currently masks bind
failures by always returning Ok(()), so the error branch here
(tcp_ipc::setup(app)) is effectively unreachable; update tcp_ipc::setup in
tcp_ipc.rs to propagate and return an Err when socket bind/listen fails (return
a Result with the real io::Error or a boxed error), and keep this call site
as-is so the Err path (eprintln!("Failed to setup TCP IPC: {}", e)) is reachable
and logs the actual failure; ensure the tcp_ipc::setup signature and any callers
are updated to handle the Result change.
- Line 15: The tcp_ipc module is currently compiled unconditionally, exposing an
unauthenticated TCP listener (127.0.0.1:7474); gate this to be opt-in and add
simple auth: protect the module with a cfg feature (e.g., #[cfg(feature =
"tcp-ipc")] on the mod tcp_ipc declaration and corresponding Cargo.toml feature)
and/or add token-based auth inside the tcp_ipc listener (generate a random token
stored in a file with 0o600 perms and require clients to present it when
connecting, validating in the tcp listener accept/handle path); update any
public APIs that assume tcp_ipc exists to compile without it (use
#[cfg(any(...))] or conditional stubs) and document the feature and token file
location so consumers can opt in securely.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 12-35: The setup() function currently spawns an async task and
returns Ok(()) before TcpListener::bind runs, so binding failures are never
propagated; fix by performing the bind synchronously (await the bind) or by
using a oneshot channel to relay the bind result back to the caller before
returning. Concretely, call TcpListener::bind(&addr).await (or bind on the sync
thread) and handle Err by returning Err(String) from setup(), or if you must
spawn first, create a tokio::sync::oneshot sender/receiver, have the spawned
task send Ok(()) or Err(e.to_string()) after attempting TcpListener::bind, and
have setup() block/await on the receiver and return the corresponding Result;
update references in this flow around setup, TcpListener::bind,
tcp_listener_loop, and the app_handle state assignment so errors are returned to
the caller instead of always returning Ok(()).
- Around line 37-43: cleanup() only removes the stored address string and does
not stop the spawned accept loop or active connections; update TcpSocketState to
hold a cancellation mechanism (e.g., a tokio::sync::watch,
tokio_util::sync::CancellationToken, or the JoinHandle of the spawned task) and
modify the accept loop to listen for that cancellation token or be abortable,
then in cleanup() signal cancellation or abort the JoinHandle and await/cleanup
resources before dropping the address; reference TcpSocketState, cleanup(), the
spawned accept loop (the task that owns the TcpListener) and whichever chosen
token/handle so you can set and trigger it from cleanup().
- Around line 64-94: The handle_client function currently logs raw,
attacker-controlled TCP input and command strings (the `line` contents and
`cmd.command`) which can contain newlines and inject fake log entries; sanitize
or escape/newline-normalize and/or truncate these values before logging (e.g.,
replace newlines with escaped sequences and limit length) when using the
eprintln! calls and when logging inside the match branch that processes
`process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.
- Around line 7-8: The current setup() spawns the listener asynchronously and
always returns Ok(()), and the hardcoded DEFAULT_PORT = 7474 can silently
conflict with Neo4j; change setup() to synchronously bind with
TcpListener::bind((DEFAULT_HOST, port)) (propagating any bind Err back to the
caller) before spawning the listener loop, and make the port configurable (read
from an env var or config with DEFAULT_PORT as fallback) so callers see bind
failures immediately and the port can be changed without code edits.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 529-533: The current call to tcp_ipc::cleanup(tcp_socket_state)
only clears the stored address but does not stop the accept loop or close active
connections; update the shutdown so the TCP listener and any active connections
are closed and the accept loop signalled to exit. Either call a dedicated
function (e.g., tcp_ipc::stop_listener or tcp_ipc::shutdown_listener) from this
location, or extend tcp_ipc::cleanup to close the listener socket stored in
tcp_ipc::TcpSocketState, close active connections and signal/join the accept
loop so it terminates cleanly rather than leaving it running until process exit.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 45-62: The accept loop in tcp_listener_loop currently allows
unlimited concurrent connections; add a tokio::sync::Semaphore to limit max
concurrent clients (acquire a permit before spawning handle_client and release
when that client's task finishes) and wrap per-connection read/write operations
inside tokio::time::timeout in handle_client to enforce a per-connection
inactivity/read timeout; ensure tcp_listener_loop attempts to accept new
connections even when semaphore is exhausted (e.g., reject/close the stream
immediately with a log) and propagate/handle timeout errors from handle_client
so they are logged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53dbe11 and 29db134.

📒 Files selected for processing (3)
  • apps/tauri/src-tauri/src/ipc.rs
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src-tauri/src/tcp_ipc.rs

mod cli_installer;
#[cfg(unix)]
mod ipc;
mod tcp_ipc;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TCP IPC module is unconditionally compiled — security implications of an unauthenticated local TCP listener.

Unlike the Unix socket (gated behind #[cfg(unix)] with 0o600 permissions), the TCP listener on 127.0.0.1:7474 has no authentication or authorization. Any local process (including those in containers, sandboxes, or running as different users) can connect and issue commands like open (to open arbitrary files the app user can read) or show (to manipulate window focus).

On multi-user systems or shared hosts this is a local attack surface. Consider adding at minimum:

  • A shared secret / token-based auth (e.g., random token written to a file with restrictive permissions, client must present it)
  • Or restricting TCP IPC behind a feature flag / cfg gate so it's opt-in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 15, The tcp_ipc module is currently
compiled unconditionally, exposing an unauthenticated TCP listener
(127.0.0.1:7474); gate this to be opt-in and add simple auth: protect the module
with a cfg feature (e.g., #[cfg(feature = "tcp-ipc")] on the mod tcp_ipc
declaration and corresponding Cargo.toml feature) and/or add token-based auth
inside the tcp_ipc listener (generate a random token stored in a file with 0o600
perms and require clients to present it when connecting, validating in the tcp
listener accept/handle path); update any public APIs that assume tcp_ipc exists
to compile without it (use #[cfg(any(...))] or conditional stubs) and document
the feature and token file location so consumers can opt in securely.

Comment on lines +416 to +419
// TCP IPC setup (works on all platforms, including from containers)
if let Err(e) = tcp_ipc::setup(app) {
eprintln!("Failed to setup TCP IPC: {}", e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TCP IPC setup error is logged but doesn't prevent app startup — acceptable.

This mirrors the Unix socket setup pattern. Since the TCP IPC is supplementary, failing gracefully is reasonable. However, note that as discussed in tcp_ipc.rs, setup() currently always returns Ok(()) even on bind failure, making this error branch unreachable for the most important failure case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 416 - 419, The tcp_ipc::setup()
function currently masks bind failures by always returning Ok(()), so the error
branch here (tcp_ipc::setup(app)) is effectively unreachable; update
tcp_ipc::setup in tcp_ipc.rs to propagate and return an Err when socket
bind/listen fails (return a Result with the real io::Error or a boxed error),
and keep this call site as-is so the Err path (eprintln!("Failed to setup TCP
IPC: {}", e)) is reachable and logs the actual failure; ensure the
tcp_ipc::setup signature and any callers are updated to handle the Result
change.

Comment on lines +7 to +8
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 7474;

Copy link
Copy Markdown

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

🌐 Web query:

Neo4j default port database

💡 Result:

Neo4j’s default ports depend on how you connect:

  • Bolt (drivers / neo4j:// / bolt://): 7687 [1][3]
  • HTTP (Neo4j Browser / HTTP API): 7474 [1][3]
  • HTTPS: 7473 [1][3]

(Other common defaults include Backup: 6362.) [1]


🏁 Script executed:

# First, let's find and examine the tcp_ipc.rs file
find . -name "tcp_ipc.rs" -type f | head -5

Repository: devitools/arandu

Length of output: 97


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/tcp_ipc.rs

Repository: devitools/arandu

Length of output: 3886


🏁 Script executed:

# Now let's check lib.rs to see how setup() is called and if it handles the result
find . -name "lib.rs" -path "*/src-tauri/src/lib.rs" -type f

Repository: devitools/arandu

Length of output: 93


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/lib.rs

Repository: devitools/arandu

Length of output: 23262


Hardcoded port 7474 conflicts with Neo4j and fails silently.

Port 7474 is Neo4j's default HTTP port. The TCP bind happens asynchronously inside a spawned task (lines 17–30), but setup() returns Ok(()) immediately (line 34) before the bind attempt completes. If another process holds the port, the error is only logged to stderr; the caller in lib.rs (lines 417–419) cannot distinguish success from failure since setup() always returns Ok(()).

Consider either:

  • Synchronously binding before spawning the listener loop, or
  • Using a oneshot channel to propagate bind errors back to the caller, or
  • Making the port configurable (via config file or environment variable) with a fallback port range.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 7 - 8, The current setup()
spawns the listener asynchronously and always returns Ok(()), and the hardcoded
DEFAULT_PORT = 7474 can silently conflict with Neo4j; change setup() to
synchronously bind with TcpListener::bind((DEFAULT_HOST, port)) (propagating any
bind Err back to the caller) before spawning the listener loop, and make the
port configurable (read from an env var or config with DEFAULT_PORT as fallback)
so callers see bind failures immediately and the port can be changed without
code edits.

Comment on lines +12 to +35
pub fn setup(app: &tauri::App) -> Result<(), String> {
let addr = format!("{}:{}", DEFAULT_HOST, DEFAULT_PORT);

let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
match TcpListener::bind(&addr).await {
Ok(listener) => {
eprintln!("[TCP IPC] Listening on {}", addr);

let state = app_handle.state::<TcpSocketState>();
if let Ok(mut guard) = state.0.lock() {
*guard = Some(addr.clone());
}

tcp_listener_loop(listener, app_handle).await;
}
Err(e) => {
eprintln!("[TCP IPC] Failed to bind to {}: {}", addr, e);
}
}
});

Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

setup() always returns Ok(()) regardless of bind outcome.

Because TcpListener::bind happens inside the spawned async task, setup() returns Ok(()) before the bind attempt completes. The caller in lib.rs (line 417) checks for Err but will never see one from a bind failure. This makes the error handling at the call site dead code for the most critical failure mode.

Compare with the Unix socket ipc::setup() which also has this pattern — but at minimum, consider a oneshot channel to relay the bind result, or bind synchronously before spawning the accept loop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 12 - 35, The setup()
function currently spawns an async task and returns Ok(()) before
TcpListener::bind runs, so binding failures are never propagated; fix by
performing the bind synchronously (await the bind) or by using a oneshot channel
to relay the bind result back to the caller before returning. Concretely, call
TcpListener::bind(&addr).await (or bind on the sync thread) and handle Err by
returning Err(String) from setup(), or if you must spawn first, create a
tokio::sync::oneshot sender/receiver, have the spawned task send Ok(()) or
Err(e.to_string()) after attempting TcpListener::bind, and have setup()
block/await on the receiver and return the corresponding Result; update
references in this flow around setup, TcpListener::bind, tcp_listener_loop, and
the app_handle state assignment so errors are returned to the caller instead of
always returning Ok(()).

Comment on lines +37 to +43
pub fn cleanup(state: tauri::State<TcpSocketState>) {
if let Ok(mut guard) = state.0.lock() {
if let Some(addr) = guard.take() {
eprintln!("[TCP IPC] Shutting down listener on {}", addr);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cleanup() does not actually stop the TCP listener.

cleanup() only clears the stored address string from state. The TcpListener and its accept loop (and any active client connections) continue running because the listener is owned by the spawned task, not by TcpSocketState. Unlike the Unix socket ipc::cleanup() which removes the socket file (preventing new connections), this cleanup is purely cosmetic.

To actually shut down, you'd need a cancellation mechanism (e.g., a tokio::sync::watch or CancellationToken) that the accept loop checks, or store a JoinHandle to abort the task.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 37 - 43, cleanup() only
removes the stored address string and does not stop the spawned accept loop or
active connections; update TcpSocketState to hold a cancellation mechanism
(e.g., a tokio::sync::watch, tokio_util::sync::CancellationToken, or the
JoinHandle of the spawned task) and modify the accept loop to listen for that
cancellation token or be abortable, then in cleanup() signal cancellation or
abort the JoinHandle and await/cleanup resources before dropping the address;
reference TcpSocketState, cleanup(), the spawned accept loop (the task that owns
the TcpListener) and whichever chosen token/handle so you can set and trigger it
from cleanup().

Comment on lines +64 to +94
async fn handle_client(stream: TcpStream, app: tauri::AppHandle) -> Result<(), String> {
let peer_addr = stream.peer_addr().map_err(|e| e.to_string())?;
let (reader, mut writer) = stream.into_split();
let reader = BufReader::new(reader);
let mut lines = reader.lines();

while let Some(line) = lines.next_line().await.map_err(|e| e.to_string())? {
eprintln!("[TCP IPC] Received from {}: {}", peer_addr, line);

let response = match serde_json::from_str::<IpcCommand>(&line) {
Ok(cmd) => {
eprintln!("[TCP IPC] Processing command: {}", cmd.command);
process_command(cmd, &app)
}
Err(e) => IpcResponse {
success: false,
error: Some(format!("Invalid JSON: {}", e)),
},
};

let json = serde_json::to_string(&response).unwrap_or_default();
eprintln!("[TCP IPC] Sending response to {}: {}", peer_addr, json);

writer
.write_all(format!("{}\n", json).as_bytes())
.await
.map_err(|e| e.to_string())?;
}

eprintln!("[TCP IPC] Connection closed: {}", peer_addr);
Ok(())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential log injection via unsanitized TCP input.

Line 71 logs the raw line received from the TCP socket, and line 75 logs cmd.command which is attacker-controlled input from any local process. While eprintln! is less exploitable than structured logging frameworks, newlines within the JSON string could forge log entries. Consider sanitizing or escaping logged values, or at minimum truncating them.

Also, Line 84: serde_json::to_string(&response).unwrap_or_default() will send an empty line "\n" to the client if serialization fails. Since IpcResponse is a simple struct this is unlikely, but sending an empty line could confuse a client expecting valid JSON. Consider sending a hardcoded error JSON instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 64 - 94, The handle_client
function currently logs raw, attacker-controlled TCP input and command strings
(the `line` contents and `cmd.command`) which can contain newlines and inject
fake log entries; sanitize or escape/newline-normalize and/or truncate these
values before logging (e.g., replace newlines with escaped sequences and limit
length) when using the eprintln! calls and when logging inside the match branch
that processes `process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.

The app icon had a solid background that extended to the edges, preventing
macOS from properly applying its automatic corner rounding. This resulted in
a square icon appearance instead of the expected rounded corners.
Changes:
- Applied ~12% corner radius to all icon assets (matching macOS guidelines)
- Regenerated icon.icns with proper alpha channel masks
- Updated all PNG sizes (32x32, 64x64, 128x128, 256x256, 512x512, 1024x1024)
The icon now displays with proper rounded corners in Dock, Finder, and app switcher.
@wilcorrea
wilcorrea merged commit 31f187b into mainFeb 23, 2026
1 check passed
@wilcorrea
wilcorrea deleted the fix/relative-paths branch February 23, 2026 20:42
Sign up for freeto 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.

Relative paths lead to a blank screen

1 participant

@wilcorrea
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(tauri): resolve relative paths and prepare for IPC merge - #12

Merged
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths
Feb 23, 2026
Merged

fix(tauri): resolve relative paths and prepare for IPC merge#12
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths

Conversation

@wilcorrea

@wilcorreawilcorrea commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#11 - Relative paths lead to a blank screen

  • Canonicalize file paths in read_file command to correctly resolve relative paths before reading files
  • Fix macOS Opened event to canonicalize paths when opening files via file associations (critical bug fix)
  • Add comprehensive debug logging at all file loading entry points (CLI args, single instance, Opened event, read_file)
  • Display full path in toolbar instead of just filename for better debugging visibility
  • Restructure builder for merge compatibility with feat/unix-socket-ipc branch to prevent future conflicts
  • Add merge guide documentation with step-by-step instructions for integrating IPC changes

Technical Details

The root cause was that relative paths (e.g., ./README.md) were not being canonicalized at all entry points:

  • ✅ CLI args: Already canonicalized
  • ✅ Single instance: Already canonicalized
  • Opened event (macOS): Missing canonicalization → FIXED
  • read_file command: No path resolution → FIXED

When a relative path reached read_file, it would try to read relative to the process working directory (not the file's directory), causing file not found errors and blank screens.

Merge Compatibility

This PR restructures the builder to match feat/unix-socket-ipc branch structure:

  • Separates builder initialization from setup
  • Adds placeholder comments showing exactly where to add IPC socket code
  • Includes MERGE_GUIDE.md with complete merge instructions

This ensures clean merges between branches with zero conflicts.

Test plan

  • Build and install: cd apps/tauri && make build-dev && make install
  • Test with relative path: cd ~/Documents && arandu ./README.md
  • Verify file loads correctly (no blank screen)
  • Test with absolute path: arandu /full/path/to/file.md
  • Check toolbar shows full path (not just filename)
  • Open Console.app and filter by "arandu" to see debug logs showing path resolution
  • Test file associations (double-click .md file) works correctly
  • Verify compilation: cargo check --manifest-path src-tauri/Cargo.toml

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a TCP-based IPC endpoint to allow external tooling to send open-file and command requests.
    • Toolbar now displays the full file path with hover tooltip.
  • Improvements

    • Expanded debug logging for file loading, CLI/opened-file and URL handling for better troubleshooting.
    • File path resolution now prefers canonical paths and falls back reliably; socket state is cleaned up on exit.

- Canonicalize paths in read_file command to handle relative paths
- Canonicalize paths in macOS Opened event (fixes#11)
- Add debug logging at all file loading entry points
- Restructure builder to allow conditional IPC state management
- Add placeholders for feat/unix-socket-ipc merge compatibility
- Display full path in toolbar for better debugging
@coderabbitai

coderabbitaiBot commented Feb 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • apps/tauri/src-tauri/icons/icon.icns
⛔ Files ignored due to path filters (5)
  • apps/tauri/src-tauri/icons/128x128.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/128x128@2x.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/32x32.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/64x64.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/icon.png is excluded by !**/*.png

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a TCP IPC server and state to the Tauri backend; makes IPC structs/functions public; adds path canonicalization and extensive [DEBUG] logging for CLI / opened-file flows; updates frontend to show full file paths and add debug logs.

Changes

Cohort / File(s)Summary
TCP IPC server & state
apps/tauri/src-tauri/src/tcp_ipc.rs
New TCP IPC implementation: binds listener, accepts connections, parses newline-delimited JSON IpcCommand, calls process_command, responds with IpcResponse; exposes TcpSocketState, setup, and cleanup.
Backend integration & path handling
apps/tauri/src-tauri/src/lib.rs
Integrated TCP IPC setup/cleanup and TcpSocketState into app init/teardown; added path canonicalization in read_file and all open-file / CLI / macOS opened-url handling; added many [DEBUG] logs and fallback behavior when canonicalization fails.
IPC types & API visibility
apps/tauri/src-tauri/src/ipc.rs
Made IpcCommand and IpcResponse public with exported fields; made process_command public for reuse by tcp_ipc.
Frontend: toolbar & logging
apps/tauri/src/main.js
Show full file path in toolbar (with hover title) instead of only last segment; added debug logs around loadFile and open-file event handling.

Sequence Diagram(s)

sequenceDiagram
participant Client as TCP Client
participant TCP as Tcp Listener (tcp_ipc)
participant App as Tauri App / process_command
participant FS as Filesystem
participant UI as Frontend (webview)
Client->>TCP: connect, send JSON line (IpcCommand)
TCP->>App: parse JSON -> call process_command(cmd)
App->>FS: read_file / resolve path (canonicalize)
alt command produces response
FS-->>App: file contents / error
App-->>TCP: IpcResponse (JSON)
App->>UI: emit "open-file" with resolved path (if applicable)
UI-->>UI: loadFile(resolved path), update toolbar
else error
App-->>TCP: IpcResponse (error)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nudged a socket, sniffed a path so neat,
I chased the canonical footprints down the street,
I told the app and sang with glee,
"Full paths now shine for you and me!"
— a rabbit, debugging with tiny feet 🌿✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main changes: fixing relative path resolution and preparing code structure for IPC integration.
Linked Issues check✅ PassedThe PR successfully addresses issue #11 by canonicalizing file paths at all entry points (CLI args, single-instance handler, macOS Opened event, and read_file command) to resolve relative paths correctly.
Out of Scope Changes check✅ PassedAll changes directly support the PR objectives: path canonicalization fixes, debug logging, toolbar display improvements, and TCP IPC setup for future merge compatibility.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/relative-paths

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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

366-387: Consider adding fallback behavior for consistency.

The single-instance handler silently drops the file-open request when canonicalize fails (lines 383-385 only log, no open-file event emitted). In contrast:

  • CLI args handler (line 462) uses unwrap_or_else to fall back to the original path
  • macOS Opened event (line 535) uses unwrap_or to fall back

This inconsistency could cause confusing behavior where files open in some scenarios but not others.

♻️ Suggested fix to add fallback
 if !file_path.is_empty() && !file_path.starts_with('-') {
- if let Ok(abs_path) = std::fs::canonicalize(file_path) {- let path_str = abs_path.to_string_lossy().to_string();- eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);- let _ = app.emit("open-file", &path_str);- } else {- eprintln!("[DEBUG] Failed to canonicalize path: {:?}", file_path);- }+ let abs_path = std::fs::canonicalize(file_path).unwrap_or_else(|e| {+ eprintln!("[DEBUG] Canonicalize failed ({}), using as-is", e);+ PathBuf::from(file_path)+ });+ let path_str = abs_path.to_string_lossy().to_string();+ eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);+ let _ = app.emit("open-file", &path_str);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 366 - 387, The single-instance
handler in the tauri_plugin_single_instance::init closure drops the open-file
event when std::fs::canonicalize(file_path) fails; change the logic so that on
canonicalize failure you fall back to the original file_path (or its
to_string_lossy) and still call app.emit("open-file", &path_str) so behavior
matches the CLI and macOS handlers; update the block handling args[1] to compute
a path_str variable from canonicalize if Ok or else from the original file_path,
log the fallback, and always emit "open-file" with that path_str.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 366-387: The single-instance handler in the
tauri_plugin_single_instance::init closure drops the open-file event when
std::fs::canonicalize(file_path) fails; change the logic so that on canonicalize
failure you fall back to the original file_path (or its to_string_lossy) and
still call app.emit("open-file", &path_str) so behavior matches the CLI and
macOS handlers; update the block handling args[1] to compute a path_str variable
from canonicalize if Ok or else from the original file_path, log the fallback,
and always emit "open-file" with that path_str.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51408db and a075094.

📒 Files selected for processing (2)
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src/main.js

@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed the CodeRabbit nitpick on the single-instance handler canonicalize fallback.

lib.rs lines 378-385: Changed from if let Ok(abs_path) = ... (which silently dropped the open-file event on failure) to unwrap_or_else with fallback to the original path. This now matches the behavior of both the CLI args handler (line 462) and the macOS Opened event handler (line 535).

Fixed in 53dbe11.

@coderabbitaicoderabbitaiBot 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: 1

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

66-66: Consider structured logging for production.

The [DEBUG] prefix in eprintln! calls works for development, but for production builds you might want to use the log or tracing crate with configurable log levels. This would allow filtering debug output without code changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 66, The debug print in read_file
currently uses eprintln!("[DEBUG] ...", path); switch this to a structured
logging call (e.g. log::debug! or tracing::debug!) so debug output can be
filtered at runtime, and ensure the application initializes the chosen
logger/subscriber (e.g. env_logger::init() or tracing_subscriber) in
main/startup; replace the eprintln! invocation inside read_file with the
appropriate debug macro and remove the hardcoded "[DEBUG]" prefix so the logging
framework controls level and formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 533-537: The opened-event handler uses
std::fs::canonicalize(&path).unwrap_or(path) which silently falls back; change
it to use unwrap_or_else so you can log the canonicalize error (matching
single-instance and CLI handlers). In the opened event block, replace
unwrap_or(path) with unwrap_or_else(|err| { eprintln!("[DEBUG] canonicalize
failed: {:?}", err); path.clone() }) (ensure abs_path and path_str keep the same
names) so failures are logged consistently.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Line 66: The debug print in read_file currently uses eprintln!("[DEBUG] ...",
path); switch this to a structured logging call (e.g. log::debug! or
tracing::debug!) so debug output can be filtered at runtime, and ensure the
application initializes the chosen logger/subscriber (e.g. env_logger::init() or
tracing_subscriber) in main/startup; replace the eprintln! invocation inside
read_file with the appropriate debug macro and remove the hardcoded "[DEBUG]"
prefix so the logging framework controls level and formatting.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a075094 and 53dbe11.

📒 Files selected for processing (1)
  • apps/tauri/src-tauri/src/lib.rs

Comment threadapps/tauri/src-tauri/src/lib.rs
…onicalize
Align the macOS Opened event handler with the single-instance and CLI
handlers by replacing silent unwrap_or fallback with unwrap_or_else
that logs the canonicalize failure before falling back.
@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed all remaining CodeRabbit review comments from both review rounds:

1. Inconsistent canonicalization fallback (Opened event, line 535) -- Fixed in 6fb6ca2. Replaced unwrap_or(path) with unwrap_or_else that logs the error, matching the single-instance and CLI handlers.

2. Single-instance handler dropping file-open on canonicalize failure (lines 366-387) -- Already addressed in 53dbe11 (previous commit).

3. Structured logging suggestion (line 66) -- Acknowledged as valid but deferred. Switching from eprintln! to log/tracing is a broader change that affects the entire codebase and is better handled in a dedicated PR. All debug prints in this PR are consistent with the existing eprintln!("[DEBUG] ...") pattern used throughout the project.

All three canonicalization entry points now use the same unwrap_or_else + debug logging pattern for consistent diagnostics.

- Add tcp_ipc.rs module with TCP listener on 127.0.0.1:7474
- Refactor ipc.rs to make IpcCommand/Response and process_command public
- TCP socket works in parallel with Unix socket for compatibility
- Supports same JSON protocol: open, ping, show commands
- Fixes Unix socket limitation with Docker/Podman containers
- Both sockets share same command processing logic
Resolves container accessibility issue where Unix sockets from macOS
are not accessible within Linux containers due to different kernel stacks.

@coderabbitaicoderabbitaiBot 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: 6

🧹 Nitpick comments (2)
apps/tauri/src-tauri/src/tcp_ipc.rs (1)

45-62: No connection limits or rate-limiting on the accept loop.

Any local process can open unlimited connections to this TCP server. Without a maximum connection count or per-connection timeout, a misbehaving local process could exhaust file descriptors or memory by opening many connections and sending data slowly.

Consider adding:

  • A maximum concurrent connection semaphore (tokio::sync::Semaphore)
  • A read timeout per connection (tokio::time::timeout)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 45 - 62, The accept loop in
tcp_listener_loop currently allows unlimited concurrent connections; add a
tokio::sync::Semaphore to limit max concurrent clients (acquire a permit before
spawning handle_client and release when that client's task finishes) and wrap
per-connection read/write operations inside tokio::time::timeout in
handle_client to enforce a per-connection inactivity/read timeout; ensure
tcp_listener_loop attempts to accept new connections even when semaphore is
exhausted (e.g., reject/close the stream immediately with a log) and
propagate/handle timeout errors from handle_client so they are logged.
apps/tauri/src-tauri/src/lib.rs (1)

529-533: TCP cleanup only clears state, doesn't stop the listener (see tcp_ipc.rs comment).

As noted in the tcp_ipc.rs review, cleanup() only nulls out the stored address. The accept loop and active connections continue until the process exits. For the ExitRequested + ExplicitQuit path this is likely fine since the process terminates shortly after, but it's worth noting the asymmetry with the Unix socket cleanup which actually removes the socket file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 529 - 533, The current call to
tcp_ipc::cleanup(tcp_socket_state) only clears the stored address but does not
stop the accept loop or close active connections; update the shutdown so the TCP
listener and any active connections are closed and the accept loop signalled to
exit. Either call a dedicated function (e.g., tcp_ipc::stop_listener or
tcp_ipc::shutdown_listener) from this location, or extend tcp_ipc::cleanup to
close the listener socket stored in tcp_ipc::TcpSocketState, close active
connections and signal/join the accept loop so it terminates cleanly rather than
leaving it running until process exit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 416-419: The tcp_ipc::setup() function currently masks bind
failures by always returning Ok(()), so the error branch here
(tcp_ipc::setup(app)) is effectively unreachable; update tcp_ipc::setup in
tcp_ipc.rs to propagate and return an Err when socket bind/listen fails (return
a Result with the real io::Error or a boxed error), and keep this call site
as-is so the Err path (eprintln!("Failed to setup TCP IPC: {}", e)) is reachable
and logs the actual failure; ensure the tcp_ipc::setup signature and any callers
are updated to handle the Result change.
- Line 15: The tcp_ipc module is currently compiled unconditionally, exposing an
unauthenticated TCP listener (127.0.0.1:7474); gate this to be opt-in and add
simple auth: protect the module with a cfg feature (e.g., #[cfg(feature =
"tcp-ipc")] on the mod tcp_ipc declaration and corresponding Cargo.toml feature)
and/or add token-based auth inside the tcp_ipc listener (generate a random token
stored in a file with 0o600 perms and require clients to present it when
connecting, validating in the tcp listener accept/handle path); update any
public APIs that assume tcp_ipc exists to compile without it (use
#[cfg(any(...))] or conditional stubs) and document the feature and token file
location so consumers can opt in securely.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 12-35: The setup() function currently spawns an async task and
returns Ok(()) before TcpListener::bind runs, so binding failures are never
propagated; fix by performing the bind synchronously (await the bind) or by
using a oneshot channel to relay the bind result back to the caller before
returning. Concretely, call TcpListener::bind(&addr).await (or bind on the sync
thread) and handle Err by returning Err(String) from setup(), or if you must
spawn first, create a tokio::sync::oneshot sender/receiver, have the spawned
task send Ok(()) or Err(e.to_string()) after attempting TcpListener::bind, and
have setup() block/await on the receiver and return the corresponding Result;
update references in this flow around setup, TcpListener::bind,
tcp_listener_loop, and the app_handle state assignment so errors are returned to
the caller instead of always returning Ok(()).
- Around line 37-43: cleanup() only removes the stored address string and does
not stop the spawned accept loop or active connections; update TcpSocketState to
hold a cancellation mechanism (e.g., a tokio::sync::watch,
tokio_util::sync::CancellationToken, or the JoinHandle of the spawned task) and
modify the accept loop to listen for that cancellation token or be abortable,
then in cleanup() signal cancellation or abort the JoinHandle and await/cleanup
resources before dropping the address; reference TcpSocketState, cleanup(), the
spawned accept loop (the task that owns the TcpListener) and whichever chosen
token/handle so you can set and trigger it from cleanup().
- Around line 64-94: The handle_client function currently logs raw,
attacker-controlled TCP input and command strings (the `line` contents and
`cmd.command`) which can contain newlines and inject fake log entries; sanitize
or escape/newline-normalize and/or truncate these values before logging (e.g.,
replace newlines with escaped sequences and limit length) when using the
eprintln! calls and when logging inside the match branch that processes
`process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.
- Around line 7-8: The current setup() spawns the listener asynchronously and
always returns Ok(()), and the hardcoded DEFAULT_PORT = 7474 can silently
conflict with Neo4j; change setup() to synchronously bind with
TcpListener::bind((DEFAULT_HOST, port)) (propagating any bind Err back to the
caller) before spawning the listener loop, and make the port configurable (read
from an env var or config with DEFAULT_PORT as fallback) so callers see bind
failures immediately and the port can be changed without code edits.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 529-533: The current call to tcp_ipc::cleanup(tcp_socket_state)
only clears the stored address but does not stop the accept loop or close active
connections; update the shutdown so the TCP listener and any active connections
are closed and the accept loop signalled to exit. Either call a dedicated
function (e.g., tcp_ipc::stop_listener or tcp_ipc::shutdown_listener) from this
location, or extend tcp_ipc::cleanup to close the listener socket stored in
tcp_ipc::TcpSocketState, close active connections and signal/join the accept
loop so it terminates cleanly rather than leaving it running until process exit.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 45-62: The accept loop in tcp_listener_loop currently allows
unlimited concurrent connections; add a tokio::sync::Semaphore to limit max
concurrent clients (acquire a permit before spawning handle_client and release
when that client's task finishes) and wrap per-connection read/write operations
inside tokio::time::timeout in handle_client to enforce a per-connection
inactivity/read timeout; ensure tcp_listener_loop attempts to accept new
connections even when semaphore is exhausted (e.g., reject/close the stream
immediately with a log) and propagate/handle timeout errors from handle_client
so they are logged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53dbe11 and 29db134.

📒 Files selected for processing (3)
  • apps/tauri/src-tauri/src/ipc.rs
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src-tauri/src/tcp_ipc.rs

mod cli_installer;
#[cfg(unix)]
mod ipc;
mod tcp_ipc;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TCP IPC module is unconditionally compiled — security implications of an unauthenticated local TCP listener.

Unlike the Unix socket (gated behind #[cfg(unix)] with 0o600 permissions), the TCP listener on 127.0.0.1:7474 has no authentication or authorization. Any local process (including those in containers, sandboxes, or running as different users) can connect and issue commands like open (to open arbitrary files the app user can read) or show (to manipulate window focus).

On multi-user systems or shared hosts this is a local attack surface. Consider adding at minimum:

  • A shared secret / token-based auth (e.g., random token written to a file with restrictive permissions, client must present it)
  • Or restricting TCP IPC behind a feature flag / cfg gate so it's opt-in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 15, The tcp_ipc module is currently
compiled unconditionally, exposing an unauthenticated TCP listener
(127.0.0.1:7474); gate this to be opt-in and add simple auth: protect the module
with a cfg feature (e.g., #[cfg(feature = "tcp-ipc")] on the mod tcp_ipc
declaration and corresponding Cargo.toml feature) and/or add token-based auth
inside the tcp_ipc listener (generate a random token stored in a file with 0o600
perms and require clients to present it when connecting, validating in the tcp
listener accept/handle path); update any public APIs that assume tcp_ipc exists
to compile without it (use #[cfg(any(...))] or conditional stubs) and document
the feature and token file location so consumers can opt in securely.

Comment on lines +416 to +419
// TCP IPC setup (works on all platforms, including from containers)
if let Err(e) = tcp_ipc::setup(app) {
eprintln!("Failed to setup TCP IPC: {}", e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TCP IPC setup error is logged but doesn't prevent app startup — acceptable.

This mirrors the Unix socket setup pattern. Since the TCP IPC is supplementary, failing gracefully is reasonable. However, note that as discussed in tcp_ipc.rs, setup() currently always returns Ok(()) even on bind failure, making this error branch unreachable for the most important failure case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 416 - 419, The tcp_ipc::setup()
function currently masks bind failures by always returning Ok(()), so the error
branch here (tcp_ipc::setup(app)) is effectively unreachable; update
tcp_ipc::setup in tcp_ipc.rs to propagate and return an Err when socket
bind/listen fails (return a Result with the real io::Error or a boxed error),
and keep this call site as-is so the Err path (eprintln!("Failed to setup TCP
IPC: {}", e)) is reachable and logs the actual failure; ensure the
tcp_ipc::setup signature and any callers are updated to handle the Result
change.

Comment on lines +7 to +8
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 7474;

Copy link
Copy Markdown

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

🌐 Web query:

Neo4j default port database

💡 Result:

Neo4j’s default ports depend on how you connect:

  • Bolt (drivers / neo4j:// / bolt://): 7687 [1][3]
  • HTTP (Neo4j Browser / HTTP API): 7474 [1][3]
  • HTTPS: 7473 [1][3]

(Other common defaults include Backup: 6362.) [1]


🏁 Script executed:

# First, let's find and examine the tcp_ipc.rs file
find . -name "tcp_ipc.rs" -type f | head -5

Repository: devitools/arandu

Length of output: 97


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/tcp_ipc.rs

Repository: devitools/arandu

Length of output: 3886


🏁 Script executed:

# Now let's check lib.rs to see how setup() is called and if it handles the result
find . -name "lib.rs" -path "*/src-tauri/src/lib.rs" -type f

Repository: devitools/arandu

Length of output: 93


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/lib.rs

Repository: devitools/arandu

Length of output: 23262


Hardcoded port 7474 conflicts with Neo4j and fails silently.

Port 7474 is Neo4j's default HTTP port. The TCP bind happens asynchronously inside a spawned task (lines 17–30), but setup() returns Ok(()) immediately (line 34) before the bind attempt completes. If another process holds the port, the error is only logged to stderr; the caller in lib.rs (lines 417–419) cannot distinguish success from failure since setup() always returns Ok(()).

Consider either:

  • Synchronously binding before spawning the listener loop, or
  • Using a oneshot channel to propagate bind errors back to the caller, or
  • Making the port configurable (via config file or environment variable) with a fallback port range.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 7 - 8, The current setup()
spawns the listener asynchronously and always returns Ok(()), and the hardcoded
DEFAULT_PORT = 7474 can silently conflict with Neo4j; change setup() to
synchronously bind with TcpListener::bind((DEFAULT_HOST, port)) (propagating any
bind Err back to the caller) before spawning the listener loop, and make the
port configurable (read from an env var or config with DEFAULT_PORT as fallback)
so callers see bind failures immediately and the port can be changed without
code edits.

Comment on lines +12 to +35
pub fn setup(app: &tauri::App) -> Result<(), String> {
let addr = format!("{}:{}", DEFAULT_HOST, DEFAULT_PORT);

let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
match TcpListener::bind(&addr).await {
Ok(listener) => {
eprintln!("[TCP IPC] Listening on {}", addr);

let state = app_handle.state::<TcpSocketState>();
if let Ok(mut guard) = state.0.lock() {
*guard = Some(addr.clone());
}

tcp_listener_loop(listener, app_handle).await;
}
Err(e) => {
eprintln!("[TCP IPC] Failed to bind to {}: {}", addr, e);
}
}
});

Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

setup() always returns Ok(()) regardless of bind outcome.

Because TcpListener::bind happens inside the spawned async task, setup() returns Ok(()) before the bind attempt completes. The caller in lib.rs (line 417) checks for Err but will never see one from a bind failure. This makes the error handling at the call site dead code for the most critical failure mode.

Compare with the Unix socket ipc::setup() which also has this pattern — but at minimum, consider a oneshot channel to relay the bind result, or bind synchronously before spawning the accept loop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 12 - 35, The setup()
function currently spawns an async task and returns Ok(()) before
TcpListener::bind runs, so binding failures are never propagated; fix by
performing the bind synchronously (await the bind) or by using a oneshot channel
to relay the bind result back to the caller before returning. Concretely, call
TcpListener::bind(&addr).await (or bind on the sync thread) and handle Err by
returning Err(String) from setup(), or if you must spawn first, create a
tokio::sync::oneshot sender/receiver, have the spawned task send Ok(()) or
Err(e.to_string()) after attempting TcpListener::bind, and have setup()
block/await on the receiver and return the corresponding Result; update
references in this flow around setup, TcpListener::bind, tcp_listener_loop, and
the app_handle state assignment so errors are returned to the caller instead of
always returning Ok(()).

Comment on lines +37 to +43
pub fn cleanup(state: tauri::State<TcpSocketState>) {
if let Ok(mut guard) = state.0.lock() {
if let Some(addr) = guard.take() {
eprintln!("[TCP IPC] Shutting down listener on {}", addr);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cleanup() does not actually stop the TCP listener.

cleanup() only clears the stored address string from state. The TcpListener and its accept loop (and any active client connections) continue running because the listener is owned by the spawned task, not by TcpSocketState. Unlike the Unix socket ipc::cleanup() which removes the socket file (preventing new connections), this cleanup is purely cosmetic.

To actually shut down, you'd need a cancellation mechanism (e.g., a tokio::sync::watch or CancellationToken) that the accept loop checks, or store a JoinHandle to abort the task.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 37 - 43, cleanup() only
removes the stored address string and does not stop the spawned accept loop or
active connections; update TcpSocketState to hold a cancellation mechanism
(e.g., a tokio::sync::watch, tokio_util::sync::CancellationToken, or the
JoinHandle of the spawned task) and modify the accept loop to listen for that
cancellation token or be abortable, then in cleanup() signal cancellation or
abort the JoinHandle and await/cleanup resources before dropping the address;
reference TcpSocketState, cleanup(), the spawned accept loop (the task that owns
the TcpListener) and whichever chosen token/handle so you can set and trigger it
from cleanup().

Comment on lines +64 to +94
async fn handle_client(stream: TcpStream, app: tauri::AppHandle) -> Result<(), String> {
let peer_addr = stream.peer_addr().map_err(|e| e.to_string())?;
let (reader, mut writer) = stream.into_split();
let reader = BufReader::new(reader);
let mut lines = reader.lines();

while let Some(line) = lines.next_line().await.map_err(|e| e.to_string())? {
eprintln!("[TCP IPC] Received from {}: {}", peer_addr, line);

let response = match serde_json::from_str::<IpcCommand>(&line) {
Ok(cmd) => {
eprintln!("[TCP IPC] Processing command: {}", cmd.command);
process_command(cmd, &app)
}
Err(e) => IpcResponse {
success: false,
error: Some(format!("Invalid JSON: {}", e)),
},
};

let json = serde_json::to_string(&response).unwrap_or_default();
eprintln!("[TCP IPC] Sending response to {}: {}", peer_addr, json);

writer
.write_all(format!("{}\n", json).as_bytes())
.await
.map_err(|e| e.to_string())?;
}

eprintln!("[TCP IPC] Connection closed: {}", peer_addr);
Ok(())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential log injection via unsanitized TCP input.

Line 71 logs the raw line received from the TCP socket, and line 75 logs cmd.command which is attacker-controlled input from any local process. While eprintln! is less exploitable than structured logging frameworks, newlines within the JSON string could forge log entries. Consider sanitizing or escaping logged values, or at minimum truncating them.

Also, Line 84: serde_json::to_string(&response).unwrap_or_default() will send an empty line "\n" to the client if serialization fails. Since IpcResponse is a simple struct this is unlikely, but sending an empty line could confuse a client expecting valid JSON. Consider sending a hardcoded error JSON instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 64 - 94, The handle_client
function currently logs raw, attacker-controlled TCP input and command strings
(the `line` contents and `cmd.command`) which can contain newlines and inject
fake log entries; sanitize or escape/newline-normalize and/or truncate these
values before logging (e.g., replace newlines with escaped sequences and limit
length) when using the eprintln! calls and when logging inside the match branch
that processes `process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.

The app icon had a solid background that extended to the edges, preventing
macOS from properly applying its automatic corner rounding. This resulted in
a square icon appearance instead of the expected rounded corners.
Changes:
- Applied ~12% corner radius to all icon assets (matching macOS guidelines)
- Regenerated icon.icns with proper alpha channel masks
- Updated all PNG sizes (32x32, 64x64, 128x128, 256x256, 512x512, 1024x1024)
The icon now displays with proper rounded corners in Dock, Finder, and app switcher.
@wilcorrea
wilcorrea merged commit 31f187b into mainFeb 23, 2026
1 check passed
@wilcorrea
wilcorrea deleted the fix/relative-paths branch February 23, 2026 20:42
Sign up for freeto 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.

Relative paths lead to a blank screen

1 participant

@wilcorrea
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(tauri): resolve relative paths and prepare for IPC merge - #12

Merged
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths
Feb 23, 2026
Merged

fix(tauri): resolve relative paths and prepare for IPC merge#12
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths

Conversation

@wilcorrea

@wilcorreawilcorrea commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#11 - Relative paths lead to a blank screen

  • Canonicalize file paths in read_file command to correctly resolve relative paths before reading files
  • Fix macOS Opened event to canonicalize paths when opening files via file associations (critical bug fix)
  • Add comprehensive debug logging at all file loading entry points (CLI args, single instance, Opened event, read_file)
  • Display full path in toolbar instead of just filename for better debugging visibility
  • Restructure builder for merge compatibility with feat/unix-socket-ipc branch to prevent future conflicts
  • Add merge guide documentation with step-by-step instructions for integrating IPC changes

Technical Details

The root cause was that relative paths (e.g., ./README.md) were not being canonicalized at all entry points:

  • ✅ CLI args: Already canonicalized
  • ✅ Single instance: Already canonicalized
  • Opened event (macOS): Missing canonicalization → FIXED
  • read_file command: No path resolution → FIXED

When a relative path reached read_file, it would try to read relative to the process working directory (not the file's directory), causing file not found errors and blank screens.

Merge Compatibility

This PR restructures the builder to match feat/unix-socket-ipc branch structure:

  • Separates builder initialization from setup
  • Adds placeholder comments showing exactly where to add IPC socket code
  • Includes MERGE_GUIDE.md with complete merge instructions

This ensures clean merges between branches with zero conflicts.

Test plan

  • Build and install: cd apps/tauri && make build-dev && make install
  • Test with relative path: cd ~/Documents && arandu ./README.md
  • Verify file loads correctly (no blank screen)
  • Test with absolute path: arandu /full/path/to/file.md
  • Check toolbar shows full path (not just filename)
  • Open Console.app and filter by "arandu" to see debug logs showing path resolution
  • Test file associations (double-click .md file) works correctly
  • Verify compilation: cargo check --manifest-path src-tauri/Cargo.toml

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a TCP-based IPC endpoint to allow external tooling to send open-file and command requests.
    • Toolbar now displays the full file path with hover tooltip.
  • Improvements

    • Expanded debug logging for file loading, CLI/opened-file and URL handling for better troubleshooting.
    • File path resolution now prefers canonical paths and falls back reliably; socket state is cleaned up on exit.

- Canonicalize paths in read_file command to handle relative paths
- Canonicalize paths in macOS Opened event (fixes#11)
- Add debug logging at all file loading entry points
- Restructure builder to allow conditional IPC state management
- Add placeholders for feat/unix-socket-ipc merge compatibility
- Display full path in toolbar for better debugging
@coderabbitai

coderabbitaiBot commented Feb 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • apps/tauri/src-tauri/icons/icon.icns
⛔ Files ignored due to path filters (5)
  • apps/tauri/src-tauri/icons/128x128.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/128x128@2x.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/32x32.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/64x64.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/icon.png is excluded by !**/*.png

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a TCP IPC server and state to the Tauri backend; makes IPC structs/functions public; adds path canonicalization and extensive [DEBUG] logging for CLI / opened-file flows; updates frontend to show full file paths and add debug logs.

Changes

Cohort / File(s)Summary
TCP IPC server & state
apps/tauri/src-tauri/src/tcp_ipc.rs
New TCP IPC implementation: binds listener, accepts connections, parses newline-delimited JSON IpcCommand, calls process_command, responds with IpcResponse; exposes TcpSocketState, setup, and cleanup.
Backend integration & path handling
apps/tauri/src-tauri/src/lib.rs
Integrated TCP IPC setup/cleanup and TcpSocketState into app init/teardown; added path canonicalization in read_file and all open-file / CLI / macOS opened-url handling; added many [DEBUG] logs and fallback behavior when canonicalization fails.
IPC types & API visibility
apps/tauri/src-tauri/src/ipc.rs
Made IpcCommand and IpcResponse public with exported fields; made process_command public for reuse by tcp_ipc.
Frontend: toolbar & logging
apps/tauri/src/main.js
Show full file path in toolbar (with hover title) instead of only last segment; added debug logs around loadFile and open-file event handling.

Sequence Diagram(s)

sequenceDiagram
participant Client as TCP Client
participant TCP as Tcp Listener (tcp_ipc)
participant App as Tauri App / process_command
participant FS as Filesystem
participant UI as Frontend (webview)
Client->>TCP: connect, send JSON line (IpcCommand)
TCP->>App: parse JSON -> call process_command(cmd)
App->>FS: read_file / resolve path (canonicalize)
alt command produces response
FS-->>App: file contents / error
App-->>TCP: IpcResponse (JSON)
App->>UI: emit "open-file" with resolved path (if applicable)
UI-->>UI: loadFile(resolved path), update toolbar
else error
App-->>TCP: IpcResponse (error)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nudged a socket, sniffed a path so neat,
I chased the canonical footprints down the street,
I told the app and sang with glee,
"Full paths now shine for you and me!"
— a rabbit, debugging with tiny feet 🌿✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main changes: fixing relative path resolution and preparing code structure for IPC integration.
Linked Issues check✅ PassedThe PR successfully addresses issue #11 by canonicalizing file paths at all entry points (CLI args, single-instance handler, macOS Opened event, and read_file command) to resolve relative paths correctly.
Out of Scope Changes check✅ PassedAll changes directly support the PR objectives: path canonicalization fixes, debug logging, toolbar display improvements, and TCP IPC setup for future merge compatibility.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/relative-paths

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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

366-387: Consider adding fallback behavior for consistency.

The single-instance handler silently drops the file-open request when canonicalize fails (lines 383-385 only log, no open-file event emitted). In contrast:

  • CLI args handler (line 462) uses unwrap_or_else to fall back to the original path
  • macOS Opened event (line 535) uses unwrap_or to fall back

This inconsistency could cause confusing behavior where files open in some scenarios but not others.

♻️ Suggested fix to add fallback
 if !file_path.is_empty() && !file_path.starts_with('-') {
- if let Ok(abs_path) = std::fs::canonicalize(file_path) {- let path_str = abs_path.to_string_lossy().to_string();- eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);- let _ = app.emit("open-file", &path_str);- } else {- eprintln!("[DEBUG] Failed to canonicalize path: {:?}", file_path);- }+ let abs_path = std::fs::canonicalize(file_path).unwrap_or_else(|e| {+ eprintln!("[DEBUG] Canonicalize failed ({}), using as-is", e);+ PathBuf::from(file_path)+ });+ let path_str = abs_path.to_string_lossy().to_string();+ eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);+ let _ = app.emit("open-file", &path_str);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 366 - 387, The single-instance
handler in the tauri_plugin_single_instance::init closure drops the open-file
event when std::fs::canonicalize(file_path) fails; change the logic so that on
canonicalize failure you fall back to the original file_path (or its
to_string_lossy) and still call app.emit("open-file", &path_str) so behavior
matches the CLI and macOS handlers; update the block handling args[1] to compute
a path_str variable from canonicalize if Ok or else from the original file_path,
log the fallback, and always emit "open-file" with that path_str.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 366-387: The single-instance handler in the
tauri_plugin_single_instance::init closure drops the open-file event when
std::fs::canonicalize(file_path) fails; change the logic so that on canonicalize
failure you fall back to the original file_path (or its to_string_lossy) and
still call app.emit("open-file", &path_str) so behavior matches the CLI and
macOS handlers; update the block handling args[1] to compute a path_str variable
from canonicalize if Ok or else from the original file_path, log the fallback,
and always emit "open-file" with that path_str.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51408db and a075094.

📒 Files selected for processing (2)
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src/main.js

@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed the CodeRabbit nitpick on the single-instance handler canonicalize fallback.

lib.rs lines 378-385: Changed from if let Ok(abs_path) = ... (which silently dropped the open-file event on failure) to unwrap_or_else with fallback to the original path. This now matches the behavior of both the CLI args handler (line 462) and the macOS Opened event handler (line 535).

Fixed in 53dbe11.

@coderabbitaicoderabbitaiBot 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: 1

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

66-66: Consider structured logging for production.

The [DEBUG] prefix in eprintln! calls works for development, but for production builds you might want to use the log or tracing crate with configurable log levels. This would allow filtering debug output without code changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 66, The debug print in read_file
currently uses eprintln!("[DEBUG] ...", path); switch this to a structured
logging call (e.g. log::debug! or tracing::debug!) so debug output can be
filtered at runtime, and ensure the application initializes the chosen
logger/subscriber (e.g. env_logger::init() or tracing_subscriber) in
main/startup; replace the eprintln! invocation inside read_file with the
appropriate debug macro and remove the hardcoded "[DEBUG]" prefix so the logging
framework controls level and formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 533-537: The opened-event handler uses
std::fs::canonicalize(&path).unwrap_or(path) which silently falls back; change
it to use unwrap_or_else so you can log the canonicalize error (matching
single-instance and CLI handlers). In the opened event block, replace
unwrap_or(path) with unwrap_or_else(|err| { eprintln!("[DEBUG] canonicalize
failed: {:?}", err); path.clone() }) (ensure abs_path and path_str keep the same
names) so failures are logged consistently.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Line 66: The debug print in read_file currently uses eprintln!("[DEBUG] ...",
path); switch this to a structured logging call (e.g. log::debug! or
tracing::debug!) so debug output can be filtered at runtime, and ensure the
application initializes the chosen logger/subscriber (e.g. env_logger::init() or
tracing_subscriber) in main/startup; replace the eprintln! invocation inside
read_file with the appropriate debug macro and remove the hardcoded "[DEBUG]"
prefix so the logging framework controls level and formatting.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a075094 and 53dbe11.

📒 Files selected for processing (1)
  • apps/tauri/src-tauri/src/lib.rs

Comment threadapps/tauri/src-tauri/src/lib.rs
…onicalize
Align the macOS Opened event handler with the single-instance and CLI
handlers by replacing silent unwrap_or fallback with unwrap_or_else
that logs the canonicalize failure before falling back.
@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed all remaining CodeRabbit review comments from both review rounds:

1. Inconsistent canonicalization fallback (Opened event, line 535) -- Fixed in 6fb6ca2. Replaced unwrap_or(path) with unwrap_or_else that logs the error, matching the single-instance and CLI handlers.

2. Single-instance handler dropping file-open on canonicalize failure (lines 366-387) -- Already addressed in 53dbe11 (previous commit).

3. Structured logging suggestion (line 66) -- Acknowledged as valid but deferred. Switching from eprintln! to log/tracing is a broader change that affects the entire codebase and is better handled in a dedicated PR. All debug prints in this PR are consistent with the existing eprintln!("[DEBUG] ...") pattern used throughout the project.

All three canonicalization entry points now use the same unwrap_or_else + debug logging pattern for consistent diagnostics.

- Add tcp_ipc.rs module with TCP listener on 127.0.0.1:7474
- Refactor ipc.rs to make IpcCommand/Response and process_command public
- TCP socket works in parallel with Unix socket for compatibility
- Supports same JSON protocol: open, ping, show commands
- Fixes Unix socket limitation with Docker/Podman containers
- Both sockets share same command processing logic
Resolves container accessibility issue where Unix sockets from macOS
are not accessible within Linux containers due to different kernel stacks.

@coderabbitaicoderabbitaiBot 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: 6

🧹 Nitpick comments (2)
apps/tauri/src-tauri/src/tcp_ipc.rs (1)

45-62: No connection limits or rate-limiting on the accept loop.

Any local process can open unlimited connections to this TCP server. Without a maximum connection count or per-connection timeout, a misbehaving local process could exhaust file descriptors or memory by opening many connections and sending data slowly.

Consider adding:

  • A maximum concurrent connection semaphore (tokio::sync::Semaphore)
  • A read timeout per connection (tokio::time::timeout)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 45 - 62, The accept loop in
tcp_listener_loop currently allows unlimited concurrent connections; add a
tokio::sync::Semaphore to limit max concurrent clients (acquire a permit before
spawning handle_client and release when that client's task finishes) and wrap
per-connection read/write operations inside tokio::time::timeout in
handle_client to enforce a per-connection inactivity/read timeout; ensure
tcp_listener_loop attempts to accept new connections even when semaphore is
exhausted (e.g., reject/close the stream immediately with a log) and
propagate/handle timeout errors from handle_client so they are logged.
apps/tauri/src-tauri/src/lib.rs (1)

529-533: TCP cleanup only clears state, doesn't stop the listener (see tcp_ipc.rs comment).

As noted in the tcp_ipc.rs review, cleanup() only nulls out the stored address. The accept loop and active connections continue until the process exits. For the ExitRequested + ExplicitQuit path this is likely fine since the process terminates shortly after, but it's worth noting the asymmetry with the Unix socket cleanup which actually removes the socket file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 529 - 533, The current call to
tcp_ipc::cleanup(tcp_socket_state) only clears the stored address but does not
stop the accept loop or close active connections; update the shutdown so the TCP
listener and any active connections are closed and the accept loop signalled to
exit. Either call a dedicated function (e.g., tcp_ipc::stop_listener or
tcp_ipc::shutdown_listener) from this location, or extend tcp_ipc::cleanup to
close the listener socket stored in tcp_ipc::TcpSocketState, close active
connections and signal/join the accept loop so it terminates cleanly rather than
leaving it running until process exit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 416-419: The tcp_ipc::setup() function currently masks bind
failures by always returning Ok(()), so the error branch here
(tcp_ipc::setup(app)) is effectively unreachable; update tcp_ipc::setup in
tcp_ipc.rs to propagate and return an Err when socket bind/listen fails (return
a Result with the real io::Error or a boxed error), and keep this call site
as-is so the Err path (eprintln!("Failed to setup TCP IPC: {}", e)) is reachable
and logs the actual failure; ensure the tcp_ipc::setup signature and any callers
are updated to handle the Result change.
- Line 15: The tcp_ipc module is currently compiled unconditionally, exposing an
unauthenticated TCP listener (127.0.0.1:7474); gate this to be opt-in and add
simple auth: protect the module with a cfg feature (e.g., #[cfg(feature =
"tcp-ipc")] on the mod tcp_ipc declaration and corresponding Cargo.toml feature)
and/or add token-based auth inside the tcp_ipc listener (generate a random token
stored in a file with 0o600 perms and require clients to present it when
connecting, validating in the tcp listener accept/handle path); update any
public APIs that assume tcp_ipc exists to compile without it (use
#[cfg(any(...))] or conditional stubs) and document the feature and token file
location so consumers can opt in securely.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 12-35: The setup() function currently spawns an async task and
returns Ok(()) before TcpListener::bind runs, so binding failures are never
propagated; fix by performing the bind synchronously (await the bind) or by
using a oneshot channel to relay the bind result back to the caller before
returning. Concretely, call TcpListener::bind(&addr).await (or bind on the sync
thread) and handle Err by returning Err(String) from setup(), or if you must
spawn first, create a tokio::sync::oneshot sender/receiver, have the spawned
task send Ok(()) or Err(e.to_string()) after attempting TcpListener::bind, and
have setup() block/await on the receiver and return the corresponding Result;
update references in this flow around setup, TcpListener::bind,
tcp_listener_loop, and the app_handle state assignment so errors are returned to
the caller instead of always returning Ok(()).
- Around line 37-43: cleanup() only removes the stored address string and does
not stop the spawned accept loop or active connections; update TcpSocketState to
hold a cancellation mechanism (e.g., a tokio::sync::watch,
tokio_util::sync::CancellationToken, or the JoinHandle of the spawned task) and
modify the accept loop to listen for that cancellation token or be abortable,
then in cleanup() signal cancellation or abort the JoinHandle and await/cleanup
resources before dropping the address; reference TcpSocketState, cleanup(), the
spawned accept loop (the task that owns the TcpListener) and whichever chosen
token/handle so you can set and trigger it from cleanup().
- Around line 64-94: The handle_client function currently logs raw,
attacker-controlled TCP input and command strings (the `line` contents and
`cmd.command`) which can contain newlines and inject fake log entries; sanitize
or escape/newline-normalize and/or truncate these values before logging (e.g.,
replace newlines with escaped sequences and limit length) when using the
eprintln! calls and when logging inside the match branch that processes
`process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.
- Around line 7-8: The current setup() spawns the listener asynchronously and
always returns Ok(()), and the hardcoded DEFAULT_PORT = 7474 can silently
conflict with Neo4j; change setup() to synchronously bind with
TcpListener::bind((DEFAULT_HOST, port)) (propagating any bind Err back to the
caller) before spawning the listener loop, and make the port configurable (read
from an env var or config with DEFAULT_PORT as fallback) so callers see bind
failures immediately and the port can be changed without code edits.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 529-533: The current call to tcp_ipc::cleanup(tcp_socket_state)
only clears the stored address but does not stop the accept loop or close active
connections; update the shutdown so the TCP listener and any active connections
are closed and the accept loop signalled to exit. Either call a dedicated
function (e.g., tcp_ipc::stop_listener or tcp_ipc::shutdown_listener) from this
location, or extend tcp_ipc::cleanup to close the listener socket stored in
tcp_ipc::TcpSocketState, close active connections and signal/join the accept
loop so it terminates cleanly rather than leaving it running until process exit.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 45-62: The accept loop in tcp_listener_loop currently allows
unlimited concurrent connections; add a tokio::sync::Semaphore to limit max
concurrent clients (acquire a permit before spawning handle_client and release
when that client's task finishes) and wrap per-connection read/write operations
inside tokio::time::timeout in handle_client to enforce a per-connection
inactivity/read timeout; ensure tcp_listener_loop attempts to accept new
connections even when semaphore is exhausted (e.g., reject/close the stream
immediately with a log) and propagate/handle timeout errors from handle_client
so they are logged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53dbe11 and 29db134.

📒 Files selected for processing (3)
  • apps/tauri/src-tauri/src/ipc.rs
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src-tauri/src/tcp_ipc.rs

mod cli_installer;
#[cfg(unix)]
mod ipc;
mod tcp_ipc;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TCP IPC module is unconditionally compiled — security implications of an unauthenticated local TCP listener.

Unlike the Unix socket (gated behind #[cfg(unix)] with 0o600 permissions), the TCP listener on 127.0.0.1:7474 has no authentication or authorization. Any local process (including those in containers, sandboxes, or running as different users) can connect and issue commands like open (to open arbitrary files the app user can read) or show (to manipulate window focus).

On multi-user systems or shared hosts this is a local attack surface. Consider adding at minimum:

  • A shared secret / token-based auth (e.g., random token written to a file with restrictive permissions, client must present it)
  • Or restricting TCP IPC behind a feature flag / cfg gate so it's opt-in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 15, The tcp_ipc module is currently
compiled unconditionally, exposing an unauthenticated TCP listener
(127.0.0.1:7474); gate this to be opt-in and add simple auth: protect the module
with a cfg feature (e.g., #[cfg(feature = "tcp-ipc")] on the mod tcp_ipc
declaration and corresponding Cargo.toml feature) and/or add token-based auth
inside the tcp_ipc listener (generate a random token stored in a file with 0o600
perms and require clients to present it when connecting, validating in the tcp
listener accept/handle path); update any public APIs that assume tcp_ipc exists
to compile without it (use #[cfg(any(...))] or conditional stubs) and document
the feature and token file location so consumers can opt in securely.

Comment on lines +416 to +419
// TCP IPC setup (works on all platforms, including from containers)
if let Err(e) = tcp_ipc::setup(app) {
eprintln!("Failed to setup TCP IPC: {}", e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TCP IPC setup error is logged but doesn't prevent app startup — acceptable.

This mirrors the Unix socket setup pattern. Since the TCP IPC is supplementary, failing gracefully is reasonable. However, note that as discussed in tcp_ipc.rs, setup() currently always returns Ok(()) even on bind failure, making this error branch unreachable for the most important failure case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 416 - 419, The tcp_ipc::setup()
function currently masks bind failures by always returning Ok(()), so the error
branch here (tcp_ipc::setup(app)) is effectively unreachable; update
tcp_ipc::setup in tcp_ipc.rs to propagate and return an Err when socket
bind/listen fails (return a Result with the real io::Error or a boxed error),
and keep this call site as-is so the Err path (eprintln!("Failed to setup TCP
IPC: {}", e)) is reachable and logs the actual failure; ensure the
tcp_ipc::setup signature and any callers are updated to handle the Result
change.

Comment on lines +7 to +8
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 7474;

Copy link
Copy Markdown

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

🌐 Web query:

Neo4j default port database

💡 Result:

Neo4j’s default ports depend on how you connect:

  • Bolt (drivers / neo4j:// / bolt://): 7687 [1][3]
  • HTTP (Neo4j Browser / HTTP API): 7474 [1][3]
  • HTTPS: 7473 [1][3]

(Other common defaults include Backup: 6362.) [1]


🏁 Script executed:

# First, let's find and examine the tcp_ipc.rs file
find . -name "tcp_ipc.rs" -type f | head -5

Repository: devitools/arandu

Length of output: 97


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/tcp_ipc.rs

Repository: devitools/arandu

Length of output: 3886


🏁 Script executed:

# Now let's check lib.rs to see how setup() is called and if it handles the result
find . -name "lib.rs" -path "*/src-tauri/src/lib.rs" -type f

Repository: devitools/arandu

Length of output: 93


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/lib.rs

Repository: devitools/arandu

Length of output: 23262


Hardcoded port 7474 conflicts with Neo4j and fails silently.

Port 7474 is Neo4j's default HTTP port. The TCP bind happens asynchronously inside a spawned task (lines 17–30), but setup() returns Ok(()) immediately (line 34) before the bind attempt completes. If another process holds the port, the error is only logged to stderr; the caller in lib.rs (lines 417–419) cannot distinguish success from failure since setup() always returns Ok(()).

Consider either:

  • Synchronously binding before spawning the listener loop, or
  • Using a oneshot channel to propagate bind errors back to the caller, or
  • Making the port configurable (via config file or environment variable) with a fallback port range.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 7 - 8, The current setup()
spawns the listener asynchronously and always returns Ok(()), and the hardcoded
DEFAULT_PORT = 7474 can silently conflict with Neo4j; change setup() to
synchronously bind with TcpListener::bind((DEFAULT_HOST, port)) (propagating any
bind Err back to the caller) before spawning the listener loop, and make the
port configurable (read from an env var or config with DEFAULT_PORT as fallback)
so callers see bind failures immediately and the port can be changed without
code edits.

Comment on lines +12 to +35
pub fn setup(app: &tauri::App) -> Result<(), String> {
let addr = format!("{}:{}", DEFAULT_HOST, DEFAULT_PORT);

let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
match TcpListener::bind(&addr).await {
Ok(listener) => {
eprintln!("[TCP IPC] Listening on {}", addr);

let state = app_handle.state::<TcpSocketState>();
if let Ok(mut guard) = state.0.lock() {
*guard = Some(addr.clone());
}

tcp_listener_loop(listener, app_handle).await;
}
Err(e) => {
eprintln!("[TCP IPC] Failed to bind to {}: {}", addr, e);
}
}
});

Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

setup() always returns Ok(()) regardless of bind outcome.

Because TcpListener::bind happens inside the spawned async task, setup() returns Ok(()) before the bind attempt completes. The caller in lib.rs (line 417) checks for Err but will never see one from a bind failure. This makes the error handling at the call site dead code for the most critical failure mode.

Compare with the Unix socket ipc::setup() which also has this pattern — but at minimum, consider a oneshot channel to relay the bind result, or bind synchronously before spawning the accept loop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 12 - 35, The setup()
function currently spawns an async task and returns Ok(()) before
TcpListener::bind runs, so binding failures are never propagated; fix by
performing the bind synchronously (await the bind) or by using a oneshot channel
to relay the bind result back to the caller before returning. Concretely, call
TcpListener::bind(&addr).await (or bind on the sync thread) and handle Err by
returning Err(String) from setup(), or if you must spawn first, create a
tokio::sync::oneshot sender/receiver, have the spawned task send Ok(()) or
Err(e.to_string()) after attempting TcpListener::bind, and have setup()
block/await on the receiver and return the corresponding Result; update
references in this flow around setup, TcpListener::bind, tcp_listener_loop, and
the app_handle state assignment so errors are returned to the caller instead of
always returning Ok(()).

Comment on lines +37 to +43
pub fn cleanup(state: tauri::State<TcpSocketState>) {
if let Ok(mut guard) = state.0.lock() {
if let Some(addr) = guard.take() {
eprintln!("[TCP IPC] Shutting down listener on {}", addr);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cleanup() does not actually stop the TCP listener.

cleanup() only clears the stored address string from state. The TcpListener and its accept loop (and any active client connections) continue running because the listener is owned by the spawned task, not by TcpSocketState. Unlike the Unix socket ipc::cleanup() which removes the socket file (preventing new connections), this cleanup is purely cosmetic.

To actually shut down, you'd need a cancellation mechanism (e.g., a tokio::sync::watch or CancellationToken) that the accept loop checks, or store a JoinHandle to abort the task.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 37 - 43, cleanup() only
removes the stored address string and does not stop the spawned accept loop or
active connections; update TcpSocketState to hold a cancellation mechanism
(e.g., a tokio::sync::watch, tokio_util::sync::CancellationToken, or the
JoinHandle of the spawned task) and modify the accept loop to listen for that
cancellation token or be abortable, then in cleanup() signal cancellation or
abort the JoinHandle and await/cleanup resources before dropping the address;
reference TcpSocketState, cleanup(), the spawned accept loop (the task that owns
the TcpListener) and whichever chosen token/handle so you can set and trigger it
from cleanup().

Comment on lines +64 to +94
async fn handle_client(stream: TcpStream, app: tauri::AppHandle) -> Result<(), String> {
let peer_addr = stream.peer_addr().map_err(|e| e.to_string())?;
let (reader, mut writer) = stream.into_split();
let reader = BufReader::new(reader);
let mut lines = reader.lines();

while let Some(line) = lines.next_line().await.map_err(|e| e.to_string())? {
eprintln!("[TCP IPC] Received from {}: {}", peer_addr, line);

let response = match serde_json::from_str::<IpcCommand>(&line) {
Ok(cmd) => {
eprintln!("[TCP IPC] Processing command: {}", cmd.command);
process_command(cmd, &app)
}
Err(e) => IpcResponse {
success: false,
error: Some(format!("Invalid JSON: {}", e)),
},
};

let json = serde_json::to_string(&response).unwrap_or_default();
eprintln!("[TCP IPC] Sending response to {}: {}", peer_addr, json);

writer
.write_all(format!("{}\n", json).as_bytes())
.await
.map_err(|e| e.to_string())?;
}

eprintln!("[TCP IPC] Connection closed: {}", peer_addr);
Ok(())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential log injection via unsanitized TCP input.

Line 71 logs the raw line received from the TCP socket, and line 75 logs cmd.command which is attacker-controlled input from any local process. While eprintln! is less exploitable than structured logging frameworks, newlines within the JSON string could forge log entries. Consider sanitizing or escaping logged values, or at minimum truncating them.

Also, Line 84: serde_json::to_string(&response).unwrap_or_default() will send an empty line "\n" to the client if serialization fails. Since IpcResponse is a simple struct this is unlikely, but sending an empty line could confuse a client expecting valid JSON. Consider sending a hardcoded error JSON instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 64 - 94, The handle_client
function currently logs raw, attacker-controlled TCP input and command strings
(the `line` contents and `cmd.command`) which can contain newlines and inject
fake log entries; sanitize or escape/newline-normalize and/or truncate these
values before logging (e.g., replace newlines with escaped sequences and limit
length) when using the eprintln! calls and when logging inside the match branch
that processes `process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.

The app icon had a solid background that extended to the edges, preventing
macOS from properly applying its automatic corner rounding. This resulted in
a square icon appearance instead of the expected rounded corners.
Changes:
- Applied ~12% corner radius to all icon assets (matching macOS guidelines)
- Regenerated icon.icns with proper alpha channel masks
- Updated all PNG sizes (32x32, 64x64, 128x128, 256x256, 512x512, 1024x1024)
The icon now displays with proper rounded corners in Dock, Finder, and app switcher.
@wilcorrea
wilcorrea merged commit 31f187b into mainFeb 23, 2026
1 check passed
@wilcorrea
wilcorrea deleted the fix/relative-paths branch February 23, 2026 20:42
Sign up for freeto 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.

Relative paths lead to a blank screen

1 participant

@wilcorrea
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(tauri): resolve relative paths and prepare for IPC merge - #12

Merged
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths
Feb 23, 2026
Merged

fix(tauri): resolve relative paths and prepare for IPC merge#12
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths

Conversation

@wilcorrea

@wilcorreawilcorrea commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#11 - Relative paths lead to a blank screen

  • Canonicalize file paths in read_file command to correctly resolve relative paths before reading files
  • Fix macOS Opened event to canonicalize paths when opening files via file associations (critical bug fix)
  • Add comprehensive debug logging at all file loading entry points (CLI args, single instance, Opened event, read_file)
  • Display full path in toolbar instead of just filename for better debugging visibility
  • Restructure builder for merge compatibility with feat/unix-socket-ipc branch to prevent future conflicts
  • Add merge guide documentation with step-by-step instructions for integrating IPC changes

Technical Details

The root cause was that relative paths (e.g., ./README.md) were not being canonicalized at all entry points:

  • ✅ CLI args: Already canonicalized
  • ✅ Single instance: Already canonicalized
  • Opened event (macOS): Missing canonicalization → FIXED
  • read_file command: No path resolution → FIXED

When a relative path reached read_file, it would try to read relative to the process working directory (not the file's directory), causing file not found errors and blank screens.

Merge Compatibility

This PR restructures the builder to match feat/unix-socket-ipc branch structure:

  • Separates builder initialization from setup
  • Adds placeholder comments showing exactly where to add IPC socket code
  • Includes MERGE_GUIDE.md with complete merge instructions

This ensures clean merges between branches with zero conflicts.

Test plan

  • Build and install: cd apps/tauri && make build-dev && make install
  • Test with relative path: cd ~/Documents && arandu ./README.md
  • Verify file loads correctly (no blank screen)
  • Test with absolute path: arandu /full/path/to/file.md
  • Check toolbar shows full path (not just filename)
  • Open Console.app and filter by "arandu" to see debug logs showing path resolution
  • Test file associations (double-click .md file) works correctly
  • Verify compilation: cargo check --manifest-path src-tauri/Cargo.toml

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a TCP-based IPC endpoint to allow external tooling to send open-file and command requests.
    • Toolbar now displays the full file path with hover tooltip.
  • Improvements

    • Expanded debug logging for file loading, CLI/opened-file and URL handling for better troubleshooting.
    • File path resolution now prefers canonical paths and falls back reliably; socket state is cleaned up on exit.

- Canonicalize paths in read_file command to handle relative paths
- Canonicalize paths in macOS Opened event (fixes#11)
- Add debug logging at all file loading entry points
- Restructure builder to allow conditional IPC state management
- Add placeholders for feat/unix-socket-ipc merge compatibility
- Display full path in toolbar for better debugging
@coderabbitai

coderabbitaiBot commented Feb 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • apps/tauri/src-tauri/icons/icon.icns
⛔ Files ignored due to path filters (5)
  • apps/tauri/src-tauri/icons/128x128.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/128x128@2x.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/32x32.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/64x64.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/icon.png is excluded by !**/*.png

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a TCP IPC server and state to the Tauri backend; makes IPC structs/functions public; adds path canonicalization and extensive [DEBUG] logging for CLI / opened-file flows; updates frontend to show full file paths and add debug logs.

Changes

Cohort / File(s)Summary
TCP IPC server & state
apps/tauri/src-tauri/src/tcp_ipc.rs
New TCP IPC implementation: binds listener, accepts connections, parses newline-delimited JSON IpcCommand, calls process_command, responds with IpcResponse; exposes TcpSocketState, setup, and cleanup.
Backend integration & path handling
apps/tauri/src-tauri/src/lib.rs
Integrated TCP IPC setup/cleanup and TcpSocketState into app init/teardown; added path canonicalization in read_file and all open-file / CLI / macOS opened-url handling; added many [DEBUG] logs and fallback behavior when canonicalization fails.
IPC types & API visibility
apps/tauri/src-tauri/src/ipc.rs
Made IpcCommand and IpcResponse public with exported fields; made process_command public for reuse by tcp_ipc.
Frontend: toolbar & logging
apps/tauri/src/main.js
Show full file path in toolbar (with hover title) instead of only last segment; added debug logs around loadFile and open-file event handling.

Sequence Diagram(s)

sequenceDiagram
participant Client as TCP Client
participant TCP as Tcp Listener (tcp_ipc)
participant App as Tauri App / process_command
participant FS as Filesystem
participant UI as Frontend (webview)
Client->>TCP: connect, send JSON line (IpcCommand)
TCP->>App: parse JSON -> call process_command(cmd)
App->>FS: read_file / resolve path (canonicalize)
alt command produces response
FS-->>App: file contents / error
App-->>TCP: IpcResponse (JSON)
App->>UI: emit "open-file" with resolved path (if applicable)
UI-->>UI: loadFile(resolved path), update toolbar
else error
App-->>TCP: IpcResponse (error)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nudged a socket, sniffed a path so neat,
I chased the canonical footprints down the street,
I told the app and sang with glee,
"Full paths now shine for you and me!"
— a rabbit, debugging with tiny feet 🌿✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main changes: fixing relative path resolution and preparing code structure for IPC integration.
Linked Issues check✅ PassedThe PR successfully addresses issue #11 by canonicalizing file paths at all entry points (CLI args, single-instance handler, macOS Opened event, and read_file command) to resolve relative paths correctly.
Out of Scope Changes check✅ PassedAll changes directly support the PR objectives: path canonicalization fixes, debug logging, toolbar display improvements, and TCP IPC setup for future merge compatibility.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/relative-paths

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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

366-387: Consider adding fallback behavior for consistency.

The single-instance handler silently drops the file-open request when canonicalize fails (lines 383-385 only log, no open-file event emitted). In contrast:

  • CLI args handler (line 462) uses unwrap_or_else to fall back to the original path
  • macOS Opened event (line 535) uses unwrap_or to fall back

This inconsistency could cause confusing behavior where files open in some scenarios but not others.

♻️ Suggested fix to add fallback
 if !file_path.is_empty() && !file_path.starts_with('-') {
- if let Ok(abs_path) = std::fs::canonicalize(file_path) {- let path_str = abs_path.to_string_lossy().to_string();- eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);- let _ = app.emit("open-file", &path_str);- } else {- eprintln!("[DEBUG] Failed to canonicalize path: {:?}", file_path);- }+ let abs_path = std::fs::canonicalize(file_path).unwrap_or_else(|e| {+ eprintln!("[DEBUG] Canonicalize failed ({}), using as-is", e);+ PathBuf::from(file_path)+ });+ let path_str = abs_path.to_string_lossy().to_string();+ eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);+ let _ = app.emit("open-file", &path_str);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 366 - 387, The single-instance
handler in the tauri_plugin_single_instance::init closure drops the open-file
event when std::fs::canonicalize(file_path) fails; change the logic so that on
canonicalize failure you fall back to the original file_path (or its
to_string_lossy) and still call app.emit("open-file", &path_str) so behavior
matches the CLI and macOS handlers; update the block handling args[1] to compute
a path_str variable from canonicalize if Ok or else from the original file_path,
log the fallback, and always emit "open-file" with that path_str.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 366-387: The single-instance handler in the
tauri_plugin_single_instance::init closure drops the open-file event when
std::fs::canonicalize(file_path) fails; change the logic so that on canonicalize
failure you fall back to the original file_path (or its to_string_lossy) and
still call app.emit("open-file", &path_str) so behavior matches the CLI and
macOS handlers; update the block handling args[1] to compute a path_str variable
from canonicalize if Ok or else from the original file_path, log the fallback,
and always emit "open-file" with that path_str.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51408db and a075094.

📒 Files selected for processing (2)
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src/main.js

@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed the CodeRabbit nitpick on the single-instance handler canonicalize fallback.

lib.rs lines 378-385: Changed from if let Ok(abs_path) = ... (which silently dropped the open-file event on failure) to unwrap_or_else with fallback to the original path. This now matches the behavior of both the CLI args handler (line 462) and the macOS Opened event handler (line 535).

Fixed in 53dbe11.

@coderabbitaicoderabbitaiBot 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: 1

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

66-66: Consider structured logging for production.

The [DEBUG] prefix in eprintln! calls works for development, but for production builds you might want to use the log or tracing crate with configurable log levels. This would allow filtering debug output without code changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 66, The debug print in read_file
currently uses eprintln!("[DEBUG] ...", path); switch this to a structured
logging call (e.g. log::debug! or tracing::debug!) so debug output can be
filtered at runtime, and ensure the application initializes the chosen
logger/subscriber (e.g. env_logger::init() or tracing_subscriber) in
main/startup; replace the eprintln! invocation inside read_file with the
appropriate debug macro and remove the hardcoded "[DEBUG]" prefix so the logging
framework controls level and formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 533-537: The opened-event handler uses
std::fs::canonicalize(&path).unwrap_or(path) which silently falls back; change
it to use unwrap_or_else so you can log the canonicalize error (matching
single-instance and CLI handlers). In the opened event block, replace
unwrap_or(path) with unwrap_or_else(|err| { eprintln!("[DEBUG] canonicalize
failed: {:?}", err); path.clone() }) (ensure abs_path and path_str keep the same
names) so failures are logged consistently.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Line 66: The debug print in read_file currently uses eprintln!("[DEBUG] ...",
path); switch this to a structured logging call (e.g. log::debug! or
tracing::debug!) so debug output can be filtered at runtime, and ensure the
application initializes the chosen logger/subscriber (e.g. env_logger::init() or
tracing_subscriber) in main/startup; replace the eprintln! invocation inside
read_file with the appropriate debug macro and remove the hardcoded "[DEBUG]"
prefix so the logging framework controls level and formatting.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a075094 and 53dbe11.

📒 Files selected for processing (1)
  • apps/tauri/src-tauri/src/lib.rs

Comment threadapps/tauri/src-tauri/src/lib.rs
…onicalize
Align the macOS Opened event handler with the single-instance and CLI
handlers by replacing silent unwrap_or fallback with unwrap_or_else
that logs the canonicalize failure before falling back.
@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed all remaining CodeRabbit review comments from both review rounds:

1. Inconsistent canonicalization fallback (Opened event, line 535) -- Fixed in 6fb6ca2. Replaced unwrap_or(path) with unwrap_or_else that logs the error, matching the single-instance and CLI handlers.

2. Single-instance handler dropping file-open on canonicalize failure (lines 366-387) -- Already addressed in 53dbe11 (previous commit).

3. Structured logging suggestion (line 66) -- Acknowledged as valid but deferred. Switching from eprintln! to log/tracing is a broader change that affects the entire codebase and is better handled in a dedicated PR. All debug prints in this PR are consistent with the existing eprintln!("[DEBUG] ...") pattern used throughout the project.

All three canonicalization entry points now use the same unwrap_or_else + debug logging pattern for consistent diagnostics.

- Add tcp_ipc.rs module with TCP listener on 127.0.0.1:7474
- Refactor ipc.rs to make IpcCommand/Response and process_command public
- TCP socket works in parallel with Unix socket for compatibility
- Supports same JSON protocol: open, ping, show commands
- Fixes Unix socket limitation with Docker/Podman containers
- Both sockets share same command processing logic
Resolves container accessibility issue where Unix sockets from macOS
are not accessible within Linux containers due to different kernel stacks.

@coderabbitaicoderabbitaiBot 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: 6

🧹 Nitpick comments (2)
apps/tauri/src-tauri/src/tcp_ipc.rs (1)

45-62: No connection limits or rate-limiting on the accept loop.

Any local process can open unlimited connections to this TCP server. Without a maximum connection count or per-connection timeout, a misbehaving local process could exhaust file descriptors or memory by opening many connections and sending data slowly.

Consider adding:

  • A maximum concurrent connection semaphore (tokio::sync::Semaphore)
  • A read timeout per connection (tokio::time::timeout)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 45 - 62, The accept loop in
tcp_listener_loop currently allows unlimited concurrent connections; add a
tokio::sync::Semaphore to limit max concurrent clients (acquire a permit before
spawning handle_client and release when that client's task finishes) and wrap
per-connection read/write operations inside tokio::time::timeout in
handle_client to enforce a per-connection inactivity/read timeout; ensure
tcp_listener_loop attempts to accept new connections even when semaphore is
exhausted (e.g., reject/close the stream immediately with a log) and
propagate/handle timeout errors from handle_client so they are logged.
apps/tauri/src-tauri/src/lib.rs (1)

529-533: TCP cleanup only clears state, doesn't stop the listener (see tcp_ipc.rs comment).

As noted in the tcp_ipc.rs review, cleanup() only nulls out the stored address. The accept loop and active connections continue until the process exits. For the ExitRequested + ExplicitQuit path this is likely fine since the process terminates shortly after, but it's worth noting the asymmetry with the Unix socket cleanup which actually removes the socket file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 529 - 533, The current call to
tcp_ipc::cleanup(tcp_socket_state) only clears the stored address but does not
stop the accept loop or close active connections; update the shutdown so the TCP
listener and any active connections are closed and the accept loop signalled to
exit. Either call a dedicated function (e.g., tcp_ipc::stop_listener or
tcp_ipc::shutdown_listener) from this location, or extend tcp_ipc::cleanup to
close the listener socket stored in tcp_ipc::TcpSocketState, close active
connections and signal/join the accept loop so it terminates cleanly rather than
leaving it running until process exit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 416-419: The tcp_ipc::setup() function currently masks bind
failures by always returning Ok(()), so the error branch here
(tcp_ipc::setup(app)) is effectively unreachable; update tcp_ipc::setup in
tcp_ipc.rs to propagate and return an Err when socket bind/listen fails (return
a Result with the real io::Error or a boxed error), and keep this call site
as-is so the Err path (eprintln!("Failed to setup TCP IPC: {}", e)) is reachable
and logs the actual failure; ensure the tcp_ipc::setup signature and any callers
are updated to handle the Result change.
- Line 15: The tcp_ipc module is currently compiled unconditionally, exposing an
unauthenticated TCP listener (127.0.0.1:7474); gate this to be opt-in and add
simple auth: protect the module with a cfg feature (e.g., #[cfg(feature =
"tcp-ipc")] on the mod tcp_ipc declaration and corresponding Cargo.toml feature)
and/or add token-based auth inside the tcp_ipc listener (generate a random token
stored in a file with 0o600 perms and require clients to present it when
connecting, validating in the tcp listener accept/handle path); update any
public APIs that assume tcp_ipc exists to compile without it (use
#[cfg(any(...))] or conditional stubs) and document the feature and token file
location so consumers can opt in securely.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 12-35: The setup() function currently spawns an async task and
returns Ok(()) before TcpListener::bind runs, so binding failures are never
propagated; fix by performing the bind synchronously (await the bind) or by
using a oneshot channel to relay the bind result back to the caller before
returning. Concretely, call TcpListener::bind(&addr).await (or bind on the sync
thread) and handle Err by returning Err(String) from setup(), or if you must
spawn first, create a tokio::sync::oneshot sender/receiver, have the spawned
task send Ok(()) or Err(e.to_string()) after attempting TcpListener::bind, and
have setup() block/await on the receiver and return the corresponding Result;
update references in this flow around setup, TcpListener::bind,
tcp_listener_loop, and the app_handle state assignment so errors are returned to
the caller instead of always returning Ok(()).
- Around line 37-43: cleanup() only removes the stored address string and does
not stop the spawned accept loop or active connections; update TcpSocketState to
hold a cancellation mechanism (e.g., a tokio::sync::watch,
tokio_util::sync::CancellationToken, or the JoinHandle of the spawned task) and
modify the accept loop to listen for that cancellation token or be abortable,
then in cleanup() signal cancellation or abort the JoinHandle and await/cleanup
resources before dropping the address; reference TcpSocketState, cleanup(), the
spawned accept loop (the task that owns the TcpListener) and whichever chosen
token/handle so you can set and trigger it from cleanup().
- Around line 64-94: The handle_client function currently logs raw,
attacker-controlled TCP input and command strings (the `line` contents and
`cmd.command`) which can contain newlines and inject fake log entries; sanitize
or escape/newline-normalize and/or truncate these values before logging (e.g.,
replace newlines with escaped sequences and limit length) when using the
eprintln! calls and when logging inside the match branch that processes
`process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.
- Around line 7-8: The current setup() spawns the listener asynchronously and
always returns Ok(()), and the hardcoded DEFAULT_PORT = 7474 can silently
conflict with Neo4j; change setup() to synchronously bind with
TcpListener::bind((DEFAULT_HOST, port)) (propagating any bind Err back to the
caller) before spawning the listener loop, and make the port configurable (read
from an env var or config with DEFAULT_PORT as fallback) so callers see bind
failures immediately and the port can be changed without code edits.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 529-533: The current call to tcp_ipc::cleanup(tcp_socket_state)
only clears the stored address but does not stop the accept loop or close active
connections; update the shutdown so the TCP listener and any active connections
are closed and the accept loop signalled to exit. Either call a dedicated
function (e.g., tcp_ipc::stop_listener or tcp_ipc::shutdown_listener) from this
location, or extend tcp_ipc::cleanup to close the listener socket stored in
tcp_ipc::TcpSocketState, close active connections and signal/join the accept
loop so it terminates cleanly rather than leaving it running until process exit.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 45-62: The accept loop in tcp_listener_loop currently allows
unlimited concurrent connections; add a tokio::sync::Semaphore to limit max
concurrent clients (acquire a permit before spawning handle_client and release
when that client's task finishes) and wrap per-connection read/write operations
inside tokio::time::timeout in handle_client to enforce a per-connection
inactivity/read timeout; ensure tcp_listener_loop attempts to accept new
connections even when semaphore is exhausted (e.g., reject/close the stream
immediately with a log) and propagate/handle timeout errors from handle_client
so they are logged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53dbe11 and 29db134.

📒 Files selected for processing (3)
  • apps/tauri/src-tauri/src/ipc.rs
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src-tauri/src/tcp_ipc.rs

mod cli_installer;
#[cfg(unix)]
mod ipc;
mod tcp_ipc;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TCP IPC module is unconditionally compiled — security implications of an unauthenticated local TCP listener.

Unlike the Unix socket (gated behind #[cfg(unix)] with 0o600 permissions), the TCP listener on 127.0.0.1:7474 has no authentication or authorization. Any local process (including those in containers, sandboxes, or running as different users) can connect and issue commands like open (to open arbitrary files the app user can read) or show (to manipulate window focus).

On multi-user systems or shared hosts this is a local attack surface. Consider adding at minimum:

  • A shared secret / token-based auth (e.g., random token written to a file with restrictive permissions, client must present it)
  • Or restricting TCP IPC behind a feature flag / cfg gate so it's opt-in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 15, The tcp_ipc module is currently
compiled unconditionally, exposing an unauthenticated TCP listener
(127.0.0.1:7474); gate this to be opt-in and add simple auth: protect the module
with a cfg feature (e.g., #[cfg(feature = "tcp-ipc")] on the mod tcp_ipc
declaration and corresponding Cargo.toml feature) and/or add token-based auth
inside the tcp_ipc listener (generate a random token stored in a file with 0o600
perms and require clients to present it when connecting, validating in the tcp
listener accept/handle path); update any public APIs that assume tcp_ipc exists
to compile without it (use #[cfg(any(...))] or conditional stubs) and document
the feature and token file location so consumers can opt in securely.

Comment on lines +416 to +419
// TCP IPC setup (works on all platforms, including from containers)
if let Err(e) = tcp_ipc::setup(app) {
eprintln!("Failed to setup TCP IPC: {}", e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TCP IPC setup error is logged but doesn't prevent app startup — acceptable.

This mirrors the Unix socket setup pattern. Since the TCP IPC is supplementary, failing gracefully is reasonable. However, note that as discussed in tcp_ipc.rs, setup() currently always returns Ok(()) even on bind failure, making this error branch unreachable for the most important failure case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 416 - 419, The tcp_ipc::setup()
function currently masks bind failures by always returning Ok(()), so the error
branch here (tcp_ipc::setup(app)) is effectively unreachable; update
tcp_ipc::setup in tcp_ipc.rs to propagate and return an Err when socket
bind/listen fails (return a Result with the real io::Error or a boxed error),
and keep this call site as-is so the Err path (eprintln!("Failed to setup TCP
IPC: {}", e)) is reachable and logs the actual failure; ensure the
tcp_ipc::setup signature and any callers are updated to handle the Result
change.

Comment on lines +7 to +8
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 7474;

Copy link
Copy Markdown

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

🌐 Web query:

Neo4j default port database

💡 Result:

Neo4j’s default ports depend on how you connect:

  • Bolt (drivers / neo4j:// / bolt://): 7687 [1][3]
  • HTTP (Neo4j Browser / HTTP API): 7474 [1][3]
  • HTTPS: 7473 [1][3]

(Other common defaults include Backup: 6362.) [1]


🏁 Script executed:

# First, let's find and examine the tcp_ipc.rs file
find . -name "tcp_ipc.rs" -type f | head -5

Repository: devitools/arandu

Length of output: 97


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/tcp_ipc.rs

Repository: devitools/arandu

Length of output: 3886


🏁 Script executed:

# Now let's check lib.rs to see how setup() is called and if it handles the result
find . -name "lib.rs" -path "*/src-tauri/src/lib.rs" -type f

Repository: devitools/arandu

Length of output: 93


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/lib.rs

Repository: devitools/arandu

Length of output: 23262


Hardcoded port 7474 conflicts with Neo4j and fails silently.

Port 7474 is Neo4j's default HTTP port. The TCP bind happens asynchronously inside a spawned task (lines 17–30), but setup() returns Ok(()) immediately (line 34) before the bind attempt completes. If another process holds the port, the error is only logged to stderr; the caller in lib.rs (lines 417–419) cannot distinguish success from failure since setup() always returns Ok(()).

Consider either:

  • Synchronously binding before spawning the listener loop, or
  • Using a oneshot channel to propagate bind errors back to the caller, or
  • Making the port configurable (via config file or environment variable) with a fallback port range.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 7 - 8, The current setup()
spawns the listener asynchronously and always returns Ok(()), and the hardcoded
DEFAULT_PORT = 7474 can silently conflict with Neo4j; change setup() to
synchronously bind with TcpListener::bind((DEFAULT_HOST, port)) (propagating any
bind Err back to the caller) before spawning the listener loop, and make the
port configurable (read from an env var or config with DEFAULT_PORT as fallback)
so callers see bind failures immediately and the port can be changed without
code edits.

Comment on lines +12 to +35
pub fn setup(app: &tauri::App) -> Result<(), String> {
let addr = format!("{}:{}", DEFAULT_HOST, DEFAULT_PORT);

let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
match TcpListener::bind(&addr).await {
Ok(listener) => {
eprintln!("[TCP IPC] Listening on {}", addr);

let state = app_handle.state::<TcpSocketState>();
if let Ok(mut guard) = state.0.lock() {
*guard = Some(addr.clone());
}

tcp_listener_loop(listener, app_handle).await;
}
Err(e) => {
eprintln!("[TCP IPC] Failed to bind to {}: {}", addr, e);
}
}
});

Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

setup() always returns Ok(()) regardless of bind outcome.

Because TcpListener::bind happens inside the spawned async task, setup() returns Ok(()) before the bind attempt completes. The caller in lib.rs (line 417) checks for Err but will never see one from a bind failure. This makes the error handling at the call site dead code for the most critical failure mode.

Compare with the Unix socket ipc::setup() which also has this pattern — but at minimum, consider a oneshot channel to relay the bind result, or bind synchronously before spawning the accept loop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 12 - 35, The setup()
function currently spawns an async task and returns Ok(()) before
TcpListener::bind runs, so binding failures are never propagated; fix by
performing the bind synchronously (await the bind) or by using a oneshot channel
to relay the bind result back to the caller before returning. Concretely, call
TcpListener::bind(&addr).await (or bind on the sync thread) and handle Err by
returning Err(String) from setup(), or if you must spawn first, create a
tokio::sync::oneshot sender/receiver, have the spawned task send Ok(()) or
Err(e.to_string()) after attempting TcpListener::bind, and have setup()
block/await on the receiver and return the corresponding Result; update
references in this flow around setup, TcpListener::bind, tcp_listener_loop, and
the app_handle state assignment so errors are returned to the caller instead of
always returning Ok(()).

Comment on lines +37 to +43
pub fn cleanup(state: tauri::State<TcpSocketState>) {
if let Ok(mut guard) = state.0.lock() {
if let Some(addr) = guard.take() {
eprintln!("[TCP IPC] Shutting down listener on {}", addr);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cleanup() does not actually stop the TCP listener.

cleanup() only clears the stored address string from state. The TcpListener and its accept loop (and any active client connections) continue running because the listener is owned by the spawned task, not by TcpSocketState. Unlike the Unix socket ipc::cleanup() which removes the socket file (preventing new connections), this cleanup is purely cosmetic.

To actually shut down, you'd need a cancellation mechanism (e.g., a tokio::sync::watch or CancellationToken) that the accept loop checks, or store a JoinHandle to abort the task.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 37 - 43, cleanup() only
removes the stored address string and does not stop the spawned accept loop or
active connections; update TcpSocketState to hold a cancellation mechanism
(e.g., a tokio::sync::watch, tokio_util::sync::CancellationToken, or the
JoinHandle of the spawned task) and modify the accept loop to listen for that
cancellation token or be abortable, then in cleanup() signal cancellation or
abort the JoinHandle and await/cleanup resources before dropping the address;
reference TcpSocketState, cleanup(), the spawned accept loop (the task that owns
the TcpListener) and whichever chosen token/handle so you can set and trigger it
from cleanup().

Comment on lines +64 to +94
async fn handle_client(stream: TcpStream, app: tauri::AppHandle) -> Result<(), String> {
let peer_addr = stream.peer_addr().map_err(|e| e.to_string())?;
let (reader, mut writer) = stream.into_split();
let reader = BufReader::new(reader);
let mut lines = reader.lines();

while let Some(line) = lines.next_line().await.map_err(|e| e.to_string())? {
eprintln!("[TCP IPC] Received from {}: {}", peer_addr, line);

let response = match serde_json::from_str::<IpcCommand>(&line) {
Ok(cmd) => {
eprintln!("[TCP IPC] Processing command: {}", cmd.command);
process_command(cmd, &app)
}
Err(e) => IpcResponse {
success: false,
error: Some(format!("Invalid JSON: {}", e)),
},
};

let json = serde_json::to_string(&response).unwrap_or_default();
eprintln!("[TCP IPC] Sending response to {}: {}", peer_addr, json);

writer
.write_all(format!("{}\n", json).as_bytes())
.await
.map_err(|e| e.to_string())?;
}

eprintln!("[TCP IPC] Connection closed: {}", peer_addr);
Ok(())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential log injection via unsanitized TCP input.

Line 71 logs the raw line received from the TCP socket, and line 75 logs cmd.command which is attacker-controlled input from any local process. While eprintln! is less exploitable than structured logging frameworks, newlines within the JSON string could forge log entries. Consider sanitizing or escaping logged values, or at minimum truncating them.

Also, Line 84: serde_json::to_string(&response).unwrap_or_default() will send an empty line "\n" to the client if serialization fails. Since IpcResponse is a simple struct this is unlikely, but sending an empty line could confuse a client expecting valid JSON. Consider sending a hardcoded error JSON instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 64 - 94, The handle_client
function currently logs raw, attacker-controlled TCP input and command strings
(the `line` contents and `cmd.command`) which can contain newlines and inject
fake log entries; sanitize or escape/newline-normalize and/or truncate these
values before logging (e.g., replace newlines with escaped sequences and limit
length) when using the eprintln! calls and when logging inside the match branch
that processes `process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.

The app icon had a solid background that extended to the edges, preventing
macOS from properly applying its automatic corner rounding. This resulted in
a square icon appearance instead of the expected rounded corners.
Changes:
- Applied ~12% corner radius to all icon assets (matching macOS guidelines)
- Regenerated icon.icns with proper alpha channel masks
- Updated all PNG sizes (32x32, 64x64, 128x128, 256x256, 512x512, 1024x1024)
The icon now displays with proper rounded corners in Dock, Finder, and app switcher.
@wilcorrea
wilcorrea merged commit 31f187b into mainFeb 23, 2026
1 check passed
@wilcorrea
wilcorrea deleted the fix/relative-paths branch February 23, 2026 20:42
Sign up for freeto 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.

Relative paths lead to a blank screen

1 participant

@wilcorrea
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(tauri): resolve relative paths and prepare for IPC merge - #12

Merged
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths
Feb 23, 2026
Merged

fix(tauri): resolve relative paths and prepare for IPC merge#12
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths

Conversation

@wilcorrea

@wilcorreawilcorrea commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#11 - Relative paths lead to a blank screen

  • Canonicalize file paths in read_file command to correctly resolve relative paths before reading files
  • Fix macOS Opened event to canonicalize paths when opening files via file associations (critical bug fix)
  • Add comprehensive debug logging at all file loading entry points (CLI args, single instance, Opened event, read_file)
  • Display full path in toolbar instead of just filename for better debugging visibility
  • Restructure builder for merge compatibility with feat/unix-socket-ipc branch to prevent future conflicts
  • Add merge guide documentation with step-by-step instructions for integrating IPC changes

Technical Details

The root cause was that relative paths (e.g., ./README.md) were not being canonicalized at all entry points:

  • ✅ CLI args: Already canonicalized
  • ✅ Single instance: Already canonicalized
  • Opened event (macOS): Missing canonicalization → FIXED
  • read_file command: No path resolution → FIXED

When a relative path reached read_file, it would try to read relative to the process working directory (not the file's directory), causing file not found errors and blank screens.

Merge Compatibility

This PR restructures the builder to match feat/unix-socket-ipc branch structure:

  • Separates builder initialization from setup
  • Adds placeholder comments showing exactly where to add IPC socket code
  • Includes MERGE_GUIDE.md with complete merge instructions

This ensures clean merges between branches with zero conflicts.

Test plan

  • Build and install: cd apps/tauri && make build-dev && make install
  • Test with relative path: cd ~/Documents && arandu ./README.md
  • Verify file loads correctly (no blank screen)
  • Test with absolute path: arandu /full/path/to/file.md
  • Check toolbar shows full path (not just filename)
  • Open Console.app and filter by "arandu" to see debug logs showing path resolution
  • Test file associations (double-click .md file) works correctly
  • Verify compilation: cargo check --manifest-path src-tauri/Cargo.toml

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a TCP-based IPC endpoint to allow external tooling to send open-file and command requests.
    • Toolbar now displays the full file path with hover tooltip.
  • Improvements

    • Expanded debug logging for file loading, CLI/opened-file and URL handling for better troubleshooting.
    • File path resolution now prefers canonical paths and falls back reliably; socket state is cleaned up on exit.

- Canonicalize paths in read_file command to handle relative paths
- Canonicalize paths in macOS Opened event (fixes#11)
- Add debug logging at all file loading entry points
- Restructure builder to allow conditional IPC state management
- Add placeholders for feat/unix-socket-ipc merge compatibility
- Display full path in toolbar for better debugging
@coderabbitai

coderabbitaiBot commented Feb 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • apps/tauri/src-tauri/icons/icon.icns
⛔ Files ignored due to path filters (5)
  • apps/tauri/src-tauri/icons/128x128.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/128x128@2x.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/32x32.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/64x64.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/icon.png is excluded by !**/*.png

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a TCP IPC server and state to the Tauri backend; makes IPC structs/functions public; adds path canonicalization and extensive [DEBUG] logging for CLI / opened-file flows; updates frontend to show full file paths and add debug logs.

Changes

Cohort / File(s)Summary
TCP IPC server & state
apps/tauri/src-tauri/src/tcp_ipc.rs
New TCP IPC implementation: binds listener, accepts connections, parses newline-delimited JSON IpcCommand, calls process_command, responds with IpcResponse; exposes TcpSocketState, setup, and cleanup.
Backend integration & path handling
apps/tauri/src-tauri/src/lib.rs
Integrated TCP IPC setup/cleanup and TcpSocketState into app init/teardown; added path canonicalization in read_file and all open-file / CLI / macOS opened-url handling; added many [DEBUG] logs and fallback behavior when canonicalization fails.
IPC types & API visibility
apps/tauri/src-tauri/src/ipc.rs
Made IpcCommand and IpcResponse public with exported fields; made process_command public for reuse by tcp_ipc.
Frontend: toolbar & logging
apps/tauri/src/main.js
Show full file path in toolbar (with hover title) instead of only last segment; added debug logs around loadFile and open-file event handling.

Sequence Diagram(s)

sequenceDiagram
participant Client as TCP Client
participant TCP as Tcp Listener (tcp_ipc)
participant App as Tauri App / process_command
participant FS as Filesystem
participant UI as Frontend (webview)
Client->>TCP: connect, send JSON line (IpcCommand)
TCP->>App: parse JSON -> call process_command(cmd)
App->>FS: read_file / resolve path (canonicalize)
alt command produces response
FS-->>App: file contents / error
App-->>TCP: IpcResponse (JSON)
App->>UI: emit "open-file" with resolved path (if applicable)
UI-->>UI: loadFile(resolved path), update toolbar
else error
App-->>TCP: IpcResponse (error)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nudged a socket, sniffed a path so neat,
I chased the canonical footprints down the street,
I told the app and sang with glee,
"Full paths now shine for you and me!"
— a rabbit, debugging with tiny feet 🌿✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main changes: fixing relative path resolution and preparing code structure for IPC integration.
Linked Issues check✅ PassedThe PR successfully addresses issue #11 by canonicalizing file paths at all entry points (CLI args, single-instance handler, macOS Opened event, and read_file command) to resolve relative paths correctly.
Out of Scope Changes check✅ PassedAll changes directly support the PR objectives: path canonicalization fixes, debug logging, toolbar display improvements, and TCP IPC setup for future merge compatibility.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/relative-paths

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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

366-387: Consider adding fallback behavior for consistency.

The single-instance handler silently drops the file-open request when canonicalize fails (lines 383-385 only log, no open-file event emitted). In contrast:

  • CLI args handler (line 462) uses unwrap_or_else to fall back to the original path
  • macOS Opened event (line 535) uses unwrap_or to fall back

This inconsistency could cause confusing behavior where files open in some scenarios but not others.

♻️ Suggested fix to add fallback
 if !file_path.is_empty() && !file_path.starts_with('-') {
- if let Ok(abs_path) = std::fs::canonicalize(file_path) {- let path_str = abs_path.to_string_lossy().to_string();- eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);- let _ = app.emit("open-file", &path_str);- } else {- eprintln!("[DEBUG] Failed to canonicalize path: {:?}", file_path);- }+ let abs_path = std::fs::canonicalize(file_path).unwrap_or_else(|e| {+ eprintln!("[DEBUG] Canonicalize failed ({}), using as-is", e);+ PathBuf::from(file_path)+ });+ let path_str = abs_path.to_string_lossy().to_string();+ eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);+ let _ = app.emit("open-file", &path_str);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 366 - 387, The single-instance
handler in the tauri_plugin_single_instance::init closure drops the open-file
event when std::fs::canonicalize(file_path) fails; change the logic so that on
canonicalize failure you fall back to the original file_path (or its
to_string_lossy) and still call app.emit("open-file", &path_str) so behavior
matches the CLI and macOS handlers; update the block handling args[1] to compute
a path_str variable from canonicalize if Ok or else from the original file_path,
log the fallback, and always emit "open-file" with that path_str.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 366-387: The single-instance handler in the
tauri_plugin_single_instance::init closure drops the open-file event when
std::fs::canonicalize(file_path) fails; change the logic so that on canonicalize
failure you fall back to the original file_path (or its to_string_lossy) and
still call app.emit("open-file", &path_str) so behavior matches the CLI and
macOS handlers; update the block handling args[1] to compute a path_str variable
from canonicalize if Ok or else from the original file_path, log the fallback,
and always emit "open-file" with that path_str.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51408db and a075094.

📒 Files selected for processing (2)
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src/main.js

@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed the CodeRabbit nitpick on the single-instance handler canonicalize fallback.

lib.rs lines 378-385: Changed from if let Ok(abs_path) = ... (which silently dropped the open-file event on failure) to unwrap_or_else with fallback to the original path. This now matches the behavior of both the CLI args handler (line 462) and the macOS Opened event handler (line 535).

Fixed in 53dbe11.

@coderabbitaicoderabbitaiBot 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: 1

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

66-66: Consider structured logging for production.

The [DEBUG] prefix in eprintln! calls works for development, but for production builds you might want to use the log or tracing crate with configurable log levels. This would allow filtering debug output without code changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 66, The debug print in read_file
currently uses eprintln!("[DEBUG] ...", path); switch this to a structured
logging call (e.g. log::debug! or tracing::debug!) so debug output can be
filtered at runtime, and ensure the application initializes the chosen
logger/subscriber (e.g. env_logger::init() or tracing_subscriber) in
main/startup; replace the eprintln! invocation inside read_file with the
appropriate debug macro and remove the hardcoded "[DEBUG]" prefix so the logging
framework controls level and formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 533-537: The opened-event handler uses
std::fs::canonicalize(&path).unwrap_or(path) which silently falls back; change
it to use unwrap_or_else so you can log the canonicalize error (matching
single-instance and CLI handlers). In the opened event block, replace
unwrap_or(path) with unwrap_or_else(|err| { eprintln!("[DEBUG] canonicalize
failed: {:?}", err); path.clone() }) (ensure abs_path and path_str keep the same
names) so failures are logged consistently.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Line 66: The debug print in read_file currently uses eprintln!("[DEBUG] ...",
path); switch this to a structured logging call (e.g. log::debug! or
tracing::debug!) so debug output can be filtered at runtime, and ensure the
application initializes the chosen logger/subscriber (e.g. env_logger::init() or
tracing_subscriber) in main/startup; replace the eprintln! invocation inside
read_file with the appropriate debug macro and remove the hardcoded "[DEBUG]"
prefix so the logging framework controls level and formatting.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a075094 and 53dbe11.

📒 Files selected for processing (1)
  • apps/tauri/src-tauri/src/lib.rs

Comment threadapps/tauri/src-tauri/src/lib.rs
…onicalize
Align the macOS Opened event handler with the single-instance and CLI
handlers by replacing silent unwrap_or fallback with unwrap_or_else
that logs the canonicalize failure before falling back.
@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed all remaining CodeRabbit review comments from both review rounds:

1. Inconsistent canonicalization fallback (Opened event, line 535) -- Fixed in 6fb6ca2. Replaced unwrap_or(path) with unwrap_or_else that logs the error, matching the single-instance and CLI handlers.

2. Single-instance handler dropping file-open on canonicalize failure (lines 366-387) -- Already addressed in 53dbe11 (previous commit).

3. Structured logging suggestion (line 66) -- Acknowledged as valid but deferred. Switching from eprintln! to log/tracing is a broader change that affects the entire codebase and is better handled in a dedicated PR. All debug prints in this PR are consistent with the existing eprintln!("[DEBUG] ...") pattern used throughout the project.

All three canonicalization entry points now use the same unwrap_or_else + debug logging pattern for consistent diagnostics.

- Add tcp_ipc.rs module with TCP listener on 127.0.0.1:7474
- Refactor ipc.rs to make IpcCommand/Response and process_command public
- TCP socket works in parallel with Unix socket for compatibility
- Supports same JSON protocol: open, ping, show commands
- Fixes Unix socket limitation with Docker/Podman containers
- Both sockets share same command processing logic
Resolves container accessibility issue where Unix sockets from macOS
are not accessible within Linux containers due to different kernel stacks.

@coderabbitaicoderabbitaiBot 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: 6

🧹 Nitpick comments (2)
apps/tauri/src-tauri/src/tcp_ipc.rs (1)

45-62: No connection limits or rate-limiting on the accept loop.

Any local process can open unlimited connections to this TCP server. Without a maximum connection count or per-connection timeout, a misbehaving local process could exhaust file descriptors or memory by opening many connections and sending data slowly.

Consider adding:

  • A maximum concurrent connection semaphore (tokio::sync::Semaphore)
  • A read timeout per connection (tokio::time::timeout)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 45 - 62, The accept loop in
tcp_listener_loop currently allows unlimited concurrent connections; add a
tokio::sync::Semaphore to limit max concurrent clients (acquire a permit before
spawning handle_client and release when that client's task finishes) and wrap
per-connection read/write operations inside tokio::time::timeout in
handle_client to enforce a per-connection inactivity/read timeout; ensure
tcp_listener_loop attempts to accept new connections even when semaphore is
exhausted (e.g., reject/close the stream immediately with a log) and
propagate/handle timeout errors from handle_client so they are logged.
apps/tauri/src-tauri/src/lib.rs (1)

529-533: TCP cleanup only clears state, doesn't stop the listener (see tcp_ipc.rs comment).

As noted in the tcp_ipc.rs review, cleanup() only nulls out the stored address. The accept loop and active connections continue until the process exits. For the ExitRequested + ExplicitQuit path this is likely fine since the process terminates shortly after, but it's worth noting the asymmetry with the Unix socket cleanup which actually removes the socket file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 529 - 533, The current call to
tcp_ipc::cleanup(tcp_socket_state) only clears the stored address but does not
stop the accept loop or close active connections; update the shutdown so the TCP
listener and any active connections are closed and the accept loop signalled to
exit. Either call a dedicated function (e.g., tcp_ipc::stop_listener or
tcp_ipc::shutdown_listener) from this location, or extend tcp_ipc::cleanup to
close the listener socket stored in tcp_ipc::TcpSocketState, close active
connections and signal/join the accept loop so it terminates cleanly rather than
leaving it running until process exit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 416-419: The tcp_ipc::setup() function currently masks bind
failures by always returning Ok(()), so the error branch here
(tcp_ipc::setup(app)) is effectively unreachable; update tcp_ipc::setup in
tcp_ipc.rs to propagate and return an Err when socket bind/listen fails (return
a Result with the real io::Error or a boxed error), and keep this call site
as-is so the Err path (eprintln!("Failed to setup TCP IPC: {}", e)) is reachable
and logs the actual failure; ensure the tcp_ipc::setup signature and any callers
are updated to handle the Result change.
- Line 15: The tcp_ipc module is currently compiled unconditionally, exposing an
unauthenticated TCP listener (127.0.0.1:7474); gate this to be opt-in and add
simple auth: protect the module with a cfg feature (e.g., #[cfg(feature =
"tcp-ipc")] on the mod tcp_ipc declaration and corresponding Cargo.toml feature)
and/or add token-based auth inside the tcp_ipc listener (generate a random token
stored in a file with 0o600 perms and require clients to present it when
connecting, validating in the tcp listener accept/handle path); update any
public APIs that assume tcp_ipc exists to compile without it (use
#[cfg(any(...))] or conditional stubs) and document the feature and token file
location so consumers can opt in securely.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 12-35: The setup() function currently spawns an async task and
returns Ok(()) before TcpListener::bind runs, so binding failures are never
propagated; fix by performing the bind synchronously (await the bind) or by
using a oneshot channel to relay the bind result back to the caller before
returning. Concretely, call TcpListener::bind(&addr).await (or bind on the sync
thread) and handle Err by returning Err(String) from setup(), or if you must
spawn first, create a tokio::sync::oneshot sender/receiver, have the spawned
task send Ok(()) or Err(e.to_string()) after attempting TcpListener::bind, and
have setup() block/await on the receiver and return the corresponding Result;
update references in this flow around setup, TcpListener::bind,
tcp_listener_loop, and the app_handle state assignment so errors are returned to
the caller instead of always returning Ok(()).
- Around line 37-43: cleanup() only removes the stored address string and does
not stop the spawned accept loop or active connections; update TcpSocketState to
hold a cancellation mechanism (e.g., a tokio::sync::watch,
tokio_util::sync::CancellationToken, or the JoinHandle of the spawned task) and
modify the accept loop to listen for that cancellation token or be abortable,
then in cleanup() signal cancellation or abort the JoinHandle and await/cleanup
resources before dropping the address; reference TcpSocketState, cleanup(), the
spawned accept loop (the task that owns the TcpListener) and whichever chosen
token/handle so you can set and trigger it from cleanup().
- Around line 64-94: The handle_client function currently logs raw,
attacker-controlled TCP input and command strings (the `line` contents and
`cmd.command`) which can contain newlines and inject fake log entries; sanitize
or escape/newline-normalize and/or truncate these values before logging (e.g.,
replace newlines with escaped sequences and limit length) when using the
eprintln! calls and when logging inside the match branch that processes
`process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.
- Around line 7-8: The current setup() spawns the listener asynchronously and
always returns Ok(()), and the hardcoded DEFAULT_PORT = 7474 can silently
conflict with Neo4j; change setup() to synchronously bind with
TcpListener::bind((DEFAULT_HOST, port)) (propagating any bind Err back to the
caller) before spawning the listener loop, and make the port configurable (read
from an env var or config with DEFAULT_PORT as fallback) so callers see bind
failures immediately and the port can be changed without code edits.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 529-533: The current call to tcp_ipc::cleanup(tcp_socket_state)
only clears the stored address but does not stop the accept loop or close active
connections; update the shutdown so the TCP listener and any active connections
are closed and the accept loop signalled to exit. Either call a dedicated
function (e.g., tcp_ipc::stop_listener or tcp_ipc::shutdown_listener) from this
location, or extend tcp_ipc::cleanup to close the listener socket stored in
tcp_ipc::TcpSocketState, close active connections and signal/join the accept
loop so it terminates cleanly rather than leaving it running until process exit.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 45-62: The accept loop in tcp_listener_loop currently allows
unlimited concurrent connections; add a tokio::sync::Semaphore to limit max
concurrent clients (acquire a permit before spawning handle_client and release
when that client's task finishes) and wrap per-connection read/write operations
inside tokio::time::timeout in handle_client to enforce a per-connection
inactivity/read timeout; ensure tcp_listener_loop attempts to accept new
connections even when semaphore is exhausted (e.g., reject/close the stream
immediately with a log) and propagate/handle timeout errors from handle_client
so they are logged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53dbe11 and 29db134.

📒 Files selected for processing (3)
  • apps/tauri/src-tauri/src/ipc.rs
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src-tauri/src/tcp_ipc.rs

mod cli_installer;
#[cfg(unix)]
mod ipc;
mod tcp_ipc;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TCP IPC module is unconditionally compiled — security implications of an unauthenticated local TCP listener.

Unlike the Unix socket (gated behind #[cfg(unix)] with 0o600 permissions), the TCP listener on 127.0.0.1:7474 has no authentication or authorization. Any local process (including those in containers, sandboxes, or running as different users) can connect and issue commands like open (to open arbitrary files the app user can read) or show (to manipulate window focus).

On multi-user systems or shared hosts this is a local attack surface. Consider adding at minimum:

  • A shared secret / token-based auth (e.g., random token written to a file with restrictive permissions, client must present it)
  • Or restricting TCP IPC behind a feature flag / cfg gate so it's opt-in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 15, The tcp_ipc module is currently
compiled unconditionally, exposing an unauthenticated TCP listener
(127.0.0.1:7474); gate this to be opt-in and add simple auth: protect the module
with a cfg feature (e.g., #[cfg(feature = "tcp-ipc")] on the mod tcp_ipc
declaration and corresponding Cargo.toml feature) and/or add token-based auth
inside the tcp_ipc listener (generate a random token stored in a file with 0o600
perms and require clients to present it when connecting, validating in the tcp
listener accept/handle path); update any public APIs that assume tcp_ipc exists
to compile without it (use #[cfg(any(...))] or conditional stubs) and document
the feature and token file location so consumers can opt in securely.

Comment on lines +416 to +419
// TCP IPC setup (works on all platforms, including from containers)
if let Err(e) = tcp_ipc::setup(app) {
eprintln!("Failed to setup TCP IPC: {}", e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TCP IPC setup error is logged but doesn't prevent app startup — acceptable.

This mirrors the Unix socket setup pattern. Since the TCP IPC is supplementary, failing gracefully is reasonable. However, note that as discussed in tcp_ipc.rs, setup() currently always returns Ok(()) even on bind failure, making this error branch unreachable for the most important failure case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 416 - 419, The tcp_ipc::setup()
function currently masks bind failures by always returning Ok(()), so the error
branch here (tcp_ipc::setup(app)) is effectively unreachable; update
tcp_ipc::setup in tcp_ipc.rs to propagate and return an Err when socket
bind/listen fails (return a Result with the real io::Error or a boxed error),
and keep this call site as-is so the Err path (eprintln!("Failed to setup TCP
IPC: {}", e)) is reachable and logs the actual failure; ensure the
tcp_ipc::setup signature and any callers are updated to handle the Result
change.

Comment on lines +7 to +8
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 7474;

Copy link
Copy Markdown

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

🌐 Web query:

Neo4j default port database

💡 Result:

Neo4j’s default ports depend on how you connect:

  • Bolt (drivers / neo4j:// / bolt://): 7687 [1][3]
  • HTTP (Neo4j Browser / HTTP API): 7474 [1][3]
  • HTTPS: 7473 [1][3]

(Other common defaults include Backup: 6362.) [1]


🏁 Script executed:

# First, let's find and examine the tcp_ipc.rs file
find . -name "tcp_ipc.rs" -type f | head -5

Repository: devitools/arandu

Length of output: 97


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/tcp_ipc.rs

Repository: devitools/arandu

Length of output: 3886


🏁 Script executed:

# Now let's check lib.rs to see how setup() is called and if it handles the result
find . -name "lib.rs" -path "*/src-tauri/src/lib.rs" -type f

Repository: devitools/arandu

Length of output: 93


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/lib.rs

Repository: devitools/arandu

Length of output: 23262


Hardcoded port 7474 conflicts with Neo4j and fails silently.

Port 7474 is Neo4j's default HTTP port. The TCP bind happens asynchronously inside a spawned task (lines 17–30), but setup() returns Ok(()) immediately (line 34) before the bind attempt completes. If another process holds the port, the error is only logged to stderr; the caller in lib.rs (lines 417–419) cannot distinguish success from failure since setup() always returns Ok(()).

Consider either:

  • Synchronously binding before spawning the listener loop, or
  • Using a oneshot channel to propagate bind errors back to the caller, or
  • Making the port configurable (via config file or environment variable) with a fallback port range.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 7 - 8, The current setup()
spawns the listener asynchronously and always returns Ok(()), and the hardcoded
DEFAULT_PORT = 7474 can silently conflict with Neo4j; change setup() to
synchronously bind with TcpListener::bind((DEFAULT_HOST, port)) (propagating any
bind Err back to the caller) before spawning the listener loop, and make the
port configurable (read from an env var or config with DEFAULT_PORT as fallback)
so callers see bind failures immediately and the port can be changed without
code edits.

Comment on lines +12 to +35
pub fn setup(app: &tauri::App) -> Result<(), String> {
let addr = format!("{}:{}", DEFAULT_HOST, DEFAULT_PORT);

let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
match TcpListener::bind(&addr).await {
Ok(listener) => {
eprintln!("[TCP IPC] Listening on {}", addr);

let state = app_handle.state::<TcpSocketState>();
if let Ok(mut guard) = state.0.lock() {
*guard = Some(addr.clone());
}

tcp_listener_loop(listener, app_handle).await;
}
Err(e) => {
eprintln!("[TCP IPC] Failed to bind to {}: {}", addr, e);
}
}
});

Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

setup() always returns Ok(()) regardless of bind outcome.

Because TcpListener::bind happens inside the spawned async task, setup() returns Ok(()) before the bind attempt completes. The caller in lib.rs (line 417) checks for Err but will never see one from a bind failure. This makes the error handling at the call site dead code for the most critical failure mode.

Compare with the Unix socket ipc::setup() which also has this pattern — but at minimum, consider a oneshot channel to relay the bind result, or bind synchronously before spawning the accept loop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 12 - 35, The setup()
function currently spawns an async task and returns Ok(()) before
TcpListener::bind runs, so binding failures are never propagated; fix by
performing the bind synchronously (await the bind) or by using a oneshot channel
to relay the bind result back to the caller before returning. Concretely, call
TcpListener::bind(&addr).await (or bind on the sync thread) and handle Err by
returning Err(String) from setup(), or if you must spawn first, create a
tokio::sync::oneshot sender/receiver, have the spawned task send Ok(()) or
Err(e.to_string()) after attempting TcpListener::bind, and have setup()
block/await on the receiver and return the corresponding Result; update
references in this flow around setup, TcpListener::bind, tcp_listener_loop, and
the app_handle state assignment so errors are returned to the caller instead of
always returning Ok(()).

Comment on lines +37 to +43
pub fn cleanup(state: tauri::State<TcpSocketState>) {
if let Ok(mut guard) = state.0.lock() {
if let Some(addr) = guard.take() {
eprintln!("[TCP IPC] Shutting down listener on {}", addr);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cleanup() does not actually stop the TCP listener.

cleanup() only clears the stored address string from state. The TcpListener and its accept loop (and any active client connections) continue running because the listener is owned by the spawned task, not by TcpSocketState. Unlike the Unix socket ipc::cleanup() which removes the socket file (preventing new connections), this cleanup is purely cosmetic.

To actually shut down, you'd need a cancellation mechanism (e.g., a tokio::sync::watch or CancellationToken) that the accept loop checks, or store a JoinHandle to abort the task.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 37 - 43, cleanup() only
removes the stored address string and does not stop the spawned accept loop or
active connections; update TcpSocketState to hold a cancellation mechanism
(e.g., a tokio::sync::watch, tokio_util::sync::CancellationToken, or the
JoinHandle of the spawned task) and modify the accept loop to listen for that
cancellation token or be abortable, then in cleanup() signal cancellation or
abort the JoinHandle and await/cleanup resources before dropping the address;
reference TcpSocketState, cleanup(), the spawned accept loop (the task that owns
the TcpListener) and whichever chosen token/handle so you can set and trigger it
from cleanup().

Comment on lines +64 to +94
async fn handle_client(stream: TcpStream, app: tauri::AppHandle) -> Result<(), String> {
let peer_addr = stream.peer_addr().map_err(|e| e.to_string())?;
let (reader, mut writer) = stream.into_split();
let reader = BufReader::new(reader);
let mut lines = reader.lines();

while let Some(line) = lines.next_line().await.map_err(|e| e.to_string())? {
eprintln!("[TCP IPC] Received from {}: {}", peer_addr, line);

let response = match serde_json::from_str::<IpcCommand>(&line) {
Ok(cmd) => {
eprintln!("[TCP IPC] Processing command: {}", cmd.command);
process_command(cmd, &app)
}
Err(e) => IpcResponse {
success: false,
error: Some(format!("Invalid JSON: {}", e)),
},
};

let json = serde_json::to_string(&response).unwrap_or_default();
eprintln!("[TCP IPC] Sending response to {}: {}", peer_addr, json);

writer
.write_all(format!("{}\n", json).as_bytes())
.await
.map_err(|e| e.to_string())?;
}

eprintln!("[TCP IPC] Connection closed: {}", peer_addr);
Ok(())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential log injection via unsanitized TCP input.

Line 71 logs the raw line received from the TCP socket, and line 75 logs cmd.command which is attacker-controlled input from any local process. While eprintln! is less exploitable than structured logging frameworks, newlines within the JSON string could forge log entries. Consider sanitizing or escaping logged values, or at minimum truncating them.

Also, Line 84: serde_json::to_string(&response).unwrap_or_default() will send an empty line "\n" to the client if serialization fails. Since IpcResponse is a simple struct this is unlikely, but sending an empty line could confuse a client expecting valid JSON. Consider sending a hardcoded error JSON instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 64 - 94, The handle_client
function currently logs raw, attacker-controlled TCP input and command strings
(the `line` contents and `cmd.command`) which can contain newlines and inject
fake log entries; sanitize or escape/newline-normalize and/or truncate these
values before logging (e.g., replace newlines with escaped sequences and limit
length) when using the eprintln! calls and when logging inside the match branch
that processes `process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.

The app icon had a solid background that extended to the edges, preventing
macOS from properly applying its automatic corner rounding. This resulted in
a square icon appearance instead of the expected rounded corners.
Changes:
- Applied ~12% corner radius to all icon assets (matching macOS guidelines)
- Regenerated icon.icns with proper alpha channel masks
- Updated all PNG sizes (32x32, 64x64, 128x128, 256x256, 512x512, 1024x1024)
The icon now displays with proper rounded corners in Dock, Finder, and app switcher.
@wilcorrea
wilcorrea merged commit 31f187b into mainFeb 23, 2026
1 check passed
@wilcorrea
wilcorrea deleted the fix/relative-paths branch February 23, 2026 20:42
Sign up for freeto 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.

Relative paths lead to a blank screen

1 participant

@wilcorrea
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(tauri): resolve relative paths and prepare for IPC merge - #12

Merged
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths
Feb 23, 2026
Merged

fix(tauri): resolve relative paths and prepare for IPC merge#12
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths

Conversation

@wilcorrea

@wilcorreawilcorrea commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#11 - Relative paths lead to a blank screen

  • Canonicalize file paths in read_file command to correctly resolve relative paths before reading files
  • Fix macOS Opened event to canonicalize paths when opening files via file associations (critical bug fix)
  • Add comprehensive debug logging at all file loading entry points (CLI args, single instance, Opened event, read_file)
  • Display full path in toolbar instead of just filename for better debugging visibility
  • Restructure builder for merge compatibility with feat/unix-socket-ipc branch to prevent future conflicts
  • Add merge guide documentation with step-by-step instructions for integrating IPC changes

Technical Details

The root cause was that relative paths (e.g., ./README.md) were not being canonicalized at all entry points:

  • ✅ CLI args: Already canonicalized
  • ✅ Single instance: Already canonicalized
  • Opened event (macOS): Missing canonicalization → FIXED
  • read_file command: No path resolution → FIXED

When a relative path reached read_file, it would try to read relative to the process working directory (not the file's directory), causing file not found errors and blank screens.

Merge Compatibility

This PR restructures the builder to match feat/unix-socket-ipc branch structure:

  • Separates builder initialization from setup
  • Adds placeholder comments showing exactly where to add IPC socket code
  • Includes MERGE_GUIDE.md with complete merge instructions

This ensures clean merges between branches with zero conflicts.

Test plan

  • Build and install: cd apps/tauri && make build-dev && make install
  • Test with relative path: cd ~/Documents && arandu ./README.md
  • Verify file loads correctly (no blank screen)
  • Test with absolute path: arandu /full/path/to/file.md
  • Check toolbar shows full path (not just filename)
  • Open Console.app and filter by "arandu" to see debug logs showing path resolution
  • Test file associations (double-click .md file) works correctly
  • Verify compilation: cargo check --manifest-path src-tauri/Cargo.toml

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a TCP-based IPC endpoint to allow external tooling to send open-file and command requests.
    • Toolbar now displays the full file path with hover tooltip.
  • Improvements

    • Expanded debug logging for file loading, CLI/opened-file and URL handling for better troubleshooting.
    • File path resolution now prefers canonical paths and falls back reliably; socket state is cleaned up on exit.

- Canonicalize paths in read_file command to handle relative paths
- Canonicalize paths in macOS Opened event (fixes#11)
- Add debug logging at all file loading entry points
- Restructure builder to allow conditional IPC state management
- Add placeholders for feat/unix-socket-ipc merge compatibility
- Display full path in toolbar for better debugging
@coderabbitai

coderabbitaiBot commented Feb 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • apps/tauri/src-tauri/icons/icon.icns
⛔ Files ignored due to path filters (5)
  • apps/tauri/src-tauri/icons/128x128.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/128x128@2x.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/32x32.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/64x64.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/icon.png is excluded by !**/*.png

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a TCP IPC server and state to the Tauri backend; makes IPC structs/functions public; adds path canonicalization and extensive [DEBUG] logging for CLI / opened-file flows; updates frontend to show full file paths and add debug logs.

Changes

Cohort / File(s)Summary
TCP IPC server & state
apps/tauri/src-tauri/src/tcp_ipc.rs
New TCP IPC implementation: binds listener, accepts connections, parses newline-delimited JSON IpcCommand, calls process_command, responds with IpcResponse; exposes TcpSocketState, setup, and cleanup.
Backend integration & path handling
apps/tauri/src-tauri/src/lib.rs
Integrated TCP IPC setup/cleanup and TcpSocketState into app init/teardown; added path canonicalization in read_file and all open-file / CLI / macOS opened-url handling; added many [DEBUG] logs and fallback behavior when canonicalization fails.
IPC types & API visibility
apps/tauri/src-tauri/src/ipc.rs
Made IpcCommand and IpcResponse public with exported fields; made process_command public for reuse by tcp_ipc.
Frontend: toolbar & logging
apps/tauri/src/main.js
Show full file path in toolbar (with hover title) instead of only last segment; added debug logs around loadFile and open-file event handling.

Sequence Diagram(s)

sequenceDiagram
participant Client as TCP Client
participant TCP as Tcp Listener (tcp_ipc)
participant App as Tauri App / process_command
participant FS as Filesystem
participant UI as Frontend (webview)
Client->>TCP: connect, send JSON line (IpcCommand)
TCP->>App: parse JSON -> call process_command(cmd)
App->>FS: read_file / resolve path (canonicalize)
alt command produces response
FS-->>App: file contents / error
App-->>TCP: IpcResponse (JSON)
App->>UI: emit "open-file" with resolved path (if applicable)
UI-->>UI: loadFile(resolved path), update toolbar
else error
App-->>TCP: IpcResponse (error)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nudged a socket, sniffed a path so neat,
I chased the canonical footprints down the street,
I told the app and sang with glee,
"Full paths now shine for you and me!"
— a rabbit, debugging with tiny feet 🌿✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main changes: fixing relative path resolution and preparing code structure for IPC integration.
Linked Issues check✅ PassedThe PR successfully addresses issue #11 by canonicalizing file paths at all entry points (CLI args, single-instance handler, macOS Opened event, and read_file command) to resolve relative paths correctly.
Out of Scope Changes check✅ PassedAll changes directly support the PR objectives: path canonicalization fixes, debug logging, toolbar display improvements, and TCP IPC setup for future merge compatibility.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/relative-paths

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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

366-387: Consider adding fallback behavior for consistency.

The single-instance handler silently drops the file-open request when canonicalize fails (lines 383-385 only log, no open-file event emitted). In contrast:

  • CLI args handler (line 462) uses unwrap_or_else to fall back to the original path
  • macOS Opened event (line 535) uses unwrap_or to fall back

This inconsistency could cause confusing behavior where files open in some scenarios but not others.

♻️ Suggested fix to add fallback
 if !file_path.is_empty() && !file_path.starts_with('-') {
- if let Ok(abs_path) = std::fs::canonicalize(file_path) {- let path_str = abs_path.to_string_lossy().to_string();- eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);- let _ = app.emit("open-file", &path_str);- } else {- eprintln!("[DEBUG] Failed to canonicalize path: {:?}", file_path);- }+ let abs_path = std::fs::canonicalize(file_path).unwrap_or_else(|e| {+ eprintln!("[DEBUG] Canonicalize failed ({}), using as-is", e);+ PathBuf::from(file_path)+ });+ let path_str = abs_path.to_string_lossy().to_string();+ eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);+ let _ = app.emit("open-file", &path_str);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 366 - 387, The single-instance
handler in the tauri_plugin_single_instance::init closure drops the open-file
event when std::fs::canonicalize(file_path) fails; change the logic so that on
canonicalize failure you fall back to the original file_path (or its
to_string_lossy) and still call app.emit("open-file", &path_str) so behavior
matches the CLI and macOS handlers; update the block handling args[1] to compute
a path_str variable from canonicalize if Ok or else from the original file_path,
log the fallback, and always emit "open-file" with that path_str.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 366-387: The single-instance handler in the
tauri_plugin_single_instance::init closure drops the open-file event when
std::fs::canonicalize(file_path) fails; change the logic so that on canonicalize
failure you fall back to the original file_path (or its to_string_lossy) and
still call app.emit("open-file", &path_str) so behavior matches the CLI and
macOS handlers; update the block handling args[1] to compute a path_str variable
from canonicalize if Ok or else from the original file_path, log the fallback,
and always emit "open-file" with that path_str.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51408db and a075094.

📒 Files selected for processing (2)
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src/main.js

@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed the CodeRabbit nitpick on the single-instance handler canonicalize fallback.

lib.rs lines 378-385: Changed from if let Ok(abs_path) = ... (which silently dropped the open-file event on failure) to unwrap_or_else with fallback to the original path. This now matches the behavior of both the CLI args handler (line 462) and the macOS Opened event handler (line 535).

Fixed in 53dbe11.

@coderabbitaicoderabbitaiBot 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: 1

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

66-66: Consider structured logging for production.

The [DEBUG] prefix in eprintln! calls works for development, but for production builds you might want to use the log or tracing crate with configurable log levels. This would allow filtering debug output without code changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 66, The debug print in read_file
currently uses eprintln!("[DEBUG] ...", path); switch this to a structured
logging call (e.g. log::debug! or tracing::debug!) so debug output can be
filtered at runtime, and ensure the application initializes the chosen
logger/subscriber (e.g. env_logger::init() or tracing_subscriber) in
main/startup; replace the eprintln! invocation inside read_file with the
appropriate debug macro and remove the hardcoded "[DEBUG]" prefix so the logging
framework controls level and formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 533-537: The opened-event handler uses
std::fs::canonicalize(&path).unwrap_or(path) which silently falls back; change
it to use unwrap_or_else so you can log the canonicalize error (matching
single-instance and CLI handlers). In the opened event block, replace
unwrap_or(path) with unwrap_or_else(|err| { eprintln!("[DEBUG] canonicalize
failed: {:?}", err); path.clone() }) (ensure abs_path and path_str keep the same
names) so failures are logged consistently.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Line 66: The debug print in read_file currently uses eprintln!("[DEBUG] ...",
path); switch this to a structured logging call (e.g. log::debug! or
tracing::debug!) so debug output can be filtered at runtime, and ensure the
application initializes the chosen logger/subscriber (e.g. env_logger::init() or
tracing_subscriber) in main/startup; replace the eprintln! invocation inside
read_file with the appropriate debug macro and remove the hardcoded "[DEBUG]"
prefix so the logging framework controls level and formatting.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a075094 and 53dbe11.

📒 Files selected for processing (1)
  • apps/tauri/src-tauri/src/lib.rs

Comment threadapps/tauri/src-tauri/src/lib.rs
…onicalize
Align the macOS Opened event handler with the single-instance and CLI
handlers by replacing silent unwrap_or fallback with unwrap_or_else
that logs the canonicalize failure before falling back.
@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed all remaining CodeRabbit review comments from both review rounds:

1. Inconsistent canonicalization fallback (Opened event, line 535) -- Fixed in 6fb6ca2. Replaced unwrap_or(path) with unwrap_or_else that logs the error, matching the single-instance and CLI handlers.

2. Single-instance handler dropping file-open on canonicalize failure (lines 366-387) -- Already addressed in 53dbe11 (previous commit).

3. Structured logging suggestion (line 66) -- Acknowledged as valid but deferred. Switching from eprintln! to log/tracing is a broader change that affects the entire codebase and is better handled in a dedicated PR. All debug prints in this PR are consistent with the existing eprintln!("[DEBUG] ...") pattern used throughout the project.

All three canonicalization entry points now use the same unwrap_or_else + debug logging pattern for consistent diagnostics.

- Add tcp_ipc.rs module with TCP listener on 127.0.0.1:7474
- Refactor ipc.rs to make IpcCommand/Response and process_command public
- TCP socket works in parallel with Unix socket for compatibility
- Supports same JSON protocol: open, ping, show commands
- Fixes Unix socket limitation with Docker/Podman containers
- Both sockets share same command processing logic
Resolves container accessibility issue where Unix sockets from macOS
are not accessible within Linux containers due to different kernel stacks.

@coderabbitaicoderabbitaiBot 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: 6

🧹 Nitpick comments (2)
apps/tauri/src-tauri/src/tcp_ipc.rs (1)

45-62: No connection limits or rate-limiting on the accept loop.

Any local process can open unlimited connections to this TCP server. Without a maximum connection count or per-connection timeout, a misbehaving local process could exhaust file descriptors or memory by opening many connections and sending data slowly.

Consider adding:

  • A maximum concurrent connection semaphore (tokio::sync::Semaphore)
  • A read timeout per connection (tokio::time::timeout)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 45 - 62, The accept loop in
tcp_listener_loop currently allows unlimited concurrent connections; add a
tokio::sync::Semaphore to limit max concurrent clients (acquire a permit before
spawning handle_client and release when that client's task finishes) and wrap
per-connection read/write operations inside tokio::time::timeout in
handle_client to enforce a per-connection inactivity/read timeout; ensure
tcp_listener_loop attempts to accept new connections even when semaphore is
exhausted (e.g., reject/close the stream immediately with a log) and
propagate/handle timeout errors from handle_client so they are logged.
apps/tauri/src-tauri/src/lib.rs (1)

529-533: TCP cleanup only clears state, doesn't stop the listener (see tcp_ipc.rs comment).

As noted in the tcp_ipc.rs review, cleanup() only nulls out the stored address. The accept loop and active connections continue until the process exits. For the ExitRequested + ExplicitQuit path this is likely fine since the process terminates shortly after, but it's worth noting the asymmetry with the Unix socket cleanup which actually removes the socket file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 529 - 533, The current call to
tcp_ipc::cleanup(tcp_socket_state) only clears the stored address but does not
stop the accept loop or close active connections; update the shutdown so the TCP
listener and any active connections are closed and the accept loop signalled to
exit. Either call a dedicated function (e.g., tcp_ipc::stop_listener or
tcp_ipc::shutdown_listener) from this location, or extend tcp_ipc::cleanup to
close the listener socket stored in tcp_ipc::TcpSocketState, close active
connections and signal/join the accept loop so it terminates cleanly rather than
leaving it running until process exit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 416-419: The tcp_ipc::setup() function currently masks bind
failures by always returning Ok(()), so the error branch here
(tcp_ipc::setup(app)) is effectively unreachable; update tcp_ipc::setup in
tcp_ipc.rs to propagate and return an Err when socket bind/listen fails (return
a Result with the real io::Error or a boxed error), and keep this call site
as-is so the Err path (eprintln!("Failed to setup TCP IPC: {}", e)) is reachable
and logs the actual failure; ensure the tcp_ipc::setup signature and any callers
are updated to handle the Result change.
- Line 15: The tcp_ipc module is currently compiled unconditionally, exposing an
unauthenticated TCP listener (127.0.0.1:7474); gate this to be opt-in and add
simple auth: protect the module with a cfg feature (e.g., #[cfg(feature =
"tcp-ipc")] on the mod tcp_ipc declaration and corresponding Cargo.toml feature)
and/or add token-based auth inside the tcp_ipc listener (generate a random token
stored in a file with 0o600 perms and require clients to present it when
connecting, validating in the tcp listener accept/handle path); update any
public APIs that assume tcp_ipc exists to compile without it (use
#[cfg(any(...))] or conditional stubs) and document the feature and token file
location so consumers can opt in securely.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 12-35: The setup() function currently spawns an async task and
returns Ok(()) before TcpListener::bind runs, so binding failures are never
propagated; fix by performing the bind synchronously (await the bind) or by
using a oneshot channel to relay the bind result back to the caller before
returning. Concretely, call TcpListener::bind(&addr).await (or bind on the sync
thread) and handle Err by returning Err(String) from setup(), or if you must
spawn first, create a tokio::sync::oneshot sender/receiver, have the spawned
task send Ok(()) or Err(e.to_string()) after attempting TcpListener::bind, and
have setup() block/await on the receiver and return the corresponding Result;
update references in this flow around setup, TcpListener::bind,
tcp_listener_loop, and the app_handle state assignment so errors are returned to
the caller instead of always returning Ok(()).
- Around line 37-43: cleanup() only removes the stored address string and does
not stop the spawned accept loop or active connections; update TcpSocketState to
hold a cancellation mechanism (e.g., a tokio::sync::watch,
tokio_util::sync::CancellationToken, or the JoinHandle of the spawned task) and
modify the accept loop to listen for that cancellation token or be abortable,
then in cleanup() signal cancellation or abort the JoinHandle and await/cleanup
resources before dropping the address; reference TcpSocketState, cleanup(), the
spawned accept loop (the task that owns the TcpListener) and whichever chosen
token/handle so you can set and trigger it from cleanup().
- Around line 64-94: The handle_client function currently logs raw,
attacker-controlled TCP input and command strings (the `line` contents and
`cmd.command`) which can contain newlines and inject fake log entries; sanitize
or escape/newline-normalize and/or truncate these values before logging (e.g.,
replace newlines with escaped sequences and limit length) when using the
eprintln! calls and when logging inside the match branch that processes
`process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.
- Around line 7-8: The current setup() spawns the listener asynchronously and
always returns Ok(()), and the hardcoded DEFAULT_PORT = 7474 can silently
conflict with Neo4j; change setup() to synchronously bind with
TcpListener::bind((DEFAULT_HOST, port)) (propagating any bind Err back to the
caller) before spawning the listener loop, and make the port configurable (read
from an env var or config with DEFAULT_PORT as fallback) so callers see bind
failures immediately and the port can be changed without code edits.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 529-533: The current call to tcp_ipc::cleanup(tcp_socket_state)
only clears the stored address but does not stop the accept loop or close active
connections; update the shutdown so the TCP listener and any active connections
are closed and the accept loop signalled to exit. Either call a dedicated
function (e.g., tcp_ipc::stop_listener or tcp_ipc::shutdown_listener) from this
location, or extend tcp_ipc::cleanup to close the listener socket stored in
tcp_ipc::TcpSocketState, close active connections and signal/join the accept
loop so it terminates cleanly rather than leaving it running until process exit.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 45-62: The accept loop in tcp_listener_loop currently allows
unlimited concurrent connections; add a tokio::sync::Semaphore to limit max
concurrent clients (acquire a permit before spawning handle_client and release
when that client's task finishes) and wrap per-connection read/write operations
inside tokio::time::timeout in handle_client to enforce a per-connection
inactivity/read timeout; ensure tcp_listener_loop attempts to accept new
connections even when semaphore is exhausted (e.g., reject/close the stream
immediately with a log) and propagate/handle timeout errors from handle_client
so they are logged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53dbe11 and 29db134.

📒 Files selected for processing (3)
  • apps/tauri/src-tauri/src/ipc.rs
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src-tauri/src/tcp_ipc.rs

mod cli_installer;
#[cfg(unix)]
mod ipc;
mod tcp_ipc;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TCP IPC module is unconditionally compiled — security implications of an unauthenticated local TCP listener.

Unlike the Unix socket (gated behind #[cfg(unix)] with 0o600 permissions), the TCP listener on 127.0.0.1:7474 has no authentication or authorization. Any local process (including those in containers, sandboxes, or running as different users) can connect and issue commands like open (to open arbitrary files the app user can read) or show (to manipulate window focus).

On multi-user systems or shared hosts this is a local attack surface. Consider adding at minimum:

  • A shared secret / token-based auth (e.g., random token written to a file with restrictive permissions, client must present it)
  • Or restricting TCP IPC behind a feature flag / cfg gate so it's opt-in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 15, The tcp_ipc module is currently
compiled unconditionally, exposing an unauthenticated TCP listener
(127.0.0.1:7474); gate this to be opt-in and add simple auth: protect the module
with a cfg feature (e.g., #[cfg(feature = "tcp-ipc")] on the mod tcp_ipc
declaration and corresponding Cargo.toml feature) and/or add token-based auth
inside the tcp_ipc listener (generate a random token stored in a file with 0o600
perms and require clients to present it when connecting, validating in the tcp
listener accept/handle path); update any public APIs that assume tcp_ipc exists
to compile without it (use #[cfg(any(...))] or conditional stubs) and document
the feature and token file location so consumers can opt in securely.

Comment on lines +416 to +419
// TCP IPC setup (works on all platforms, including from containers)
if let Err(e) = tcp_ipc::setup(app) {
eprintln!("Failed to setup TCP IPC: {}", e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TCP IPC setup error is logged but doesn't prevent app startup — acceptable.

This mirrors the Unix socket setup pattern. Since the TCP IPC is supplementary, failing gracefully is reasonable. However, note that as discussed in tcp_ipc.rs, setup() currently always returns Ok(()) even on bind failure, making this error branch unreachable for the most important failure case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 416 - 419, The tcp_ipc::setup()
function currently masks bind failures by always returning Ok(()), so the error
branch here (tcp_ipc::setup(app)) is effectively unreachable; update
tcp_ipc::setup in tcp_ipc.rs to propagate and return an Err when socket
bind/listen fails (return a Result with the real io::Error or a boxed error),
and keep this call site as-is so the Err path (eprintln!("Failed to setup TCP
IPC: {}", e)) is reachable and logs the actual failure; ensure the
tcp_ipc::setup signature and any callers are updated to handle the Result
change.

Comment on lines +7 to +8
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 7474;

Copy link
Copy Markdown

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

🌐 Web query:

Neo4j default port database

💡 Result:

Neo4j’s default ports depend on how you connect:

  • Bolt (drivers / neo4j:// / bolt://): 7687 [1][3]
  • HTTP (Neo4j Browser / HTTP API): 7474 [1][3]
  • HTTPS: 7473 [1][3]

(Other common defaults include Backup: 6362.) [1]


🏁 Script executed:

# First, let's find and examine the tcp_ipc.rs file
find . -name "tcp_ipc.rs" -type f | head -5

Repository: devitools/arandu

Length of output: 97


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/tcp_ipc.rs

Repository: devitools/arandu

Length of output: 3886


🏁 Script executed:

# Now let's check lib.rs to see how setup() is called and if it handles the result
find . -name "lib.rs" -path "*/src-tauri/src/lib.rs" -type f

Repository: devitools/arandu

Length of output: 93


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/lib.rs

Repository: devitools/arandu

Length of output: 23262


Hardcoded port 7474 conflicts with Neo4j and fails silently.

Port 7474 is Neo4j's default HTTP port. The TCP bind happens asynchronously inside a spawned task (lines 17–30), but setup() returns Ok(()) immediately (line 34) before the bind attempt completes. If another process holds the port, the error is only logged to stderr; the caller in lib.rs (lines 417–419) cannot distinguish success from failure since setup() always returns Ok(()).

Consider either:

  • Synchronously binding before spawning the listener loop, or
  • Using a oneshot channel to propagate bind errors back to the caller, or
  • Making the port configurable (via config file or environment variable) with a fallback port range.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 7 - 8, The current setup()
spawns the listener asynchronously and always returns Ok(()), and the hardcoded
DEFAULT_PORT = 7474 can silently conflict with Neo4j; change setup() to
synchronously bind with TcpListener::bind((DEFAULT_HOST, port)) (propagating any
bind Err back to the caller) before spawning the listener loop, and make the
port configurable (read from an env var or config with DEFAULT_PORT as fallback)
so callers see bind failures immediately and the port can be changed without
code edits.

Comment on lines +12 to +35
pub fn setup(app: &tauri::App) -> Result<(), String> {
let addr = format!("{}:{}", DEFAULT_HOST, DEFAULT_PORT);

let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
match TcpListener::bind(&addr).await {
Ok(listener) => {
eprintln!("[TCP IPC] Listening on {}", addr);

let state = app_handle.state::<TcpSocketState>();
if let Ok(mut guard) = state.0.lock() {
*guard = Some(addr.clone());
}

tcp_listener_loop(listener, app_handle).await;
}
Err(e) => {
eprintln!("[TCP IPC] Failed to bind to {}: {}", addr, e);
}
}
});

Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

setup() always returns Ok(()) regardless of bind outcome.

Because TcpListener::bind happens inside the spawned async task, setup() returns Ok(()) before the bind attempt completes. The caller in lib.rs (line 417) checks for Err but will never see one from a bind failure. This makes the error handling at the call site dead code for the most critical failure mode.

Compare with the Unix socket ipc::setup() which also has this pattern — but at minimum, consider a oneshot channel to relay the bind result, or bind synchronously before spawning the accept loop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 12 - 35, The setup()
function currently spawns an async task and returns Ok(()) before
TcpListener::bind runs, so binding failures are never propagated; fix by
performing the bind synchronously (await the bind) or by using a oneshot channel
to relay the bind result back to the caller before returning. Concretely, call
TcpListener::bind(&addr).await (or bind on the sync thread) and handle Err by
returning Err(String) from setup(), or if you must spawn first, create a
tokio::sync::oneshot sender/receiver, have the spawned task send Ok(()) or
Err(e.to_string()) after attempting TcpListener::bind, and have setup()
block/await on the receiver and return the corresponding Result; update
references in this flow around setup, TcpListener::bind, tcp_listener_loop, and
the app_handle state assignment so errors are returned to the caller instead of
always returning Ok(()).

Comment on lines +37 to +43
pub fn cleanup(state: tauri::State<TcpSocketState>) {
if let Ok(mut guard) = state.0.lock() {
if let Some(addr) = guard.take() {
eprintln!("[TCP IPC] Shutting down listener on {}", addr);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cleanup() does not actually stop the TCP listener.

cleanup() only clears the stored address string from state. The TcpListener and its accept loop (and any active client connections) continue running because the listener is owned by the spawned task, not by TcpSocketState. Unlike the Unix socket ipc::cleanup() which removes the socket file (preventing new connections), this cleanup is purely cosmetic.

To actually shut down, you'd need a cancellation mechanism (e.g., a tokio::sync::watch or CancellationToken) that the accept loop checks, or store a JoinHandle to abort the task.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 37 - 43, cleanup() only
removes the stored address string and does not stop the spawned accept loop or
active connections; update TcpSocketState to hold a cancellation mechanism
(e.g., a tokio::sync::watch, tokio_util::sync::CancellationToken, or the
JoinHandle of the spawned task) and modify the accept loop to listen for that
cancellation token or be abortable, then in cleanup() signal cancellation or
abort the JoinHandle and await/cleanup resources before dropping the address;
reference TcpSocketState, cleanup(), the spawned accept loop (the task that owns
the TcpListener) and whichever chosen token/handle so you can set and trigger it
from cleanup().

Comment on lines +64 to +94
async fn handle_client(stream: TcpStream, app: tauri::AppHandle) -> Result<(), String> {
let peer_addr = stream.peer_addr().map_err(|e| e.to_string())?;
let (reader, mut writer) = stream.into_split();
let reader = BufReader::new(reader);
let mut lines = reader.lines();

while let Some(line) = lines.next_line().await.map_err(|e| e.to_string())? {
eprintln!("[TCP IPC] Received from {}: {}", peer_addr, line);

let response = match serde_json::from_str::<IpcCommand>(&line) {
Ok(cmd) => {
eprintln!("[TCP IPC] Processing command: {}", cmd.command);
process_command(cmd, &app)
}
Err(e) => IpcResponse {
success: false,
error: Some(format!("Invalid JSON: {}", e)),
},
};

let json = serde_json::to_string(&response).unwrap_or_default();
eprintln!("[TCP IPC] Sending response to {}: {}", peer_addr, json);

writer
.write_all(format!("{}\n", json).as_bytes())
.await
.map_err(|e| e.to_string())?;
}

eprintln!("[TCP IPC] Connection closed: {}", peer_addr);
Ok(())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential log injection via unsanitized TCP input.

Line 71 logs the raw line received from the TCP socket, and line 75 logs cmd.command which is attacker-controlled input from any local process. While eprintln! is less exploitable than structured logging frameworks, newlines within the JSON string could forge log entries. Consider sanitizing or escaping logged values, or at minimum truncating them.

Also, Line 84: serde_json::to_string(&response).unwrap_or_default() will send an empty line "\n" to the client if serialization fails. Since IpcResponse is a simple struct this is unlikely, but sending an empty line could confuse a client expecting valid JSON. Consider sending a hardcoded error JSON instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 64 - 94, The handle_client
function currently logs raw, attacker-controlled TCP input and command strings
(the `line` contents and `cmd.command`) which can contain newlines and inject
fake log entries; sanitize or escape/newline-normalize and/or truncate these
values before logging (e.g., replace newlines with escaped sequences and limit
length) when using the eprintln! calls and when logging inside the match branch
that processes `process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.

The app icon had a solid background that extended to the edges, preventing
macOS from properly applying its automatic corner rounding. This resulted in
a square icon appearance instead of the expected rounded corners.
Changes:
- Applied ~12% corner radius to all icon assets (matching macOS guidelines)
- Regenerated icon.icns with proper alpha channel masks
- Updated all PNG sizes (32x32, 64x64, 128x128, 256x256, 512x512, 1024x1024)
The icon now displays with proper rounded corners in Dock, Finder, and app switcher.
@wilcorrea
wilcorrea merged commit 31f187b into mainFeb 23, 2026
1 check passed
@wilcorrea
wilcorrea deleted the fix/relative-paths branch February 23, 2026 20:42
Sign up for freeto 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.

Relative paths lead to a blank screen

1 participant

@wilcorrea
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(tauri): resolve relative paths and prepare for IPC merge - #12

Merged
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths
Feb 23, 2026
Merged

fix(tauri): resolve relative paths and prepare for IPC merge#12
wilcorrea merged 8 commits into
mainfrom
fix/relative-paths

Conversation

@wilcorrea

@wilcorreawilcorrea commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#11 - Relative paths lead to a blank screen

  • Canonicalize file paths in read_file command to correctly resolve relative paths before reading files
  • Fix macOS Opened event to canonicalize paths when opening files via file associations (critical bug fix)
  • Add comprehensive debug logging at all file loading entry points (CLI args, single instance, Opened event, read_file)
  • Display full path in toolbar instead of just filename for better debugging visibility
  • Restructure builder for merge compatibility with feat/unix-socket-ipc branch to prevent future conflicts
  • Add merge guide documentation with step-by-step instructions for integrating IPC changes

Technical Details

The root cause was that relative paths (e.g., ./README.md) were not being canonicalized at all entry points:

  • ✅ CLI args: Already canonicalized
  • ✅ Single instance: Already canonicalized
  • Opened event (macOS): Missing canonicalization → FIXED
  • read_file command: No path resolution → FIXED

When a relative path reached read_file, it would try to read relative to the process working directory (not the file's directory), causing file not found errors and blank screens.

Merge Compatibility

This PR restructures the builder to match feat/unix-socket-ipc branch structure:

  • Separates builder initialization from setup
  • Adds placeholder comments showing exactly where to add IPC socket code
  • Includes MERGE_GUIDE.md with complete merge instructions

This ensures clean merges between branches with zero conflicts.

Test plan

  • Build and install: cd apps/tauri && make build-dev && make install
  • Test with relative path: cd ~/Documents && arandu ./README.md
  • Verify file loads correctly (no blank screen)
  • Test with absolute path: arandu /full/path/to/file.md
  • Check toolbar shows full path (not just filename)
  • Open Console.app and filter by "arandu" to see debug logs showing path resolution
  • Test file associations (double-click .md file) works correctly
  • Verify compilation: cargo check --manifest-path src-tauri/Cargo.toml

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a TCP-based IPC endpoint to allow external tooling to send open-file and command requests.
    • Toolbar now displays the full file path with hover tooltip.
  • Improvements

    • Expanded debug logging for file loading, CLI/opened-file and URL handling for better troubleshooting.
    • File path resolution now prefers canonical paths and falls back reliably; socket state is cleaned up on exit.

- Canonicalize paths in read_file command to handle relative paths
- Canonicalize paths in macOS Opened event (fixes#11)
- Add debug logging at all file loading entry points
- Restructure builder to allow conditional IPC state management
- Add placeholders for feat/unix-socket-ipc merge compatibility
- Display full path in toolbar for better debugging
@coderabbitai

coderabbitaiBot commented Feb 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • apps/tauri/src-tauri/icons/icon.icns
⛔ Files ignored due to path filters (5)
  • apps/tauri/src-tauri/icons/128x128.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/128x128@2x.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/32x32.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/64x64.png is excluded by !**/*.png
  • apps/tauri/src-tauri/icons/icon.png is excluded by !**/*.png

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a TCP IPC server and state to the Tauri backend; makes IPC structs/functions public; adds path canonicalization and extensive [DEBUG] logging for CLI / opened-file flows; updates frontend to show full file paths and add debug logs.

Changes

Cohort / File(s)Summary
TCP IPC server & state
apps/tauri/src-tauri/src/tcp_ipc.rs
New TCP IPC implementation: binds listener, accepts connections, parses newline-delimited JSON IpcCommand, calls process_command, responds with IpcResponse; exposes TcpSocketState, setup, and cleanup.
Backend integration & path handling
apps/tauri/src-tauri/src/lib.rs
Integrated TCP IPC setup/cleanup and TcpSocketState into app init/teardown; added path canonicalization in read_file and all open-file / CLI / macOS opened-url handling; added many [DEBUG] logs and fallback behavior when canonicalization fails.
IPC types & API visibility
apps/tauri/src-tauri/src/ipc.rs
Made IpcCommand and IpcResponse public with exported fields; made process_command public for reuse by tcp_ipc.
Frontend: toolbar & logging
apps/tauri/src/main.js
Show full file path in toolbar (with hover title) instead of only last segment; added debug logs around loadFile and open-file event handling.

Sequence Diagram(s)

sequenceDiagram
participant Client as TCP Client
participant TCP as Tcp Listener (tcp_ipc)
participant App as Tauri App / process_command
participant FS as Filesystem
participant UI as Frontend (webview)
Client->>TCP: connect, send JSON line (IpcCommand)
TCP->>App: parse JSON -> call process_command(cmd)
App->>FS: read_file / resolve path (canonicalize)
alt command produces response
FS-->>App: file contents / error
App-->>TCP: IpcResponse (JSON)
App->>UI: emit "open-file" with resolved path (if applicable)
UI-->>UI: loadFile(resolved path), update toolbar
else error
App-->>TCP: IpcResponse (error)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nudged a socket, sniffed a path so neat,
I chased the canonical footprints down the street,
I told the app and sang with glee,
"Full paths now shine for you and me!"
— a rabbit, debugging with tiny feet 🌿✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main changes: fixing relative path resolution and preparing code structure for IPC integration.
Linked Issues check✅ PassedThe PR successfully addresses issue #11 by canonicalizing file paths at all entry points (CLI args, single-instance handler, macOS Opened event, and read_file command) to resolve relative paths correctly.
Out of Scope Changes check✅ PassedAll changes directly support the PR objectives: path canonicalization fixes, debug logging, toolbar display improvements, and TCP IPC setup for future merge compatibility.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/relative-paths

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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

366-387: Consider adding fallback behavior for consistency.

The single-instance handler silently drops the file-open request when canonicalize fails (lines 383-385 only log, no open-file event emitted). In contrast:

  • CLI args handler (line 462) uses unwrap_or_else to fall back to the original path
  • macOS Opened event (line 535) uses unwrap_or to fall back

This inconsistency could cause confusing behavior where files open in some scenarios but not others.

♻️ Suggested fix to add fallback
 if !file_path.is_empty() && !file_path.starts_with('-') {
- if let Ok(abs_path) = std::fs::canonicalize(file_path) {- let path_str = abs_path.to_string_lossy().to_string();- eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);- let _ = app.emit("open-file", &path_str);- } else {- eprintln!("[DEBUG] Failed to canonicalize path: {:?}", file_path);- }+ let abs_path = std::fs::canonicalize(file_path).unwrap_or_else(|e| {+ eprintln!("[DEBUG] Canonicalize failed ({}), using as-is", e);+ PathBuf::from(file_path)+ });+ let path_str = abs_path.to_string_lossy().to_string();+ eprintln!("[DEBUG] Emitting open-file with: {:?}", path_str);+ let _ = app.emit("open-file", &path_str);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 366 - 387, The single-instance
handler in the tauri_plugin_single_instance::init closure drops the open-file
event when std::fs::canonicalize(file_path) fails; change the logic so that on
canonicalize failure you fall back to the original file_path (or its
to_string_lossy) and still call app.emit("open-file", &path_str) so behavior
matches the CLI and macOS handlers; update the block handling args[1] to compute
a path_str variable from canonicalize if Ok or else from the original file_path,
log the fallback, and always emit "open-file" with that path_str.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 366-387: The single-instance handler in the
tauri_plugin_single_instance::init closure drops the open-file event when
std::fs::canonicalize(file_path) fails; change the logic so that on canonicalize
failure you fall back to the original file_path (or its to_string_lossy) and
still call app.emit("open-file", &path_str) so behavior matches the CLI and
macOS handlers; update the block handling args[1] to compute a path_str variable
from canonicalize if Ok or else from the original file_path, log the fallback,
and always emit "open-file" with that path_str.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51408db and a075094.

📒 Files selected for processing (2)
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src/main.js

@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed the CodeRabbit nitpick on the single-instance handler canonicalize fallback.

lib.rs lines 378-385: Changed from if let Ok(abs_path) = ... (which silently dropped the open-file event on failure) to unwrap_or_else with fallback to the original path. This now matches the behavior of both the CLI args handler (line 462) and the macOS Opened event handler (line 535).

Fixed in 53dbe11.

@coderabbitaicoderabbitaiBot 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: 1

🧹 Nitpick comments (1)
apps/tauri/src-tauri/src/lib.rs (1)

66-66: Consider structured logging for production.

The [DEBUG] prefix in eprintln! calls works for development, but for production builds you might want to use the log or tracing crate with configurable log levels. This would allow filtering debug output without code changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 66, The debug print in read_file
currently uses eprintln!("[DEBUG] ...", path); switch this to a structured
logging call (e.g. log::debug! or tracing::debug!) so debug output can be
filtered at runtime, and ensure the application initializes the chosen
logger/subscriber (e.g. env_logger::init() or tracing_subscriber) in
main/startup; replace the eprintln! invocation inside read_file with the
appropriate debug macro and remove the hardcoded "[DEBUG]" prefix so the logging
framework controls level and formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 533-537: The opened-event handler uses
std::fs::canonicalize(&path).unwrap_or(path) which silently falls back; change
it to use unwrap_or_else so you can log the canonicalize error (matching
single-instance and CLI handlers). In the opened event block, replace
unwrap_or(path) with unwrap_or_else(|err| { eprintln!("[DEBUG] canonicalize
failed: {:?}", err); path.clone() }) (ensure abs_path and path_str keep the same
names) so failures are logged consistently.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Line 66: The debug print in read_file currently uses eprintln!("[DEBUG] ...",
path); switch this to a structured logging call (e.g. log::debug! or
tracing::debug!) so debug output can be filtered at runtime, and ensure the
application initializes the chosen logger/subscriber (e.g. env_logger::init() or
tracing_subscriber) in main/startup; replace the eprintln! invocation inside
read_file with the appropriate debug macro and remove the hardcoded "[DEBUG]"
prefix so the logging framework controls level and formatting.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a075094 and 53dbe11.

📒 Files selected for processing (1)
  • apps/tauri/src-tauri/src/lib.rs

Comment threadapps/tauri/src-tauri/src/lib.rs
…onicalize
Align the macOS Opened event handler with the single-instance and CLI
handlers by replacing silent unwrap_or fallback with unwrap_or_else
that logs the canonicalize failure before falling back.
@wilcorrea

Copy link
Copy Markdown
ContributorAuthor

Addressed all remaining CodeRabbit review comments from both review rounds:

1. Inconsistent canonicalization fallback (Opened event, line 535) -- Fixed in 6fb6ca2. Replaced unwrap_or(path) with unwrap_or_else that logs the error, matching the single-instance and CLI handlers.

2. Single-instance handler dropping file-open on canonicalize failure (lines 366-387) -- Already addressed in 53dbe11 (previous commit).

3. Structured logging suggestion (line 66) -- Acknowledged as valid but deferred. Switching from eprintln! to log/tracing is a broader change that affects the entire codebase and is better handled in a dedicated PR. All debug prints in this PR are consistent with the existing eprintln!("[DEBUG] ...") pattern used throughout the project.

All three canonicalization entry points now use the same unwrap_or_else + debug logging pattern for consistent diagnostics.

- Add tcp_ipc.rs module with TCP listener on 127.0.0.1:7474
- Refactor ipc.rs to make IpcCommand/Response and process_command public
- TCP socket works in parallel with Unix socket for compatibility
- Supports same JSON protocol: open, ping, show commands
- Fixes Unix socket limitation with Docker/Podman containers
- Both sockets share same command processing logic
Resolves container accessibility issue where Unix sockets from macOS
are not accessible within Linux containers due to different kernel stacks.

@coderabbitaicoderabbitaiBot 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: 6

🧹 Nitpick comments (2)
apps/tauri/src-tauri/src/tcp_ipc.rs (1)

45-62: No connection limits or rate-limiting on the accept loop.

Any local process can open unlimited connections to this TCP server. Without a maximum connection count or per-connection timeout, a misbehaving local process could exhaust file descriptors or memory by opening many connections and sending data slowly.

Consider adding:

  • A maximum concurrent connection semaphore (tokio::sync::Semaphore)
  • A read timeout per connection (tokio::time::timeout)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 45 - 62, The accept loop in
tcp_listener_loop currently allows unlimited concurrent connections; add a
tokio::sync::Semaphore to limit max concurrent clients (acquire a permit before
spawning handle_client and release when that client's task finishes) and wrap
per-connection read/write operations inside tokio::time::timeout in
handle_client to enforce a per-connection inactivity/read timeout; ensure
tcp_listener_loop attempts to accept new connections even when semaphore is
exhausted (e.g., reject/close the stream immediately with a log) and
propagate/handle timeout errors from handle_client so they are logged.
apps/tauri/src-tauri/src/lib.rs (1)

529-533: TCP cleanup only clears state, doesn't stop the listener (see tcp_ipc.rs comment).

As noted in the tcp_ipc.rs review, cleanup() only nulls out the stored address. The accept loop and active connections continue until the process exits. For the ExitRequested + ExplicitQuit path this is likely fine since the process terminates shortly after, but it's worth noting the asymmetry with the Unix socket cleanup which actually removes the socket file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 529 - 533, The current call to
tcp_ipc::cleanup(tcp_socket_state) only clears the stored address but does not
stop the accept loop or close active connections; update the shutdown so the TCP
listener and any active connections are closed and the accept loop signalled to
exit. Either call a dedicated function (e.g., tcp_ipc::stop_listener or
tcp_ipc::shutdown_listener) from this location, or extend tcp_ipc::cleanup to
close the listener socket stored in tcp_ipc::TcpSocketState, close active
connections and signal/join the accept loop so it terminates cleanly rather than
leaving it running until process exit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 416-419: The tcp_ipc::setup() function currently masks bind
failures by always returning Ok(()), so the error branch here
(tcp_ipc::setup(app)) is effectively unreachable; update tcp_ipc::setup in
tcp_ipc.rs to propagate and return an Err when socket bind/listen fails (return
a Result with the real io::Error or a boxed error), and keep this call site
as-is so the Err path (eprintln!("Failed to setup TCP IPC: {}", e)) is reachable
and logs the actual failure; ensure the tcp_ipc::setup signature and any callers
are updated to handle the Result change.
- Line 15: The tcp_ipc module is currently compiled unconditionally, exposing an
unauthenticated TCP listener (127.0.0.1:7474); gate this to be opt-in and add
simple auth: protect the module with a cfg feature (e.g., #[cfg(feature =
"tcp-ipc")] on the mod tcp_ipc declaration and corresponding Cargo.toml feature)
and/or add token-based auth inside the tcp_ipc listener (generate a random token
stored in a file with 0o600 perms and require clients to present it when
connecting, validating in the tcp listener accept/handle path); update any
public APIs that assume tcp_ipc exists to compile without it (use
#[cfg(any(...))] or conditional stubs) and document the feature and token file
location so consumers can opt in securely.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 12-35: The setup() function currently spawns an async task and
returns Ok(()) before TcpListener::bind runs, so binding failures are never
propagated; fix by performing the bind synchronously (await the bind) or by
using a oneshot channel to relay the bind result back to the caller before
returning. Concretely, call TcpListener::bind(&addr).await (or bind on the sync
thread) and handle Err by returning Err(String) from setup(), or if you must
spawn first, create a tokio::sync::oneshot sender/receiver, have the spawned
task send Ok(()) or Err(e.to_string()) after attempting TcpListener::bind, and
have setup() block/await on the receiver and return the corresponding Result;
update references in this flow around setup, TcpListener::bind,
tcp_listener_loop, and the app_handle state assignment so errors are returned to
the caller instead of always returning Ok(()).
- Around line 37-43: cleanup() only removes the stored address string and does
not stop the spawned accept loop or active connections; update TcpSocketState to
hold a cancellation mechanism (e.g., a tokio::sync::watch,
tokio_util::sync::CancellationToken, or the JoinHandle of the spawned task) and
modify the accept loop to listen for that cancellation token or be abortable,
then in cleanup() signal cancellation or abort the JoinHandle and await/cleanup
resources before dropping the address; reference TcpSocketState, cleanup(), the
spawned accept loop (the task that owns the TcpListener) and whichever chosen
token/handle so you can set and trigger it from cleanup().
- Around line 64-94: The handle_client function currently logs raw,
attacker-controlled TCP input and command strings (the `line` contents and
`cmd.command`) which can contain newlines and inject fake log entries; sanitize
or escape/newline-normalize and/or truncate these values before logging (e.g.,
replace newlines with escaped sequences and limit length) when using the
eprintln! calls and when logging inside the match branch that processes
`process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.
- Around line 7-8: The current setup() spawns the listener asynchronously and
always returns Ok(()), and the hardcoded DEFAULT_PORT = 7474 can silently
conflict with Neo4j; change setup() to synchronously bind with
TcpListener::bind((DEFAULT_HOST, port)) (propagating any bind Err back to the
caller) before spawning the listener loop, and make the port configurable (read
from an env var or config with DEFAULT_PORT as fallback) so callers see bind
failures immediately and the port can be changed without code edits.
---
Nitpick comments:
In `@apps/tauri/src-tauri/src/lib.rs`:
- Around line 529-533: The current call to tcp_ipc::cleanup(tcp_socket_state)
only clears the stored address but does not stop the accept loop or close active
connections; update the shutdown so the TCP listener and any active connections
are closed and the accept loop signalled to exit. Either call a dedicated
function (e.g., tcp_ipc::stop_listener or tcp_ipc::shutdown_listener) from this
location, or extend tcp_ipc::cleanup to close the listener socket stored in
tcp_ipc::TcpSocketState, close active connections and signal/join the accept
loop so it terminates cleanly rather than leaving it running until process exit.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs`:
- Around line 45-62: The accept loop in tcp_listener_loop currently allows
unlimited concurrent connections; add a tokio::sync::Semaphore to limit max
concurrent clients (acquire a permit before spawning handle_client and release
when that client's task finishes) and wrap per-connection read/write operations
inside tokio::time::timeout in handle_client to enforce a per-connection
inactivity/read timeout; ensure tcp_listener_loop attempts to accept new
connections even when semaphore is exhausted (e.g., reject/close the stream
immediately with a log) and propagate/handle timeout errors from handle_client
so they are logged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53dbe11 and 29db134.

📒 Files selected for processing (3)
  • apps/tauri/src-tauri/src/ipc.rs
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src-tauri/src/tcp_ipc.rs

mod cli_installer;
#[cfg(unix)]
mod ipc;
mod tcp_ipc;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TCP IPC module is unconditionally compiled — security implications of an unauthenticated local TCP listener.

Unlike the Unix socket (gated behind #[cfg(unix)] with 0o600 permissions), the TCP listener on 127.0.0.1:7474 has no authentication or authorization. Any local process (including those in containers, sandboxes, or running as different users) can connect and issue commands like open (to open arbitrary files the app user can read) or show (to manipulate window focus).

On multi-user systems or shared hosts this is a local attack surface. Consider adding at minimum:

  • A shared secret / token-based auth (e.g., random token written to a file with restrictive permissions, client must present it)
  • Or restricting TCP IPC behind a feature flag / cfg gate so it's opt-in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` at line 15, The tcp_ipc module is currently
compiled unconditionally, exposing an unauthenticated TCP listener
(127.0.0.1:7474); gate this to be opt-in and add simple auth: protect the module
with a cfg feature (e.g., #[cfg(feature = "tcp-ipc")] on the mod tcp_ipc
declaration and corresponding Cargo.toml feature) and/or add token-based auth
inside the tcp_ipc listener (generate a random token stored in a file with 0o600
perms and require clients to present it when connecting, validating in the tcp
listener accept/handle path); update any public APIs that assume tcp_ipc exists
to compile without it (use #[cfg(any(...))] or conditional stubs) and document
the feature and token file location so consumers can opt in securely.

Comment on lines +416 to +419
// TCP IPC setup (works on all platforms, including from containers)
if let Err(e) = tcp_ipc::setup(app) {
eprintln!("Failed to setup TCP IPC: {}", e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TCP IPC setup error is logged but doesn't prevent app startup — acceptable.

This mirrors the Unix socket setup pattern. Since the TCP IPC is supplementary, failing gracefully is reasonable. However, note that as discussed in tcp_ipc.rs, setup() currently always returns Ok(()) even on bind failure, making this error branch unreachable for the most important failure case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/lib.rs` around lines 416 - 419, The tcp_ipc::setup()
function currently masks bind failures by always returning Ok(()), so the error
branch here (tcp_ipc::setup(app)) is effectively unreachable; update
tcp_ipc::setup in tcp_ipc.rs to propagate and return an Err when socket
bind/listen fails (return a Result with the real io::Error or a boxed error),
and keep this call site as-is so the Err path (eprintln!("Failed to setup TCP
IPC: {}", e)) is reachable and logs the actual failure; ensure the
tcp_ipc::setup signature and any callers are updated to handle the Result
change.

Comment on lines +7 to +8
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 7474;

Copy link
Copy Markdown

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

🌐 Web query:

Neo4j default port database

💡 Result:

Neo4j’s default ports depend on how you connect:

  • Bolt (drivers / neo4j:// / bolt://): 7687 [1][3]
  • HTTP (Neo4j Browser / HTTP API): 7474 [1][3]
  • HTTPS: 7473 [1][3]

(Other common defaults include Backup: 6362.) [1]


🏁 Script executed:

# First, let's find and examine the tcp_ipc.rs file
find . -name "tcp_ipc.rs" -type f | head -5

Repository: devitools/arandu

Length of output: 97


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/tcp_ipc.rs

Repository: devitools/arandu

Length of output: 3886


🏁 Script executed:

# Now let's check lib.rs to see how setup() is called and if it handles the result
find . -name "lib.rs" -path "*/src-tauri/src/lib.rs" -type f

Repository: devitools/arandu

Length of output: 93


🏁 Script executed:

cat -n ./apps/tauri/src-tauri/src/lib.rs

Repository: devitools/arandu

Length of output: 23262


Hardcoded port 7474 conflicts with Neo4j and fails silently.

Port 7474 is Neo4j's default HTTP port. The TCP bind happens asynchronously inside a spawned task (lines 17–30), but setup() returns Ok(()) immediately (line 34) before the bind attempt completes. If another process holds the port, the error is only logged to stderr; the caller in lib.rs (lines 417–419) cannot distinguish success from failure since setup() always returns Ok(()).

Consider either:

  • Synchronously binding before spawning the listener loop, or
  • Using a oneshot channel to propagate bind errors back to the caller, or
  • Making the port configurable (via config file or environment variable) with a fallback port range.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 7 - 8, The current setup()
spawns the listener asynchronously and always returns Ok(()), and the hardcoded
DEFAULT_PORT = 7474 can silently conflict with Neo4j; change setup() to
synchronously bind with TcpListener::bind((DEFAULT_HOST, port)) (propagating any
bind Err back to the caller) before spawning the listener loop, and make the
port configurable (read from an env var or config with DEFAULT_PORT as fallback)
so callers see bind failures immediately and the port can be changed without
code edits.

Comment on lines +12 to +35
pub fn setup(app: &tauri::App) -> Result<(), String> {
let addr = format!("{}:{}", DEFAULT_HOST, DEFAULT_PORT);

let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
match TcpListener::bind(&addr).await {
Ok(listener) => {
eprintln!("[TCP IPC] Listening on {}", addr);

let state = app_handle.state::<TcpSocketState>();
if let Ok(mut guard) = state.0.lock() {
*guard = Some(addr.clone());
}

tcp_listener_loop(listener, app_handle).await;
}
Err(e) => {
eprintln!("[TCP IPC] Failed to bind to {}: {}", addr, e);
}
}
});

Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

setup() always returns Ok(()) regardless of bind outcome.

Because TcpListener::bind happens inside the spawned async task, setup() returns Ok(()) before the bind attempt completes. The caller in lib.rs (line 417) checks for Err but will never see one from a bind failure. This makes the error handling at the call site dead code for the most critical failure mode.

Compare with the Unix socket ipc::setup() which also has this pattern — but at minimum, consider a oneshot channel to relay the bind result, or bind synchronously before spawning the accept loop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 12 - 35, The setup()
function currently spawns an async task and returns Ok(()) before
TcpListener::bind runs, so binding failures are never propagated; fix by
performing the bind synchronously (await the bind) or by using a oneshot channel
to relay the bind result back to the caller before returning. Concretely, call
TcpListener::bind(&addr).await (or bind on the sync thread) and handle Err by
returning Err(String) from setup(), or if you must spawn first, create a
tokio::sync::oneshot sender/receiver, have the spawned task send Ok(()) or
Err(e.to_string()) after attempting TcpListener::bind, and have setup()
block/await on the receiver and return the corresponding Result; update
references in this flow around setup, TcpListener::bind, tcp_listener_loop, and
the app_handle state assignment so errors are returned to the caller instead of
always returning Ok(()).

Comment on lines +37 to +43
pub fn cleanup(state: tauri::State<TcpSocketState>) {
if let Ok(mut guard) = state.0.lock() {
if let Some(addr) = guard.take() {
eprintln!("[TCP IPC] Shutting down listener on {}", addr);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cleanup() does not actually stop the TCP listener.

cleanup() only clears the stored address string from state. The TcpListener and its accept loop (and any active client connections) continue running because the listener is owned by the spawned task, not by TcpSocketState. Unlike the Unix socket ipc::cleanup() which removes the socket file (preventing new connections), this cleanup is purely cosmetic.

To actually shut down, you'd need a cancellation mechanism (e.g., a tokio::sync::watch or CancellationToken) that the accept loop checks, or store a JoinHandle to abort the task.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 37 - 43, cleanup() only
removes the stored address string and does not stop the spawned accept loop or
active connections; update TcpSocketState to hold a cancellation mechanism
(e.g., a tokio::sync::watch, tokio_util::sync::CancellationToken, or the
JoinHandle of the spawned task) and modify the accept loop to listen for that
cancellation token or be abortable, then in cleanup() signal cancellation or
abort the JoinHandle and await/cleanup resources before dropping the address;
reference TcpSocketState, cleanup(), the spawned accept loop (the task that owns
the TcpListener) and whichever chosen token/handle so you can set and trigger it
from cleanup().

Comment on lines +64 to +94
async fn handle_client(stream: TcpStream, app: tauri::AppHandle) -> Result<(), String> {
let peer_addr = stream.peer_addr().map_err(|e| e.to_string())?;
let (reader, mut writer) = stream.into_split();
let reader = BufReader::new(reader);
let mut lines = reader.lines();

while let Some(line) = lines.next_line().await.map_err(|e| e.to_string())? {
eprintln!("[TCP IPC] Received from {}: {}", peer_addr, line);

let response = match serde_json::from_str::<IpcCommand>(&line) {
Ok(cmd) => {
eprintln!("[TCP IPC] Processing command: {}", cmd.command);
process_command(cmd, &app)
}
Err(e) => IpcResponse {
success: false,
error: Some(format!("Invalid JSON: {}", e)),
},
};

let json = serde_json::to_string(&response).unwrap_or_default();
eprintln!("[TCP IPC] Sending response to {}: {}", peer_addr, json);

writer
.write_all(format!("{}\n", json).as_bytes())
.await
.map_err(|e| e.to_string())?;
}

eprintln!("[TCP IPC] Connection closed: {}", peer_addr);
Ok(())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential log injection via unsanitized TCP input.

Line 71 logs the raw line received from the TCP socket, and line 75 logs cmd.command which is attacker-controlled input from any local process. While eprintln! is less exploitable than structured logging frameworks, newlines within the JSON string could forge log entries. Consider sanitizing or escaping logged values, or at minimum truncating them.

Also, Line 84: serde_json::to_string(&response).unwrap_or_default() will send an empty line "\n" to the client if serialization fails. Since IpcResponse is a simple struct this is unlikely, but sending an empty line could confuse a client expecting valid JSON. Consider sending a hardcoded error JSON instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/tauri/src-tauri/src/tcp_ipc.rs` around lines 64 - 94, The handle_client
function currently logs raw, attacker-controlled TCP input and command strings
(the `line` contents and `cmd.command`) which can contain newlines and inject
fake log entries; sanitize or escape/newline-normalize and/or truncate these
values before logging (e.g., replace newlines with escaped sequences and limit
length) when using the eprintln! calls and when logging inside the match branch
that processes `process_command`. Also replace the use of
serde_json::to_string(&response).unwrap_or_default() by detecting serialization
errors and returning a definite error payload (a hardcoded JSON error string) to
the client if serialization fails, so you never send an empty line; reference
`handle_client`, `process_command`, `IpcCommand`, and `IpcResponse` when making
these changes.

The app icon had a solid background that extended to the edges, preventing
macOS from properly applying its automatic corner rounding. This resulted in
a square icon appearance instead of the expected rounded corners.
Changes:
- Applied ~12% corner radius to all icon assets (matching macOS guidelines)
- Regenerated icon.icns with proper alpha channel masks
- Updated all PNG sizes (32x32, 64x64, 128x128, 256x256, 512x512, 1024x1024)
The icon now displays with proper rounded corners in Dock, Finder, and app switcher.
@wilcorrea
wilcorrea merged commit 31f187b into mainFeb 23, 2026
1 check passed
@wilcorrea
wilcorrea deleted the fix/relative-paths branch February 23, 2026 20:42
Sign up for freeto 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.

Relative paths lead to a blank screen

1 participant

@wilcorrea