diff --git a/cmd/debugger.go b/cmd/debugger.go new file mode 100644 index 00000000..67119255 --- /dev/null +++ b/cmd/debugger.go @@ -0,0 +1,521 @@ +package cmd + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "os" + "strings" + + godap "github.com/google/go-dap" + "github.com/spf13/cobra" + + "github.com/open-policy-agent/opa/ast/location" + "github.com/open-policy-agent/opa/debug" + "github.com/open-policy-agent/opa/logging" + + "github.com/styrainc/regal/internal/dap" +) + +func init() { + verboseLogging := false + serverMode := false + address := "localhost:4712" + + debuggerCommand := &cobra.Command{ + Use: "debug", + Short: "Run the Regal OPA Debugger", + Long: `Start the Regal OPA debugger and listen on stdin/stdout for client editor messages.`, + + RunE: wrapProfiling(func([]string) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logger := dap.NewDebugLogger(logging.New(), logging.Debug) + if verboseLogging { + logger.Local.SetLevel(logging.Debug) + } + + if serverMode { + return startServer(ctx, address, logger) + } + + return startCmd(ctx, logger) + }), + } + + debuggerCommand.Flags().BoolVarP( + &verboseLogging, "verbose", "v", verboseLogging, "Enable verbose logging") + debuggerCommand.Flags().BoolVarP( + &serverMode, "server", "s", serverMode, "Start the debugger in server mode") + debuggerCommand.Flags().StringVarP( + &address, "address", "a", address, "Address to listen on. For use with --server flag.") + + RootCommand.AddCommand(debuggerCommand) +} + +func startCmd(ctx context.Context, logger *dap.DebugLogger) error { + protoManager := dap.NewProtocolManager(logger.Local) + logger.ProtocolManager = protoManager + + debugParams := []debug.DebuggerOption{ + debug.SetEventHandler(newEventHandler(protoManager)), + debug.SetLogger(logger), + } + + debugger := debug.NewDebugger(debugParams...) + + conn := newCmdConn(os.Stdin, os.Stdout) + s := newState(protoManager, debugger, logger) + + if err := protoManager.Start(ctx, conn, s.messageHandler); err != nil { + return fmt.Errorf("failed to handle connection: %w", err) + } + + return nil +} + +func startServer(ctx context.Context, address string, logger *dap.DebugLogger) error { + l, err := net.Listen("tcp", address) + if err != nil { + return fmt.Errorf("could not listen: %w", err) + } + + logger.Local.Info("Listening on %s", address) + + for { + conn, err := l.Accept() + if err != nil { + return fmt.Errorf("could not accept: %w", err) + } + + logger.Local.Info("New connection from %s", conn.RemoteAddr()) + + go func() { + defer func() { + if err := conn.Close(); err != nil { + logger.Local.Error("Error closing connection: %v", err) + } + + logger.Local.Info("Connection closed") + }() + + protoManager := dap.NewProtocolManager(logger.Local) + logger.ProtocolManager = protoManager + + debugParams := []debug.DebuggerOption{ + debug.SetEventHandler(newEventHandler(protoManager)), + debug.SetLogger(logger), + } + + debugger := debug.NewDebugger(debugParams...) + + s := newState(protoManager, debugger, logger) + if err := protoManager.Start(ctx, conn, s.messageHandler); err != nil { + logger.Local.Error("Failed to handle connection: %v", err) + } + + logger.Local.Info("Closing connection...") + }() + } +} + +type state struct { + protocolManager *dap.ProtocolManager + debugger debug.Debugger + session debug.Session + logger *dap.DebugLogger + serverCapabilities *godap.Capabilities + clientCapabilities *godap.InitializeRequestArguments +} + +func newState(protocolManager *dap.ProtocolManager, debugger debug.Debugger, logger *dap.DebugLogger) *state { + return &state{ + protocolManager: protocolManager, + debugger: debugger, + logger: logger, + serverCapabilities: &godap.Capabilities{ + SupportsBreakpointLocationsRequest: true, + SupportsCancelRequest: true, + SupportsConfigurationDoneRequest: true, + SupportsSingleThreadExecutionRequests: true, + SupportSuspendDebuggee: true, + SupportTerminateDebuggee: true, + SupportsTerminateRequest: true, + }, + } +} + +func newEventHandler(pm *dap.ProtocolManager) debug.EventHandler { + return func(e debug.Event) { + switch e.Type { + case debug.ExceptionEventType: + pm.SendEvent(dap.NewStoppedExceptionEvent(e.Thread, e.Message)) + case debug.StdoutEventType: + pm.SendEvent(dap.NewOutputEvent("stdout", e.Message)) + case debug.StoppedEventType: + pm.SendEvent(dap.NewStoppedEvent(e.Message, e.Thread, nil, "", "")) + case debug.TerminatedEventType: + pm.SendEvent(dap.NewTerminatedEvent()) + case debug.ThreadEventType: + pm.SendEvent(dap.NewThreadEvent(e.Thread, e.Message)) + } + } +} + +func (s *state) messageHandler(ctx context.Context, message godap.Message) (bool, godap.ResponseMessage, error) { + var resp godap.ResponseMessage + + var err error + + switch request := message.(type) { + case *godap.AttachRequest: + resp = dap.NewAttachResponse() + err = errors.New("attach not supported") + case *godap.BreakpointLocationsRequest: + resp = s.breakpointLocations(request) + case *godap.ConfigurationDoneRequest: + err = s.start() + resp = dap.NewConfigurationDoneResponse() + case *godap.ContinueRequest: + resp, err = s.resume(request) + case *godap.DisconnectRequest: + return true, dap.NewDisconnectResponse(), nil + case *godap.EvaluateRequest: + resp, err = s.evaluate(request) + case *godap.InitializeRequest: + resp = s.initialize(request) + case *godap.LaunchRequest: + resp, err = s.launch(ctx, request) + case *godap.NextRequest: + resp, err = s.next(request) + case *godap.ScopesRequest: + resp, err = s.scopes(request) + case *godap.SetBreakpointsRequest: + resp, err = s.setBreakpoints(request) + case *godap.StackTraceRequest: + resp, err = s.stackTrace(request) + case *godap.StepInRequest: + resp, err = s.stepIn(request) + case *godap.StepOutRequest: + resp, err = s.stepOut(request) + case *godap.TerminateRequest: + resp, err = s.terminate(request) + case *godap.ThreadsRequest: + resp, err = s.threads(request) + case *godap.VariablesRequest: + resp, err = s.variables(request) + default: + s.logger.Warn("Handler not found for request: %T", message) + err = fmt.Errorf("handler not found for request: %T", message) + } + + return false, resp, err +} + +func (s *state) initialize(r *godap.InitializeRequest) *godap.InitializeResponse { + if args, err := json.Marshal(r.Arguments); err == nil { + s.logger.Info("Initializing: %s", args) + } else { + s.logger.Info("Initializing") + } + + s.clientCapabilities = &r.Arguments + + return dap.NewInitializeResponse(*s.serverCapabilities) +} + +type launchProperties struct { + Command string `json:"command"` + LogLevel string `json:"logLevel"` +} + +func (s *state) launch(ctx context.Context, r *godap.LaunchRequest) (*godap.LaunchResponse, error) { + var props launchProperties + if err := json.Unmarshal(r.Arguments, &props); err != nil { + return dap.NewLaunchResponse(), fmt.Errorf("invalid launch properties: %w", err) + } + + if props.LogLevel != "" { + s.logger.SetLevelFromString(props.LogLevel) + } else { + s.logger.SetRemoteEnabled(false) + } + + s.logger.Info("Launching: %s", props) + + var err error + + switch props.Command { + case "eval": + var evalProps debug.LaunchEvalProperties + if err := json.Unmarshal(r.Arguments, &evalProps); err != nil { + return dap.NewLaunchResponse(), fmt.Errorf("invalid launch eval properties: %w", err) + } + + // FIXME: Should we protect this with a mutex? + s.session, err = s.debugger.LaunchEval(ctx, evalProps) + case "test": + err = errors.New("test not supported") + case "": + err = errors.New("missing launch command") + default: + err = fmt.Errorf("unsupported launch command: '%s'", props.Command) + } + + if err == nil { + // err = s.session.ResumeAll() + s.protocolManager.SendEvent(dap.NewInitializedEvent()) + } + + return dap.NewLaunchResponse(), err +} + +func (s *state) start() error { + if err := s.session.ResumeAll(); err != nil { + return fmt.Errorf("failed to start debug session: %w", err) + } + + return nil +} + +func (*state) evaluate(_ *godap.EvaluateRequest) (*godap.EvaluateResponse, error) { + return dap.NewEvaluateResponse(""), errors.New("evaluate not supported") +} + +func (s *state) resume(r *godap.ContinueRequest) (*godap.ContinueResponse, error) { + return dap.NewContinueResponse(), s.session.Resume(debug.ThreadID(r.Arguments.ThreadId)) +} + +func (s *state) next(r *godap.NextRequest) (*godap.NextResponse, error) { + return dap.NewNextResponse(), s.session.StepOver(debug.ThreadID(r.Arguments.ThreadId)) +} + +func (s *state) stepIn(r *godap.StepInRequest) (*godap.StepInResponse, error) { + return dap.NewStepInResponse(), s.session.StepIn(debug.ThreadID(r.Arguments.ThreadId)) +} + +func (s *state) stepOut(r *godap.StepOutRequest) (*godap.StepOutResponse, error) { + return dap.NewStepOutResponse(), s.session.StepOut(debug.ThreadID(r.Arguments.ThreadId)) +} + +func (s *state) threads(_ *godap.ThreadsRequest) (*godap.ThreadsResponse, error) { + var threads []godap.Thread + + ts, err := s.session.Threads() + if err == nil { + for _, t := range ts { + threads = append(threads, godap.Thread{Id: int(t.ID()), Name: t.Name()}) + } + } + + return dap.NewThreadsResponse(threads), err +} + +func (s *state) stackTrace(r *godap.StackTraceRequest) (*godap.StackTraceResponse, error) { + var stackFrames []godap.StackFrame + + fs, err := s.session.StackTrace(debug.ThreadID(r.Arguments.ThreadId)) + if err == nil { + for _, f := range fs { + var source *godap.Source + source, line, col, endLine, endCol := pos(f.Location()) + stackFrames = append(stackFrames, godap.StackFrame{ + Id: int(f.ID()), + Name: f.Name(), + Source: source, + Line: line, + Column: col, + EndLine: endLine, + EndColumn: endCol, + PresentationHint: "normal", + }) + } + } + + return dap.NewStackTraceResponse(stackFrames), err +} + +func pos(loc *location.Location) (source *godap.Source, line, col, endLine, endCol int) { + if loc == nil { + return nil, 1, 0, 1, 0 + } + + if loc.File != "" { + source = &godap.Source{ + Path: loc.File, + } + } + + lines := strings.Split(string(loc.Text), "\n") + line = loc.Row + col = loc.Col + + // vs-code will select text if multiple lines are present; avoid this + // endLine = loc.Row + len(lines) - 1 + // endCol = col + len(lines[len(lines)-1]) + endLine = line + endCol = col + len(lines[0]) + + return +} + +func (s *state) scopes(r *godap.ScopesRequest) (*godap.ScopesResponse, error) { + var scopes []godap.Scope + + ss, err := s.session.Scopes(debug.FrameID(r.Arguments.FrameId)) + if err == nil { + for _, s := range ss { + var source *godap.Source + + line := 1 + + if loc := s.Location(); loc != nil { + line = loc.Row + + if loc.File != "" { + source = &godap.Source{ + Path: loc.File, + } + } + } + + scopes = append(scopes, godap.Scope{ + Name: s.Name(), + NamedVariables: s.NamedVariables(), + VariablesReference: int(s.VariablesReference()), + Source: source, + Line: line, + }) + } + } + + return dap.NewScopesResponse(scopes), err +} + +func (s *state) variables(r *godap.VariablesRequest) (*godap.VariablesResponse, error) { + var variables []godap.Variable + + vs, err := s.session.Variables(debug.VarRef(r.Arguments.VariablesReference)) + if err == nil { + for _, v := range vs { + variables = append(variables, godap.Variable{ + Name: v.Name(), + Value: v.Value(), + Type: v.Type(), + VariablesReference: int(v.VariablesReference()), + }) + } + } + + return dap.NewVariablesResponse(variables), err +} + +func (s *state) breakpointLocations(request *godap.BreakpointLocationsRequest) *godap.BreakpointLocationsResponse { + line := request.Arguments.Line + s.logger.Debug("Breakpoint locations requested for: %s:%d", request.Arguments.Source.Name, line) + + // TODO: Actually assert where breakpoints can be placed. + return dap.NewBreakpointLocationsResponse([]godap.BreakpointLocation{ + { + Line: line, + Column: 1, + }, + }) +} + +func (s *state) setBreakpoints(request *godap.SetBreakpointsRequest) (*godap.SetBreakpointsResponse, error) { + bps, err := s.session.Breakpoints() + if err != nil { + return dap.NewSetBreakpointsResponse(nil), err + } + + // Remove all breakpoints for the given source. + for _, bp := range bps { + if bp.Location().File != request.Arguments.Source.Path { + continue + } + + if _, err := s.session.RemoveBreakpoint(bp.ID()); err != nil { + return dap.NewSetBreakpointsResponse(nil), err + } + } + + breakpoints := make([]godap.Breakpoint, 0, len(request.Arguments.Breakpoints)) + + for _, sbp := range request.Arguments.Breakpoints { + loc := location.Location{ + File: request.Arguments.Source.Path, + Row: sbp.Line, + } + + bp, err := s.session.AddBreakpoint(loc) + if err != nil { + return dap.NewSetBreakpointsResponse(breakpoints), err + } + + breakpoints = append(breakpoints, godap.Breakpoint{ + Id: int(bp.ID()), + Source: &godap.Source{Path: loc.File}, + Line: bp.Location().Row, + Verified: true, + }) + } + + return dap.NewSetBreakpointsResponse(breakpoints), err +} + +func (s *state) terminate(_ *godap.TerminateRequest) (*godap.TerminateResponse, error) { + return dap.NewTerminateResponse(), s.session.Terminate() +} + +type cmdConn struct { + in io.ReadCloser + out io.WriteCloser +} + +func newCmdConn(in io.ReadCloser, out io.WriteCloser) *cmdConn { + return &cmdConn{ + in: in, + out: out, + } +} + +func (c *cmdConn) Read(p []byte) (int, error) { + n, err := c.in.Read(p) + if err != nil { + return n, fmt.Errorf("failed to read: %w", err) + } + + return n, nil +} + +func (c *cmdConn) Write(p []byte) (int, error) { + n, err := c.out.Write(p) + if err != nil { + return n, fmt.Errorf("failed to write: %w", err) + } + + return n, nil +} + +func (c *cmdConn) Close() error { + var errs []error + + if err := c.in.Close(); err != nil { + errs = append(errs, err) + } + + if err := c.out.Close(); err != nil { + errs = append(errs, err) + } + + if len(errs) > 0 { + return fmt.Errorf("errors: %v", errs) + } + + return nil +} diff --git a/go.mod b/go.mod index 7589e6cd..9fc3a982 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 github.com/gobwas/glob v0.2.3 github.com/google/go-cmp v0.6.0 + github.com/google/go-dap v0.12.0 github.com/jstemmer/go-junit-report/v2 v2.1.0 github.com/mitchellh/mapstructure v1.5.0 github.com/olekukonko/tablewriter v0.0.5 diff --git a/go.sum b/go.sum index 9ae2d522..ff3ab6dc 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,8 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-dap v0.12.0 h1:rVcjv3SyMIrpaOoTAdFDyHs99CwVOItIJGKLQFQhNeM= +github.com/google/go-dap v0.12.0/go.mod h1:tNjCASCm5cqePi/RVXXWEVqtnNLV1KTWtYOqu6rZNzc= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= diff --git a/internal/dap/dap.go b/internal/dap/dap.go new file mode 100644 index 00000000..aca2de97 --- /dev/null +++ b/internal/dap/dap.go @@ -0,0 +1,461 @@ +package dap + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "sync" + + godap "github.com/google/go-dap" + + "github.com/open-policy-agent/opa/debug" + "github.com/open-policy-agent/opa/logging" +) + +type MessageHandler func(ctx context.Context, request godap.Message) (bool, godap.ResponseMessage, error) + +type ProtocolManager struct { + inChan chan godap.Message + outChan chan godap.Message + logger logging.Logger + seq int + seqLock sync.Mutex +} + +func NewProtocolManager(logger logging.Logger) *ProtocolManager { + return &ProtocolManager{ + inChan: make(chan godap.Message), + outChan: make(chan godap.Message), + logger: logger, + } +} + +func (pm *ProtocolManager) Start(ctx context.Context, conn io.ReadWriteCloser, handle MessageHandler) error { + reader := bufio.NewReader(conn) + done := make(chan error) + + go func() { + for resp := range pm.outChan { + if pm.logger.GetLevel() == logging.Debug { + if respData, err := json.Marshal(resp); respData != nil && err == nil { + pm.logger.Debug("Sending %T\n%s", resp, respData) + } else { + pm.logger.Debug("Sending %T", resp) + } + } + + if err := godap.WriteProtocolMessage(conn, resp); err != nil { + done <- err + + return + } + } + }() + + go func() { + for { + pm.logger.Debug("Waiting for message...") + + req, err := godap.ReadProtocolMessage(reader) + if err != nil { + done <- err + + return + } + + if pm.logger.GetLevel() == logging.Debug { + if reqData, err := json.Marshal(req); reqData != nil && err == nil { + pm.logger.Debug("Received %T\n%s", req, reqData) + } else { + pm.logger.Debug("Received %T", req) + } + } + + stop, resp, err := handle(ctx, req) + if err != nil { + pm.logger.Warn("Error handling request: %v", err) + } + + pm.SendResponse(resp, req, err) + + if stop { + done <- err + + return + } + } + }() + + for { + select { + case err := <-done: + return err + case <-ctx.Done(): + return fmt.Errorf("context closed: %w", ctx.Err()) + } + } +} + +func (pm *ProtocolManager) SendEvent(e godap.EventMessage) { + e.GetEvent().Seq = pm.nextSeq() + pm.outChan <- e +} + +func (pm *ProtocolManager) SendResponse(resp godap.ResponseMessage, req godap.Message, err error) { + if resp == nil { + return + } + + if r := resp.GetResponse(); r != nil { + r.Success = err == nil + + if err != nil { + r.Message = err.Error() + } + + r.Seq = pm.nextSeq() + if req != nil { + r.RequestSeq = req.GetSeq() + } + } + pm.outChan <- resp +} + +func (pm *ProtocolManager) Close() { + close(pm.outChan) + close(pm.inChan) +} + +func (pm *ProtocolManager) nextSeq() int { + if pm == nil { + return 0 + } + + pm.seqLock.Lock() + defer pm.seqLock.Unlock() + + pm.seq++ + + return pm.seq +} + +func NewContinueResponse() *godap.ContinueResponse { + return &godap.ContinueResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "continue", + Success: true, + }, + } +} + +func NewNextResponse() *godap.NextResponse { + return &godap.NextResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "next", + Success: true, + }, + } +} + +func NewStepInResponse() *godap.StepInResponse { + return &godap.StepInResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "stepIn", + Success: true, + }, + } +} + +func NewStepOutResponse() *godap.StepOutResponse { + return &godap.StepOutResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "stepOut", + Success: true, + }, + } +} + +func NewInitializeResponse(capabilities godap.Capabilities) *godap.InitializeResponse { + return &godap.InitializeResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "initialize", + Success: true, + }, + Body: capabilities, + } +} + +func NewAttachResponse() *godap.AttachResponse { + return &godap.AttachResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "attach", + Success: true, + }, + } +} + +func NewBreakpointLocationsResponse(breakpoints []godap.BreakpointLocation) *godap.BreakpointLocationsResponse { + return &godap.BreakpointLocationsResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "breakpointLocations", + Success: true, + }, + Body: godap.BreakpointLocationsResponseBody{ + Breakpoints: breakpoints, + }, + } +} + +func NewSetBreakpointsResponse(breakpoints []godap.Breakpoint) *godap.SetBreakpointsResponse { + return &godap.SetBreakpointsResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "setBreakpoints", + Success: true, + }, + Body: godap.SetBreakpointsResponseBody{ + Breakpoints: breakpoints, + }, + } +} + +func NewConfigurationDoneResponse() *godap.ConfigurationDoneResponse { + return &godap.ConfigurationDoneResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "configurationDone", + Success: true, + }, + } +} + +func NewDisconnectResponse() *godap.DisconnectResponse { + return &godap.DisconnectResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "disconnect", + Success: true, + }, + } +} + +func NewEvaluateResponse(value string) *godap.EvaluateResponse { + return &godap.EvaluateResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "evaluate", + Success: true, + }, + Body: godap.EvaluateResponseBody{ + Result: value, + }, + } +} + +func NewLaunchResponse() *godap.LaunchResponse { + return &godap.LaunchResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "launch", + Success: true, + }, + } +} + +func NewScopesResponse(scopes []godap.Scope) *godap.ScopesResponse { + return &godap.ScopesResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "scopes", + Success: true, + }, + Body: godap.ScopesResponseBody{ + Scopes: scopes, + }, + } +} + +func NewStackTraceResponse(stack []godap.StackFrame) *godap.StackTraceResponse { + return &godap.StackTraceResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "stackTrace", + Success: true, + }, + Body: godap.StackTraceResponseBody{ + StackFrames: stack, + TotalFrames: len(stack), + }, + } +} + +func NewTerminateResponse() *godap.TerminateResponse { + return &godap.TerminateResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "terminate", + Success: true, + }, + } +} + +func NewThreadsResponse(threads []godap.Thread) *godap.ThreadsResponse { + return &godap.ThreadsResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "threads", + Success: true, + }, + Body: godap.ThreadsResponseBody{ + Threads: threads, + }, + } +} + +func NewVariablesResponse(variables []godap.Variable) *godap.VariablesResponse { + return &godap.VariablesResponse{ + Response: godap.Response{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "response", + }, + Command: "variables", + Success: true, + }, + Body: godap.VariablesResponseBody{ + Variables: variables, + }, + } +} + +// Events + +func NewInitializedEvent() *godap.InitializedEvent { + return &godap.InitializedEvent{ + Event: godap.Event{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "event", + }, + Event: "initialized", + }, + } +} + +func NewOutputEvent(category string, output string) *godap.OutputEvent { + return &godap.OutputEvent{ + Event: godap.Event{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "event", + }, + Event: "output", + }, + Body: godap.OutputEventBody{ + Output: output, + Category: category, + }, + } +} + +func NewThreadEvent(threadID debug.ThreadID, reason string) *godap.ThreadEvent { + return &godap.ThreadEvent{ + Event: godap.Event{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "event", + }, + Event: "thread", + }, + Body: godap.ThreadEventBody{ + Reason: reason, + ThreadId: int(threadID), + }, + } +} + +func NewTerminatedEvent() *godap.TerminatedEvent { + return &godap.TerminatedEvent{ + Event: godap.Event{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "event", + }, + Event: "terminated", + }, + } +} + +func NewStoppedEntryEvent(threadID debug.ThreadID) *godap.StoppedEvent { + return NewStoppedEvent("entry", threadID, nil, "", "") +} + +func NewStoppedExceptionEvent(threadID debug.ThreadID, text string) *godap.StoppedEvent { + return NewStoppedEvent("exception", threadID, nil, "", text) +} + +func NewStoppedResultEvent(threadID debug.ThreadID) *godap.StoppedEvent { + return NewStoppedEvent("result", threadID, nil, "", "") +} + +func NewStoppedBreakpointEvent(threadID debug.ThreadID, bp *godap.Breakpoint) *godap.StoppedEvent { + return NewStoppedEvent("breakpoint", threadID, []int{bp.Id}, "", "") +} + +func NewStoppedEvent(reason string, threadID debug.ThreadID, bps []int, description string, + text string, +) *godap.StoppedEvent { + return &godap.StoppedEvent{ + Event: godap.Event{ + ProtocolMessage: godap.ProtocolMessage{ + Type: "event", + }, + Event: "stopped", + }, + Body: godap.StoppedEventBody{ + Reason: reason, + ThreadId: int(threadID), + Text: text, + Description: description, + AllThreadsStopped: true, + HitBreakpointIds: bps, + PreserveFocusHint: false, + }, + } +} diff --git a/internal/dap/logger.go b/internal/dap/logger.go new file mode 100644 index 00000000..533732b7 --- /dev/null +++ b/internal/dap/logger.go @@ -0,0 +1,143 @@ +package dap + +import ( + strFmt "fmt" + + "github.com/open-policy-agent/opa/logging" +) + +type DebugLogger struct { + Local logging.Logger + ProtocolManager *ProtocolManager + + level logging.Level + remoteEnabled bool +} + +func NewDebugLogger(localLogger logging.Logger, level logging.Level) *DebugLogger { + return &DebugLogger{ + Local: localLogger, + level: level, + } +} + +func (l *DebugLogger) Debug(fmt string, a ...interface{}) { + if l == nil { + return + } + + l.Local.Debug(fmt, a...) + + l.send(logging.Debug, fmt, a...) +} + +func (l *DebugLogger) Info(fmt string, a ...interface{}) { + if l == nil { + return + } + + l.Local.Info(fmt, a...) + + l.send(logging.Info, fmt, a...) +} + +func (l *DebugLogger) Error(fmt string, a ...interface{}) { + if l == nil { + return + } + + l.Local.Error(fmt, a...) + + l.send(logging.Error, fmt, a...) +} + +func (l *DebugLogger) Warn(fmt string, a ...interface{}) { + if l == nil { + return + } + + l.Local.Warn(fmt, a...) + + l.send(logging.Warn, fmt, a...) +} + +func (l *DebugLogger) WithFields(map[string]interface{}) logging.Logger { + if l == nil { + return nil + } + + return l +} + +func (l *DebugLogger) GetLevel() logging.Level { + if l == nil { + return 0 + } + + if l.Local.GetLevel() > l.level { + return l.level + } + + return l.level +} + +func (l *DebugLogger) SetRemoteEnabled(enabled bool) { + if l == nil { + return + } + + l.remoteEnabled = enabled +} + +func (l *DebugLogger) SetLevel(level logging.Level) { + if l == nil { + return + } + + l.level = level +} + +func (l *DebugLogger) SetLevelFromString(level string) { + if l == nil { + return + } + + l.remoteEnabled = true + + switch level { + case "error": + l.level = logging.Error + case "warn": + l.level = logging.Warn + case "info": + l.level = logging.Info + case "debug": + l.level = logging.Debug + } +} + +func (l *DebugLogger) send(level logging.Level, fmt string, a ...interface{}) { + if l == nil || l.ProtocolManager == nil || !l.remoteEnabled || level > l.level { + return + } + + var levelStr string + + switch level { + case logging.Error: + levelStr = "ERROR" + case logging.Warn: + levelStr = "WARN" + case logging.Info: + levelStr = "INFO" + case logging.Debug: + levelStr = "DEBUG" + default: + levelStr = "UNKNOWN" + } + + message := strFmt.Sprintf(fmt, a...) + output := strFmt.Sprintf("%s: %s\n", levelStr, message) + + l.ProtocolManager.SendEvent(NewOutputEvent("console", output)) +}