proc: implement target.Interface for core files

This commit is contained in:
aarzilli 2017-04-03 11:17:54 +02:00 committed by Derek Parker
parent 3dacc25d2e
commit c1879472a1
13 changed files with 624 additions and 33 deletions

@ -1,5 +1,6 @@
package main
func main() {
panic("BOOM!")
msg := "BOOM!"
panic(msg)
}

@ -159,7 +159,7 @@ consider compiling debugging binaries with -gcflags="-N -l".`,
return nil
},
Run: func(cmd *cobra.Command, args []string) {
os.Exit(execute(0, args, conf, executingExistingFile))
os.Exit(execute(0, args, conf, "", executingExistingFile))
},
}
RootCommand.AddCommand(execCommand)
@ -205,6 +205,24 @@ to know what functions your process is executing.`,
traceCommand.Flags().IntVarP(&traceStackDepth, "stack", "s", 0, "Show stack trace with given depth.")
RootCommand.AddCommand(traceCommand)
coreCommand := &cobra.Command{
Use: "core <executable> <core>",
Short: "Examine a core dump.",
Long: `Examine a core dump.
The core command will open the specified core file and the associated
executable and let you examine the state of the process when the
core dump was taken.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return errors.New("you must provide a core file and an executable")
}
return nil
},
Run: coreCmd,
}
RootCommand.AddCommand(coreCommand)
// 'version' subcommand.
versionCommand := &cobra.Command{
Use: "version",
@ -243,7 +261,7 @@ func debugCmd(cmd *cobra.Command, args []string) {
return 1
}
processArgs := append([]string{abs}, targetArgs...)
return execute(0, processArgs, conf, executingGeneratedFile)
return execute(0, processArgs, conf, "", executingGeneratedFile)
}()
os.Exit(status)
}
@ -332,7 +350,7 @@ func testCmd(cmd *cobra.Command, args []string) {
defer os.Remove("./" + testdebugname)
processArgs := append([]string{"./" + testdebugname}, targetArgs...)
return execute(0, processArgs, conf, executingOther)
return execute(0, processArgs, conf, "", executingOther)
}()
os.Exit(status)
}
@ -343,7 +361,11 @@ func attachCmd(cmd *cobra.Command, args []string) {
fmt.Fprintf(os.Stderr, "Invalid pid: %s\n", args[0])
os.Exit(1)
}
os.Exit(execute(pid, nil, conf, executingOther))
os.Exit(execute(pid, nil, conf, "", executingOther))
}
func coreCmd(cmd *cobra.Command, args []string) {
os.Exit(execute(0, []string{args[0]}, conf, args[1], executingOther))
}
func connectCmd(cmd *cobra.Command, args []string) {
@ -382,7 +404,7 @@ const (
executingOther
)
func execute(attachPid int, processArgs []string, conf *config.Config, kind executeKind) int {
func execute(attachPid int, processArgs []string, conf *config.Config, coreFile string, kind executeKind) int {
// Make a TCP listener
listener, err := net.Listen("tcp", Addr)
if err != nil {
@ -410,6 +432,7 @@ func execute(attachPid int, processArgs []string, conf *config.Config, kind exec
AcceptMulti: AcceptMulti,
APIVersion: APIVersion,
WorkingDir: WorkingDir,
CoreFile: coreFile,
}, Log)
default:
fmt.Printf("Unknown API version: %d\n", APIVersion)

@ -1,8 +1,12 @@
package proc
import (
"debug/gosym"
"errors"
"fmt"
"go/ast"
"io"
"sync"
)
// MemoryReader is like io.ReaderAt, but the offset is a uintptr so that it
@ -144,3 +148,214 @@ type OffsetReaderAt struct {
func (r *OffsetReaderAt) ReadMemory(buf []byte, addr uintptr) (n int, err error) {
return r.reader.ReadAt(buf, int64(addr-r.offset))
}
type CoreProcess struct {
bi BinaryInfo
core *Core
breakpoints map[uint64]*Breakpoint
currentThread *LinuxPrStatus
selectedGoroutine *G
allGCache []*G
}
type CoreThread struct {
th *LinuxPrStatus
p *CoreProcess
}
var ErrWriteCore = errors.New("can not to core process")
var ErrShortRead = errors.New("short read")
var ErrContinueCore = errors.New("can not continue execution of core process")
func OpenCore(corePath, exePath string) (*CoreProcess, error) {
core, err := readCore(corePath, exePath)
if err != nil {
return nil, err
}
p := &CoreProcess{
core: core,
breakpoints: make(map[uint64]*Breakpoint),
bi: NewBinaryInfo("linux", "amd64"),
}
var wg sync.WaitGroup
p.bi.LoadBinaryInfo(exePath, &wg)
wg.Wait()
for _, th := range p.core.Threads {
p.currentThread = th
break
}
scope := &EvalScope{0, 0, p.CurrentThread(), nil, &p.bi}
ver, isextld, err := scope.getGoInformation()
if err != nil {
return nil, err
}
p.bi.arch.SetGStructOffset(ver, isextld)
p.selectedGoroutine, _ = GetG(p.CurrentThread())
return p, nil
}
func (p *CoreProcess) BinInfo() *BinaryInfo {
return &p.bi
}
func (thread *CoreThread) readMemory(addr uintptr, size int) (data []byte, err error) {
data = make([]byte, size)
n, err := thread.p.core.ReadMemory(data, addr)
if err == nil && n != len(data) {
err = ErrShortRead
}
return data, err
}
func (thread *CoreThread) writeMemory(addr uintptr, data []byte) (int, error) {
return 0, ErrWriteCore
}
func (t *CoreThread) Location() (*Location, error) {
f, l, fn := t.p.bi.PCToLine(t.th.Reg.Rip)
return &Location{PC: t.th.Reg.Rip, File: f, Line: l, Fn: fn}, nil
}
func (t *CoreThread) Breakpoint() (*Breakpoint, bool, error) {
return nil, false, nil
}
func (t *CoreThread) ThreadID() int {
return int(t.th.Pid)
}
func (t *CoreThread) Registers(floatingPoint bool) (Registers, error) {
//TODO(aarzilli): handle floating point registers
return &t.th.Reg, nil
}
func (t *CoreThread) Arch() Arch {
return t.p.bi.arch
}
func (t *CoreThread) BinInfo() *BinaryInfo {
return &t.p.bi
}
func (t *CoreThread) StepInstruction() error {
return ErrContinueCore
}
func (p *CoreProcess) Breakpoints() map[uint64]*Breakpoint {
return p.breakpoints
}
func (p *CoreProcess) ClearBreakpoint(addr uint64) (*Breakpoint, error) {
return nil, NoBreakpointError{addr: addr}
}
func (p *CoreProcess) ClearInternalBreakpoints() error {
return nil
}
func (p *CoreProcess) ContinueOnce() (IThread, error) {
return nil, ErrContinueCore
}
func (p *CoreProcess) StepInstruction() error {
return ErrContinueCore
}
func (p *CoreProcess) RequestManualStop() error {
return nil
}
func (p *CoreProcess) CurrentThread() IThread {
return &CoreThread{p.currentThread, p}
}
func (p *CoreProcess) Detach(bool) error {
return nil
}
func (p *CoreProcess) Exited() bool {
return false
}
func (p *CoreProcess) FindFileLocation(fileName string, lineNumber int) (uint64, error) {
return FindFileLocation(p.CurrentThread(), p.breakpoints, &p.bi, fileName, lineNumber)
}
func (p *CoreProcess) FirstPCAfterPrologue(fn *gosym.Func, sameline bool) (uint64, error) {
return FirstPCAfterPrologue(p.CurrentThread(), p.breakpoints, &p.bi, fn, sameline)
}
func (p *CoreProcess) FindFunctionLocation(funcName string, firstLine bool, lineOffset int) (uint64, error) {
return FindFunctionLocation(p.CurrentThread(), p.breakpoints, &p.bi, funcName, firstLine, lineOffset)
}
func (p *CoreProcess) AllGCache() *[]*G {
return &p.allGCache
}
func (p *CoreProcess) Halt() error {
return nil
}
func (p *CoreProcess) Kill() error {
return nil
}
func (p *CoreProcess) Pid() int {
return p.core.Pid
}
func (p *CoreProcess) Running() bool {
return false
}
func (p *CoreProcess) SelectedGoroutine() *G {
return p.selectedGoroutine
}
func (p *CoreProcess) SetBreakpoint(addr uint64, kind BreakpointKind, cond ast.Expr) (*Breakpoint, error) {
return nil, ErrWriteCore
}
func (p *CoreProcess) SwitchGoroutine(gid int) error {
g, err := FindGoroutine(p, gid)
if err != nil {
return err
}
if g == nil {
// user specified -1 and selectedGoroutine is nil
return nil
}
if g.thread != nil {
return p.SwitchThread(g.thread.ThreadID())
}
p.selectedGoroutine = g
return nil
}
func (p *CoreProcess) SwitchThread(tid int) error {
if th, ok := p.core.Threads[tid]; ok {
p.currentThread = th
p.selectedGoroutine, _ = GetG(p.CurrentThread())
return nil
}
return fmt.Errorf("thread %d does not exist", tid)
}
func (p *CoreProcess) ThreadList() []IThread {
r := make([]IThread, 0, len(p.core.Threads))
for _, v := range p.core.Threads {
r = append(r, &CoreThread{v, p})
}
return r
}
func (p *CoreProcess) FindThread(threadID int) (IThread, bool) {
t, ok := p.core.Threads[threadID]
return &CoreThread{t, p}, ok
}

@ -2,14 +2,16 @@ package proc
import (
"bytes"
"fmt"
"go/constant"
"io/ioutil"
"os/exec"
"reflect"
"testing"
"fmt"
"path"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/derekparker/delve/pkg/proc/test"
)
@ -122,9 +124,12 @@ func TestSplicedReader(t *testing.T) {
}
}
func TestReadCore(t *testing.T) {
func TestCore(t *testing.T) {
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
return
}
// This is all very fragile and won't work on hosts with non-default core patterns.
// Might be better to check in the core?
// Might be better to check in the binary and core?
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
@ -132,20 +137,72 @@ func TestReadCore(t *testing.T) {
fix := test.BuildFixture("panic")
bashCmd := fmt.Sprintf("cd %v && ulimit -c unlimited && GOTRACEBACK=crash %v", tempDir, fix.Path)
exec.Command("bash", "-c", bashCmd).Run()
corePath := path.Join(tempDir, "core")
cores, err := filepath.Glob(path.Join(tempDir, "core*"))
switch {
case err != nil || len(cores) > 1:
t.Fatalf("Got %v, wanted one file named core* in %v", cores, tempDir)
case len(cores) == 0:
t.Logf("core file was not produced, could not run test")
return
}
corePath := cores[0]
core, err := readCore(corePath, fix.Path)
p, err := OpenCore(corePath, fix.Path)
if err != nil {
t.Fatal(err)
pat, err := ioutil.ReadFile("/proc/sys/kernel/core_pattern")
t.Errorf("read core_pattern: %q, %v", pat, err)
apport, err := ioutil.ReadFile("/var/log/apport.log")
t.Errorf("read apport log: %q, %v", apport, err)
t.Fatalf("ReadCore() failed: %v", err)
}
if len(core.Threads) == 0 {
t.Error("expected at least one thread")
gs, err := GoroutinesInfo(p)
if err != nil || len(gs) == 0 {
t.Fatalf("GoroutinesInfo() = %v, %v; wanted at least one goroutine", gs, err)
}
// Punch through the abstraction to verify that we got some mappings.
spliced := core.MemoryReader.(*SplicedMemory)
// There should be at least an RO section, RW section, RX section, the heap, and a thread stack.
if len(spliced.readers) < 5 {
t.Errorf("expected at least 5 memory regions, got only %v", len(spliced.readers))
var panicking *G
var panickingStack []Stackframe
for _, g := range gs {
stack, err := g.Stacktrace(10)
if err != nil {
t.Errorf("Stacktrace() on goroutine %v = %v", g, err)
}
for _, frame := range stack {
if strings.Contains(frame.Current.Fn.Name, "panic") {
panicking = g
panickingStack = stack
}
}
}
if panicking == nil {
t.Fatalf("Didn't find a call to panic in goroutine stacks: %v", gs)
}
var mainFrame *Stackframe
// Walk backward, because the current function seems to be main.main
// in the actual call to panic().
for i := len(panickingStack) - 1; i >= 0; i-- {
if panickingStack[i].Current.Fn.Name == "main.main" {
mainFrame = &panickingStack[i]
}
}
if mainFrame == nil {
t.Fatalf("Couldn't find main in stack %v", panickingStack)
}
msg, err := FrameToScope(p, *mainFrame).EvalVariable("msg", LoadConfig{MaxStringLen: 64})
if err != nil {
t.Fatalf("Couldn't EvalVariable(msg, ...): %v", err)
}
if constant.StringVal(msg.Value) != "BOOM!" {
t.Errorf("main.msg = %q, want %q", msg.Value, "BOOM!")
}
regs, err := p.CurrentThread().Registers(true)
if err != nil {
t.Fatalf("Couldn't get current thread registers: %v", err)
}
regslice := regs.Slice()
for _, reg := range regslice {
t.Logf("%s = %s", reg.Name, reg.Value)
}
// Would be good to test more stuff but not sure what without reading debug information, etc.
}

@ -3,16 +3,286 @@ package proc
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"golang.org/x/debug/elf"
"golang.org/x/sys/unix"
"golang.org/x/arch/x86/x86asm"
)
// Copied from golang.org/x/sys/unix.PtraceRegs since it's not available on
// all systems.
type LinuxCoreRegisters struct {
R15 uint64
R14 uint64
R13 uint64
R12 uint64
Rbp uint64
Rbx uint64
R11 uint64
R10 uint64
R9 uint64
R8 uint64
Rax uint64
Rcx uint64
Rdx uint64
Rsi uint64
Rdi uint64
Orig_rax uint64
Rip uint64
Cs uint64
Eflags uint64
Rsp uint64
Ss uint64
Fs_base uint64
Gs_base uint64
Ds uint64
Es uint64
Fs uint64
Gs uint64
}
// Copied from golang.org/x/sys/unix.Timeval since it's not available on all
// systems.
type LinuxCoreTimeval struct {
Sec int64
Usec int64
}
const NT_FILE elf.NType = 0x46494c45 // "FILE".
func (r *LinuxCoreRegisters) PC() uint64 {
return r.Rip
}
func (r *LinuxCoreRegisters) SP() uint64 {
return r.Rsp
}
func (r *LinuxCoreRegisters) BP() uint64 {
return r.Rbp
}
func (r *LinuxCoreRegisters) CX() uint64 {
return r.Rcx
}
func (r *LinuxCoreRegisters) TLS() uint64 {
return r.Fs_base
}
func (r *LinuxCoreRegisters) GAddr() (uint64, bool) {
return 0, false
}
func (r *LinuxCoreRegisters) Get(n int) (uint64, error) {
reg := x86asm.Reg(n)
const (
mask8 = 0x000f
mask16 = 0x00ff
mask32 = 0xffff
)
switch reg {
// 8-bit
case x86asm.AL:
return r.Rax & mask8, nil
case x86asm.CL:
return r.Rcx & mask8, nil
case x86asm.DL:
return r.Rdx & mask8, nil
case x86asm.BL:
return r.Rbx & mask8, nil
case x86asm.AH:
return (r.Rax >> 8) & mask8, nil
case x86asm.CH:
return (r.Rcx >> 8) & mask8, nil
case x86asm.DH:
return (r.Rdx >> 8) & mask8, nil
case x86asm.BH:
return (r.Rbx >> 8) & mask8, nil
case x86asm.SPB:
return r.Rsp & mask8, nil
case x86asm.BPB:
return r.Rbp & mask8, nil
case x86asm.SIB:
return r.Rsi & mask8, nil
case x86asm.DIB:
return r.Rdi & mask8, nil
case x86asm.R8B:
return r.R8 & mask8, nil
case x86asm.R9B:
return r.R9 & mask8, nil
case x86asm.R10B:
return r.R10 & mask8, nil
case x86asm.R11B:
return r.R11 & mask8, nil
case x86asm.R12B:
return r.R12 & mask8, nil
case x86asm.R13B:
return r.R13 & mask8, nil
case x86asm.R14B:
return r.R14 & mask8, nil
case x86asm.R15B:
return r.R15 & mask8, nil
// 16-bit
case x86asm.AX:
return r.Rax & mask16, nil
case x86asm.CX:
return r.Rcx & mask16, nil
case x86asm.DX:
return r.Rdx & mask16, nil
case x86asm.BX:
return r.Rbx & mask16, nil
case x86asm.SP:
return r.Rsp & mask16, nil
case x86asm.BP:
return r.Rbp & mask16, nil
case x86asm.SI:
return r.Rsi & mask16, nil
case x86asm.DI:
return r.Rdi & mask16, nil
case x86asm.R8W:
return r.R8 & mask16, nil
case x86asm.R9W:
return r.R9 & mask16, nil
case x86asm.R10W:
return r.R10 & mask16, nil
case x86asm.R11W:
return r.R11 & mask16, nil
case x86asm.R12W:
return r.R12 & mask16, nil
case x86asm.R13W:
return r.R13 & mask16, nil
case x86asm.R14W:
return r.R14 & mask16, nil
case x86asm.R15W:
return r.R15 & mask16, nil
// 32-bit
case x86asm.EAX:
return r.Rax & mask32, nil
case x86asm.ECX:
return r.Rcx & mask32, nil
case x86asm.EDX:
return r.Rdx & mask32, nil
case x86asm.EBX:
return r.Rbx & mask32, nil
case x86asm.ESP:
return r.Rsp & mask32, nil
case x86asm.EBP:
return r.Rbp & mask32, nil
case x86asm.ESI:
return r.Rsi & mask32, nil
case x86asm.EDI:
return r.Rdi & mask32, nil
case x86asm.R8L:
return r.R8 & mask32, nil
case x86asm.R9L:
return r.R9 & mask32, nil
case x86asm.R10L:
return r.R10 & mask32, nil
case x86asm.R11L:
return r.R11 & mask32, nil
case x86asm.R12L:
return r.R12 & mask32, nil
case x86asm.R13L:
return r.R13 & mask32, nil
case x86asm.R14L:
return r.R14 & mask32, nil
case x86asm.R15L:
return r.R15 & mask32, nil
// 64-bit
case x86asm.RAX:
return r.Rax, nil
case x86asm.RCX:
return r.Rcx, nil
case x86asm.RDX:
return r.Rdx, nil
case x86asm.RBX:
return r.Rbx, nil
case x86asm.RSP:
return r.Rsp, nil
case x86asm.RBP:
return r.Rbp, nil
case x86asm.RSI:
return r.Rsi, nil
case x86asm.RDI:
return r.Rdi, nil
case x86asm.R8:
return r.R8, nil
case x86asm.R9:
return r.R9, nil
case x86asm.R10:
return r.R10, nil
case x86asm.R11:
return r.R11, nil
case x86asm.R12:
return r.R12, nil
case x86asm.R13:
return r.R13, nil
case x86asm.R14:
return r.R14, nil
case x86asm.R15:
return r.R15, nil
}
return 0, UnknownRegisterError
}
func (r *LinuxCoreRegisters) SetPC(IThread, uint64) error {
return errors.New("not supported")
}
func (r *LinuxCoreRegisters) Slice() []Register {
var regs = []struct {
k string
v uint64
}{
{"Rip", r.Rip},
{"Rsp", r.Rsp},
{"Rax", r.Rax},
{"Rbx", r.Rbx},
{"Rcx", r.Rcx},
{"Rdx", r.Rdx},
{"Rdi", r.Rdi},
{"Rsi", r.Rsi},
{"Rbp", r.Rbp},
{"R8", r.R8},
{"R9", r.R9},
{"R10", r.R10},
{"R11", r.R11},
{"R12", r.R12},
{"R13", r.R13},
{"R14", r.R14},
{"R15", r.R15},
{"Orig_rax", r.Orig_rax},
{"Cs", r.Cs},
{"Eflags", r.Eflags},
{"Ss", r.Ss},
{"Fs_base", r.Fs_base},
{"Gs_base", r.Gs_base},
{"Ds", r.Ds},
{"Es", r.Es},
{"Fs", r.Fs},
{"Gs", r.Gs},
}
out := make([]Register, 0, len(regs))
for _, reg := range regs {
if reg.k == "Eflags" {
out = appendFlagReg(out, reg.k, reg.v, eflagsDescription, 64)
} else {
out = appendQwordReg(out, reg.k, reg.v)
}
}
return out
}
// readCore reads a core file from corePath corresponding to the executable at
// exePath. For details on the Linux ELF core format, see:
// http://www.gabriel.urdhr.fr/2015/05/29/core-file/,
@ -234,8 +504,8 @@ type LinuxPrStatus struct {
Sigpend uint64
Sighold uint64
Pid, Ppid, Pgrp, Sid int32
Utime, Stime, CUtime, CStime unix.Timeval
Reg unix.PtraceRegs
Utime, Stime, CUtime, CStime LinuxCoreTimeval
Reg LinuxCoreRegisters
Fpvalid int32
}

@ -21,7 +21,7 @@ type Registers interface {
CX() uint64
TLS() uint64
Get(int) (uint64, error)
SetPC(*Thread, uint64) error
SetPC(IThread, uint64) error
Slice() []Register
}

@ -106,7 +106,8 @@ func (r *Regs) TLS() uint64 {
}
// SetPC sets the RIP register to the value specified by `pc`.
func (r *Regs) SetPC(thread *Thread, pc uint64) error {
func (r *Regs) SetPC(t IThread, pc uint64) error {
thread := t.(*Thread)
kret := C.set_pc(thread.os.threadAct, C.uint64_t(pc))
if kret != C.KERN_SUCCESS {
return fmt.Errorf("could not set pc")

@ -84,7 +84,8 @@ func (r *Regs) TLS() uint64 {
}
// SetPC sets RIP to the value specified by 'pc'.
func (r *Regs) SetPC(thread *Thread, pc uint64) (err error) {
func (r *Regs) SetPC(t IThread, pc uint64) (err error) {
thread := t.(*Thread)
r.regs.SetPC(pc)
thread.dbp.execPtraceFunc(func() { err = sys.PtraceSetRegs(thread.ID, r.regs) })
return

@ -125,7 +125,8 @@ func (r *Regs) TLS() uint64 {
}
// SetPC sets the RIP register to the value specified by `pc`.
func (r *Regs) SetPC(thread *Thread, pc uint64) error {
func (r *Regs) SetPC(t IThread, pc uint64) error {
thread := t.(*Thread)
context := newCONTEXT()
context.ContextFlags = _CONTEXT_ALL

@ -90,3 +90,4 @@ type VariableEval interface {
}
var _ Interface = &proc.Process{}
var _ Interface = &proc.CoreProcess{}

@ -25,4 +25,7 @@ type Config struct {
AcceptMulti bool
// APIVersion selects which version of the API to serve (default: 1).
APIVersion int
// CoreFile specifies the path to the core dump to open
CoreFile string
}

@ -48,6 +48,9 @@ type Config struct {
// AttachPid is the PID of an existing process to which the debugger should
// attach.
AttachPid int
// CoreFile specifies the path to the core dump to open
CoreFile string
}
// New creates a new Debugger.
@ -57,14 +60,24 @@ func New(config *Config) (*Debugger, error) {
}
// Create the process by either attaching or launching.
if d.config.AttachPid > 0 {
switch {
case d.config.AttachPid > 0:
log.Printf("attaching to pid %d", d.config.AttachPid)
p, err := proc.Attach(d.config.AttachPid)
if err != nil {
return nil, attachErrorMessage(d.config.AttachPid, err)
}
d.target = p
} else {
case d.config.CoreFile != "":
log.Printf("opening core file %s (executable %s)", d.config.CoreFile, d.config.ProcessArgs[0])
p, err := proc.OpenCore(d.config.CoreFile, d.config.ProcessArgs[0])
if err != nil {
return nil, err
}
d.target = p
default:
log.Printf("launching process with args: %v", d.config.ProcessArgs)
p, err := proc.Launch(d.config.ProcessArgs, d.config.WorkingDir)
if err != nil {
@ -113,6 +126,10 @@ func (d *Debugger) Restart() ([]api.DiscardedBreakpoint, error) {
d.processMutex.Lock()
defer d.processMutex.Unlock()
if d.config.CoreFile != "" {
return nil, errors.New("can not restart core dump")
}
if !d.target.Exited() {
if d.target.Running() {
d.target.Halt()

@ -114,6 +114,7 @@ func (s *ServerImpl) Run() error {
ProcessArgs: s.config.ProcessArgs,
AttachPid: s.config.AttachPid,
WorkingDir: s.config.WorkingDir,
CoreFile: s.config.CoreFile,
}); err != nil {
return err
}