wgo doesn't shutdown when using templ --cmd
guilhas07 opened this issue · comments
I'm trying to use templ
to restart on templ file changes with the additional --cmd
flag to run wgo in order to watch go files:
templ generate --watch --proxy="http://localhost:8000" --cmd="wgo run main.go"
However, when templ restarts, the wgo
cmd seems to not shutdown correctly, producing the error: listen tcp :8000: bind: address already in use
as opposed to the same example using simply go:
- Using
go run main.go
a new executable is generated:
- Using
wgo run main.go
seems to keep the previous executable despite the wgo process being dead:
OS: WSL2 Ubuntu 22.04
I don't know if this behaviour is intended or not. Let me know if you need more info!
Do you have some sample code I could try with? As well as which files to change to trigger the error, etc
I suspect I know the reason -- templ is using SIGKILL to kill its child processes, which doesn't give them a chance to clean up and causes to wgo die immediately, leaving its own children behind. But I need to replicate the problem first and then test if changing from SIGKILL to SIGTERM works.
Ordinarily this won't be a problem, because templ sets a new process group ID for its descendants and kills them by their process group ID. But wgo pulls the same trick and also assigns its descendants a new process group ID. This means that wgo and wgo's descendants share different process group IDs, and templ killing its children by process group ID means only wgo gets killed, not wgo's descendants (they don't have the same process group ID). The correct way would be to kill wgo with SIGTERM so that wgo can intercept the SIGTERM and kill its own children (SIGKILL is too aggressive and can't be intercepted).
Do you have some sample code I could try with? As well as which files to change to trigger the error, etc
main.go
package main
import (
"log"
"net/http"
)
func main() {
log.Print("Listening on port 8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
file.templ
package main
templ test(name1 string) {
<div><p>{ name1 }</p></div>
}
command: templ generate --watch --cmd "wgo run main.go"
Steps to reproduce:
- Create main.go and file.templ files.
- Run the command above
- Make a change to the file.templ file e.g., change the variable name
The correct way would be to kill wgo with SIGTERM so that wgo can intercept the SIGTERM and kill its own children (SIGKILL is too aggressive and can't be intercepted).
Hmm, interesting. So what is your suspicion for the correct behaviour when using the go command instead?
Hmm, interesting. So what is your suspicion for the correct behaviour when using the go command instead?
Because the go command doesn't allocate a new process group ID for its children (so its children inherit the go command's process group ID). Killing that process group ID kills everything cleanly. In contrast, wgo does allocate a new process group ID for its children by nature of it being a supervisor process i.e. it has to be able to kill other process groups without being killed itself, so it possesses a separate process group ID from its children. Killing wgo's process group ID will not kill its children.
Here is an example program that spawns a child process.
main.go
package main
import (
"os/exec"
"log"
)
func main() {
cmd := exec.Command("/bin/sh", "-c", "sleep 3600")
log.Println("sleeping...")
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
}
go run
with SIGKILL (clean exit):
$ go run main.go
2024/04/16 19:50:39 sleeping...
# Press CTRL+Z to put the process into the background.
# Check running processes.
$ ps -j
PID PGID SID TTY TIME CMD
136 136 136 pts/0 00:00:00 bash
3136 3136 136 pts/0 00:00:00 go # <-- PGID of go command is 3136
3204 3136 136 pts/0 00:00:00 main
3209 3136 136 pts/0 00:00:00 sh
3210 3136 136 pts/0 00:00:00 sleep
3211 3211 136 pts/0 00:00:00 ps
# sigkill PGID 3136
$ kill -s SIGKILL -- -3136
# All processes with PGID 3136 were killed.
$ ps -j
PID PGID SID TTY TIME CMD
136 136 136 pts/0 00:00:00 bash
3212 3212 136 pts/0 00:00:00 ps
wgo run
with SIGKILL (unclean exit):
$ wgo run main.go
2024/04/16 19:54:22 sleeping...
# Press CTRL+Z to put the process into the background.
# Check running processes.
$ ps -j
PID PGID SID TTY TIME CMD
136 136 136 pts/0 00:00:00 bash
3213 3213 136 pts/0 00:00:00 wgo # <-- PGID of wgo command is 3213
3288 3288 136 pts/0 00:00:00 wgo_20240416195
3293 3288 136 pts/0 00:00:00 sh
3294 3288 136 pts/0 00:00:00 sleep
3295 3295 136 pts/0 00:00:00 ps
# sigkill PGID 3213
$ kill -s SIGKILL -- -3213
# Only wgo was killed (PGID 3213), wgo's child processes (PGID 3288) are still hanging around.
$ ps -j
PID PGID SID TTY TIME CMD
136 136 136 pts/0 00:00:00 bash
3288 3288 136 pts/0 00:00:00 wgo_20240416195
3293 3288 136 pts/0 00:00:00 sh
3294 3288 136 pts/0 00:00:00 sleep
3296 3296 136 pts/0 00:00:00 ps
wgo run
with SIGTERM (clean exit):
$ wgo run main.go
2024/04/16 19:56:13 sleeping...
# Press CTRL+Z to put the process into the background.
# Check running processes.
$ ps -j
PID PGID SID TTY TIME CMD
136 136 136 pts/0 00:00:00 bash
3299 3299 136 pts/0 00:00:00 wgo # <-- PGID of wgo is 3299
3365 3365 136 pts/0 00:00:00 wgo_20240416195
3372 3365 136 pts/0 00:00:00 sh
3373 3365 136 pts/0 00:00:00 sleep
3374 3374 136 pts/0 00:00:00 ps
# sigterm PGID 3299
$ kill -s SIGTERM -- -3299
# wgo was killed (PGID 3299), but it intercepted SIGTERM and was able to kill its
# child processes (PGID 3365) before exiting.
$ ps -j
PID PGID SID TTY TIME CMD
136 136 136 pts/0 00:00:00 bash
3375 3375 136 pts/0 00:00:00 ps
I've checked, changing all instances of SIGKILL to SIGTERM in templ/cmd/templ/generatecmd/run/run_unix.go does indeed solve the problem.
Thank you for the thorough explanation!
So I supposed the way forward is to make a PR on the temple repo?
Yup, you can do that
You're using 2 different tools to do the same thing. Don't use --watch
on templ generate
to run wgo, use wgo to run templ generate
https://templ.guide/commands-and-tools/hot-reload#alternative-1-wgo
You're using 2 different tools to do the same thing. Don't use
--watch
ontempl generate
to run wgo, use wgo to runtempl generate
https://templ.guide/commands-and-tools/hot-reload#alternative-1-wgo
That doesn't do the same thing though. Correct me if I'm wrong but I believe that way templ generate
would run every time a go file changed, and not only a templ file. And it doesn't have the proxy benefit nor the txt fast recompilation benefit of the templ --watch
command.
This working setup allows me to get all the benefits above:
templ generate --watch --proxy="http://localhost:8000"& wgo run .
Closed due to a-h/templ#687 being merged!