├── .gitignore ├── web ├── example │ ├── test │ │ ├── .gitignore │ │ ├── Makefile │ │ └── dummy.c │ ├── README.md │ ├── main.go │ └── static │ │ ├── style.css │ │ ├── index.html │ │ └── script.js ├── go.mod ├── go.sum ├── README.md ├── doc.go └── handler.go ├── go.mod ├── go.sum ├── interrupt_test.go ├── LICENSE ├── doc.go ├── commands_test.go ├── io_test.go ├── lexer.go ├── grammar.y ├── parser_test.go ├── commands.go ├── gdb.go ├── README.md └── grammar.go /.gitignore: -------------------------------------------------------------------------------- 1 | /_tmp/ 2 | y.output 3 | -------------------------------------------------------------------------------- /web/example/test/.gitignore: -------------------------------------------------------------------------------- 1 | dummy 2 | -------------------------------------------------------------------------------- /web/example/test/Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS=-g 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cyrus-and/gdb 2 | 3 | go 1.18 4 | 5 | require github.com/kr/pty v1.1.8 6 | 7 | require github.com/creack/pty v1.1.18 // indirect 8 | -------------------------------------------------------------------------------- /web/example/test/dummy.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char *argv[]) 4 | { 5 | int x; 6 | 7 | printf("Hello %s, write a number!\n", argv[1]); 8 | printf("x = "); 9 | scanf("%d", &x); 10 | printf("x * x = %d\n", x * x); 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /web/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cyrus-and/gdbweb 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/cyrus-and/gdb v0.0.0-20180213171718-0306a029f42f 7 | golang.org/x/net v0.7.0 8 | ) 9 | 10 | require ( 11 | github.com/creack/pty v1.1.18 // indirect 12 | github.com/kr/pty v1.1.8 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 2 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 3 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 4 | github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= 5 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 6 | -------------------------------------------------------------------------------- /web/example/README.md: -------------------------------------------------------------------------------- 1 | # web example 2 | 3 | This is a very simple example of how the `web` package can be used to build a web-based front end for GDB. 4 | 5 | ![Screenshot](http://i.imgur.com/Ql48hXT.png) 6 | 7 | ## Usage 8 | 9 | 1. move into this directory; 10 | 11 | 2. compile the dummy test program with: 12 | 13 | ``` 14 | make -C test/ dummy 15 | ``` 16 | 3. start the server with: 17 | 18 | ``` 19 | go run main.go 20 | ``` 21 | 22 | then load `http://localhost:8080/` in your browser 23 | 24 | 4. load the test program using a relative path `test/dummy`; 25 | 26 | 5. the rest should be straightforward... 27 | -------------------------------------------------------------------------------- /web/example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/cyrus-and/gdb/web" 5 | "log" 6 | "net/http" 7 | "path/filepath" 8 | ) 9 | 10 | func main() { 11 | mux, err := web.NewHandler() 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | // static assets 16 | mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 17 | http.ServeFile(w, req, filepath.Join("static", req.URL.Path[1:])) 18 | }) 19 | // source files 20 | mux.HandleFunc("/source/", func(w http.ResponseWriter, req *http.Request) { 21 | http.ServeFile(w, req, req.URL.Path[len("/source"):]) 22 | }) 23 | log.Fatal(http.ListenAndServe("localhost:8080", mux)) 24 | } 25 | -------------------------------------------------------------------------------- /web/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 2 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 3 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 4 | github.com/cyrus-and/gdb v0.0.0-20180213171718-0306a029f42f h1:EsEZxjb7ni/7s8POJenkO8wwbbrbOO4Omr8zAQG5tO8= 5 | github.com/cyrus-and/gdb v0.0.0-20180213171718-0306a029f42f/go.mod h1:9K120i9a8gTrRiYUpFXiLMBNH3yFnNs8J66+FWOtPYE= 6 | github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= 7 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 8 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 9 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 10 | -------------------------------------------------------------------------------- /web/example/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans; 3 | max-width: 1000px; 4 | margin: 0 auto; 5 | padding: 0 1em; 6 | } 7 | 8 | label { 9 | display: block; 10 | margin-top: 1em; 11 | } 12 | 13 | .toolbar { 14 | overflow: hidden; 15 | } 16 | 17 | .toolbar-button { 18 | float: right; 19 | margin-left: 0.5em; 20 | } 21 | 22 | .toolbar-text { 23 | overflow: hidden; 24 | display: block; 25 | } 26 | 27 | input[type=text] { 28 | width: 100%; 29 | box-sizing: border-box; 30 | } 31 | 32 | .window { 33 | font-family: monospace; 34 | white-space: pre-wrap; 35 | background: black; 36 | color: #0f0; 37 | padding: 0.5em; 38 | margin-bottom: 1em; 39 | } 40 | 41 | #source span { 42 | display: block; 43 | } 44 | 45 | .selected { 46 | background: #0f0; 47 | color: #000; 48 | } 49 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # web 2 | 3 | Provides a simple web interface for the `gdb` module. 4 | 5 | Every new handler spawns a new GDB instance which is not meant to serve more than one client. 6 | 7 | Several URLs are available: 8 | 9 | - a WebSocket will be bound to `NotificationsUrl`, every notification sent by GDB will be delivered through this WebSocket as a JSON object; 10 | 11 | - a WebSocket will be bound to `TerminalUrl`: data sent through it will be delivered to the target program's input whereas data read from it will reflect the the target program's output; 12 | 13 | - POST requests are expected to `CommandsUrl` to send commands to GDB. Commands are represented by JSON arrays `[command, arguments...]`. Command responses are sent back as JSON objects in the response body; 14 | 15 | - POST requests are expected to `InterruptUrl` to send interrupts to GDB, no parameters are allowed. 16 | 17 | HTTP methods return 200 on success and 500 on error. 18 | 19 | ## Example 20 | 21 | A dummy example can be found in the [example](example/) directory. 22 | -------------------------------------------------------------------------------- /web/doc.go: -------------------------------------------------------------------------------- 1 | // Package web provides a simple web interface for the gdb module. 2 | // 3 | // Every new handler spawns a new GDB instance which is not meant to serve more 4 | // than one client. 5 | // 6 | // Several URLs are available: 7 | // 8 | // - a WebSocket will be bound to NotificationsUrl, every notification sent by 9 | // GDB will be delivered through this WebSocket as a JSON object; 10 | // 11 | // - a WebSocket will be bound to TerminalUrl: data sent through it will be 12 | // delivered to the target program's input whereas data read from it will 13 | // reflect the the target program's output; 14 | // 15 | // - POST requests are expected to CommandsUrl to send commands to GDB. Commands 16 | // are represented by JSON arrays [command, arguments...]. Command responses 17 | // are sent back as JSON objects in the response body; 18 | // 19 | // - POST requests are expected to InterruptUrl to send interrupts to GDB, no 20 | // parameters are allowed. 21 | // 22 | // HTTP methods return 200 on success and 500 on error. 23 | package web 24 | -------------------------------------------------------------------------------- /interrupt_test.go: -------------------------------------------------------------------------------- 1 | package gdb 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestInterrupt(t *testing.T) { 9 | // start gdb 10 | gdb, err := New(nil) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | // load and start gofmt (a program which will wait for input) 16 | _, err = gdb.Send("file-exec-and-symbols", "gofmt") 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | _, err = gdb.Send("exec-run") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | interrupt := false 26 | go func() { 27 | // allow the program to start up 28 | time.Sleep(time.Second) 29 | 30 | // then interrupt the execution 31 | interrupt = true 32 | if err := gdb.Interrupt(); err != nil { 33 | t.Fatal(err) 34 | } 35 | }() 36 | 37 | // try to execute a command 38 | _, err = gdb.Send("gdb-version") 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | if interrupt == false { 43 | t.Fatal("GDB should wait for the interrupt") 44 | } 45 | 46 | // exit gdb 47 | if err := gdb.Exit(); err != nil { 48 | t.Fatal(err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Andrea Cardaci 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package gdb provides a convenient way to interact with the GDB/MI 2 | // interface. The methods offered by this module are very low level, the main 3 | // goals are: 4 | // 5 | // - avoid the tedious parsing of the MI2 line-based text interface; 6 | // 7 | // - bypass a known bug(https://sourceware.org/bugzilla/show_bug.cgi?id=8759) 8 | // which prevents to distinguish the target program's output from MI2 records. 9 | // 10 | // The objects returned as a result of the commands or as asynchronous 11 | // notifications are generic Go maps suitable to be converted to JSON format 12 | // with json.Unmarshal(). The fields present in such objects are blindly added 13 | // according to the records returned from GDB (see 14 | // https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Output-Syntax.html): 15 | // tuples are map[string]interface{} and lists are []interface{}. There are a 16 | // couple of exceptions to this: 17 | // 18 | // - the record class, where present, is represented by the "class" field; 19 | // 20 | // - the record type is represented using the "type" field as follows: 21 | // "+": "status" 22 | // "=": "notify" 23 | // "~": "console" 24 | // "@": "target" 25 | // "&": "log" 26 | // 27 | // - the optional result list is stored into a tuple under the "payload" field. 28 | package gdb 29 | 30 | //go:generate goyacc -o grammar.go grammar.y 31 | -------------------------------------------------------------------------------- /web/example/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | gdb/web example 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 |
13 | 14 |
15 | 16 |
17 |

