You can store the context.CancelFunc
in a map and use it to cancel the context, which will in turn cause the child process to be terminated.
See the demo below:
main.go:
package main
import (
"context"
"log"
"os"
"os/exec"
"os/signal"
"sync"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/utils"
)
type manager struct {
wg sync.WaitGroup
mu sync.Mutex
jobs map[string]context.CancelFunc
}
func newManger() *manager {
return &manager{
jobs: make(map[string]context.CancelFunc),
}
}
func (m *manager) startJob(ctx context.Context, id string) {
log.Println("start job:", id)
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.jobs[id]; ok {
return
}
ctx, cancel := context.WithCancel(ctx)
cmd := exec.CommandContext(ctx, "./do.sh", id)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
m.wg.Add(1)
go func() {
defer m.wg.Done()
err := cmd.Run()
log.Printf("job: %s done. error: %v", id, err)
m.mu.Lock()
defer m.mu.Unlock()
delete(m.jobs, id)
}()
m.jobs[id] = cancel
}
func (m *manager) stopJob(id string) {
log.Println("stop job:", id)
m.mu.Lock()
defer m.mu.Unlock()
if cancel, ok := m.jobs[id]; ok {
cancel()
return
}
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
m := newManger()
app := fiber.New()
app.Post("/start/:id", func(c *fiber.Ctx) error {
id := utils.CopyString(c.Params("id"))
if id == "" {
return nil
}
m.startJob(ctx, id)
return nil
})
app.Post("/stop/:id", func(c *fiber.Ctx) error {
id := utils.CopyString(c.Params("id", ""))
if id == "" {
return nil
}
m.stopJob(id)
return nil
})
go func() {
err := app.Listen(":3000")
if err != nil {
log.Println(err)
}
}()
<-ctx.Done()
_ = app.Shutdown()
m.wg.Wait()
}
do.sh (chmod +x do.sh
):
#!/usr/bin/env bash
while :; do
echo $1
sleep 1s
done
Usage:
send a request to start a new job:
$ curl -X POST 'http://localhost:3000/start/1'
send a request to stop a running job:
$ curl -X POST 'http://localhost:3000/stop/1'
Note: this is only a simple demo to show the idea. It's not fully tested. Use with caution.