Skip to content

cli: add dev lock file to prevent 2 dev processes running at the same time in the same dir #1854

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 31, 2025

Conversation

ericallam
Copy link
Member

@ericallam ericallam commented Mar 31, 2025

Summary by CodeRabbit

  • New Features
    • Introduced an automatic lock file management system for development sessions. A lock file is created at the start and reliably removed when the session stops, ensuring improved session stability and reducing potential conflicts.

Copy link

changeset-bot bot commented Mar 31, 2025

⚠️ No Changeset found

Latest commit: fb61d95

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

coderabbitai bot commented Mar 31, 2025

Walkthrough

This pull request introduces a lock file mechanism within the development session. The dev.ts file now imports and calls a new function, createLockFile, to generate a lock file during the session and later remove it when the session stops. A new file, lock.ts, implements the functionality by checking for an existing lock, handling process termination if necessary, and returning a cleanup function. This update ensures that only one development process runs at a time by managing the lock file lifecycle.

Changes

Files Change Summary
packages/cli-v3/src/commands/dev.ts Added an import for createLockFile and invoked it within startDev to get a cleanup function, which is then called in the stop method to remove the lock file.
packages/cli-v3/src/dev/lock.ts Introduced a new file implementing createLockFile. The function handles lock file creation, checks existing locks (terminating stale processes if needed), and returns a cleanup function.

Sequence Diagram(s)

sequenceDiagram
    participant DevCmd as Dev Command
    participant Lock as createLockFile (lock.ts)
    participant Proc as Other Process

    DevCmd->>Lock: Call createLockFile(cwd)
    alt Lock file exists and is owned by same process
        Lock-->>DevCmd: Return cleanup function
    else Lock file exists and owned by another process
        Lock->>Proc: Attempt to terminate process (PID check)
        Proc-->>Lock: Process termination / error logged
        Lock-->>DevCmd: Return cleanup function after wait
    else No lock file exists
        Lock-->>DevCmd: Write lock file and return cleanup function
    end
    DevCmd->>Lock: On stop, execute cleanup to remove lock file
Loading

Possibly related issues

Poem

I'm a rabbit, hopping with glee,
Managing locks so carefully.
A file named "dev.lock" keeps mischief at bay,
Ensuring one session plays at a time each day.
CodeRabbit cheers—onward we hop away!


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ca3183 and fb61d95.

📒 Files selected for processing (2)
  • packages/cli-v3/src/commands/dev.ts (3 hunks)
  • packages/cli-v3/src/dev/lock.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli-v3/src/commands/dev.ts
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: units / 🧪 Unit Tests
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
packages/cli-v3/src/dev/lock.ts (2)

48-76: Be cautious about continuing when unable to kill the existing process.

When process.kill(existingPid) fails, the code logs a debug statement and continues as though the existing process doesn’t hold the lock anymore. This can introduce a situation where two processes run concurrently under certain conditions (e.g., insufficient permissions to kill). Consider adding a fallback check for the lock file or surfacing an error if the kill fails to ensure there isn't an unintended overlap.


89-93: Good job ensuring the directory’s existence before writing the lock file.

Verifying and creating the containing folder prevents errors on systems without a pre-existing .trigger directory.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 2

🧹 Nitpick comments (3)
packages/cli-v3/src/dev/lock.ts (3)

29-32: Consider using a more structured lock file format.

