1// Copyright 2016 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Package breakpoints handles breakpoint requests we get from the user through
16// the Debuglet Controller, and manages corresponding breakpoints set in the code.
17package breakpoints
18
19import (
20	"log"
21	"sync"
22
23	"cloud.google.com/go/cmd/go-cloud-debug-agent/internal/debug"
24	cd "google.golang.org/api/clouddebugger/v2"
25)
26
27// BreakpointStore stores the set of breakpoints for a program.
28type BreakpointStore struct {
29	mu sync.Mutex
30	// prog is the program being debugged.
31	prog debug.Program
32	// idToBreakpoint is a map from breakpoint identifier to *cd.Breakpoint.  The
33	// map value is nil if the breakpoint is inactive.  A breakpoint is active if:
34	// - We received it from the Debuglet Controller, and it was active at the time;
35	// - We were able to set code breakpoints for it;
36	// - We have not reached any of those code breakpoints while satisfying the
37	//   breakpoint's conditions, or the breakpoint has action LOG; and
38	// - The Debuglet Controller hasn't informed us the breakpoint has become inactive.
39	idToBreakpoint map[string]*cd.Breakpoint
40	// pcToBps and bpToPCs store the many-to-many relationship between breakpoints we
41	// received from the Debuglet Controller and the code breakpoints we set for them.
42	pcToBps map[uint64][]*cd.Breakpoint
43	bpToPCs map[*cd.Breakpoint][]uint64
44	// errors contains any breakpoints which couldn't be set because they caused an
45	// error.  These are retrieved with ErrorBreakpoints, and the caller is
46	// expected to handle sending updates for them.
47	errors []*cd.Breakpoint
48}
49
50// NewBreakpointStore returns a BreakpointStore for the given program.
51func NewBreakpointStore(prog debug.Program) *BreakpointStore {
52	return &BreakpointStore{
53		idToBreakpoint: make(map[string]*cd.Breakpoint),
54		pcToBps:        make(map[uint64][]*cd.Breakpoint),
55		bpToPCs:        make(map[*cd.Breakpoint][]uint64),
56		prog:           prog,
57	}
58}
59
60// ProcessBreakpointList applies updates received from the Debuglet Controller through a List call.
61func (bs *BreakpointStore) ProcessBreakpointList(bps []*cd.Breakpoint) {
62	bs.mu.Lock()
63	defer bs.mu.Unlock()
64	for _, bp := range bps {
65		if storedBp, ok := bs.idToBreakpoint[bp.Id]; ok {
66			if storedBp != nil && bp.IsFinalState {
67				// IsFinalState indicates that the breakpoint has been made inactive.
68				bs.removeBreakpointLocked(storedBp)
69			}
70		} else {
71			if bp.IsFinalState {
72				// The controller is notifying us that the breakpoint is no longer active,
73				// but we didn't know about it anyway.
74				continue
75			}
76			if bp.Action != "" && bp.Action != "CAPTURE" && bp.Action != "LOG" {
77				bp.IsFinalState = true
78				bp.Status = &cd.StatusMessage{
79					Description: &cd.FormatMessage{Format: "Action is not supported"},
80					IsError:     true,
81				}
82				bs.errors = append(bs.errors, bp)
83				// Note in idToBreakpoint that we've already seen this breakpoint, so that we
84				// don't try to report it as an error multiple times.
85				bs.idToBreakpoint[bp.Id] = nil
86				continue
87			}
88			pcs, err := bs.prog.BreakpointAtLine(bp.Location.Path, uint64(bp.Location.Line))
89			if err != nil {
90				log.Printf("error setting breakpoint at %s:%d: %v", bp.Location.Path, bp.Location.Line, err)
91			}
92			if len(pcs) == 0 {
93				// We can't find a PC for this breakpoint's source line, so don't make it active.
94				// TODO: we could snap the line to a location where we can break, or report an error to the user.
95				bs.idToBreakpoint[bp.Id] = nil
96			} else {
97				bs.idToBreakpoint[bp.Id] = bp
98				for _, pc := range pcs {
99					bs.pcToBps[pc] = append(bs.pcToBps[pc], bp)
100				}
101				bs.bpToPCs[bp] = pcs
102			}
103		}
104	}
105}
106
107// ErrorBreakpoints returns a slice of Breakpoints that caused errors when the
108// BreakpointStore tried to process them, and resets the list of such
109// breakpoints.
110// The caller is expected to send updates to the server to indicate the errors.
111func (bs *BreakpointStore) ErrorBreakpoints() []*cd.Breakpoint {
112	bs.mu.Lock()
113	defer bs.mu.Unlock()
114	bps := bs.errors
115	bs.errors = nil
116	return bps
117}
118
119// BreakpointsAtPC returns all the breakpoints for which we set a code
120// breakpoint at the given address.
121func (bs *BreakpointStore) BreakpointsAtPC(pc uint64) []*cd.Breakpoint {
122	bs.mu.Lock()
123	defer bs.mu.Unlock()
124	return bs.pcToBps[pc]
125}
126
127// RemoveBreakpoint makes the given breakpoint inactive.
128// This is called when either the debugged program hits the breakpoint, or the Debuglet
129// Controller informs us that the breakpoint is now inactive.
130func (bs *BreakpointStore) RemoveBreakpoint(bp *cd.Breakpoint) {
131	bs.mu.Lock()
132	bs.removeBreakpointLocked(bp)
133	bs.mu.Unlock()
134}
135
136func (bs *BreakpointStore) removeBreakpointLocked(bp *cd.Breakpoint) {
137	// Set the ID's corresponding breakpoint to nil, so that we won't activate it
138	// if we see it again.
139	// TODO: we could delete it after a few seconds.
140	bs.idToBreakpoint[bp.Id] = nil
141
142	// Delete bp from the list of cd breakpoints at each of its corresponding
143	// code breakpoint locations, and delete any code breakpoints which no longer
144	// have a corresponding cd breakpoint.
145	var codeBreakpointsToDelete []uint64
146	for _, pc := range bs.bpToPCs[bp] {
147		bps := remove(bs.pcToBps[pc], bp)
148		if len(bps) == 0 {
149			// bp was the last breakpoint set at this PC, so delete the code breakpoint.
150			codeBreakpointsToDelete = append(codeBreakpointsToDelete, pc)
151			delete(bs.pcToBps, pc)
152		} else {
153			bs.pcToBps[pc] = bps
154		}
155	}
156	if len(codeBreakpointsToDelete) > 0 {
157		bs.prog.DeleteBreakpoints(codeBreakpointsToDelete)
158	}
159	delete(bs.bpToPCs, bp)
160}
161
162// remove updates rs by removing r, then returns rs.
163// The mutex in the BreakpointStore which contains rs should be held.
164func remove(rs []*cd.Breakpoint, r *cd.Breakpoint) []*cd.Breakpoint {
165	for i := range rs {
166		if rs[i] == r {
167			rs[i] = rs[len(rs)-1]
168			rs = rs[0 : len(rs)-1]
169			return rs
170		}
171	}
172	// We shouldn't reach here.
173	return rs
174}
175