Just a quick post to go over some problems / frustrations I've had with golang's exec.Command and os.Process APIs during my live stream, as well as how I eventually solved them.

ProcessState is a big fat liar

First of all, I was blindsided by the ProcessState property on the *exec.Cmd class. As far as I could tell, ProcessState originally appeared to be almost 100% useless. Upon a breif investigation, it appears that it cannot be used, when we try to use it as intended: to determine the process' state.

Here is an example:


		log.Println()
		debugStatus(command)
		log.Println()

		command.Process.Kill()

		time.Sleep(time.Second)

		log.Println()
		debugStatus(command)
		log.Println()

Where debugStatus simply prints a message to the console detailing the presence/absence/values of fields on the ProcessState.

This code will output:

golang sez: hasProcessState: false, processStatePid: nil, processStateExited: nil, processStateExitCode: nil

command.Wait() returned 'signal: killed' for child process 'test child'
child process 'test child' ended with exit code -1

golang sez: hasProcessState: true, processStatePid: 115564, processStateExited: false, processStateExitCode: -1
                    

I am curious if perhaps Exited() returned false because the process was killed forcefully — command.Process.Kill() is equivalent to kill -SIGKILL <pid> or kill -9 <pid> in unix/linux, it removes the process immediately, no questions asked. Notably, the exit code is the only difference before and after the process was killed. It seems extremely terrible that Exited() returns false even though the stdlib seems to know that the process does not exist any more. The docs say:

Exited reports whether the program has exited.

Perhaps the go authors should add a new method called ExitedOrWasKilled(). This alternate method's presence in the autocomplete popup would help programmers understand this API and the rules it follows without even reading the documentation.

For now, at very least, I've submitted a pull request to edit the description and clarify the function's behavior

os.FindProcess is unhelpful

At least the documentation on this one is correct. It warns us;

On Unix systems, FindProcess always succeeds and returns a Process for the given pid, regardless of whether the process exists.

What? Ok, fuck me then. I guess I can't use this to determine if a process exists or not 🤡

SIGINT/SIGTERM do not exist on windows

This one isn't really golangs fault: Interrupts on windows are fundamentally different from MacOS and Linux, which both have a shared unix heritage; There is no easy 1-to-1 mapping between the two operating systems, so if you want to use interrupts on windows similarly to how you might use them on a unix system, there are multiple different ways to do it, and each one has its caveats. So of course, the golang standard library can't "just work", as doing so would try to force "one size fits all" when in fact it really does not fit.

Luckily, I discovered that all the tools neccesary are in golang.org/x/sys/windows, and there's an excellent implementation example for windows in the github.com/mattn/goreman repository

(goreman isn't Doom or Quake related 😛, it's a golang rewrite of the cross platform automation tool foreman)

So I essentially copy-and-pasted that code into my own project 😇

Here is a quick, but thorough demo showing everything that I learned:

https://git.sequentialread.com/forest/golang-crossplatform-child-process-test

Comments