The current implementation stores only the PID in the lock file. Consider using a JSON format that includes additional information such as the app name, start time, and command line, which would help verify the PID actually belongs to another instance of this application.

  if (existingLockfileContents) {
-    // Read the pid number from the lockfile
-    const existingPid = Number(existingLockfileContents);
+    // Try to parse the lock file content as JSON first
+    try {
+      const lockData = JSON.parse(existingLockfileContents);
+      const existingPid = lockData.pid;
+      const timestamp = lockData.timestamp;
+      const appName = lockData.appName;
+      
+      logger.debug("Lockfile exists", { 
+        lockFilePath, 
+        existingPid,
+        currentPid,
+        timestamp,
+        appName 
+      });
+      
+      // Continue with existing logic...
+    } catch (e) {
+      // Fallback to the simple PID format for backward compatibility
+      const existingPid = Number(existingLockfileContents);
+      logger.debug("Lockfile exists (legacy format)", { lockFilePath, existingPid, currentPid });

This would be paired with a change to the lock file creation:

  // Now write the current pid to the lockfile
-  await writeFile(lockFilePath, currentPid.toString());
+  const lockData = {
+    pid: currentPid,
+    timestamp: new Date().toISOString(),
+    appName: "trigger-cli-dev",
+    command: process.argv.join(" ")
+  };
+  await writeFile(lockFilePath, JSON.stringify(lockData, null, 2));

55-75: Improve process termination with escalation to SIGKILL.

The current implementation sends a SIGTERM to the existing process, but doesn't escalate to SIGKILL if the process doesn't terminate. Additionally, consider a more graceful approach by first sending SIGINT (Ctrl+C equivalent) before escalating.

    try {
-      process.kill(existingPid);
+      // First try SIGINT (like Ctrl+C) for a graceful shutdown
+      process.kill(existingPid, 'SIGINT');
+      
       // If it did kill the process, it will have exited, deleting the lockfile, so let's wait for that to happen
       // But let's not wait forever
       await new Promise((resolve, reject) => {
+        let killAttempts = 0;
         const timeout = setTimeout(() => {
           clearInterval(interval);
-          reject(new Error("Timed out waiting for lockfile to be deleted"));
+          // Last resort: force kill with SIGKILL
+          try {
+            logger.debug("Timed out waiting for process to exit, sending SIGKILL", { existingPid });
+            process.kill(existingPid, 'SIGKILL');
+            // Give it a moment to clean up
+            setTimeout(() => {
+              if (existsSync(lockFilePath)) {
+                // If lock file still exists, remove it forcefully
+                try {
+                  unlinkSync(lockFilePath);
+                  resolve(true);
+                } catch (e) {
+                  reject(new Error(`Failed to forcefully remove lock file: ${e}`));
+                }
+              } else {
+                resolve(true);
+              }
+            }, 500);
+          } catch (killError) {
+            logger.debug("Failed to kill process with SIGKILL", { error: killError });
+            reject(new Error("Failed to kill existing process"));
+          }
         }, 5000);

         const interval = setInterval(() => {
           if (!existsSync(lockFilePath)) {
             clearInterval(interval);
             clearTimeout(timeout);
             resolve(true);
           }
+          // Escalate to SIGTERM after a few seconds of SIGINT not working
+          killAttempts++;
+          if (killAttempts === 20) { // After ~2 seconds
+            try {
+              logger.debug("Process did not exit with SIGINT, trying SIGTERM", { existingPid });
+              process.kill(existingPid, 'SIGTERM');
+            } catch (e) {
+              // Ignore errors when sending signals
+            }
+          }
         }, 100);
       });
     } catch (error) {
       logger.debug("Failed to kill existing process, lets assume it's not running", { error });
+      // If we couldn't kill it, try to remove the lock file anyway
+      try {
+        unlinkSync(lockFilePath);
+        logger.debug("Removed stale lock file", { lockFilePath });
+      } catch (e) {
+        logger.debug("Failed to remove stale lock file", { error: e });
+      }
     }

17-24: Consider consistent async/sync file operations.

The code mixes synchronous (unlinkSync) and asynchronous (writeFile, readFile) file operations. While this works, consistency would improve maintainability.

  const removeLockFile = () => {
    try {
      logger.debug("Removing lockfile", { lockFilePath });
-      return unlinkSync(lockFilePath);
+      // Use the asynchronous version for consistency
+      // However, since this is used in an exit handler, sync is actually better
+      // Just adding a comment to clarify the intentional use of sync methods
+      // in an otherwise async codebase
+      return unlinkSync(lockFilePath); // Using sync version intentionally for exit handler
    } catch (e) {
      // This sometimes fails on Windows with EBUSY
+      logger.debug("Failed to remove lock file", { error: e });
    }
  };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8977d25 and 2ca3183.

📒 Files selected for processing (2)
  • packages/cli-v3/src/commands/dev.ts (3 hunks)
  • packages/cli-v3/src/dev/lock.ts (1 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
packages/cli-v3/src/commands/dev.ts (1)
packages/cli-v3/src/dev/lock.ts (1)
  • createLockFile (11-87)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: units / 🧪 Unit Tests
  • GitHub Check: typecheck / typecheck
🔇 Additional comments (3)
packages/cli-v3/src/commands/dev.ts (3)

14-14: Good addition of the lock file import.

This import enables the locking mechanism to prevent multiple development processes from running simultaneously.


124-125: Lock file implementation looks good.

The createLockFile function is called with the correct working directory and its return value (cleanup function) is properly stored for later use.


184-184: Proper cleanup in the stop method.

The removeLockFile function is correctly called when stopping the dev session, ensuring the lock file is removed when the application exits normally.

@matt-aitken matt-aitken merged commit f320e14 into main Mar 31, 2025
12 checks passed
@matt-aitken matt-aitken deleted the ea-branch-31 branch March 31, 2025 15:27
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.

2 participants