Skip to content

Feature/add kill to stdio client #183

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

Crayzero
Copy link

@Crayzero Crayzero commented Apr 21, 2025

fix #167
According to the mcp sepecification
QQ_1745239110398

Kill proces on windows should recursively kill all children process because there are no process group on windows.
Kill process on linux/unix just send SIGTERM AND SIGKILL to the process.

Summary by CodeRabbit

  • New Features
    • Improved process termination: processes now have a timeout-based forced termination fallback during closure.
    • Enhanced Windows process termination to recursively kill child processes.
  • Bug Fixes
    • Ensured child processes on Windows are terminated when closing.
  • Tests
    • Added tests verifying proper process closure and failure of requests after closure.
  • Chores
    • Updated dependencies to support enhanced process management and system utilities.

Copy link
Contributor

coderabbitai bot commented Apr 21, 2025

"""

Walkthrough

This update introduces platform-specific process termination logic for the killProcess function in the client/transport package, with separate implementations for Windows and non-Windows systems. The Stdio.Close method is modified to implement a timeout when waiting for a subprocess to exit, invoking killProcess if the process does not terminate within three seconds. The test suite for the Stdio transport is expanded to verify that processes are properly terminated and that requests fail after closure. Several new dependencies are added to go.mod to support process management across platforms.

Changes

File(s) Change Summary
client/transport/close_on_other.go, client/transport/close_on_windows.go Added platform-specific killProcess functions for process termination using gopsutil and OS-specific logic.
client/transport/stdio.go Modified Stdio.Close to implement a timeout and invoke killProcess if the subprocess does not exit in time.
client/transport/stdio_test.go Added tests to verify process termination and error handling after closure.
go.mod Added new indirect dependencies for system/process management and utilities.

Assessment against linked issues

Objective Addressed Explanation
Ensure StdioMCPClient.Close terminates the mcp server child process (#167)
Add tests to verify process termination and error on requests after close (#167)
"""
✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 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 generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @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: 0

🧹 Nitpick comments (4)
client/transport/close_on_other.go (1)

12-30: Consider improving error handling logic when terminating processes

The implementation first attempts graceful termination with SIGTERM and then forceful termination with SIGKILL, which is good practice. However:

  1. The function only logs but ignores errors from proc.Terminate().
  2. The status check logic may be confusing - it returns nil when there's no error from Status(), which may not necessarily mean the process was terminated successfully.

Consider making the error handling more explicit:

