Skip to content

Add a ProcessManager interface for managing processes by pid file #293

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 1 commit into
base: main
Choose a base branch
from

Conversation

vyasgun
Copy link
Contributor

@vyasgun vyasgun commented Apr 4, 2025

This commit does the following:

  • Adds ProcessManager interface and Process struct
  • Read and write to a pidFile specified by the user
  • Adds tests for process lifecycle with a dummy process

Fixes: #278

Summary by Sourcery

Implement a ProcessManager interface for managing processes using pid files

New Features:

  • Create a Process struct and Manager interface to handle process lifecycle management using pid files

Tests:

  • Add comprehensive test suite for process management, including process creation, finding, checking existence, and killing processes

Copy link

sourcery-ai bot commented Apr 4, 2025

Reviewer's Guide by Sourcery

This pull request introduces a ProcessManager interface and a Process struct to manage external processes by reading and writing their PIDs to a specified file. It includes functionality to find, check the existence of, and kill processes, along with comprehensive tests to ensure the correct lifecycle management of processes.

Sequence diagram for finding and killing a process

sequenceDiagram
    participant P as Process
    participant OS as Operating System

    P->>P: Pid()
    P->>OS: ReadFile(PidFilePath())
    OS-->>P: pid (string)
    P->>P: Atoi(pid)
    P->>OS: FindProcess(pid)
    OS-->>P: process, error
    alt error != nil
        P-->>P: Return error
    else
        P->>OS: Signal(SIGKILL)
        OS-->>P: error
        alt error != nil
            P-->>P: Return error
        else
            P->>OS: Wait()
            OS-->>P: _, error
            alt error != nil
                P-->>P: Return error
            else
                P-->>P: Return nil
            end
        end
    end
Loading

File-Level Changes

Change Details Files
Introduce a ProcessManager interface and its implementation, the Process struct, for managing processes via PID files.
  • Define a ProcessManager interface with methods for process lifecycle management.
  • Implement the ProcessManager interface with a Process struct.
  • Implement methods to read and write the PID to a file.
  • Implement methods to find, check existence, and kill processes.
pkg/process/process.go
Add tests for process lifecycle management.
  • Create a dummy process for testing purposes.
  • Test finding an existing process.
  • Test checking the existence of a process.
  • Test killing a process.
  • Test the scenario when the pid file contains invalid data.
pkg/process/process_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!
  • Generate a plan of action for an issue: Comment @sourcery-ai plan on
    an issue to generate a plan of action for it.

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@openshift-ci openshift-ci bot requested review from anjannath and praveenkumar April 4, 2025 02:24
Copy link

openshift-ci bot commented Apr 4, 2025

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign praveenkumar for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @vyasgun - I've reviewed your changes - here's some feedback:

Overall Comments:

  • Consider adding a Close() method to the ProcessManager interface to handle cleanup operations like removing the pid file.
  • The New function doesn't return an error, even though the signature allows it; consider removing the error return or adding a check.
Here's what I looked at during the review
  • 🟡 General issues: 1 issue found
  • 🟢 Security: all looks good
  • 🟡 Testing: 2 issues found
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@vyasgun vyasgun force-pushed the pr/pid-management branch from 112f790 to 4a6f13b Compare April 4, 2025 04:42
@vyasgun
Copy link
Contributor Author

vyasgun commented Apr 4, 2025

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @vyasgun - I've reviewed your changes - here's some feedback:

Overall Comments:

  • Consider adding a Close() method to the Process struct to handle cleaning up resources like the pid file.
  • The Kill() method should probably remove the pid file after successfully killing the process.
