Skip to content

Conversation

@QaidVoid
Copy link
Member

@QaidVoid QaidVoid commented Feb 11, 2026

Summary by CodeRabbit

  • New Features

    • Added cross-process file locking for package installation to prevent concurrent installs of the same package.
  • Bug Fixes

    • Prevented double-installation and incorrect install counts during simultaneous installation attempts; installer now skips already-installed packages post-lock.
  • Chores

    • Adjusted workspace dependency features to include filesystem locking support.

@codecov
Copy link

codecov bot commented Feb 11, 2026

Codecov Report

❌ Patch coverage is 64.74820% with 49 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/soar-cli/src/install.rs 0.00% 39 Missing ⚠️
crates/soar-utils/src/lock.rs 90.90% 9 Missing ⚠️
crates/soar-utils/src/fs.rs 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@QaidVoid
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

Adds a file-based cross-process locking mechanism and integrates it into package installation; updates error types and exports, adjusts install logic to acquire locks with retry and re-checks to avoid double-installation, and adds a nix fs feature in workspace dependencies.

Changes

Cohort / File(s) Summary
Locking Infrastructure
Cargo.toml, crates/soar-utils/src/lib.rs, crates/soar-utils/src/lock.rs, crates/soar-utils/src/error.rs
Adds fs feature to nix in workspace dependencies; exports new lock module; implements FileLock with acquire()/try_acquire() and path helpers; introduces LockError and LockResult for lock-related errors.
Installation Updates
crates/soar-cli/src/install.rs
Integrates FileLock into install flow with blocking acquire (500ms backoff, first-wait log), re-checks core DB after acquiring lock, and only records/increments installs when a real installation occurred to prevent double-installation.
Minor Formatting
crates/soar-utils/src/fs.rs
Small punctuation/formatting tweak in safe_remove error arm; no behavior change.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Installer
    participant Lock as FileLock
    participant FS as File System
    participant DB as CoreRepository
    participant Worker as Install Task

    Client->>Lock: acquire(package_name) (block, retry 500ms)
    activate Lock
    Lock->>FS: open/create .lock file
    FS-->>Lock: file handle
    Lock->>FS: flock exclusive
    FS-->>Lock: lock acquired
    deactivate Lock

    Client->>DB: is_installed(package_name)?
    activate DB
    DB-->>Client: installed? (yes/no)
    deactivate DB

    alt not installed
        Client->>Worker: perform installation
        activate Worker
        Worker-->>Client: install_result (install_dir)
        deactivate Worker
    else already installed
        Client-->>Client: log & skip installation
    end

    Client->>Lock: drop FileLock (unlock on drop)
    activate Lock
    Lock->>FS: release lock
    deactivate Lock
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I nibble at a lock file, tidy, neat, and small,
Guarding package burrows so installs won't fall,
With flock and patience, we queue in gentle rows,
No double hops, no scrambled toes —
Hooray for orderly installs for one and all! 🎩🔐

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and accurately describes the main objective: adding a locking mechanism for concurrent process safety. It directly relates to the primary changes across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/lock

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@crates/soar-cli/src/install.rs`:
- Around line 1094-1116: The loop in install_single_package uses
std::thread::sleep which blocks the tokio worker thread; replace that blocking
call with an awaitable tokio::time::sleep and make the surrounding
lock-acquisition loop async-aware so you can .await the sleep. Locate the loop
that calls FileLock::try_acquire and change
std::thread::sleep(Duration::from_millis(500)) to
tokio::time::sleep(Duration::from_millis(500)).await, ensuring
install_single_package remains async and any error mapping around the loop (the
.map_err and SoarError::Custom handling) is preserved.
- Around line 1108-1116: The code double-wraps the same message: the Err(err)
arm constructs SoarError::Custom("Failed to acquire package lock: {err}") and
then the chained .map_err(...) wraps it again. Remove the trailing .map_err(|e|
SoarError::Custom(format!("Failed to acquire package lock: {}", e))) so the Err
arm’s SoarError::Custom is returned directly (or alternatively, change the Err
arm to propagate the original error and keep the map_err—pick one); reference
the Err(err) arm and the .map_err call around the package-lock acquisition and
ensure only a single SoarError::Custom formatting is performed.

In `@crates/soar-utils/src/lock.rs`:
- Around line 46-57: The current sanitize closure in lock.rs maps all non
[a-zA-Z0-9._-] chars to '_' which can cause distinct package names (e.g.,
"pkg/foo" vs "pkg@foo") to collide; update the lock filename generation to
append or replace the sanitized name with a short collision-resistant digest
(e.g., a hex or base32 hash of the original package name) so filenames remain
filesystem-safe but unique — locate the sanitize closure and the code that
builds the lock filename and change it to include a hash of the original string
(or use the hash as the filename with the sanitized name as a prefix) to avoid
collisions while preserving readability.
- Around line 135-138: The Drop implementation for FileLock currently unlinks
the lock file (fs::remove_file) which creates a race where a new path can be
created while another process still holds the inode lock; remove the unlink from
FileLock::drop so the lock file is not deleted on drop (leave the file on disk
after releasing the flock), or implement the alternative verification: after
acquiring the flock in the FileLock constructor/lock method, compare the locked
fd's inode+device via fstat with the path's stat and retry if they differ;
update references to FileLock, Drop::drop, and any lock-acquisition code paths
used by install.rs accordingly.
- Around line 25-40: The fallback in lock_dir() uses temp_dir which is
world-shared; change lock_dir() so that when XDG_RUNTIME_DIR is None you append
a per-user component (e.g., "soar-{uid}") to the temp_dir before joining "locks"
to isolate users; obtain the current UID (via libc::getuid() or a crate helper)
and incorporate it into the base path used when std::env::var("XDG_RUNTIME_DIR")
is None, then create that directory as before (fs::create_dir_all) so locks are
placed under a user-specific temp subdirectory.
🧹 Nitpick comments (3)
crates/soar-utils/src/lock.rs (1)

74-91: acquire() blocks indefinitely with no timeout.

Flock::lock with LockExclusive will block forever if the holding process hangs or crashes without releasing. Consider documenting this behavior, or providing an optional timeout (e.g., converting to a try_acquire loop with a maximum attempt count, similar to how install.rs already retries).

crates/soar-cli/src/install.rs (2)

1094-1116: No upper bound on lock retry attempts — can spin indefinitely.

If a lock is held indefinitely (e.g., a crashed process that left a stale flock — though flock is released on process exit, edge cases exist), this loop never terminates. Consider adding a maximum attempt count or total timeout, and returning an error if exceeded.


1042-1050: Using an empty PathBuf as a sentinel for "skipped" is fragile.

Returning PathBuf::new() to signal "not actually installed" couples the caller to checking is_empty() on the path. A more explicit approach would be to wrap the return type in an Option or a dedicated enum, making the "skipped" case unambiguous.

Comment on lines +25 to +40
fn lock_dir() -> LockResult<PathBuf> {
let xdg_runtime = std::env::var("XDG_RUNTIME_DIR").ok();
let base = if let Some(ref runtime) = xdg_runtime {
PathBuf::from(runtime)
} else {
std::env::temp_dir()
};

let lock_dir = base.join("soar").join("locks");

if !lock_dir.exists() {
fs::create_dir_all(&lock_dir)?;
}

Ok(lock_dir)
}
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 11, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fallback lock directory in /tmp is world-writable — potential for cross-user interference.

When XDG_RUNTIME_DIR is unset, the lock directory falls back to <temp_dir>/soar/locks (typically /tmp/soar/locks). Any user can create or hold lock files there, which could cause:

  • A denial-of-service where another user holds the lock indefinitely
  • Permission errors if the directory was created by a different user

Since soar is a per-user package manager, consider including the UID in the fallback path (e.g., /tmp/soar-{uid}/locks) to isolate users.

Proposed fix
     fn lock_dir() -> LockResult<PathBuf> {
         let xdg_runtime = std::env::var("XDG_RUNTIME_DIR").ok();
         let base = if let Some(ref runtime) = xdg_runtime {
             PathBuf::from(runtime)
         } else {
-            std::env::temp_dir()
+            let uid = nix::unistd::getuid();
+            std::env::temp_dir().join(format!("soar-{}", uid))
         };
 
-        let lock_dir = base.join("soar").join("locks");
+        let lock_dir = base.join("soar").join("locks");
🤖 Prompt for AI Agents
In `@crates/soar-utils/src/lock.rs` around lines 25 - 40, The fallback in
lock_dir() uses temp_dir which is world-shared; change lock_dir() so that when
XDG_RUNTIME_DIR is None you append a per-user component (e.g., "soar-{uid}") to
the temp_dir before joining "locks" to isolate users; obtain the current UID
(via libc::getuid() or a crate helper) and incorporate it into the base path
used when std::env::var("XDG_RUNTIME_DIR") is None, then create that directory
as before (fs::create_dir_all) so locks are placed under a user-specific temp
subdirectory.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not necessary.

Copy link
Contributor

Choose a reason for hiding this comment

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

@QaidVoid, understood. I'll defer to your judgment on this.

(\_/)
( •_•)
/ >  ✓

Comment on lines +46 to +57
// Sanitize the package name to ensure a valid filename
let sanitize = |s: &str| {
s.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' {
c
} else {
'_'
}
})
.collect::<String>()
};
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 11, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Sanitization could cause lock collisions between distinct package names.

Characters outside [a-zA-Z0-9._-] are all mapped to _, so e.g. "pkg/foo" and "pkg@foo" would both produce "pkg_foo.lock". If package names can contain such characters, consider using a hash-based filename or a more collision-resistant encoding.

🤖 Prompt for AI Agents
In `@crates/soar-utils/src/lock.rs` around lines 46 - 57, The current sanitize
closure in lock.rs maps all non [a-zA-Z0-9._-] chars to '_' which can cause
distinct package names (e.g., "pkg/foo" vs "pkg@foo") to collide; update the
lock filename generation to append or replace the sanitized name with a short
collision-resistant digest (e.g., a hex or base32 hash of the original package
name) so filenames remain filesystem-safe but unique — locate the sanitize
closure and the code that builds the lock filename and change it to include a
hash of the original string (or use the hash as the filename with the sanitized
name as a prefix) to avoid collisions while preserving readability.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's fine. Package names shouldn't usually contain those characters.

Copy link
Contributor

Choose a reason for hiding this comment

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

@QaidVoid, understood! I've noted that for future reference.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@crates/soar-cli/src/install.rs`:
- Line 1096: The lock currently calls
FileLock::try_acquire(&target.package.pkg_name) which only uses pkg_name; change
it to use a composite key built from the package identity used elsewhere
(repo_name, pkg_name, pkg_id) so locks are unique per package instance. Locate
the call to FileLock::try_acquire in install.rs (the line with
FileLock::try_acquire(&target.package.pkg_name)) and replace the argument with a
formatted composite string like format!("{repo}:{name}:{id}") using
target.repository.repo_name (or equivalent), target.package.pkg_name, and
target.package.pkg_id to construct the lock key. Ensure the composite string is
deterministic and used wherever package-level locking is expected so different
repos with the same pkg_name do not contend on the same lock.
🧹 Nitpick comments (2)
crates/soar-utils/src/lock.rs (1)