func killByPid(pid int) error {
    proc, err := process.NewProcess(int32(pid))
    if err != nil {
        return fmt.Errorf("failed to find process with pid %d: %w", pid, err)
    }
    
    // first send SIGTERM
    err = proc.Terminate()
    if err != nil {
-       fmt.Printf("Failed to send SIGTERM to pid %d: %v\n", pid, err)
+       fmt.Printf("Failed to send SIGTERM to pid %d: %v, will try SIGKILL\n", pid, err)
    } else {
+       // Give the process a brief moment to terminate gracefully
+       time.Sleep(100 * time.Millisecond)
    }

    // Check if process still exists
    exists, err := process.PidExists(int32(pid))
-   _, err = proc.Status()
-   if err == nil {
-       // kill ok
+   if err != nil || !exists {
+       // Process is already gone, no need for SIGKILL
        return nil
    }
    
    // Process still exists, send SIGKILL
    killErr := proc.Kill()
-   return proc.Kill()
+   if killErr != nil {
+       return fmt.Errorf("failed to kill process with pid %d: %w", pid, killErr)
+   }
+   return nil
}
client/transport/close_on_windows.go (3)

18-27: Translate Chinese comments to English.

The code correctly implements recursive child process termination, but the comment on line 18 is in Chinese and should be translated to English for consistency and maintainability.

- // 获取所有子进程(递归)
+ // Get all child processes (recursively)

19-27: Consider more robust error handling for child process failures.

The current implementation prints errors when killing child processes but continues execution. Consider aggregating these errors or using a more robust logging mechanism rather than printing to stdout.

 if err == nil {
     for _, child := range children {
         err = killByPid(int(child.Pid)) // 递归杀子进程
         if err != nil {
-            fmt.Printf("Failed to kill pid %d: %v\n", child.Pid, err)
+            // Log error but continue with other children
+            fmt.Fprintf(os.Stderr, "Failed to kill child process %d: %v\n", child.Pid, err)
         }
     }
 }

29-39: Translate Chinese comment and consider enhancing error handling for main process.

The comment on line 29 is in Chinese and should be translated. Additionally, the error handling for the main process termination could be more descriptive.

- // 杀掉当前进程
+ // Kill the current process
 p, err := os.FindProcess(int(pid))
 if err == nil {
     // windows does not support SIGTERM, so we just use Kill()
     err = p.Kill()
     if err != nil {
-        fmt.Printf("Failed to kill pid %d: %v\n", pid, err)
+        fmt.Fprintf(os.Stderr, "Failed to kill main process %d: %v\n", pid, err)
     }
 }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9f39a43 and 174e815.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • client/transport/close_on_other.go (1 hunks)
  • client/transport/close_on_windows.go (1 hunks)
  • client/transport/stdio.go (2 hunks)
  • client/transport/stdio_test.go (2 hunks)
  • go.mod (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
client/transport/stdio_test.go (3)
client/transport/interface.go (1)
  • JSONRPCRequest (29-34)
testdata/mockstdio_server.go (1)
  • JSONRPCRequest (11-16)
mcp/types.go (1)
  • Params (104-104)
🔇 Additional comments (10)
go.mod (1)

14-23: LGTM - Dependencies for cross-platform process management added correctly

The new dependencies are appropriate for supporting the process termination functionality across different platforms. The github.com/shirou/gopsutil/v3 package and its transitive dependencies provide the necessary tools for process management.

client/transport/stdio.go (1)

111-124: Improved process termination with timeout

The implementation adds a non-blocking wait with a 3-second timeout before forcibly terminating the process. This aligns with the MCP specification for stdio lifecycle management and prevents potential hanging when processes don't exit cleanly.

client/transport/stdio_test.go (3)

16-16: LGTM - Import added to support process verification

The addition of the gopsutil process package is appropriate for verifying process termination in tests.


293-304: Good test for process termination

This test ensures that processes are properly terminated after calling Close(). It verifies the core functionality of the new process termination feature.


306-327: Good test for transport closure

This test verifies that sending requests after closing the transport fails with an error, ensuring the transport is properly unusable after closure.

client/transport/close_on_other.go (1)

1-3: LGTM - Build tags properly isolate platform-specific code

The build tags ensure this code only compiles on non-Windows platforms, which is appropriate since the implementation relies on Unix/Linux-specific signal handling.

client/transport/close_on_windows.go (4)

1-3: Build constraints properly configured for Windows.

The build constraints are correctly set up to ensure this file is only compiled on Windows systems.


6-11: Dependencies look appropriate for Windows process management.

The imports include the necessary packages: fmt for error logging, os for process management, and gopsutil for retrieving child processes - which is essential since Windows doesn't provide a built-in way to manage process groups.


13-17: Function signature and initial error handling look correct.

The killByPid function appropriately converts the integer PID to int32 for the gopsutil library and has proper error handling for the process creation.


13-39: Consider handling process tree changes during traversal.

The current implementation might miss new child processes spawned during traversal. While this is unlikely to be a major issue in typical scenarios, it's worth considering a more robust approach for critical applications.

For a more robust approach, you might need to repeatedly scan for children until no new ones are found. However, this would make the implementation more complex and might be unnecessary for most use cases. Consider whether this level of robustness is required for your application.

import (
"fmt"

"github.com/shirou/gopsutil/v3/process"
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd rather not introduce a new dependency if we can help it

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, but handling subprocess termination on Windows is tricky and can get messy. May be it's a trade off.

if err != nil {
return err
}
// 获取所有子进程(递归)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please translate to English for consistency

Copy link
Author

Choose a reason for hiding this comment

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

translated.

return err
}
// first send SIGTERM
err = proc.Terminate()
Copy link
Contributor

Choose a reason for hiding this comment

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

How about using cmd.Process.Kill()?

Copy link
Author

Choose a reason for hiding this comment

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

On linux/unix, we can simple use process.Signal() to send signal.
Changed it.

@@ -0,0 +1,39 @@
//go:build windows
Copy link
Contributor

Choose a reason for hiding this comment

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

I recommend to move it to a util package

Copy link
Author

Choose a reason for hiding this comment

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

Move to the top level package?
I think it's a pretty simple code snippet.

Copy link
Contributor

Choose a reason for hiding this comment

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

maybe client/transport/util ?

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

♻️ Duplicate comments (1)
client/transport/close_on_windows.go (1)

6-11: Third-party dependency introduction

I notice you're using github.com/shirou/gopsutil/v3 for process management. This aligns with the PR objective of properly implementing process termination on Windows, which requires handling child processes.

🧹 Nitpick comments (6)
client/transport/close_on_other.go (3)

15-16: Function comment casing doesn't match implementation

The comment uses "killprocess" while the function name is "killProcess" - the casing is inconsistent.

-// killprocess kills the process on non-windows platforms.
+// killProcess kills the process on non-windows platforms.

16-37: Implementation looks good, but consider some improvements

The process termination logic is correctly implemented with a graceful SIGTERM followed by a forced kill if needed. However, a few suggestions for improvement:

  1. The 200ms sleep is hardcoded - consider making this a constant for better maintainability
  2. Using string comparison for error detection is somewhat fragile

Consider defining the sleep duration as a constant for better maintainability:

+const gracefulTerminationWaitTime = 200 * time.Millisecond

func killProcess(proc *os.Process) error {
	err := proc.Signal(syscall.SIGTERM)
	if err != nil {
		fmt.Printf("Failed to send SIGTERM to pid %d: %v\n", proc.Pid, err)
	}
	// wait for a short time to allow the process to terminate gracefully
-	time.Sleep(200 * time.Millisecond)
+	time.Sleep(gracefulTerminationWaitTime)
	// ...rest of the function

19-20: Consider using structured logging instead of fmt.Printf

Using fmt.Printf for logging may not integrate well with the rest of the application's logging system.

client/transport/close_on_windows.go (3)

13-41: Recursive process termination looks good, but consider error collection

The recursive approach to killing child processes is appropriate for Windows. However, errors from killing child processes are logged but not returned or collected.

Consider collecting errors from child process termination to provide better diagnostics:

func killByPid(pid int) error {
	proc, err := process.NewProcess(int32(pid))
	if err != nil {
		return err
	}
+	var childErrors []error
	// get all subprocess recursively
	children, err := proc.Children()
	if err == nil {
		for _, child := range children {
			err = killByPid(int(child.Pid)) // kill all subprocesses
			if err != nil {
				fmt.Printf("Failed to kill pid %d: %v\n", child.Pid, err)
+				childErrors = append(childErrors, fmt.Errorf("failed to kill child pid %d: %w", child.Pid, err))
			}
		}
	}

	// kill current process
	p, err := os.FindProcess(int(pid))
	if err == nil {
		// windows does not support SIGTERM, so we just use Kill()
		err = p.Kill()
		if err != nil {
			fmt.Printf("Failed to kill pid %d: %v\n", pid, err)
		}
	}
+	// If we had child errors but the main process was killed successfully, still report issues
+	if err == nil && len(childErrors) > 0 {
+		return fmt.Errorf("killed pid %d but failed to kill some child processes", pid)
+	}
	return err
}

26-27: Consider using structured logging instead of fmt.Printf

Similar to the non-Windows implementation, using fmt.Printf for logging may not integrate well with the rest of the application's logging system.

Also applies to: 37-38


43-44: Function comment casing doesn't match implementation

The comment uses "KillProcess" while the function name is "killProcess" - the casing is inconsistent.

-// KillProcess kills the process on windows.
+// killProcess kills the process on windows.
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between ee3e773 and b5ef487.

📒 Files selected for processing (3)
  • client/transport/close_on_other.go (1 hunks)
  • client/transport/close_on_windows.go (1 hunks)
  • client/transport/stdio.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/transport/stdio.go
🔇 Additional comments (1)
client/transport/close_on_windows.go (1)

43-49: Proper implementation of platform-specific process termination

The implementation correctly handles process termination on Windows by:

  1. Checking for nil process pointers
  2. Using the Windows-specific recursive termination approach
  3. Maintaining API compatibility with the non-Windows version

This satisfies the PR objective of implementing proper process termination according to the MCP specification.

"fmt"
"os"

"github.com/shirou/gopsutil/v3/process"
Copy link
Contributor

Choose a reason for hiding this comment

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

I still don't like this dep. Can you justify why it is actually needed?

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.

The StdioMCPClient Close method does not teminated the mcp server child process when called
3 participants