mirror of
https://github.com/foomo/gograpple.git
synced 2026-08-13 21:00:22 +00:00
384 lines
11 KiB
Go
384 lines
11 KiB
Go
package gograpple
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"path"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-delve/delve/service/api"
|
|
"github.com/go-delve/delve/service/rpc2"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
func (g Grapple) Delve(pod, container, input string, args []string, host string, port int, delveContinue, vscode bool) error {
|
|
// input validation
|
|
if err := g.validatePod(&pod); err != nil {
|
|
return err
|
|
}
|
|
if err := g.validateContainer(&container); err != nil {
|
|
return err
|
|
}
|
|
goModDir, err := findGoProjectRoot(input)
|
|
if err != nil {
|
|
return fmt.Errorf("couldnt find go.mod dir for input %q", input)
|
|
}
|
|
|
|
// are patched?
|
|
if !g.isPatched() {
|
|
return fmt.Errorf("deployment not patched, stopping delve")
|
|
}
|
|
|
|
binDest := "/" + g.deployment.Name
|
|
chanExitRun, lockingCleanup := g.dlvGetLockingCleanup(pod, container)
|
|
cmd, errCmd := g.dlvGetCommand(container, binDest, port, delveContinue, args)
|
|
if errCmd != nil {
|
|
return errCmd
|
|
}
|
|
|
|
chanExit, chanReload, chanErr, errListenForInterrupts := listenForInterrupts(g.l.WithField("task", "interrupt-listener"))
|
|
if errListenForInterrupts != nil {
|
|
lockingCleanup("failed to listen interrupt signals: " + errListenForInterrupts.Error())
|
|
return errListenForInterrupts
|
|
}
|
|
|
|
chanRunErr := make(chan error)
|
|
iteration := 0
|
|
run := func(chanExitRun chan struct{}) {
|
|
iteration++
|
|
g.l.Infof("executing delve command on pod %v", pod)
|
|
g.kubeCmd.ExecPod(pod, container, cmd).PostStart(
|
|
g.dlvWatch(host, pod, port, iteration, lockingCleanup, vscode, goModDir, chanExitRun),
|
|
).Run()
|
|
}
|
|
g.l.Info("running initial cleanup, just in case ...")
|
|
g.dlvCleanup(g.l, pod, container)
|
|
go func() {
|
|
chanReload <- "initial load"
|
|
}()
|
|
for {
|
|
select {
|
|
case errRun := <-chanRunErr:
|
|
lockingCleanup(errRun.Error())
|
|
return errRun
|
|
case <-chanExit:
|
|
lockingCleanup("termination")
|
|
return nil
|
|
case <-chanReload:
|
|
var binTemp string
|
|
var errRebuild error
|
|
wgReload := sync.WaitGroup{}
|
|
wgReload.Add(2)
|
|
go func() {
|
|
lockingCleanup("reload it baby")
|
|
wgReload.Done()
|
|
}()
|
|
go func() {
|
|
binTemp, errRebuild = g.rebuildAndUpload(goModDir, pod, container, input, binDest)
|
|
wgReload.Done()
|
|
}()
|
|
wgReload.Wait()
|
|
if errRebuild != nil {
|
|
return errRebuild
|
|
}
|
|
errMove := g.dlvMoveBinary(pod, container, binTemp, binDest)
|
|
if errMove != nil {
|
|
return errMove
|
|
}
|
|
errExpose := g.dlvExpose(pod, host, port)
|
|
if errExpose != nil {
|
|
return errExpose
|
|
}
|
|
go run(chanExitRun)
|
|
case err := <-chanErr:
|
|
lockingCleanup("an error occurred while listeing for keyboard commands:" + err.Error())
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g Grapple) dlvGetCommand(container, binDest string, port int, delveContinue bool, args []string) (cmd []string, err error) {
|
|
cmd = []string{
|
|
"dlv", "exec", binDest,
|
|
"--api-version=2", "--headless",
|
|
fmt.Sprintf("--listen=:%v", port),
|
|
"--accept-multiclient",
|
|
}
|
|
if delveContinue {
|
|
cmd = append(cmd, "--continue")
|
|
}
|
|
if len(args) == 0 {
|
|
args, err = g.getArgsFromConfigMap(g.deployment.Name, container)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if len(args) > 0 {
|
|
cmd = append(cmd, "--")
|
|
cmd = append(cmd, args...)
|
|
}
|
|
return cmd, nil
|
|
}
|
|
|
|
func (g Grapple) dlvMoveBinary(pod, container, binTemp, binDest string) error {
|
|
cmd := []string{"mv", binTemp, binDest}
|
|
_, errRun := g.kubeCmd.ExecPod(pod, container, cmd).Run()
|
|
return errRun
|
|
|
|
}
|
|
|
|
func (g Grapple) dlvWatch(host string, pod string, port, iteration int, cleanup func(reason string) error, vscode bool, goModDir string, chanExitRun chanCommand) func() error {
|
|
return func() error {
|
|
client, errTryDelveServer := dlvTryServer(g.l, host, port, 5, 1*time.Second)
|
|
if errTryDelveServer != nil {
|
|
return errTryDelveServer
|
|
}
|
|
go func() {
|
|
i := 0
|
|
for {
|
|
select {
|
|
case <-chanExitRun:
|
|
return
|
|
case <-time.After(time.Millisecond * 500):
|
|
_, state, errState := dlvCheckServer(g.l.WithField("task", "watch-dlv"), host, port, 3*time.Second, client)
|
|
if errState != nil {
|
|
g.l.WithError(errState).Error("dlv seems to be down on", host, ":", port)
|
|
cleanup("dlv is down")
|
|
os.Exit(1)
|
|
}
|
|
if i%20 == 0 {
|
|
g.l.WithField("pid", client.ProcessPid()).Info("dlv is up")
|
|
}
|
|
if state.Exited {
|
|
cleanup("dlv state.Exited == true")
|
|
os.Exit(1)
|
|
} else if !state.Running {
|
|
// there still is the case, when you are in a breakpoint on a zombie process
|
|
// dlv will not handle that gracefully
|
|
g.l.WithField("pid", client.ProcessPid()).Info("dlv is up - process is not running - is it a zombie ?!")
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
}()
|
|
if vscode {
|
|
if err := launchVscode(g.l, goModDir, pod, host, port, 5, iteration, 1*time.Second); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (g Grapple) dlvGetLockingCleanup(pod, container string) (chanExitRun chanCommand, cleanup func(reason string) error) {
|
|
cleanupLock := sync.Mutex{}
|
|
cleanupStarted := false
|
|
chanExitRun = make(chanCommand)
|
|
return chanExitRun, func(reason string) error {
|
|
cleanupLock.Lock()
|
|
defer cleanupLock.Unlock()
|
|
cl := g.l.WithField("reason", reason)
|
|
if cleanupStarted {
|
|
cl.Warning("aborting cleanup already started")
|
|
return nil
|
|
}
|
|
cleanupStarted = true
|
|
defer func() {
|
|
cleanupStarted = false
|
|
}()
|
|
cl.Info("cleaning up")
|
|
go func() { chanExitRun <- struct{}{} }()
|
|
cl.Info("informed running tasks")
|
|
errDelveCleanup := g.dlvCleanup(cl, pod, container)
|
|
if errDelveCleanup != nil {
|
|
cl.WithError(errDelveCleanup).Error("could not clean up")
|
|
return errDelveCleanup
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (g Grapple) dlvExpose(pod, host string, port int) error {
|
|
g.l.Infof("exposing deployment %v for delve", g.deployment.Name)
|
|
_, err := g.kubeCmd.ExposePod(pod, host, port).Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g Grapple) dlvCleanup(l *logrus.Entry, pod, container string) error {
|
|
l.Infof("removing delve service")
|
|
outDeleteService, errDeleteService := g.kubeCmd.DeleteService(pod).Run()
|
|
if errDeleteService != nil {
|
|
l.WithError(errDeleteService).Warn("could not delete exposing service: " + outDeleteService)
|
|
}
|
|
l.Info("cleaning up debug processes")
|
|
const nameDLV = "dlv"
|
|
nameProgram := g.deployment.Name
|
|
pidsOfProgram, errGetPIDsOfProgram := g.getPIDsOf(pod, container, nameProgram)
|
|
if errGetPIDsOfProgram != nil {
|
|
return errGetPIDsOfProgram
|
|
}
|
|
pidsOfDLV, errGetPIDsOfDelve := g.getPIDsOf(pod, container, nameDLV)
|
|
if errGetPIDsOfDelve != nil {
|
|
return errGetPIDsOfDelve
|
|
}
|
|
kill := func(name string, pids []string, murder bool) (leftToKill []string) {
|
|
leftToKill = []string{}
|
|
for _, pid := range pids {
|
|
remainingPIDs, errGetPIDs := g.getPIDsOf(pod, container, name)
|
|
killed := true
|
|
if errGetPIDs == nil {
|
|
for _, remainingPID := range remainingPIDs {
|
|
if remainingPID == pid {
|
|
killed = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if killed {
|
|
continue
|
|
}
|
|
cmd := []string{"kill"}
|
|
if murder {
|
|
cmd = append(cmd, "-s", "9")
|
|
}
|
|
cmd = append(cmd, pid)
|
|
outKill, errKill := g.kubeCmd.ExecPod(pod, container, cmd).Run()
|
|
if errKill != nil {
|
|
l.WithError(errKill).Warn("could not kill process", outKill)
|
|
}
|
|
leftToKill = append(leftToKill, pid)
|
|
|
|
}
|
|
return leftToKill
|
|
}
|
|
const maxAttempts = 10
|
|
for i := 0; i < maxAttempts; i++ {
|
|
pidsOfProgram = kill(nameProgram, pidsOfProgram, i > 0)
|
|
pidsOfDLV = kill(nameDLV, pidsOfDLV, i > 0)
|
|
if len(pidsOfDLV) == 0 && len(pidsOfProgram) == 0 {
|
|
return nil
|
|
}
|
|
time.Sleep(time.Millisecond * 200)
|
|
}
|
|
return fmt.Errorf("could not kill processes after max attempts %v", maxAttempts)
|
|
}
|
|
|
|
func dlvTryServer(l *logrus.Entry, host string, port, tries int, sleep time.Duration) (client *rpc2.RPCClient, err error) {
|
|
errTryCall := tryCall(tries, sleep, func(i int) error {
|
|
l.Infof("checking delve connection on %v:%v (%v/%v)", host, port, i, tries)
|
|
newClient, _, errCheck := dlvCheckServer(l, host, port, 1*time.Second, nil)
|
|
client = newClient
|
|
return errCheck
|
|
})
|
|
if errTryCall != nil {
|
|
return nil, errTryCall
|
|
}
|
|
l.Infof("delve server listening on %v:%v", host, port)
|
|
return client, nil
|
|
}
|
|
|
|
func dlvCheckServer(
|
|
l *logrus.Entry, host string, port int, timeout time.Duration,
|
|
client *rpc2.RPCClient,
|
|
) (
|
|
*rpc2.RPCClient, *api.DebuggerState, error,
|
|
) {
|
|
var conn net.Conn
|
|
|
|
if client == nil {
|
|
// get a tcp connection for the rpc dlv rpc client
|
|
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%v:%v", host, port), timeout)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
timer := time.AfterFunc(timeout, func() {
|
|
errClose := conn.Close()
|
|
if errClose != nil {
|
|
l.WithError(errClose).Error("could not close stale connection")
|
|
}
|
|
l.Warn("stale connection timeout")
|
|
})
|
|
// rpc2.NewClientFromConn(conn) will implicitly call setAPIVersion on the dlv server
|
|
// while there might be a connection to a socket, that was created with
|
|
// kubctl expose it still might happen, that dlv will not answer
|
|
// for this reason we are creating this hack
|
|
client = rpc2.NewClientFromConn(conn)
|
|
if !timer.Stop() {
|
|
// we ripped out the underlying connection
|
|
return nil, nil, errors.New("stale connection to dlv, aborting after timeout")
|
|
}
|
|
}
|
|
st, errState := client.GetStateNonBlocking()
|
|
if errState == nil && conn != nil {
|
|
conn.SetDeadline(time.Now().Add(time.Second * 3600))
|
|
}
|
|
return client, st, errState
|
|
}
|
|
|
|
type chanCommand chan struct{}
|
|
|
|
func listenForInterrupts(l *logrus.Entry) (chanExit chanCommand, chanReload chan string, chanErr chan error, err error) {
|
|
sigchan := make(chan os.Signal, 1)
|
|
signal.Notify(sigchan)
|
|
chanExit = make(chanCommand)
|
|
chanReload = make(chan string)
|
|
chanErr = make(chan error)
|
|
i := 0
|
|
exiting := false
|
|
readyToReset := false
|
|
durReset := time.Millisecond * 5000
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-time.After(durReset):
|
|
if readyToReset {
|
|
l.Info("resetting termination timer")
|
|
readyToReset = false
|
|
}
|
|
i = 0
|
|
case sig := <-sigchan:
|
|
switch sig {
|
|
case os.Interrupt:
|
|
l.Info("received interrupt signal, tigger one more interrupt within ", durReset, " to terminate")
|
|
readyToReset = true
|
|
if exiting {
|
|
l.Warn("already exiting - ignoring interupt")
|
|
continue
|
|
}
|
|
if i == 0 {
|
|
l.Info("triggering reload")
|
|
chanReload <- "interrupt => reload"
|
|
} else {
|
|
l.Info("triggering exit")
|
|
exiting = true
|
|
chanExit <- struct{}{}
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return chanExit, chanReload, chanErr, nil
|
|
}
|
|
|
|
func (g Grapple) rebuildAndUpload(goModDir, pod, container, input, binDest string) (tempDest string, err error) {
|
|
binPath := path.Join(os.TempDir(), g.deployment.Name)
|
|
g.l.Infof("building %q for debug", input)
|
|
_, errBuild := g.goCmd.Build(goModDir, binPath, input, `-gcflags="all=-N -l"`).Env("GOOS=linux").Run()
|
|
if errBuild != nil {
|
|
return "", errBuild
|
|
}
|
|
|
|
g.l.Infof("copying binary to pod %v", pod)
|
|
tempDest = binDest + "-build"
|
|
_, errCopyToPod := g.kubeCmd.CopyToPod(pod, container, binPath, tempDest).Run()
|
|
return tempDest, errCopyToPod
|
|
}
|