74-91: acquire() blocks indefinitely with no timeout.

The blocking Flock::lock call has no timeout mechanism. If a peer process stalls (not crashes — the kernel reclaims on exit), callers will hang forever. The install path in install.rs wisely uses try_acquire in a polling loop, but acquire is a pub method. Consider either documenting the no-timeout semantics prominently or providing a acquire_timeout(Duration) variant.

crates/soar-cli/src/install.rs (1)

1042-1050: Using an empty PathBuf as a sentinel for "skipped" is fragile.

The skip signal from install_single_package returning (PathBuf::new(), Vec::new()) (line 1143) is detected here via install_dir.as_os_str().is_empty(). This works but couples two distant code sites via an implicit contract on an empty path. A more explicit approach would be to change the return type (e.g., Option<(PathBuf, Vec<…>)> where None = skipped), which would make the skip case self-documenting and compiler-enforced.


let mut lock_attempts = 0;
let _package_lock = loop {
match FileLock::try_acquire(&target.package.pkg_name) {
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 11, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine the FileLock implementation
find . -name "lock.rs" -o -name "*lock*" | grep -E "\.rs$" | head -10

Repository: pkgforge/soar

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Check the install.rs context around line 1096
sed -n '1085,1110p' crates/soar-cli/src/install.rs

Repository: pkgforge/soar

Length of output: 968


🏁 Script executed:

#!/bin/bash
# Search for FileLock::try_acquire usage to understand the pattern
rg -n 'FileLock::try_acquire' --type rust -B2 -A2

Repository: pkgforge/soar

Length of output: 1125


🏁 Script executed:

#!/bin/bash
# Check if pkg_name is unique across repos or if composite keys are used elsewhere
rg -n '(repo_name|pkg_name|pkg_id)' crates/soar-utils/src/ --type rust | head -30

Repository: pkgforge/soar

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Read the FileLock implementation
cat -n crates/soar-utils/src/lock.rs

Repository: pkgforge/soar

Length of output: 6806


🏁 Script executed:

#!/bin/bash
# Search for package structures and how repo_name is associated with pkg_name
rg -n 'pub struct Package|struct Package' --type rust -A10 crates/soar-utils/src/

Repository: pkgforge/soar

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check if pkg_name is guaranteed unique across repos or if there's documentation
rg -n 'pkg_name.*unique|unique.*pkg_name' --type rust -i

Repository: pkgforge/soar

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Search for how packages are stored/queried to understand if pkg_name is unique within repos only
rg -n 'repo_name.*pkg_name|pkg_name.*repo_name' --type rust -B2 -A2 | head -50

Repository: pkgforge/soar

Length of output: 3306


🏁 Script executed:

#!/bin/bash
# Check the apply.rs file around the context mentioned in the learning
sed -n '140,160p' crates/soar-cli/src/apply.rs

Repository: pkgforge/soar

Length of output: 698


🏁 Script executed:

#!/bin/bash
# Search for package query patterns to see how uniqueness is handled
rg -n 'WHERE|SELECT' --type rust -B1 -A1 | grep -E 'repo_name|pkg_name|pkg_id' | head -20

Repository: pkgforge/soar

Length of output: 166


🏁 Script executed:

#!/bin/bash
# Check if there's documentation or tests showing packages with same name from different repos
rg -n 'different.*repo|multiple.*repo' --type rust -i -C2 | head -30

Repository: pkgforge/soar

Length of output: 1896


Consider using a composite lock key if same package names can exist across different repositories.

The lock uses only pkg_name, but packages throughout the codebase are identified by the composite tuple (repo_name, pkg_name, pkg_id). Database queries and deduplication logic all use this composite key pattern. If the same pkg_name exists in different repositories, they'll unnecessarily serialize on a shared lock file. If this scenario is rare/acceptable, it's fine—but for consistency and to prevent unnecessary contention, consider using a composite key like format!("{}:{}:{}", repo_name, pkg_name, pkg_id) for the lock.

🤖 Prompt for AI Agents
In `@crates/soar-cli/src/install.rs` at line 1096, The lock currently calls
FileLock::try_acquire(&target.package.pkg_name) which only uses pkg_name; change
it to use a composite key built from the package identity used elsewhere
(repo_name, pkg_name, pkg_id) so locks are unique per package instance. Locate
the call to FileLock::try_acquire in install.rs (the line with
FileLock::try_acquire(&target.package.pkg_name)) and replace the argument with a
formatted composite string like format!("{repo}:{name}:{id}") using
target.repository.repo_name (or equivalent), target.package.pkg_name, and
target.package.pkg_id to construct the lock key. Ensure the composite string is
deterministic and used wherever package-level locking is expected so different
repos with the same pkg_name do not contend on the same lock.

Copy link
Member Author

Choose a reason for hiding this comment

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

Even though the different pkg_id or repo_name is used, they might symlink as a same binary resulting in conflicts.

Copy link
Contributor

Choose a reason for hiding this comment

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

@QaidVoid, that's a great point! You're absolutely right. Using pkg_name as the lock key makes sense because different package variants (different pkg_id or repo_name) that share the same pkg_name will ultimately create symlinks with the same binary names in the bin directory. If they installed concurrently with a more granular lock, they'd race on those symlink creations.

The current implementation correctly serializes installations at the pkg_name level to prevent these symlink conflicts. Thank you for the clarification!


✏️ Learnings added
Learnt from: QaidVoid
Repo: pkgforge/soar PR: 154
File: crates/soar-cli/src/install.rs:1096-1096
Timestamp: 2026-02-11T16:23:54.342Z
Learning: In crates/soar-cli/src/install.rs, FileLock uses only pkg_name (not a composite key with repo_name/pkg_id) intentionally. Different package variants with the same pkg_name but different pkg_id or repo_name will symlink to the same binary names in the bin directory. Locking at the pkg_name level prevents symlink race conditions during concurrent installations of different variants.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: QaidVoid
Repo: pkgforge/soar PR: 137
File: crates/soar-cli/src/apply.rs:149-153
Timestamp: 2026-01-09T16:14:21.516Z
Learning: In crates/soar-cli/src/apply.rs, for URL packages, passing non-installed records (is_installed=false) as existing_install is intentional. The query filters by repo_name, pkg_name, and pkg_id, ensuring the record is for the same package. This allows resuming partial installs and preserving settings (portable paths, profile) from previous installs.

@QaidVoid QaidVoid merged commit e3bef6a into main Feb 11, 2026
7 checks passed
@QaidVoid QaidVoid mentioned this pull request Feb 11, 2026
github-actions bot pushed a commit to Azathothas/soar that referenced this pull request Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant