Tightly couple Commander's Start/Kill processes
Our newer code for controlling processes exposes both a Commander
interface (Start()
, Wait()
) and Killer
interface (Terminate()
, ForceKill()
).
This design makes adding new kill strategies harder to implement, as so often, the way you start a process changes how it should then be killed.
For example, with Unix, because we set a group process, the PID we use to then kill with is different. We currently hard-code this logic in the Killer
code, because we know how we setup the process on the Commander
side. The interfaces make this look loosely coupled, but it only works because we know how we've setup the process.
On Windows, we need to implement a new strategy that performs a setup on the process before start (setup a new process group), after start (attach to a new job object) and then have a Killer
that doesn't work on the process at all, but acts on the job object, which is difficult to access with the current design.
For this new Windows strategy, we probably want to use a feature flag, but that now means scattering this flag through both the Commander and Killer code. It gets messy.
I think a better approach is to tightly couple how we start and kill a process and expose a different interface that can be used to implement completely different strategies. So although we tightly coupling start/kill, we gain more flexibility than we currently have.
The different strategies could be:
- Unix group process kill (as we currently do)
- Windows taskkill (as we currently do, which has many flaws)
- Windows Job Object kill (as we want to implement, which we believe solves many problems we have with process termination)
- ENV inheritance kill (a strategy used in other systems, where the use of environment variable inheritance is used to determine child processes and is mostly platform agnostic)
A feature flag / option can now determine which strategy can be used, making the entrypoint for the change much easier to reason about.
The interface would be something like:
type ProcessStrategy interface {
PreStart(c *osCmd) error
PostStart(c *osCmd) error
Terminate(c *osCmd) error
ForceKill(c *osCmd) error
}
and Commander
interface stays the same, but how we create a process is changed:
func NewOSCmd(executable string, args []string, options CommandOptions) Commander
Would be changed to:
func NewOSCmd(ctx context.Context, strategy ProcessStrategy, executable string, args []string, options CommandOptions) Commander
The Killer
interface is entirely removed. The context passed to NewOSCmd
controls when to kill, using the new process strategy provided, rather than the caller having to use NewOSKillWait
to wait and kill the process.
Iteration/incremental approach
- Update
NewOsCmd()
to accept a context, and remove the need for callers to useNewOSKillWait
. - Add new strategy interface with existing strategies.
- Add Job Object strategy for Windows, behind a feature flag.