Here's what I looked at during the review
  • 🟡 General issues: 3 issues found
  • 🟢 Security: all looks good
  • 🟡 Testing: 1 issue found
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 27 to 28
func New(name, pidFilePath string) (*Process, error) {
return &Process{name: name, pidFilePath: pidFilePath}, nil
Copy link

Choose a reason for hiding this comment

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

suggestion: Reconsider New() returning an error.

Since the constructor never fails, it might be cleaner to have New() simply return *Process. If error checking is anticipated in future extensions, a brief comment explaining the design decision would be helpful.

Suggested implementation:

func New(name, pidFilePath string) *Process { // Currently always succeeds; change signature if error handling is needed in the future.
	return &Process{name: name, pidFilePath: pidFilePath}

Copy link
Contributor

Choose a reason for hiding this comment

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

Good advice! the error is not needed, no reason to add it.

Copy link

Choose a reason for hiding this comment

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

Sounds good—let’s simplify the constructor. We can drop the error return and update any callers/tests accordingly. For example:

func New(name, pidFilePath string) *Process {
    return &Process{name: name, pidFilePath: pidFilePath}
}

Then:

  • Remove the error from the signature.
  • Delete the unused github.com/pkg/errors import if there are no other error-wrapping calls in that file.
  • Update any tests or code that expected two return values from New.

If you agree, go ahead and push that change. Otherwise, let me know if you see any reason to keep the error return in place for future-proofing.

Comment on lines 39 to 51
func (p *Process) Pid() (int, error) {
data, err := os.ReadFile(p.PidFilePath())
if err != nil {
return -1, err
}
pidStr := strings.TrimSpace(string(data))
pid, err := strconv.Atoi(pidStr)
Copy link

Choose a reason for hiding this comment

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

suggestion: Wrap conversion error with more context.

While errors from strconv.Atoi are returned directly, adding contextual information (similar to the wrapping used elsewhere) could facilitate debugging if the PID file is malformed. This would improve consistency in error handling.

Copy link
Contributor

Choose a reason for hiding this comment

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

It may help - need to test invalid pid with real code using the library and see if the error makes sense or we need to add some context.

Copy link

Choose a reason for hiding this comment

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

That sounds reasonable. To get a feel for whether additional context is useful, you could write a quick invalid‐PID test:

func TestPidMalformed(t *testing.T) {
    tmp := t.TempDir()
    pidFile := filepath.Join(tmp, "test.pid")
    // write something non‐numeric
    os.WriteFile(pidFile, []byte("notanumber"), 0644)

    p, _ := New("dummy", pidFile)
    _, err := p.Pid()
    if err == nil {
        t.Fatal("expected error when reading malformed PID")
    }
    t.Log("got error:", err)
}

If you run that and see something like strconv.Atoi: parsing "notanumber": invalid syntax, you’ll notice it doesn’t tell you which file failed. We could wrap it like this:

 func (p *Process) Pid() (int, error) {
     data, err := os.ReadFile(p.PidFilePath())
     if err != nil {
         return -1, err
     }
     pidStr := strings.TrimSpace(string(data))
-    pid, err := strconv.Atoi(pidStr)
-    return pid, err
+    pid, err := strconv.Atoi(pidStr)
+    if err != nil {
+        return -1, errors.Wrapf(err, "parsing PID from file %s", p.PidFilePath())
+    }
+    return pid, nil
 }

That way the user sees both the parse error and the file path. Let me know if that addresses your concern or if you’d prefer a different error message format.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why add an old dependency (errors.Wrapf) when we can use standard library (fmt.Errorf("...: %w", err))?

Copy link

Choose a reason for hiding this comment

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

You’re absolutely right—since we’re targeting Go 1.13+ we can just use the stdlib’s fmt.Errorf with %w and drop pkg/errors entirely. For example:

-import "github.com/pkg/errors"
+import "fmt"

 func (p *Process) Pid() (int, error) {
     data, err := os.ReadFile(p.PidFilePath())
     if err != nil {
         return -1, err
     }
     pidStr := strings.TrimSpace(string(data))
-    pid, err := strconv.Atoi(pidStr)
-    if err != nil {
-        return -1, errors.Wrapf(err, "parsing PID from file %s", p.PidFilePath())
-    }
-    return pid, nil
+    pid, err := strconv.Atoi(pidStr)
+    if err != nil {
+        return -1, fmt.Errorf("parsing PID from file %s: %w", p.PidFilePath(), err)
+    }
+    return pid, nil
 }

Then you can remove the github.com/pkg/errors import entirely. This keeps us on the standard library’s error-wrapping and avoids bringing in an extra dependency. Let me know if that works for you!

@vyasgun vyasgun force-pushed the pr/pid-management branch from 4a6f13b to adb05d0 Compare April 4, 2025 06:46
Copy link
Collaborator

@cfergeau cfergeau left a comment

Choose a reason for hiding this comment

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

Have you tried using this to replace the equivalent code in crc, or possibly in podman? This would be the best way to make sure the interface is the one we need.

if err != nil {
return false
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why syscall.Signal(0)? Could this use one of the SIGXXX constants from https://pkg.go.dev/syscall#pkg-constants ? (search for SIGABRT)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it safe to call SIGABRT on the running process? On searching I found sending 0 is the safest way to check a process is running

Copy link
Collaborator

Choose a reason for hiding this comment

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

I mentioned SIGABRT as a way to find the relevant section, I was not suggesting to use this signal :)
However, if FindProcess() found it, I’d assume it means the process is running, no need to check again that it’s running ?

This commit adds a process package with Manager interface and Process struct
The interface has the following methods:
- ReadPidFile() (int, error)
- Name() string
- PidFilePath() string
- Exists() bool
- Terminate() error
- Kill() error
- FindProcess() (*os.Process, error)
- WritePidFile(pid int) error

The commit also adds tests for process lifecycle with a dummy process
@vyasgun vyasgun force-pushed the pr/pid-management branch from adb05d0 to 5d6ee02 Compare April 23, 2025 10:08
Copy link
Contributor

@nirs nirs left a comment

Choose a reason for hiding this comment

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

We need tests showing the expected usage of this package. You can check minikube vfkit driver to see the use cases that this package should support.

}
if !exists {
return nil, fmt.Errorf("process not found")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

It will be hard to tell if the process does not exist when we return this error. We need to return which is easy to check like os.ErrNotExist for this special case.

}
if proc == nil {
return nil, fmt.Errorf("process not found")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why check if the process exists with process.PidExists(int32(pid)) if we get the same information by creating a process with process.NewProcess(int32(pid))?

Check how PidExists is implemented, I guess it create a process internally.

}
if name != p.Name() {
return nil, fmt.Errorf("pid %d is stale, and is being used by %s", pid, name)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The pid is stale. How the user can tell that it can delete the pidfile? We need an error that the user can check, meaning that there is no such process matching name, pid and executable path.

return nil, fmt.Errorf("cannot find process exe: %v", err)
}
if exe != p.ExecutablePath() {
return nil, fmt.Errorf("pid %d is stale, and is being used by %s", pid, exe)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same issue here.


func (p *Process) Exists() bool {
proc, err := p.FindProcess()
return err == nil && proc != nil
Copy link
Contributor

Choose a reason for hiding this comment

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

This may be wrong - maybe the process exists but we don't have permissions to get the details?

In minikube we return (bool, error) so it is easy to tell if the process exists or we cannot tell.
https://github.com/kubernetes/minikube/blob/89b97e001600434322b135de9631570fd24bad22/pkg/minikube/process/process.go#L53

// Try to kill the non-existent process
// This should result in an error
err = managedProcess.Kill()
assert.Error(t, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Kill will be easier to use if it is idempotent. Trying to kill a process that does not exist is not an error from the caller point of view.

@@ -0,0 +1,99 @@
package process
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like we need only public names, so we can use process_test package. This will be better tests since the test code will be the same as real code using this package.

@@ -0,0 +1,119 @@
package process
Copy link
Contributor

Choose a reason for hiding this comment

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

No copyright/license info?

return pid, nil
}

func (p *Process) FindProcess() (*process.Process, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it need to be public or it an be a private helper? Exposing the underlying process package in this packages means we can never change the implementation, for example using another package to validate the process executable path.

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.

Add "pid management" code to vfkit/the client API?
3 participants