// Copyright 2016 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package breakpoints handles breakpoint requests we get from the user through // the Debuglet Controller, and manages corresponding breakpoints set in the code. package breakpoints import ( "log" "sync" "cloud.google.com/go/cmd/go-cloud-debug-agent/internal/debug" cd "google.golang.org/api/clouddebugger/v2" ) // BreakpointStore stores the set of breakpoints for a program. type BreakpointStore struct { mu sync.Mutex // prog is the program being debugged. prog debug.Program // idToBreakpoint is a map from breakpoint identifier to *cd.Breakpoint. The // map value is nil if the breakpoint is inactive. A breakpoint is active if: // - We received it from the Debuglet Controller, and it was active at the time; // - We were able to set code breakpoints for it; // - We have not reached any of those code breakpoints while satisfying the // breakpoint's conditions, or the breakpoint has action LOG; and // - The Debuglet Controller hasn't informed us the breakpoint has become inactive. idToBreakpoint map[string]*cd.Breakpoint // pcToBps and bpToPCs store the many-to-many relationship between breakpoints we // received from the Debuglet Controller and the code breakpoints we set for them. pcToBps map[uint64][]*cd.Breakpoint bpToPCs map[*cd.Breakpoint][]uint64 // errors contains any breakpoints which couldn't be set because they caused an // error. These are retrieved with ErrorBreakpoints, and the caller is // expected to handle sending updates for them. errors []*cd.Breakpoint } // NewBreakpointStore returns a BreakpointStore for the given program. func NewBreakpointStore(prog debug.Program) *BreakpointStore { return &BreakpointStore{ idToBreakpoint: make(map[string]*cd.Breakpoint), pcToBps: make(map[uint64][]*cd.Breakpoint), bpToPCs: make(map[*cd.Breakpoint][]uint64), prog: prog, } } // ProcessBreakpointList applies updates received from the Debuglet Controller through a List call. func (bs *BreakpointStore) ProcessBreakpointList(bps []*cd.Breakpoint) { bs.mu.Lock() defer bs.mu.Unlock() for _, bp := range bps { if storedBp, ok := bs.idToBreakpoint[bp.Id]; ok { if storedBp != nil && bp.IsFinalState { // IsFinalState indicates that the breakpoint has been made inactive. bs.removeBreakpointLocked(storedBp) } } else { if bp.IsFinalState { // The controller is notifying us that the breakpoint is no longer active, // but we didn't know about it anyway. continue } if bp.Action != "" && bp.Action != "CAPTURE" && bp.Action != "LOG" { bp.IsFinalState = true bp.Status = &cd.StatusMessage{ Description: &cd.FormatMessage{Format: "Action is not supported"}, IsError: true, } bs.errors = append(bs.errors, bp) // Note in idToBreakpoint that we've already seen this breakpoint, so that we // don't try to report it as an error multiple times. bs.idToBreakpoint[bp.Id] = nil continue } pcs, err := bs.prog.BreakpointAtLine(bp.Location.Path, uint64(bp.Location.Line)) if err != nil { log.Printf("error setting breakpoint at %s:%d: %v", bp.Location.Path, bp.Location.Line, err) } if len(pcs) == 0 { // We can't find a PC for this breakpoint's source line, so don't make it active. // TODO: we could snap the line to a location where we can break, or report an error to the user. bs.idToBreakpoint[bp.Id] = nil } else { bs.idToBreakpoint[bp.Id] = bp for _, pc := range pcs { bs.pcToBps[pc] = append(bs.pcToBps[pc], bp) } bs.bpToPCs[bp] = pcs } } } } // ErrorBreakpoints returns a slice of Breakpoints that caused errors when the // BreakpointStore tried to process them, and resets the list of such // breakpoints. // The caller is expected to send updates to the server to indicate the errors. func (bs *BreakpointStore) ErrorBreakpoints() []*cd.Breakpoint { bs.mu.Lock() defer bs.mu.Unlock() bps := bs.errors bs.errors = nil return bps } // BreakpointsAtPC returns all the breakpoints for which we set a code // breakpoint at the given address. func (bs *BreakpointStore) BreakpointsAtPC(pc uint64) []*cd.Breakpoint { bs.mu.Lock() defer bs.mu.Unlock() return bs.pcToBps[pc] } // RemoveBreakpoint makes the given breakpoint inactive. // This is called when either the debugged program hits the breakpoint, or the Debuglet // Controller informs us that the breakpoint is now inactive. func (bs *BreakpointStore) RemoveBreakpoint(bp *cd.Breakpoint) { bs.mu.Lock() bs.removeBreakpointLocked(bp) bs.mu.Unlock() } func (bs *BreakpointStore) removeBreakpointLocked(bp *cd.Breakpoint) { // Set the ID's corresponding breakpoint to nil, so that we won't activate it // if we see it again. // TODO: we could delete it after a few seconds. bs.idToBreakpoint[bp.Id] = nil // Delete bp from the list of cd breakpoints at each of its corresponding // code breakpoint locations, and delete any code breakpoints which no longer // have a corresponding cd breakpoint. var codeBreakpointsToDelete []uint64 for _, pc := range bs.bpToPCs[bp] { bps := remove(bs.pcToBps[pc], bp) if len(bps) == 0 { // bp was the last breakpoint set at this PC, so delete the code breakpoint. codeBreakpointsToDelete = append(codeBreakpointsToDelete, pc) delete(bs.pcToBps, pc) } else { bs.pcToBps[pc] = bps } } if len(codeBreakpointsToDelete) > 0 { bs.prog.DeleteBreakpoints(codeBreakpointsToDelete) } delete(bs.bpToPCs, bp) } // remove updates rs by removing r, then returns rs. // The mutex in the BreakpointStore which contains rs should be held. func remove(rs []*cd.Breakpoint, r *cd.Breakpoint) []*cd.Breakpoint { for i := range rs { if rs[i] == r { rs[i] = rs[len(rs)-1] rs = rs[0 : len(rs)-1] return rs } } // We shouldn't reach here. return rs }