1package gdb
2
3import (
4	"bufio"
5	"bytes"
6	"errors"
7	"fmt"
8	"strconv"
9	"strings"
10)
11
12// NotificationCallback is a callback used to report the notifications that GDB
13// send asynchronously through the MI2 interface. Responses to the commands are
14// not part of these notifications. The notification generic object contains the
15// notification sent by GDB.
16type NotificationCallback func(notification map[string]interface{})
17
18// Send issues a command to GDB. Operation is the name of the MI2 command to
19// execute without the leading "-" (this means that it is impossible send a CLI
20// command), arguments is an optional list of arguments, in GDB parlance the can
21// be: options, parameters or "--". It returns a generic object which represents
22// the reply of GDB or an error in case the command cannot be delivered to GDB.
23func (gdb *Gdb) Send(operation string, arguments ...string) (map[string]interface{}, error) {
24	// atomically increase the sequence number and queue a pending command
25	pending := make(chan map[string]interface{})
26	gdb.mutex.Lock()
27	sequence := strconv.FormatInt(gdb.sequence, 10)
28	gdb.pending[sequence] = pending
29	gdb.sequence++
30	gdb.mutex.Unlock()
31
32	// prepare the command
33	buffer := bytes.NewBufferString(fmt.Sprintf("%s-%s", sequence, operation))
34	for _, argument := range arguments {
35		buffer.WriteByte(' ')
36		// quote the argument only if needed because GDB interprets un/quoted
37		// values differently in some contexts, e.g., when the value is a
38		// number('1' vs '"1"') or an option ('--thread' vs '"--thread"')
39		if strings.ContainsAny(argument, "\a\b\f\n\r\t\v\\'\" ") {
40			argument = strconv.Quote(argument)
41		}
42		buffer.WriteString(argument)
43	}
44	buffer.WriteByte('\n')
45
46	// send the command
47	if _, err := gdb.stdin.Write(buffer.Bytes()); err != nil {
48		return nil, err
49	}
50
51	// wait for a response
52	result := <-pending
53	gdb.mutex.Lock()
54	delete(gdb.pending, sequence)
55	gdb.mutex.Unlock()
56	return result, nil
57}
58
59// CheckedSend works like Send, except that if the result returned by
60// gdb has class=error, CheckedSend returns a non-nil error value
61// (containing the gdb error message)
62func (gdb *Gdb) CheckedSend(operation string, arguments ...string) (map[string]interface{}, error) {
63	result, err := gdb.Send(operation, arguments...)
64
65	if err != nil {
66		return nil, err
67	}
68
69	if result["class"] == "error" {
70		if payload, isMap := result["payload"].(map[string]interface{}); isMap {
71			if msg, isString := payload["msg"].(string); isString {
72				return nil, errors.New(msg)
73			}
74		}
75		// Class is error, but no message? Stringify the entire
76		// result as the error message then
77		return nil, errors.New("Unknown gdb error: " + fmt.Sprint(result["payload"]))
78	}
79
80	return result, nil
81}
82
83func (gdb *Gdb) recordReader() {
84	scanner := bufio.NewScanner(gdb.stdout)
85	for scanner.Scan() {
86		// scan the GDB output one line at a time skipping the GDB terminator
87		line := scanner.Text()
88		if line == terminator {
89			continue
90		}
91
92		// parse the line and distinguish between command result and
93		// notification
94		record := parseRecord(line)
95		sequence, isResult := record[sequenceKey]
96		if isResult {
97			// if it is a result record remove the sequence field and complete
98			// the pending command
99			delete(record, sequenceKey)
100			gdb.mutex.RLock()
101			pending := gdb.pending[sequence.(string)]
102			gdb.mutex.RUnlock()
103			pending <- record
104		} else {
105			if gdb.onNotification != nil {
106				gdb.onNotification(record)
107			}
108		}
109	}
110	if err := scanner.Err(); err != nil {
111		panic(err)
112	}
113	gdb.recordReaderDone <- true
114}
115