18 | 19 | 20 | 21 | | 22 | 23 |

(load and run a program)
24 |

25 |
(no source available)
26 |
Ready!
27 | 28 |
29 | 30 | 31 | 32 |
33 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /commands_test.go: -------------------------------------------------------------------------------- 1 | package gdb 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | var consoleRecordCount = 0 9 | 10 | func onNotification(t *testing.T, notification map[string]interface{}) { 11 | if notification["type"] != "console" { 12 | consoleRecordCount++ 13 | } 14 | } 15 | 16 | func TestSend(t *testing.T) { 17 | var expected, result map[string]interface{} 18 | 19 | // start gdb 20 | gdb, err := New(func(notification map[string]interface{}) { 21 | onNotification(t, notification) 22 | }) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | // try sending a command 28 | result, err = gdb.Send("gdb-version") 29 | expected = map[string]interface{}{"class": "done"} 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | if !reflect.DeepEqual(result, expected) { 34 | t.Fatal(result, "!=", expected) 35 | } 36 | 37 | // gdb-version should generate some console records 38 | if consoleRecordCount == 0 { 39 | t.Fatal("no console records received") 40 | } 41 | 42 | // try sending a command with an argument containing a space 43 | result, err = gdb.Send("var-create", "foo", "@", "40 + 2") 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | if class, hasClass := result["class"]; !hasClass || class != "done" { 48 | t.Fatal(result, "has not class 'done'") 49 | } 50 | 51 | // try sending a wrong command 52 | result, err = gdb.Send("foobarbaz") 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | if class, hasClass := result["class"]; !hasClass || class != "error" { 57 | t.Fatal(result, "has not class 'error'") 58 | } 59 | 60 | // exit gdb 61 | if err := gdb.Exit(); err != nil { 62 | t.Fatal(err) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /io_test.go: -------------------------------------------------------------------------------- 1 | package gdb 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestIO(t *testing.T) { 9 | input := "package foo" 10 | expectedIn := fmt.Sprintf("%s\r\n", input) // not sure why \r\n... 11 | expectedOut := "package foo\r\n" 12 | 13 | // start gdb 14 | gdb, err := New(nil) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | // start processing the output 20 | done := make(chan bool) 21 | go func() { 22 | var n int 23 | buf := make([]byte, 1024) 24 | 25 | // read the first line 26 | n, err = gdb.Read(buf) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | if string(buf[:n]) != expectedIn { 31 | fmt.Printf("'%s'\n", buf[:n]) 32 | fmt.Printf("'%s'\n", []byte(expectedIn)) 33 | t.Fatal("should read back the input") 34 | } 35 | 36 | // read the second line 37 | n, err = gdb.Read(buf) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | if string(buf[:n]) != expectedOut { 42 | fmt.Printf("'%s'\n", buf[:n]) 43 | fmt.Printf("'%s'\n", []byte(expectedOut)) 44 | t.Fatal("should read the proper output") 45 | } 46 | 47 | // try another read 48 | n, err = gdb.Read(buf) 49 | if err == nil { 50 | t.Fatal("read should fail") 51 | } 52 | 53 | done <- true 54 | }() 55 | 56 | // load a program 57 | if _, err = gdb.Send("file-exec-file", "gofmt"); err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | // provide some input 62 | if _, err := gdb.Write([]byte(input)); err != nil { 63 | t.Fatal(err) 64 | } 65 | if _, err := gdb.Write([]byte("\n\x04")); err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | // run the program 70 | if _, err = gdb.Send("exec-run"); err != nil { 71 | t.Fatal(err) 72 | } 73 | 74 | // exit gdb 75 | if err := gdb.Exit(); err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | // wait for the output processing 80 | _ = <-done 81 | } 82 | -------------------------------------------------------------------------------- /lexer.go: -------------------------------------------------------------------------------- 1 | package gdb 2 | 3 | type tokenType int 4 | type lexerState int 5 | 6 | const ( 7 | normal lexerState = iota 8 | inQuotation 9 | inEscape 10 | ) 11 | 12 | type token struct { 13 | tokenType tokenType 14 | value string 15 | } 16 | 17 | type parser struct { 18 | tokens <-chan token 19 | output map[string]interface{} 20 | } 21 | 22 | func lexer(input string) <-chan token { // no checks here... 23 | position := 0 24 | state := normal 25 | tokens := make(chan token) 26 | var value []byte 27 | go func() { 28 | for position < len(input) { 29 | next := input[position] 30 | switch state { 31 | case normal: 32 | switch next { 33 | case '^', '*', '+', '=', '~', '@', '&', ',', '{', '}', '[', ']': 34 | if value != nil { 35 | tokens <- token{tokenType(text), string(value)} 36 | value = nil 37 | } 38 | tokens <- token{tokenType(next), string(next)} 39 | case '"': 40 | state = inQuotation 41 | if value != nil { 42 | tokens <- token{tokenType(text), string(value)} 43 | value = nil 44 | } 45 | default: 46 | value = append(value, next) 47 | } 48 | case inQuotation: 49 | switch next { 50 | case '"': 51 | state = normal 52 | if value != nil { 53 | tokens <- token{tokenType(text), string(value)} 54 | } else { 55 | tokens <- token{tokenType(text), ""} 56 | } 57 | value = nil 58 | case '\\': 59 | state = inEscape 60 | default: 61 | value = append(value, next) 62 | } 63 | case inEscape: 64 | switch next { 65 | case 'a': 66 | next = '\a' 67 | case 'b': 68 | next = '\b' 69 | case 'f': 70 | next = '\f' 71 | case 'n': 72 | next = '\n' 73 | case 'r': 74 | next = '\r' 75 | case 't': 76 | next = '\t' 77 | case 'v': 78 | next = '\v' 79 | case '\\': 80 | next = '\\' 81 | case '\'': 82 | next = '\'' 83 | case '"': 84 | next = '"' 85 | } 86 | value = append(value, next) 87 | state = inQuotation 88 | } 89 | position++ 90 | } 91 | if value != nil { 92 | tokens <- token{tokenType(text), string(value)} 93 | value = nil 94 | } 95 | close(tokens) 96 | }() 97 | return tokens 98 | } 99 | 100 | func (p *parser) Lex(lval *yySymType) int { 101 | // fetch the next token 102 | token, ok := <-p.tokens 103 | if ok { 104 | // save the value and return the token type 105 | lval.text = token.value 106 | return int(token.tokenType) 107 | } else { 108 | return 0 // no more tokens 109 | } 110 | } 111 | 112 | func (p *parser) Error(err string) { 113 | // errors are GDB bugs if the grammar is correct 114 | panic(err) 115 | } 116 | 117 | func parseRecord(data string) map[string]interface{} { 118 | parser := parser{lexer(data), map[string]interface{}{}} 119 | yyParse(&parser) 120 | return parser.output 121 | } 122 | -------------------------------------------------------------------------------- /web/handler.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/cyrus-and/gdb" 6 | "golang.org/x/net/websocket" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | const ( 14 | NotificationsUrl = "/notifications" 15 | TerminalUrl = "/terminal" 16 | CommandsUrl = "/send" 17 | InterruptUrl = "/interrupt" 18 | ) 19 | 20 | func die(err error, w http.ResponseWriter) { 21 | log.Print("### ", err) 22 | w.WriteHeader(http.StatusInternalServerError) 23 | } 24 | 25 | // NewHandler returns a new http.ServeMux which automatically spawn a new 26 | // instance of GDB and offers HTTP mount points to interact with. It is assumed 27 | // that only one client at a time access this multiplexer. 28 | func NewHandler() (*http.ServeMux, error) { 29 | handler := http.NewServeMux() 30 | notificationsChan := make(chan []byte) 31 | 32 | // start a new GDB instance 33 | gdb, err := gdb.New(func(notification map[string]interface{}) { 34 | // notifications are converted to JSON and sent through the channel 35 | notificationText, err := json.Marshal(notification) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | notificationsChan <- notificationText 40 | log.Print(">>> ", string(notificationText)) 41 | }) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | // notifications WebSocket 47 | handler.Handle(NotificationsUrl, websocket.Handler(func(ws *websocket.Conn) { 48 | // deliver the notifications through the WebSocket 49 | for notification := range notificationsChan { 50 | ws.Write(notification) 51 | } 52 | })) 53 | 54 | // terminal WebSocket 55 | handler.Handle(TerminalUrl, websocket.Handler(func(ws *websocket.Conn) { 56 | // copy GDB to WS and WS to GDB 57 | go io.Copy(gdb, ws) 58 | io.Copy(ws, gdb) 59 | })) 60 | 61 | // send command action 62 | handler.HandleFunc(CommandsUrl, func(w http.ResponseWriter, req *http.Request) { 63 | if req.Method != "POST" { 64 | log.Print("### invalid method") 65 | w.WriteHeader(http.StatusInternalServerError) 66 | return 67 | } 68 | data, err := ioutil.ReadAll(req.Body) 69 | if err != nil { 70 | die(err, w) 71 | return 72 | } 73 | log.Print("<<< ", string(data)) 74 | command := []string{} 75 | err = json.Unmarshal(data, &command) 76 | if err != nil { 77 | die(err, w) 78 | return 79 | } 80 | result, err := gdb.Send(command[0], command[1:]...) 81 | if err != nil { 82 | die(err, w) 83 | return 84 | } 85 | reply, err := json.Marshal(result) 86 | if err != nil { 87 | die(err, w) 88 | return 89 | } 90 | io.WriteString(w, string(reply)) 91 | log.Print(">>> ", string(reply)) 92 | }) 93 | 94 | // send interrupt action 95 | handler.HandleFunc(InterruptUrl, func(w http.ResponseWriter, req *http.Request) { 96 | if req.Method != "POST" { 97 | log.Print("### invalid method") 98 | w.WriteHeader(http.StatusInternalServerError) 99 | return 100 | } 101 | if err := gdb.Interrupt(); err != nil { 102 | die(err, w) 103 | return 104 | } 105 | log.Print("<<< interrupt") 106 | }) 107 | 108 | return handler, nil 109 | } 110 | -------------------------------------------------------------------------------- /grammar.y: -------------------------------------------------------------------------------- 1 | // -*- go -*- 2 | %{ 3 | package gdb 4 | 5 | const ( 6 | terminator = "(gdb) " // yes there's the trailing space 7 | typeKey = "type" 8 | classKey = "class" 9 | payloadKey = "payload" 10 | sequenceKey = "sequence" 11 | ) 12 | 13 | // avoid DRY due to a poor lexer 14 | func newClassResult(typeString, class string, payload map[string]interface{}) map[string]interface{} { 15 | out := map[string]interface{}{ 16 | typeKey: typeString, 17 | classKey: class, 18 | } 19 | if payload != nil { 20 | out[payloadKey] = payload 21 | } 22 | return out 23 | } 24 | %} 25 | 26 | %union{ 27 | text string 28 | record map[string]interface{} 29 | class_result struct{class string; payload map[string]interface{}} 30 | result_pair struct{variable string; value interface{}} 31 | value interface{} 32 | list []interface{} 33 | } 34 | 35 | %token text 36 | 37 | %type text token 38 | %type result_list record async_record stream_record result_record tuple 39 | %type class_result 40 | %type result 41 | %type value 42 | %type list value_list tuple_list 43 | 44 | %% 45 | 46 | all: 47 | record { yylex.(*parser).output = $1 }; 48 | 49 | record: 50 | async_record | 51 | stream_record | 52 | result_record; 53 | 54 | async_record: 55 | token '*' class_result { $$ = newClassResult("exec", $3.class, $3.payload) } | 56 | token '+' class_result { $$ = newClassResult("status", $3.class, $3.payload) } | 57 | token '+' text ',' tuple { $$ = newClassResult("status", $3, $5) } | 58 | token '=' class_result { $$ = newClassResult("notify", $3.class, $3.payload) }; 59 | 60 | class_result: 61 | text ',' result_list { $$.class, $$.payload = $1, $3 } | 62 | text { $$.class, $$.payload = $1, nil }; 63 | 64 | stream_record: 65 | '~' text { $$ = map[string]interface{}{typeKey: "console", payloadKey: $2} } | 66 | '@' text { $$ = map[string]interface{}{typeKey: "target", payloadKey: $2} } | 67 | '&' text { $$ = map[string]interface{}{typeKey: "log", payloadKey: $2} }; 68 | 69 | result_record: 70 | token '^' class_result 71 | { 72 | $$ = map[string]interface{}{sequenceKey: $1, classKey: $3.class} 73 | if $3.payload != nil { $$[payloadKey] = $3.payload } 74 | }; 75 | 76 | result_list: 77 | result_list ',' result { $$[$3.variable] = $3.value } | 78 | result { $$ = map[string]interface{}{$1.variable: $1.value} }; 79 | 80 | token: 81 | { $$ = "" } | 82 | text { $$ = $1 } 83 | 84 | result: 85 | text '=' value { $$.variable, $$.value = $1, $3 }; 86 | 87 | value: 88 | text { $$ = $1 } | 89 | tuple { $$ = $1 } | 90 | list { $$ = $1 }; 91 | 92 | value_list: 93 | value_list ',' value { $$ = append($$, $3) } | 94 | value { $$ = []interface{}{$1} }; 95 | 96 | tuple: 97 | '{' result_list '}' { $$ = $2 } | 98 | '{' '}' { $$ = map[string]interface{}{} }; 99 | 100 | tuple_list: 101 | tuple_list ',' result { $$ = append($$, map[string]interface{}{$3.variable: $3.value}) } | 102 | result { $$ = []interface{}{map[string]interface{}{$1.variable: $1.value} } }; 103 | 104 | list: 105 | '[' value_list ']' { $$ = $2 } | 106 | '[' tuple_list ']' { $$ = $2 } | 107 | '[' ']' { $$ = []interface{}{} }; 108 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package gdb 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func checkParseRecord(t *testing.T, input, expected string) { 10 | inputObj := parseRecord(input) 11 | expectedObj := map[string]interface{}{} 12 | if err := json.Unmarshal([]byte(expected), &expectedObj); err != nil { 13 | t.Fatal(err) 14 | } 15 | if !reflect.DeepEqual(inputObj, expectedObj) { 16 | t.Fatal(inputObj, "!=", expectedObj) 17 | } 18 | } 19 | 20 | func TestParseRecord(t *testing.T) { 21 | // async record 22 | checkParseRecord(t, `*foo`, `{"type":"exec","class":"foo"}`) 23 | checkParseRecord(t, `+bar`, `{"type":"status","class":"bar"}`) 24 | checkParseRecord(t, `=baz`, `{"type":"notify","class":"baz"}`) 25 | checkParseRecord(t, `*foo,a="x"`, `{"type":"exec","class":"foo","payload":{"a":"x"}}`) 26 | checkParseRecord(t, `*foo,a="x",b="y"`, `{"type":"exec","class":"foo","payload":{"a":"x","b":"y"}}`) 27 | checkParseRecord(t, `+bar,a="x"`, `{"type":"status","class":"bar","payload":{"a":"x"}}`) 28 | checkParseRecord(t, `+bar,a="x",b="y"`, `{"type":"status","class":"bar","payload":{"a":"x","b":"y"}}`) 29 | checkParseRecord(t, `=baz,a="x"`, `{"type":"notify","class":"baz","payload":{"a":"x"}}`) 30 | checkParseRecord(t, `=baz,a="x",b="y"`, `{"type":"notify","class":"baz","payload":{"a":"x","b":"y"}}`) 31 | 32 | // stream record 33 | checkParseRecord(t, `~"foo"`, `{"type":"console","payload":"foo"}`) 34 | checkParseRecord(t, `@"bar"`, `{"type":"target","payload":"bar"}`) 35 | checkParseRecord(t, `&"baz"`, `{"type":"log","payload":"baz"}`) 36 | 37 | // result record 38 | checkParseRecord(t, `123^foo`, `{"sequence":"123","class":"foo"}`) 39 | checkParseRecord(t, `456^bar,a="x"`, `{"sequence":"456","class":"bar","payload":{"a":"x"}}`) 40 | checkParseRecord(t, `789^baz,a="x",b="y"`, `{"sequence":"789","class":"baz","payload":{"a":"x","b":"y"}}`) 41 | 42 | // complex record payload 43 | checkParseRecord(t, `000^foo,a={"b"="x","c"="y"}`, `{"sequence":"000","class":"foo","payload":{"a":{"b":"x","c":"y"}}}`) 44 | checkParseRecord(t, `000^foo,a=["b"="x","c"="y"]`, `{"sequence":"000","class":"foo","payload":{"a":[{"b":"x"},{"c":"y"}]}}`) 45 | checkParseRecord(t, `000^foo,a=["x","y"]`, `{"sequence":"000","class":"foo","payload":{"a":["x","y"]}}`) 46 | checkParseRecord(t, `000^foo,a=[[["x"]]]`, `{"sequence":"000","class":"foo","payload":{"a":[[["x"]]]}}`) 47 | checkParseRecord(t, `000^foo,a={b={c={x="y"}}}`, `{"sequence":"000","class":"foo","payload":{"a":{"b":{"c":{"x":"y"}}}}}`) 48 | checkParseRecord(t, `000^foo,a={}`, `{"sequence":"000","class":"foo","payload":{"a":{}}}`) 49 | checkParseRecord(t, `000^foo,a=[]`, `{"sequence":"000","class":"foo","payload":{"a":[]}}`) 50 | 51 | // escape sequences and strings in general 52 | checkParseRecord(t, `~""`, `{"type":"console","payload":""}`) 53 | checkParseRecord(t, `~"\b\f\n\r\t\""`, `{"type":"console","payload":"\b\f\n\r\t\""}`) 54 | checkParseRecord(t, `@""`, `{"type":"target","payload":""}`) 55 | checkParseRecord(t, `@"\b\f\n\r\t\""`, `{"type":"target","payload":"\b\f\n\r\t\""}`) 56 | checkParseRecord(t, `&""`, `{"type":"log","payload":""}`) 57 | checkParseRecord(t, `&"\b\f\n\r\t\""`, `{"type":"log","payload":"\b\f\n\r\t\""}`) 58 | checkParseRecord(t, `000^foo,a=""`, `{"sequence":"000","class":"foo","payload":{"a":""}}`) 59 | checkParseRecord(t, `000^bar,a="\b\f\n\r\t\""`, `{"sequence":"000","class":"bar","payload":{"a":"\b\f\n\r\t\""}}`) 60 | } 61 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package gdb 2 | 3 | import ( 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. 16 | type 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. 23 | func (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) 62 | func (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 | 83 | func (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 | -------------------------------------------------------------------------------- /gdb.go: -------------------------------------------------------------------------------- 1 | package gdb 2 | 3 | import ( 4 | "github.com/kr/pty" 5 | "io" 6 | "os" 7 | "os/exec" 8 | "sync" 9 | ) 10 | 11 | // Gdb represents a GDB instance. It implements the ReadWriter interface to 12 | // read/write data from/to the target program's TTY. 13 | type Gdb struct { 14 | io.ReadWriter 15 | 16 | ptm *os.File 17 | pts *os.File 18 | cmd *exec.Cmd 19 | 20 | mutex sync.RWMutex 21 | stdin io.WriteCloser 22 | stdout io.ReadCloser 23 | 24 | sequence int64 25 | pending map[string]chan map[string]interface{} 26 | 27 | onNotification NotificationCallback 28 | 29 | recordReaderDone chan bool 30 | } 31 | 32 | // New creates and starts a new GDB instance. onNotification if not nil is the 33 | // callback used to deliver to the client the asynchronous notifications sent by 34 | // GDB. It returns a pointer to the newly created instance handled or an error. 35 | func New(onNotification NotificationCallback) (*Gdb, error) { 36 | return NewCustom([]string{"gdb"}, onNotification) 37 | } 38 | 39 | // Like New, but allows to specify the GDB executable path and arguments. 40 | func NewCustom(gdbCmd []string, onNotification NotificationCallback) (*Gdb, error) { 41 | // open a new terminal (master and slave) for the target program, they are 42 | // both saved so that they are nore garbage collected after this function 43 | // ends 44 | ptm, pts, err := pty.Open() 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | // create GDB command 50 | gdbCmd = append(gdbCmd, "--nx", "--quiet", "--interpreter=mi2", "--tty", pts.Name()) 51 | gdb, err := NewCmd(gdbCmd, onNotification) 52 | 53 | if err != nil { 54 | ptm.Close() 55 | pts.Close() 56 | return nil, err 57 | } 58 | 59 | gdb.ptm = ptm 60 | gdb.pts = pts 61 | 62 | return gdb, nil 63 | } 64 | 65 | // NewCmd creates a new GDB instance like New, but allows explicitely passing 66 | // the gdb command to run (including all arguments). cmd is passed as-is to 67 | // exec.Command, so the first element should be the command to run, and the 68 | // remaining elements should each contain a single argument. 69 | func NewCmd(cmd []string, onNotification NotificationCallback) (*Gdb, error) { 70 | gdb := Gdb{onNotification: onNotification} 71 | 72 | gdb.cmd = exec.Command(cmd[0], cmd[1:]...) 73 | 74 | // GDB standard input 75 | stdin, err := gdb.cmd.StdinPipe() 76 | if err != nil { 77 | return nil, err 78 | } 79 | gdb.stdin = stdin 80 | 81 | // GDB standard ouput 82 | stdout, err := gdb.cmd.StdoutPipe() 83 | if err != nil { 84 | return nil, err 85 | } 86 | gdb.stdout = stdout 87 | 88 | // start GDB 89 | if err := gdb.cmd.Start(); err != nil { 90 | return nil, err 91 | } 92 | 93 | // prepare the command interface 94 | gdb.sequence = 1 95 | gdb.pending = make(map[string]chan map[string]interface{}) 96 | 97 | gdb.recordReaderDone = make(chan bool) 98 | 99 | // start the line reader 100 | go gdb.recordReader() 101 | 102 | return &gdb, nil 103 | } 104 | 105 | // Read reads a number of bytes from the target program's output. 106 | func (gdb *Gdb) Read(p []byte) (n int, err error) { 107 | return gdb.ptm.Read(p) 108 | } 109 | 110 | // Write writes a number of bytes to the target program's input. 111 | func (gdb *Gdb) Write(p []byte) (n int, err error) { 112 | return gdb.ptm.Write(p) 113 | } 114 | 115 | // Interrupt sends a signal (SIGINT) to GDB so it can stop the target program 116 | // and resume the processing of commands. 117 | func (gdb *Gdb) Interrupt() error { 118 | return gdb.cmd.Process.Signal(os.Interrupt) 119 | } 120 | 121 | // Exit sends the exit command to GDB and waits for the process to exit. 122 | func (gdb *Gdb) Exit() error { 123 | // send the exit command and wait for the GDB process 124 | if _, err := gdb.Send("gdb-exit"); err != nil { 125 | return err 126 | } 127 | 128 | // Wait for the recordReader to finish (due to EOF on the pipe). 129 | // If we do not wait for this, Wait() might clean up stdout 130 | // while recordReader is still using it, leading to 131 | // panic: read |0: bad file descriptor 132 | <-gdb.recordReaderDone 133 | 134 | if err := gdb.cmd.Wait(); err != nil { 135 | return err 136 | } 137 | 138 | // close the target program's terminal, since the lifetime of the terminal 139 | // is longer that the one of the targer program's instances reading from a 140 | // Gdb object ,i.e., the master side will never return EOF (at least on 141 | // Linux) so the only way to stop reading is to intercept the I/O error 142 | // caused by closing the terminal 143 | if gdb.ptm != nil { 144 | if err := gdb.ptm.Close(); err != nil { 145 | return err 146 | } 147 | } 148 | if gdb.pts != nil { 149 | if err := gdb.pts.Close(); err != nil { 150 | return err 151 | } 152 | } 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gdb 2 | 3 | Package `gdb` provides a convenient way to interact with the GDB/MI interface. The methods offered by this module are very low level, the main goals are: 4 | 5 | - avoid the tedious parsing of the MI2 line-based text interface; 6 | 7 | - bypass a [known bug][mi2-bug] which prevents to distinguish the target program's output from MI2 records. 8 | 9 | ## Web interface 10 | 11 | This package comes with an additional [HTTP/WebSocket interface](web/) which aims to provide a straightforward way to start developing web-based GDB front ends. 12 | 13 | A dummy example can be found in the [example folder](web/example). 14 | 15 | ## Example 16 | 17 | ```go 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "github.com/cyrus-and/gdb" 23 | "io" 24 | "os" 25 | ) 26 | 27 | func main() { 28 | // start a new instance and pipe the target output to stdout 29 | gdb, _ := gdb.New(nil) 30 | go io.Copy(os.Stdout, gdb) 31 | 32 | // evaluate an expression 33 | gdb.Send("var-create", "x", "@", "40 + 2") 34 | fmt.Println(gdb.Send("var-evaluate-expression", "x")) 35 | 36 | // load and run a program 37 | gdb.Send("file-exec-file", "wc") 38 | gdb.Send("exec-arguments", "-w") 39 | gdb.Write([]byte("This sentence has five words.\n\x04")) // EOT 40 | gdb.Send("exec-run") 41 | 42 | gdb.Exit() 43 | } 44 | ``` 45 | 46 | ## Installation 47 | 48 | ``` 49 | go get -u github.com/cyrus-and/gdb 50 | ``` 51 | 52 | ## Documentation 53 | 54 | [GoDoc][godoc]. 55 | 56 | ## Data representation 57 | 58 | The objects returned as a result of the commands or as asynchronous notifications are generic Go maps suitable to be converted to JSON format with `json.Marshal()`. The fields present in such objects are blindly added according to the records returned from GDB (see the [command syntax][mi2-syntax]): tuples are `map[string]interface{}` and lists are `[]interface{}`. 59 | 60 | Yet, some additional fields are added: 61 | 62 | - the record class, where present, is represented by the `"class"` field; 63 | 64 | - the record type is represented using the `"type"` field as follows: 65 | - `+`: `"status"` 66 | - `=`: `"notify"` 67 | - `~`: `"console"` 68 | - `@`: `"target"` 69 | - `&`: `"log"` 70 | 71 | - the optional result list is stored into a tuple under the `"payload"` field. 72 | 73 | For example, the notification: 74 | 75 | ``` 76 | =thread-group-exited,id="i1",exit-code="0" 77 | ``` 78 | 79 | becomes the Go map: 80 | 81 | ```go 82 | map[type:notify class:thread-group-exited payload:map[id:i1 exit-code:0]] 83 | ``` 84 | 85 | which can be converted to JSON with `json.Marshal()` obtaining: 86 | 87 | ```json 88 | { 89 | "class": "thread-group-exited", 90 | "payload": { 91 | "exit-code": "0", 92 | "id": "i1" 93 | }, 94 | "type": "notify" 95 | } 96 | ``` 97 | 98 | ## Mac OS X 99 | 100 | ### Setting up GDB on Darwin 101 | 102 | To use this module is mandatory to have a working version of GDB installed, Mac OS X users may obtain a copy using [Homebrew][homebrew] for example, then they may need to give GDB permission to control other processes as described [here][gdb-on-mac]. 103 | 104 | ### Issues 105 | 106 | The Mac OS X support, though, is partial and buggy due to the following issues. 107 | 108 | #### Pseudoterminals 109 | 110 | I/O operations on the target program happens through a pseudoterminal obtained using the [pty][pty] package which basically uses the `/dev/ptmx` on *nix systems to request new terminal instances. 111 | 112 | There are some unclear behaviors on Mac OS X. Calling `gdb.Write()` when the target program is not running is a no-op, on Linux instead writes are somewhat buffered and delivered later. Likewise, `gdb.Read()` may returns EOF even though there is actually data to read, a solution may be keep trying. 113 | 114 | #### Interrupt 115 | 116 | Sending a `SIGINT` signal to GDB has no effect on Mac OS X, on Linux instead this is equivalent to typing `^C`, so `gdb.Interrupt()` will not work. 117 | 118 | ## Development 119 | 120 | The [goyacc](https://pkg.go.dev/golang.org/x/tools/cmd/goyacc) tool is needed to generate the `grammar.go` file. Install it with: 121 | 122 | ``` 123 | go install golang.org/x/tools/cmd/goyacc@latest 124 | ``` 125 | 126 | After that use the following to update the `grammar.go` file: 127 | 128 | ``` 129 | go generate -x 130 | ``` 131 | 132 | ## Resources 133 | 134 | - [The `GDB/MI` Interface][gdb-mi] 135 | 136 | [mi2-bug]: https://sourceware.org/bugzilla/show_bug.cgi?id=8759 137 | [mi2-syntax]: https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Output-Syntax.html 138 | [godoc]: https://godoc.org/github.com/cyrus-and/gdb 139 | [homebrew]: http://brew.sh/ 140 | [gdb-on-mac]: http://sourceware.org/gdb/wiki/BuildingOnDarwin 141 | [pty]: https://github.com/kr/pty 142 | [gdb-mi]: https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI.html 143 | -------------------------------------------------------------------------------- /web/example/static/script.js: -------------------------------------------------------------------------------- 1 | function Client(host, onNotification, onOutput) { 2 | var notificationWs = new WebSocket('ws://' + host + '/notifications'); 3 | var terminalWs = new WebSocket('ws://' + host + '/terminal'); 4 | 5 | notificationWs.onmessage = function (event) { 6 | var object = JSON.parse(event.data); 7 | onNotification(object); 8 | }; 9 | 10 | terminalWs.onmessage = function (event) { 11 | onOutput(event.data); 12 | }; 13 | 14 | this.input = function (data) { 15 | terminalWs.send(data); 16 | }; 17 | 18 | this.command = function (onReply) { 19 | var command = Array.prototype.slice.call(arguments).slice(1); 20 | var ajax = new XMLHttpRequest(); 21 | ajax.open('POST', 'http://' + host + '/send'); 22 | ajax.onreadystatechange = function () { 23 | if (ajax.readyState === 4 && ajax.status === 200){ 24 | var object = JSON.parse(ajax.responseText); 25 | onReply(object); 26 | } 27 | }; 28 | ajax.send(JSON.stringify(command)); 29 | }; 30 | 31 | this.interrupt = function (onReply) { 32 | var ajax = new XMLHttpRequest(); 33 | ajax.open('POST', 'http://' + host + '/interrupt'); 34 | ajax.onreadystatechange = function () { 35 | if (ajax.readyState === 4 && ajax.status === 200){ 36 | onReply && onReply(); 37 | } 38 | }; 39 | ajax.send(null); 40 | }; 41 | } 42 | 43 | /// 44 | 45 | var host = 'localhost:8080'; 46 | 47 | function bind(id, action) { 48 | var elem = document.getElementById(id); 49 | elem.addEventListener('click', action); 50 | } 51 | 52 | function onNotification(object) { 53 | console.log(JSON.stringify(object, null, 4)); 54 | if (object.type === 'exec') { 55 | var status = object.class; 56 | if (status === 'stopped') { 57 | status += ', ' + object.payload.reason; 58 | if (typeof object.payload.frame !== 'undefined') { 59 | if (typeof object.payload.frame.func !== 'undefined') { 60 | status += ', ' + object.payload.frame.func; 61 | } 62 | if (typeof object.payload.frame.file !== 'undefined') { 63 | status += ' ' + object.payload.frame.file; 64 | } 65 | if (typeof object.payload.frame.line !== 'undefined') { 66 | status += ':' + object.payload.frame.line; 67 | } 68 | if (typeof object.payload.frame.fullname !== 'undefined') { 69 | var filePath = object.payload.frame.fullname; 70 | var ajax = new XMLHttpRequest(); 71 | ajax.open('POST', 'http://' + host + '/source' + filePath); 72 | ajax.onreadystatechange = function () { 73 | if (ajax.readyState === 4 && ajax.status == 200){ 74 | var container = document.getElementById('source'); 75 | container.innerHTML = ''; 76 | var source = ajax.responseText; 77 | var lines = source.split('\n'); 78 | for (var i = 0; i < lines.length; i++) { 79 | var line = document.createElement('span'); 80 | line.id = i + 1; 81 | line.appendChild(document.createTextNode(lines[i] || ' ')); 82 | container.appendChild(line); 83 | } 84 | var lineNumber = object.payload.frame.line; 85 | document.getElementById(lineNumber).className = 'selected'; 86 | } 87 | }; 88 | ajax.send(null); 89 | } 90 | } 91 | } 92 | document.getElementById('status').innerHTML = status; 93 | } 94 | } 95 | 96 | function onOutput(data) { 97 | document.getElementById('output').innerHTML += data; 98 | } 99 | 100 | var client = new Client(host, onNotification, onOutput); 101 | 102 | function dumpResult(object) { 103 | console.log(JSON.stringify(object, null, 4)); 104 | var result = document.getElementById('result'); 105 | if (object.class === 'error') { 106 | result.innerHTML = object.payload.msg; 107 | } else { 108 | result.innerHTML = 'Done'; 109 | } 110 | } 111 | 112 | bind('load', function () { 113 | var fileName = document.getElementById('file').value; 114 | client.command(dumpResult, 'file-exec-and-symbols', fileName); 115 | }); 116 | 117 | bind('run', function () { 118 | client.interrupt(); 119 | document.getElementById('output').innerHTML = ''; 120 | var argText = document.getElementById('arguments').value.trim(); 121 | if (argText !== '') { 122 | var args = argText.split(/ +/); 123 | client.command.apply(null, [dumpResult, 'exec-arguments'].concat(args)); 124 | } 125 | client.command(dumpResult, 'exec-run', '--start'); 126 | }); 127 | 128 | bind('continue', function () { 129 | client.command(dumpResult, 'exec-continue'); 130 | }); 131 | 132 | bind('step', function () { 133 | client.command(dumpResult, 'exec-next'); 134 | }); 135 | 136 | bind('interrupt', function () { 137 | client.interrupt(); 138 | }); 139 | 140 | bind('send', function () { 141 | var line = document.getElementById('line').value; 142 | client.input(line + '\n'); 143 | }); 144 | 145 | bind('eot', function () { 146 | client.input('\x04'); 147 | }); 148 | -------------------------------------------------------------------------------- /grammar.go: -------------------------------------------------------------------------------- 1 | // Code generated by goyacc -o grammar.go grammar.y. DO NOT EDIT. 2 | 3 | //line grammar.y:3 4 | package gdb 5 | 6 | import __yyfmt__ "fmt" 7 | 8 | //line grammar.y:3 9 | 10 | const ( 11 | terminator = "(gdb) " // yes there's the trailing space 12 | typeKey = "type" 13 | classKey = "class" 14 | payloadKey = "payload" 15 | sequenceKey = "sequence" 16 | ) 17 | 18 | // avoid DRY due to a poor lexer 19 | func newClassResult(typeString, class string, payload map[string]interface{}) map[string]interface{} { 20 | out := map[string]interface{}{ 21 | typeKey: typeString, 22 | classKey: class, 23 | } 24 | if payload != nil { 25 | out[payloadKey] = payload 26 | } 27 | return out 28 | } 29 | 30 | //line grammar.y:26 31 | type yySymType struct { 32 | yys int 33 | text string 34 | record map[string]interface{} 35 | class_result struct { 36 | class string 37 | payload map[string]interface{} 38 | } 39 | result_pair struct { 40 | variable string 41 | value interface{} 42 | } 43 | value interface{} 44 | list []interface{} 45 | } 46 | 47 | const text = 57346 48 | 49 | var yyToknames = [...]string{ 50 | "$end", 51 | "error", 52 | "$unk", 53 | "text", 54 | "'*'", 55 | "'+'", 56 | "','", 57 | "'='", 58 | "'~'", 59 | "'@'", 60 | "'&'", 61 | "'^'", 62 | "'{'", 63 | "'}'", 64 | "'['", 65 | "']'", 66 | } 67 | 68 | var yyStatenames = [...]string{} 69 | 70 | const yyEofCode = 1 71 | const yyErrCode = 2 72 | const yyInitialStackSize = 16 73 | 74 | //line yacctab:1 75 | var yyExca = [...]int8{ 76 | -1, 1, 77 | 1, -1, 78 | -2, 0, 79 | } 80 | 81 | const yyPrivate = 57344 82 | 83 | const yyLast = 59 84 | 85 | var yyAct = [...]int8{ 86 | 27, 36, 26, 38, 47, 50, 31, 18, 37, 48, 87 | 28, 32, 25, 30, 51, 40, 44, 30, 49, 40, 88 | 20, 22, 23, 31, 28, 24, 28, 19, 21, 29, 89 | 41, 17, 35, 33, 34, 30, 11, 12, 16, 13, 90 | 10, 46, 45, 14, 15, 7, 8, 9, 1, 43, 91 | 52, 53, 42, 39, 5, 4, 3, 2, 6, 92 | } 93 | 94 | var yyPact = [...]int16{ 95 | 36, -1000, -1000, -1000, -1000, -1000, 31, 40, 34, 27, 96 | -1000, 23, 24, 23, 23, -1000, -1000, -1000, -1000, 18, 97 | -1000, 5, -1000, -1000, 6, 22, -1, -1000, 3, -1000, 98 | 20, 6, 4, 16, -1000, -1000, -1000, -1000, -1000, -1000, 99 | 0, -1000, 2, -2, -1000, -1000, -1000, 3, 4, -1000, 100 | 6, -1000, -1000, -1000, 101 | } 102 | 103 | var yyPgo = [...]int8{ 104 | 0, 58, 2, 57, 56, 55, 54, 3, 7, 0, 105 | 1, 53, 52, 49, 48, 106 | } 107 | 108 | var yyR1 = [...]int8{ 109 | 0, 14, 3, 3, 3, 4, 4, 4, 4, 8, 110 | 8, 5, 5, 5, 6, 2, 2, 1, 1, 9, 111 | 10, 10, 10, 12, 12, 7, 7, 13, 13, 11, 112 | 11, 11, 113 | } 114 | 115 | var yyR2 = [...]int8{ 116 | 0, 1, 1, 1, 1, 3, 3, 5, 3, 3, 117 | 1, 2, 2, 2, 3, 3, 1, 0, 1, 3, 118 | 1, 1, 1, 3, 1, 3, 2, 3, 1, 3, 119 | 3, 2, 120 | } 121 | 122 | var yyChk = [...]int16{ 123 | -1000, -14, -3, -4, -5, -6, -1, 9, 10, 11, 124 | 4, 5, 6, 8, 12, 4, 4, 4, -8, 4, 125 | -8, 4, -8, -8, 7, 7, -2, -9, 4, -7, 126 | 13, 7, 8, -2, 14, -9, -10, 4, -7, -11, 127 | 15, 14, -12, -13, 16, -10, -9, 4, 7, 16, 128 | 7, 16, -10, -9, 129 | } 130 | 131 | var yyDef = [...]int8{ 132 | 17, -2, 1, 2, 3, 4, 0, 0, 0, 0, 133 | 18, 0, 0, 0, 0, 11, 12, 13, 5, 10, 134 | 6, 10, 8, 14, 0, 0, 9, 16, 0, 7, 135 | 0, 0, 0, 0, 26, 15, 19, 20, 21, 22, 136 | 0, 25, 0, 0, 31, 24, 28, 20, 0, 29, 137 | 0, 30, 23, 27, 138 | } 139 | 140 | var yyTok1 = [...]int8{ 141 | 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 142 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 143 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 144 | 3, 3, 3, 3, 3, 3, 3, 3, 11, 3, 145 | 3, 3, 5, 6, 7, 3, 3, 3, 3, 3, 146 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 147 | 3, 8, 3, 3, 10, 3, 3, 3, 3, 3, 148 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 149 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 150 | 3, 15, 3, 16, 12, 3, 3, 3, 3, 3, 151 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 152 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 153 | 3, 3, 3, 13, 3, 14, 9, 154 | } 155 | 156 | var yyTok2 = [...]int8{ 157 | 2, 3, 4, 158 | } 159 | 160 | var yyTok3 = [...]int8{ 161 | 0, 162 | } 163 | 164 | var yyErrorMessages = [...]struct { 165 | state int 166 | token int 167 | msg string 168 | }{} 169 | 170 | //line yaccpar:1 171 | 172 | /* parser for yacc output */ 173 | 174 | var ( 175 | yyDebug = 0 176 | yyErrorVerbose = false 177 | ) 178 | 179 | type yyLexer interface { 180 | Lex(lval *yySymType) int 181 | Error(s string) 182 | } 183 | 184 | type yyParser interface { 185 | Parse(yyLexer) int 186 | Lookahead() int 187 | } 188 | 189 | type yyParserImpl struct { 190 | lval yySymType 191 | stack [yyInitialStackSize]yySymType 192 | char int 193 | } 194 | 195 | func (p *yyParserImpl) Lookahead() int { 196 | return p.char 197 | } 198 | 199 | func yyNewParser() yyParser { 200 | return &yyParserImpl{} 201 | } 202 | 203 | const yyFlag = -1000 204 | 205 | func yyTokname(c int) string { 206 | if c >= 1 && c-1 < len(yyToknames) { 207 | if yyToknames[c-1] != "" { 208 | return yyToknames[c-1] 209 | } 210 | } 211 | return __yyfmt__.Sprintf("tok-%v", c) 212 | } 213 | 214 | func yyStatname(s int) string { 215 | if s >= 0 && s < len(yyStatenames) { 216 | if yyStatenames[s] != "" { 217 | return yyStatenames[s] 218 | } 219 | } 220 | return __yyfmt__.Sprintf("state-%v", s) 221 | } 222 | 223 | func yyErrorMessage(state, lookAhead int) string { 224 | const TOKSTART = 4 225 | 226 | if !yyErrorVerbose { 227 | return "syntax error" 228 | } 229 | 230 | for _, e := range yyErrorMessages { 231 | if e.state == state && e.token == lookAhead { 232 | return "syntax error: " + e.msg 233 | } 234 | } 235 | 236 | res := "syntax error: unexpected " + yyTokname(lookAhead) 237 | 238 | // To match Bison, suggest at most four expected tokens. 239 | expected := make([]int, 0, 4) 240 | 241 | // Look for shiftable tokens. 242 | base := int(yyPact[state]) 243 | for tok := TOKSTART; tok-1 < len(yyToknames); tok++ { 244 | if n := base + tok; n >= 0 && n < yyLast && int(yyChk[int(yyAct[n])]) == tok { 245 | if len(expected) == cap(expected) { 246 | return res 247 | } 248 | expected = append(expected, tok) 249 | } 250 | } 251 | 252 | if yyDef[state] == -2 { 253 | i := 0 254 | for yyExca[i] != -1 || int(yyExca[i+1]) != state { 255 | i += 2 256 | } 257 | 258 | // Look for tokens that we accept or reduce. 259 | for i += 2; yyExca[i] >= 0; i += 2 { 260 | tok := int(yyExca[i]) 261 | if tok < TOKSTART || yyExca[i+1] == 0 { 262 | continue 263 | } 264 | if len(expected) == cap(expected) { 265 | return res 266 | } 267 | expected = append(expected, tok) 268 | } 269 | 270 | // If the default action is to accept or reduce, give up. 271 | if yyExca[i+1] != 0 { 272 | return res 273 | } 274 | } 275 | 276 | for i, tok := range expected { 277 | if i == 0 { 278 | res += ", expecting " 279 | } else { 280 | res += " or " 281 | } 282 | res += yyTokname(tok) 283 | } 284 | return res 285 | } 286 | 287 | func yylex1(lex yyLexer, lval *yySymType) (char, token int) { 288 | token = 0 289 | char = lex.Lex(lval) 290 | if char <= 0 { 291 | token = int(yyTok1[0]) 292 | goto out 293 | } 294 | if char < len(yyTok1) { 295 | token = int(yyTok1[char]) 296 | goto out 297 | } 298 | if char >= yyPrivate { 299 | if char < yyPrivate+len(yyTok2) { 300 | token = int(yyTok2[char-yyPrivate]) 301 | goto out 302 | } 303 | } 304 | for i := 0; i < len(yyTok3); i += 2 { 305 | token = int(yyTok3[i+0]) 306 | if token == char { 307 | token = int(yyTok3[i+1]) 308 | goto out 309 | } 310 | } 311 | 312 | out: 313 | if token == 0 { 314 | token = int(yyTok2[1]) /* unknown char */ 315 | } 316 | if yyDebug >= 3 { 317 | __yyfmt__.Printf("lex %s(%d)\n", yyTokname(token), uint(char)) 318 | } 319 | return char, token 320 | } 321 | 322 | func yyParse(yylex yyLexer) int { 323 | return yyNewParser().Parse(yylex) 324 | } 325 | 326 | func (yyrcvr *yyParserImpl) Parse(yylex yyLexer) int { 327 | var yyn int 328 | var yyVAL yySymType 329 | var yyDollar []yySymType 330 | _ = yyDollar // silence set and not used 331 | yyS := yyrcvr.stack[:] 332 | 333 | Nerrs := 0 /* number of errors */ 334 | Errflag := 0 /* error recovery flag */ 335 | yystate := 0 336 | yyrcvr.char = -1 337 | yytoken := -1 // yyrcvr.char translated into internal numbering 338 | defer func() { 339 | // Make sure we report no lookahead when not parsing. 340 | yystate = -1 341 | yyrcvr.char = -1 342 | yytoken = -1 343 | }() 344 | yyp := -1 345 | goto yystack 346 | 347 | ret0: 348 | return 0 349 | 350 | ret1: 351 | return 1 352 | 353 | yystack: 354 | /* put a state and value onto the stack */ 355 | if yyDebug >= 4 { 356 | __yyfmt__.Printf("char %v in %v\n", yyTokname(yytoken), yyStatname(yystate)) 357 | } 358 | 359 | yyp++ 360 | if yyp >= len(yyS) { 361 | nyys := make([]yySymType, len(yyS)*2) 362 | copy(nyys, yyS) 363 | yyS = nyys 364 | } 365 | yyS[yyp] = yyVAL 366 | yyS[yyp].yys = yystate 367 | 368 | yynewstate: 369 | yyn = int(yyPact[yystate]) 370 | if yyn <= yyFlag { 371 | goto yydefault /* simple state */ 372 | } 373 | if yyrcvr.char < 0 { 374 | yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval) 375 | } 376 | yyn += yytoken 377 | if yyn < 0 || yyn >= yyLast { 378 | goto yydefault 379 | } 380 | yyn = int(yyAct[yyn]) 381 | if int(yyChk[yyn]) == yytoken { /* valid shift */ 382 | yyrcvr.char = -1 383 | yytoken = -1 384 | yyVAL = yyrcvr.lval 385 | yystate = yyn 386 | if Errflag > 0 { 387 | Errflag-- 388 | } 389 | goto yystack 390 | } 391 | 392 | yydefault: 393 | /* default state action */ 394 | yyn = int(yyDef[yystate]) 395 | if yyn == -2 { 396 | if yyrcvr.char < 0 { 397 | yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval) 398 | } 399 | 400 | /* look through exception table */ 401 | xi := 0 402 | for { 403 | if yyExca[xi+0] == -1 && int(yyExca[xi+1]) == yystate { 404 | break 405 | } 406 | xi += 2 407 | } 408 | for xi += 2; ; xi += 2 { 409 | yyn = int(yyExca[xi+0]) 410 | if yyn < 0 || yyn == yytoken { 411 | break 412 | } 413 | } 414 | yyn = int(yyExca[xi+1]) 415 | if yyn < 0 { 416 | goto ret0 417 | } 418 | } 419 | if yyn == 0 { 420 | /* error ... attempt to resume parsing */ 421 | switch Errflag { 422 | case 0: /* brand new error */ 423 | yylex.Error(yyErrorMessage(yystate, yytoken)) 424 | Nerrs++ 425 | if yyDebug >= 1 { 426 | __yyfmt__.Printf("%s", yyStatname(yystate)) 427 | __yyfmt__.Printf(" saw %s\n", yyTokname(yytoken)) 428 | } 429 | fallthrough 430 | 431 | case 1, 2: /* incompletely recovered error ... try again */ 432 | Errflag = 3 433 | 434 | /* find a state where "error" is a legal shift action */ 435 | for yyp >= 0 { 436 | yyn = int(yyPact[yyS[yyp].yys]) + yyErrCode 437 | if yyn >= 0 && yyn < yyLast { 438 | yystate = int(yyAct[yyn]) /* simulate a shift of "error" */ 439 | if int(yyChk[yystate]) == yyErrCode { 440 | goto yystack 441 | } 442 | } 443 | 444 | /* the current p has no shift on "error", pop stack */ 445 | if yyDebug >= 2 { 446 | __yyfmt__.Printf("error recovery pops state %d\n", yyS[yyp].yys) 447 | } 448 | yyp-- 449 | } 450 | /* there is no state on the stack with an error shift ... abort */ 451 | goto ret1 452 | 453 | case 3: /* no shift yet; clobber input char */ 454 | if yyDebug >= 2 { 455 | __yyfmt__.Printf("error recovery discards %s\n", yyTokname(yytoken)) 456 | } 457 | if yytoken == yyEofCode { 458 | goto ret1 459 | } 460 | yyrcvr.char = -1 461 | yytoken = -1 462 | goto yynewstate /* try again in the same state */ 463 | } 464 | } 465 | 466 | /* reduction by production yyn */ 467 | if yyDebug >= 2 { 468 | __yyfmt__.Printf("reduce %v in:\n\t%v\n", yyn, yyStatname(yystate)) 469 | } 470 | 471 | yynt := yyn 472 | yypt := yyp 473 | _ = yypt // guard against "declared and not used" 474 | 475 | yyp -= int(yyR2[yyn]) 476 | // yyp is now the index of $0. Perform the default action. Iff the 477 | // reduced production is ε, $1 is possibly out of range. 478 | if yyp+1 >= len(yyS) { 479 | nyys := make([]yySymType, len(yyS)*2) 480 | copy(nyys, yyS) 481 | yyS = nyys 482 | } 483 | yyVAL = yyS[yyp+1] 484 | 485 | /* consult goto table to find next state */ 486 | yyn = int(yyR1[yyn]) 487 | yyg := int(yyPgo[yyn]) 488 | yyj := yyg + yyS[yyp].yys + 1 489 | 490 | if yyj >= yyLast { 491 | yystate = int(yyAct[yyg]) 492 | } else { 493 | yystate = int(yyAct[yyj]) 494 | if int(yyChk[yystate]) != -yyn { 495 | yystate = int(yyAct[yyg]) 496 | } 497 | } 498 | // dummy call; replaced with literal code 499 | switch yynt { 500 | 501 | case 1: 502 | yyDollar = yyS[yypt-1 : yypt+1] 503 | //line grammar.y:47 504 | { 505 | yylex.(*parser).output = yyDollar[1].record 506 | } 507 | case 5: 508 | yyDollar = yyS[yypt-3 : yypt+1] 509 | //line grammar.y:55 510 | { 511 | yyVAL.record = newClassResult("exec", yyDollar[3].class_result.class, yyDollar[3].class_result.payload) 512 | } 513 | case 6: 514 | yyDollar = yyS[yypt-3 : yypt+1] 515 | //line grammar.y:56 516 | { 517 | yyVAL.record = newClassResult("status", yyDollar[3].class_result.class, yyDollar[3].class_result.payload) 518 | } 519 | case 7: 520 | yyDollar = yyS[yypt-5 : yypt+1] 521 | //line grammar.y:57 522 | { 523 | yyVAL.record = newClassResult("status", yyDollar[3].text, yyDollar[5].record) 524 | } 525 | case 8: 526 | yyDollar = yyS[yypt-3 : yypt+1] 527 | //line grammar.y:58 528 | { 529 | yyVAL.record = newClassResult("notify", yyDollar[3].class_result.class, yyDollar[3].class_result.payload) 530 | } 531 | case 9: 532 | yyDollar = yyS[yypt-3 : yypt+1] 533 | //line grammar.y:61 534 | { 535 | yyVAL.class_result.class, yyVAL.class_result.payload = yyDollar[1].text, yyDollar[3].record 536 | } 537 | case 10: 538 | yyDollar = yyS[yypt-1 : yypt+1] 539 | //line grammar.y:62 540 | { 541 | yyVAL.class_result.class, yyVAL.class_result.payload = yyDollar[1].text, nil 542 | } 543 | case 11: 544 | yyDollar = yyS[yypt-2 : yypt+1] 545 | //line grammar.y:65 546 | { 547 | yyVAL.record = map[string]interface{}{typeKey: "console", payloadKey: yyDollar[2].text} 548 | } 549 | case 12: 550 | yyDollar = yyS[yypt-2 : yypt+1] 551 | //line grammar.y:66 552 | { 553 | yyVAL.record = map[string]interface{}{typeKey: "target", payloadKey: yyDollar[2].text} 554 | } 555 | case 13: 556 | yyDollar = yyS[yypt-2 : yypt+1] 557 | //line grammar.y:67 558 | { 559 | yyVAL.record = map[string]interface{}{typeKey: "log", payloadKey: yyDollar[2].text} 560 | } 561 | case 14: 562 | yyDollar = yyS[yypt-3 : yypt+1] 563 | //line grammar.y:71 564 | { 565 | yyVAL.record = map[string]interface{}{sequenceKey: yyDollar[1].text, classKey: yyDollar[3].class_result.class} 566 | if yyDollar[3].class_result.payload != nil { 567 | yyVAL.record[payloadKey] = yyDollar[3].class_result.payload 568 | } 569 | } 570 | case 15: 571 | yyDollar = yyS[yypt-3 : yypt+1] 572 | //line grammar.y:77 573 | { 574 | yyVAL.record[yyDollar[3].result_pair.variable] = yyDollar[3].result_pair.value 575 | } 576 | case 16: 577 | yyDollar = yyS[yypt-1 : yypt+1] 578 | //line grammar.y:78 579 | { 580 | yyVAL.record = map[string]interface{}{yyDollar[1].result_pair.variable: yyDollar[1].result_pair.value} 581 | } 582 | case 17: 583 | yyDollar = yyS[yypt-0 : yypt+1] 584 | //line grammar.y:81 585 | { 586 | yyVAL.text = "" 587 | } 588 | case 18: 589 | yyDollar = yyS[yypt-1 : yypt+1] 590 | //line grammar.y:82 591 | { 592 | yyVAL.text = yyDollar[1].text 593 | } 594 | case 19: 595 | yyDollar = yyS[yypt-3 : yypt+1] 596 | //line grammar.y:85 597 | { 598 | yyVAL.result_pair.variable, yyVAL.result_pair.value = yyDollar[1].text, yyDollar[3].value 599 | } 600 | case 20: 601 | yyDollar = yyS[yypt-1 : yypt+1] 602 | //line grammar.y:88 603 | { 604 | yyVAL.value = yyDollar[1].text 605 | } 606 | case 21: 607 | yyDollar = yyS[yypt-1 : yypt+1] 608 | //line grammar.y:89 609 | { 610 | yyVAL.value = yyDollar[1].record 611 | } 612 | case 22: 613 | yyDollar = yyS[yypt-1 : yypt+1] 614 | //line grammar.y:90 615 | { 616 | yyVAL.value = yyDollar[1].list 617 | } 618 | case 23: 619 | yyDollar = yyS[yypt-3 : yypt+1] 620 | //line grammar.y:93 621 | { 622 | yyVAL.list = append(yyVAL.list, yyDollar[3].value) 623 | } 624 | case 24: 625 | yyDollar = yyS[yypt-1 : yypt+1] 626 | //line grammar.y:94 627 | { 628 | yyVAL.list = []interface{}{yyDollar[1].value} 629 | } 630 | case 25: 631 | yyDollar = yyS[yypt-3 : yypt+1] 632 | //line grammar.y:97 633 | { 634 | yyVAL.record = yyDollar[2].record 635 | } 636 | case 26: 637 | yyDollar = yyS[yypt-2 : yypt+1] 638 | //line grammar.y:98 639 | { 640 | yyVAL.record = map[string]interface{}{} 641 | } 642 | case 27: 643 | yyDollar = yyS[yypt-3 : yypt+1] 644 | //line grammar.y:101 645 | { 646 | yyVAL.list = append(yyVAL.list, map[string]interface{}{yyDollar[3].result_pair.variable: yyDollar[3].result_pair.value}) 647 | } 648 | case 28: 649 | yyDollar = yyS[yypt-1 : yypt+1] 650 | //line grammar.y:102 651 | { 652 | yyVAL.list = []interface{}{map[string]interface{}{yyDollar[1].result_pair.variable: yyDollar[1].result_pair.value}} 653 | } 654 | case 29: 655 | yyDollar = yyS[yypt-3 : yypt+1] 656 | //line grammar.y:105 657 | { 658 | yyVAL.list = yyDollar[2].list 659 | } 660 | case 30: 661 | yyDollar = yyS[yypt-3 : yypt+1] 662 | //line grammar.y:106 663 | { 664 | yyVAL.list = yyDollar[2].list 665 | } 666 | case 31: 667 | yyDollar = yyS[yypt-2 : yypt+1] 668 | //line grammar.y:107 669 | { 670 | yyVAL.list = []interface{}{} 671 | } 672 | } 673 | goto yystack /* stack new state and value */ 674 | } 675 | --------------------------------------------------------------------------------