├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── agent └── init.go ├── api.go ├── command.go ├── doc.go ├── examples ├── demo │ ├── demo1.go │ └── demo2.go ├── distributed │ ├── README.md │ ├── client │ │ └── main.go │ ├── server1 │ │ └── main.go │ └── server2 │ │ └── main.go ├── hop │ └── hopwatch_demo.go ├── mini │ └── hopwatch_minimal.go ├── multidebugger │ ├── README.md │ ├── client │ │ └── main.go │ └── server │ │ └── main.go ├── multigoroutines │ └── hopwatch_multi_goroutines.go ├── offset │ └── hopwatch_offset.go ├── scroll │ └── scroll.go ├── spew1 │ └── hopwatch_spew.go └── spew2 │ └── hopwatch_spew_offset.go ├── go.mod ├── go.sum ├── hopwatch.go ├── hopwatch_css.go ├── hopwatch_how.png ├── hopwatch_html.go ├── hopwatch_javascript.go ├── spew.go └── watchpoint.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.2 4 | before_install: go get -d -v ./... 5 | install: go build -v . 6 | script: go test . -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2022+ Ernest Micklei 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hopwatch, a debugging tool for Go 2 | 3 | Hopwatch is a simple tool in HTML5 that can help debug Go programs. 4 | It works by communicating to a WebSockets based client in Javascript. 5 | When your program calls the Break function, it sends debug information to the browser page and waits for user interaction. 6 | Using the functions Display, Printf or Dump (go-spew), you can log information on the browser page. 7 | On the hopwatch page, the developer can view debug information and choose to resume the execution of the program. 8 | 9 | [First announcement](https://ernestmicklei.com/2012/12/hopwatch-a-debugging-tool-for-go/) 10 | 11 | ![How](hopwatch_how.png) 12 | 13 | 14 | ## Distributed (work in progress) 15 | 16 | Hopwatch can be used in a distributed services architecture to hop and trace between services following an incoming request to downstream service. 17 | 18 | Consider the setup where the browser is sending a HTTP request to a GraphQL endpoint which calls a gRPC backend service, which calls a PostgreSQL Database server to perform a query. The result of that query needs to be transformed into a gRPC response which in turn needs to be transformed into a GraphQL response before transporting it back to the browser. 19 | 20 | We want to jump from client to server to server and back, for a given request. 21 | To signal the upstream services that it should break on this request, the request must be annotated using a special HTTP header `x-hopwatch : your-correlation-name`. 22 | 23 | Each upstream server must have the hopwatch agent package included: 24 | 25 | import _ "github.com/emicklei/hopwatch/agent" 26 | 27 | 28 | © 2012-2022, http://ernestmicklei.com. MIT License -------------------------------------------------------------------------------- /agent/init.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import "fmt" 4 | 5 | func init() { 6 | fmt.Println("hopwatch agent initialized") 7 | } 8 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package hopwatch 2 | 3 | import "log" 4 | 5 | // Printf formats according to a format specifier and writes to the debugger screen. 6 | // It returns a new Watchpoint to send more or break. 7 | func Printf(format string, params ...interface{}) *Watchpoint { 8 | wp := &Watchpoint{offset: 2} 9 | return wp.Printf(format, params...) 10 | } 11 | 12 | // Display sends variable name,value pairs to the debugger. 13 | // The parameter nameValuePairs must be even sized. 14 | func Display(nameValuePairs ...interface{}) *Watchpoint { 15 | wp := &Watchpoint{offset: 2} 16 | return wp.Display(nameValuePairs...) 17 | } 18 | 19 | // Break suspends the execution of the program and waits for an instruction from the debugger (e.g. Resume). 20 | // Break is only effective if all (if any) conditions are true. The program will resume otherwise. 21 | func Break(conditions ...bool) { 22 | suspend(2, conditions...) 23 | } 24 | 25 | // CallerOffset (default=2) allows you to change the file indicator in hopwatch. 26 | // Use this method when you wrap the .CallerOffset(..).Display(..).Break() in your own function. 27 | func CallerOffset(offset int) *Watchpoint { 28 | return (&Watchpoint{}).CallerOffset(offset) 29 | } 30 | 31 | func Disable() { 32 | log.Print("[hopwatch] disabled by code.\n") 33 | hopwatchEnabled = false 34 | } 35 | 36 | func Enable() { 37 | log.Print("[hopwatch] enabled by code.\n") 38 | hopwatchEnabled = true 39 | } 40 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package hopwatch 2 | 3 | // command is used to transport message to and from the debugger. 4 | type command struct { 5 | Action string 6 | Parameters map[string]string 7 | } 8 | 9 | // addParam adds a key,value string pair to the command ; no check on overwrites. 10 | func (c *command) addParam(key, value string) { 11 | if c.Parameters == nil { 12 | c.Parameters = map[string]string{} 13 | } 14 | c.Parameters[key] = value 15 | } 16 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012+ ernestmicklei.com. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Hopwatch is a debugging tool for Go programs. 7 | 8 | Hopwatch uses a (embedded) HTML5 application to connect to your program (using a Websocket). 9 | Using Hopwatch requires adding function calls at points of interest that allow you to watch program state and suspend the program. 10 | On the Hopwatch page, you can view debug information (file:line,stack) and choose to resume the execution of your program. 11 | 12 | You can provide more debug information using the Display and Dump functions which take an arbitrary number of variables. 13 | The Display and Dump functions do not suspend the program ; it is like having logging information in the browser. 14 | 15 | Usage: 16 | 17 | import ( 18 | // use a tag 19 | "gopkg.in/emicklei/hopwatch.v1" 20 | 21 | // or use the master 22 | // "github.com/emicklei/hopwatch" 23 | ) 24 | 25 | func foo() { 26 | bar := "john" 27 | // suspends execution until hitting "Resume" in the browser 28 | hopwatch.Display("foo", bar).Break() 29 | } 30 | 31 | Connect: 32 | 33 | The Hopwatch debugger is automatically started on http://localhost:23456/hopwatch.html. 34 | Your browser must support WebSockets. It has been tested with Chrome and Safari on a Mac. 35 | 36 | Other code examples: 37 | 38 | // zero or more conditions ; conditionally suspends program (or goroutine) 39 | hopwatch.Break(i > 10, j < 100) 40 | 41 | // zero or more name,value pairs ; no program suspend 42 | hopwatch.Display("i",i , "j",j") 43 | 44 | // print any formatted string ; no program suspend 45 | hopwatch.Printf("result=%v", result) 46 | 47 | // display detailed (type, nesting) information using https://github.com/davecgh/go-spew 48 | hopwatch.Dump(myVar1) 49 | 50 | // format and display detailed (type, nesting) information using https://github.com/davecgh/go-spew 51 | hopwatch.Dumpf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) 52 | 53 | Flags: 54 | 55 | -hopwatch if set to false then hopwatch is disabled. 56 | -hopwatch.open if set to false then hopwatch will not try to open the debugger page on startup. 57 | -hopwatch.break if set to false then hopwatch will not suspend the program when Break(..) is called. 58 | -hopwatch.host tcp hostname of the listener address (default = localhost). 59 | -hopwatch.port tcp port of the listener address (default = 23456). 60 | 61 | Install from master: 62 | 63 | go get -u github.com/emicklei/hopwatch 64 | 65 | Resources: 66 | 67 | https://github.com/emicklei/hopwatch (project) 68 | http://ernestmicklei.com/2012/12/14/hopwatch-a-debugging-tool-for-go/ (blog) 69 | 70 | (c) 2012-2022+, Ernest Micklei. MIT License 71 | */ 72 | package hopwatch 73 | -------------------------------------------------------------------------------- /examples/demo/demo1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | "time" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | haveABreak() 11 | } 12 | 13 | func haveABreak() { 14 | hopwatch.Break() 15 | printNow() 16 | } 17 | 18 | func printNow() { 19 | hopwatch.Printf("time is: %v", time.Now()) 20 | dumpArgs() 21 | } 22 | 23 | func dumpArgs() { 24 | hopwatch.Dump(os.Args).Break() 25 | waitHere() 26 | } -------------------------------------------------------------------------------- /examples/demo/demo2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | func waitHere() { 8 | hopwatch.Break() 9 | } -------------------------------------------------------------------------------- /examples/distributed/README.md: -------------------------------------------------------------------------------- 1 | # distributed, single debugger, agent driven -------------------------------------------------------------------------------- /examples/distributed/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "github.com/emicklei/hopwatch" 8 | ) 9 | 10 | func main() { 11 | hopwatch.Break() 12 | resp, _ := http.Get("http://localhost:8998/hop") 13 | data, _ := io.ReadAll(resp.Body) 14 | hopwatch.Display("response", string(data)).Break() 15 | } 16 | -------------------------------------------------------------------------------- /examples/distributed/server1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/emicklei/hopwatch" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/hop", handleRequest) // dont listen root, browsers want icons so bad 13 | log.Println("listing for HTTP on http://localhost:8998/hop") 14 | http.ListenAndServe(":8998", nil) 15 | } 16 | 17 | func handleRequest(w http.ResponseWriter, r *http.Request) { 18 | hopwatch.Display("request", r).Break() 19 | io.WriteString(w, "hello hopper") 20 | } 21 | -------------------------------------------------------------------------------- /examples/distributed/server2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/emicklei/hopwatch" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/hop", handleRequest) // dont listen root, browsers want icons so bad 13 | log.Println("listing for HTTP on http://localhost:8998/hop") 14 | http.ListenAndServe(":8998", nil) 15 | } 16 | 17 | func handleRequest(w http.ResponseWriter, r *http.Request) { 18 | hopwatch.Display("request", r).Break() 19 | io.WriteString(w, "hello hopper") 20 | } 21 | -------------------------------------------------------------------------------- /examples/hop/hopwatch_demo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | func main() { 8 | for i := 0; i < 6; i++ { 9 | hopwatch.Display("i", i) 10 | j := i * i 11 | hopwatch.Display("i", i, "j", j).Break(j > 10) 12 | hopwatch.Printf("%#v", "printf formatted value(s)") 13 | hopwatch.Break() 14 | quick() 15 | } 16 | } 17 | 18 | func quick() { 19 | hopwatch.Break() 20 | brown() 21 | } 22 | func brown() { 23 | hopwatch.Break() 24 | fox() 25 | } 26 | func fox() { 27 | hopwatch.Break() 28 | jumps() 29 | } 30 | func jumps() { 31 | hopwatch.Break() 32 | over() 33 | } 34 | func over() { 35 | hopwatch.Break() 36 | the() 37 | } 38 | func the() { 39 | hopwatch.Break() 40 | lazy() 41 | } 42 | func lazy() { 43 | hopwatch.Break() 44 | dog() 45 | } 46 | func dog() { 47 | hopwatch.Break() 48 | } 49 | -------------------------------------------------------------------------------- /examples/mini/hopwatch_minimal.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | func main() { 8 | hopwatch.Break() 9 | } 10 | -------------------------------------------------------------------------------- /examples/multidebugger/README.md: -------------------------------------------------------------------------------- 1 | # hopping between running applications with 2 debuggers 2 | 3 | Open 2 terminal sessions. 4 | In the first, you start the server: 5 | 6 | cd server 7 | go run *.go 8 | 9 | In the second, you start the client which will hit a breakpoint. 10 | 11 | cd client 12 | go run *.go -hopwatch.port=23455 13 | 14 | Resuming that breakpoint will hit the breakpoint in the server. 15 | Resuming that breakpoint will hit another breakpoint in the client. 16 | -------------------------------------------------------------------------------- /examples/multidebugger/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "github.com/emicklei/hopwatch" 8 | ) 9 | 10 | func main() { 11 | hopwatch.Break() 12 | resp, _ := http.Get("http://localhost:8998/hop") 13 | data, _ := io.ReadAll(resp.Body) 14 | hopwatch.Display("response", string(data)).Break() 15 | } 16 | -------------------------------------------------------------------------------- /examples/multidebugger/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/emicklei/hopwatch" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/hop", handleRequest) // dont listen root, browsers want icons so bad 13 | log.Println("listing for HTTP on http://localhost:8998/hop") 14 | http.ListenAndServe(":8998", nil) 15 | } 16 | 17 | func handleRequest(w http.ResponseWriter, r *http.Request) { 18 | hopwatch.Display("request", r).Break() 19 | io.WriteString(w, "hello hopper") 20 | } 21 | -------------------------------------------------------------------------------- /examples/multigoroutines/hopwatch_multi_goroutines.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | ready := make(chan int) 10 | for id := 0 ; id < 4 ; id++ { 11 | log.Printf("spawn doit:%v",id) 12 | go doit(id, ready) 13 | } 14 | for j := 0 ; j < 4 ; j++ { 15 | who := <- ready 16 | log.Printf("done:%v", who) 17 | } 18 | } 19 | 20 | func doit(id int, ready chan int) { 21 | log.Printf("before break:%v",id) 22 | hopwatch.Display("id",id).Break() 23 | log.Printf("after break:%v",id) 24 | ready <- id 25 | } 26 | 27 | -------------------------------------------------------------------------------- /examples/offset/hopwatch_offset.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | func main() { 8 | hopwatch.Display("8", 8) 9 | hopwatch.Display("9", 9).Break() 10 | inside() 11 | indirectDisplay("11", 11) 12 | indirectBreak() 13 | illegalOffset() 14 | } 15 | func inside() { 16 | hopwatch.Display("16", 16) 17 | hopwatch.Display("17", 17).Break() 18 | } 19 | func indirectDisplay(args ...interface{}) { 20 | hopwatch.CallerOffset(2).Display(args...) 21 | } 22 | func indirectBreak() { 23 | hopwatch.CallerOffset(2).Break() 24 | } 25 | func illegalOffset() { 26 | defer func() { 27 | if r := recover(); r != nil { 28 | print("Recovered in illegalOffset") 29 | } 30 | }() 31 | hopwatch.CallerOffset(-1).Break() 32 | } 33 | -------------------------------------------------------------------------------- /examples/scroll/scroll.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | func main() { 8 | for i:=0;i<100;i++ { line() } 9 | hopwatch.Break() 10 | } 11 | 12 | func line() { 13 | hopwatch.Printf("Layers are objects on the map that consist of one or more separate items, but are manipulated as a single unit. Layers generally reflect collections of objects that you add on top of the map to designate a common association.") 14 | } -------------------------------------------------------------------------------- /examples/spew1/hopwatch_spew.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | type node struct { 8 | label string 9 | parent *node 10 | children []node 11 | } 12 | 13 | func main() { 14 | tree := node{label:"parent", children:[]node{node{label:"child"}}} 15 | 16 | // uses go-spew, see https://github.com/davecgh/go-spew 17 | hopwatch.Dump(tree).Break() 18 | hopwatch.Dumpf("kids %#+v",tree.children).Break() 19 | } -------------------------------------------------------------------------------- /examples/spew2/hopwatch_spew_offset.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | func main() { 8 | hopwatch.Dump(8).Dump(8).Break() 9 | hopwatch.Dumpf("%v", 9).Dumpf("%v", 9).Break() 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emicklei/hopwatch 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 7 | golang.org/x/net v0.24.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 4 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 5 | -------------------------------------------------------------------------------- /hopwatch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2014 Ernest Micklei. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package hopwatch 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "log" 11 | "net/http" 12 | "os" 13 | "os/exec" 14 | "runtime" 15 | "runtime/debug" 16 | "strconv" 17 | "strings" 18 | "sync" 19 | 20 | "golang.org/x/net/websocket" 21 | ) 22 | 23 | var ( 24 | hopwatchServerAddressParam = flag.String("hopwatch.server", "", "HTTP host:port server running hopwatch server") 25 | hopwatchHostParam = flag.String("hopwatch.host", "localhost", "HTTP host the debugger is listening on") 26 | hopwatchPortParam = flag.Int("hopwatch.port", 23456, "HTTP port the debugger is listening on") 27 | hopwatchParam = flag.Bool("hopwatch", true, "controls whether hopwatch agent is started") 28 | hopwatchOpenParam = flag.Bool("hopwatch.open", true, "controls whether a browser page is opened on the hopwatch page") 29 | hopwatchBreakParam = flag.Bool("hopwatch.break", true, "do not suspend the program if Break(..) is called") 30 | 31 | hopwatchEnabled = true 32 | hopwatchOpenEnabled = true 33 | hopwatchBreakEnabled = true 34 | hopwatchHost = "localhost" 35 | hopwatchPort int64 = 23456 36 | hopwatchServerAddress = "" 37 | 38 | currentWebsocket *websocket.Conn 39 | toBrowserChannel = make(chan command) 40 | fromBrowserChannel = make(chan command) 41 | connectChannel = make(chan command) 42 | debuggerMutex = sync.Mutex{} 43 | ) 44 | 45 | func init() { 46 | // check any command line params. (needed when programs do not call flag.Parse() ) 47 | for i, arg := range os.Args { 48 | if strings.HasPrefix(arg, "-hopwatch=") { 49 | if strings.HasSuffix(arg, "false") { 50 | log.Printf("[hopwatch] disabled.\n") 51 | hopwatchEnabled = false 52 | return 53 | } 54 | } 55 | if strings.HasPrefix(arg, "-hopwatch.open") { 56 | if strings.HasSuffix(arg, "false") { 57 | log.Printf("[hopwatch] auto open debugger disabled.\n") 58 | hopwatchOpenEnabled = false 59 | } 60 | } 61 | if strings.HasPrefix(arg, "-hopwatch.break") { 62 | if strings.HasSuffix(arg, "false") { 63 | log.Printf("[hopwatch] suspend on Break(..) disabled.\n") 64 | hopwatchBreakEnabled = false 65 | } 66 | } 67 | if strings.HasPrefix(arg, "-hopwatch.host") { 68 | if eq := strings.Index(arg, "="); eq != -1 { 69 | hopwatchHost = arg[eq+1:] 70 | } else if i < len(os.Args) { 71 | hopwatchHost = os.Args[i+1] 72 | } 73 | } 74 | if strings.HasPrefix(arg, "-hopwatch.server") { 75 | if eq := strings.Index(arg, "="); eq != -1 { 76 | hopwatchServerAddress = arg[eq+1:] 77 | } else if i < len(os.Args) { 78 | hopwatchServerAddress = os.Args[i+1] 79 | } 80 | } 81 | if strings.HasPrefix(arg, "-hopwatch.port") { 82 | portString := "" 83 | if eq := strings.Index(arg, "="); eq != -1 { 84 | portString = arg[eq+1:] 85 | } else if i < len(os.Args) { 86 | portString = os.Args[i+1] 87 | } 88 | port, err := strconv.ParseInt(portString, 10, 64) 89 | if err != nil { 90 | log.Panicf("[hopwatch] illegal port parameter:%v", err) 91 | } 92 | hopwatchPort = port 93 | } 94 | } 95 | http.HandleFunc("/hopwatch.html", html) 96 | http.HandleFunc("/hopwatch.css", css) 97 | http.HandleFunc("/hopwatch.js", js) 98 | http.HandleFunc("/gosource", gosource) 99 | http.Handle("/hopwatch", websocket.Handler(connectHandler)) 100 | go listen() 101 | go sendLoop() 102 | } 103 | 104 | // Open calls the OS default program for uri 105 | func open(uri string) error { 106 | var run string 107 | switch { 108 | case "windows" == runtime.GOOS: 109 | run = "start" 110 | case "darwin" == runtime.GOOS: 111 | run = "open" 112 | case "linux" == runtime.GOOS: 113 | run = "xdg-open" 114 | default: 115 | return fmt.Errorf("Unable to open uri:%v on:%v", uri, runtime.GOOS) 116 | } 117 | return exec.Command(run, uri).Start() 118 | } 119 | 120 | // serve a (source) file for displaying in the debugger 121 | func gosource(w http.ResponseWriter, req *http.Request) { 122 | fileName := req.FormValue("file") 123 | // should check for permission? 124 | w.Header().Set("Cache-control", "no-store, no-cache, must-revalidate") 125 | http.ServeFile(w, req, fileName) 126 | } 127 | 128 | // listen starts a Http Server on a fixed port. 129 | // listen is run in parallel to the initialization process such that it does not block. 130 | func listen() { 131 | hostPort := fmt.Sprintf("%s:%d", hopwatchHost, hopwatchPort) 132 | if hopwatchOpenEnabled { 133 | log.Printf("[hopwatch] opening http://%v/hopwatch.html ...\n", hostPort) 134 | go open(fmt.Sprintf("http://%v/hopwatch.html", hostPort)) 135 | } else { 136 | log.Printf("[hopwatch] open http://%v/hopwatch.html ...\n", hostPort) 137 | } 138 | log.Printf("[hopwatch] listening to %v\n", hostPort) 139 | if err := http.ListenAndServe(hostPort, nil); err != nil { 140 | log.Printf("[hopwatch] failed to start listener:%v", err.Error()) 141 | } 142 | } 143 | 144 | // connectHandler is a Http handler and is called on loading the debugger in a browser. 145 | // As soon as a command is received the receiveLoop is started. 146 | func connectHandler(ws *websocket.Conn) { 147 | if currentWebsocket != nil { 148 | log.Printf("[hopwatch] already connected to a debugger; Ignore this\n") 149 | return 150 | } 151 | // remember the connection for the sendLoop 152 | currentWebsocket = ws 153 | var cmd command 154 | if err := websocket.JSON.Receive(currentWebsocket, &cmd); err != nil { 155 | log.Printf("[hopwatch] connectHandler.JSON.Receive failed:%v", err) 156 | } else { 157 | log.Printf("[hopwatch] connected to browser. ready to hop") 158 | connectChannel <- cmd 159 | receiveLoop() 160 | } 161 | } 162 | 163 | // receiveLoop reads commands from the websocket and puts them onto a channel. 164 | func receiveLoop() { 165 | for { 166 | var cmd command 167 | if err := websocket.JSON.Receive(currentWebsocket, &cmd); err != nil { 168 | log.Printf("[hopwatch] receiveLoop.JSON.Receive failed:%v", err) 169 | fromBrowserChannel <- command{Action: "quit"} 170 | break 171 | } 172 | if "quit" == cmd.Action { 173 | hopwatchEnabled = false 174 | log.Printf("[hopwatch] browser requests disconnect.\n") 175 | currentWebsocket.Close() 176 | currentWebsocket = nil 177 | fromBrowserChannel <- cmd 178 | break 179 | } else { 180 | fromBrowserChannel <- cmd 181 | } 182 | } 183 | } 184 | 185 | // sendLoop takes commands from a channel to send to the browser (debugger). 186 | // If no connection is available then wait for it. 187 | // If the command action is quit then abort the loop. 188 | func sendLoop() { 189 | if currentWebsocket == nil { 190 | log.Print("[hopwatch-exchange] no browser connection, wait for it ...") 191 | cmd := <-connectChannel 192 | if "quit" == cmd.Action { 193 | return 194 | } 195 | } 196 | for { 197 | next := <-toBrowserChannel 198 | if "quit" == next.Action { 199 | break 200 | } 201 | if currentWebsocket == nil { 202 | log.Print("[hopwatch-exchange] no browser connection, wait for it ...") 203 | cmd := <-connectChannel 204 | if "quit" == cmd.Action { 205 | break 206 | } 207 | } 208 | websocket.JSON.Send(currentWebsocket, &next) 209 | } 210 | } 211 | 212 | // suspend will create a new Command and send it to the browser. 213 | // callerOffset controls from which stackframe the go source file and linenumber must be read. 214 | // Ignore if option hopwatch.break=false 215 | func suspend(callerOffset int, conditions ...bool) { 216 | if !hopwatchBreakEnabled { 217 | return 218 | } 219 | for _, condition := range conditions { 220 | if !condition { 221 | return 222 | } 223 | } 224 | _, file, line, ok := runtime.Caller(callerOffset) 225 | cmd := command{Action: "break"} 226 | if ok { 227 | cmd.addParam("go.file", file) 228 | cmd.addParam("go.line", fmt.Sprint(line)) 229 | cmd.addParam("go.stack", trimStack(string(debug.Stack()), fmt.Sprintf("%s:%d", file, line))) 230 | } 231 | channelExchangeCommands(cmd) 232 | } 233 | 234 | // Peel off the part of the stack that lives in hopwatch 235 | func trimStack(stack, fileAndLine string) string { 236 | lines := strings.Split(stack, "\n") 237 | c := 0 238 | for _, each := range lines { 239 | if strings.Index(each, fileAndLine) != -1 { 240 | break 241 | } 242 | c++ 243 | } 244 | return strings.Join(lines[4:], "\n") 245 | } 246 | 247 | // Put a command on the browser channel and wait for the reply command 248 | func channelExchangeCommands(toCmd command) { 249 | if !hopwatchEnabled { 250 | return 251 | } 252 | // synchronize command exchange ; break only one goroutine at a time 253 | debuggerMutex.Lock() 254 | toBrowserChannel <- toCmd 255 | <-fromBrowserChannel 256 | debuggerMutex.Unlock() 257 | } 258 | -------------------------------------------------------------------------------- /hopwatch_css.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012+ Ernest Micklei. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package hopwatch 6 | 7 | import ( 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | func css(w http.ResponseWriter, req *http.Request) { 13 | w.Header().Set("Content-Type", "text/css") 14 | io.WriteString(w, ` 15 | body, html { 16 | margin: 0; 17 | padding: 0; 18 | font-family: Helvetica, Arial, sans-serif; 19 | font-size: 16px; 20 | color: #222; 21 | } 22 | .mono {font-family:"Lucida Console", Monaco, monospace;font-size:13px;} 23 | .wide {width:100%;} 24 | 25 | #header, #content, #footer, #log-pane, #gosource-pane { 26 | position:absolute; 27 | } 28 | 29 | /****************** 30 | * Heading 31 | */ 32 | div#heading { 33 | float: left; 34 | margin: 0 0 10px 0; 35 | padding: 21px 0; 36 | font-size: 20px; 37 | font-weight: normal; 38 | } 39 | div#heading a { 40 | color: #222; 41 | text-decoration: none; 42 | } 43 | div#header { 44 | background: #E0EBF5; 45 | height: 64px; 46 | width: 100%; 47 | } 48 | .container { 49 | padding: 0 20px; 50 | } 51 | div#menu { 52 | float: left; 53 | min-width: 590px; 54 | padding: 10px 0; 55 | text-align: right; 56 | margin-top: 10px; 57 | } 58 | div#menu > a { 59 | margin-right: 5px; 60 | margin-bottom: 10px; 61 | padding: 10px; 62 | } 63 | .buttonEnabled { 64 | color: white; 65 | background: #375EAB; 66 | } 67 | .buttonDisabled { 68 | color: #375EAB; 69 | background: white; 70 | } 71 | div#menu > a, 72 | div#menu > input { 73 | padding: 10px; 74 | text-decoration: none; 75 | font-size: 16px; 76 | -webkit-border-radius: 5px; 77 | -moz-border-radius: 5px; 78 | border-radius: 5px; 79 | } 80 | 81 | /****************** 82 | * Footer 83 | */ 84 | div#footer { 85 | bottom: 0; 86 | height: 24px; 87 | width: 100%; 88 | 89 | text-align: center; 90 | color: #666; 91 | font-size: 14px; 92 | } 93 | 94 | /****************** 95 | * Content 96 | */ 97 | #content { 98 | top: 64px; 99 | bottom: 24px; 100 | width: 100%; 101 | } 102 | 103 | /****************** 104 | * Log 105 | */ 106 | #log-pane { 107 | height: 100%; 108 | width: 60%; 109 | overflow: auto; 110 | } 111 | a { text-decoration:none; color: #375EAB; } 112 | a:hover { text-decoration:underline ; color:black } 113 | 114 | .logline {} 115 | .srcline {} 116 | .toggle {padding-left:4px;padding-right:4px;margin-left:4px;margin-right:4px;background-color:#375EAB;color:#FFF;} 117 | .stack { 118 | background-color:#FFD; 119 | padding: 4px; 120 | border-width: 1px; 121 | border-color: #ddd; 122 | border-style: solid; 123 | box-shadow: inset 0 4px 5px -5px rgba(0,0,0,0.4); 124 | margin: 1px 6px 0; 125 | } 126 | .time {color:#AAA;white-space:nowrap} 127 | .watch {width:100%;white-space:pre} 128 | .goline {color:#888;padding-left:8px;padding-right:8px;} 129 | .err {background-color:#FF3300;width:100%;} 130 | .info {width:100%;} 131 | .break {background-color:#375EAB;color:#FFF;} 132 | .suspend {} 133 | 134 | /****************** 135 | * Source 136 | */ 137 | #gosource-pane { 138 | height: 100%; 139 | left: 60% ; 140 | right: 0px; 141 | display: none; 142 | margin: 0; 143 | overflow: auto; 144 | } 145 | #gosource { 146 | margin: 0; 147 | background: #FFD; 148 | white-space: pre; 149 | } 150 | #nrs { 151 | width: 24px; 152 | float: left; 153 | } 154 | #gofile { 155 | background-color:#FFF; 156 | color:#375EAB; 157 | } 158 | `) 159 | return 160 | } 161 | -------------------------------------------------------------------------------- /hopwatch_how.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emicklei/hopwatch/adefb4c6715461325207857bd051e4506f520cf9/hopwatch_how.png -------------------------------------------------------------------------------- /hopwatch_html.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012+ ernestmicklei.com. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package hopwatch 6 | 7 | import ( 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | func html(w http.ResponseWriter, req *http.Request) { 13 | io.WriteString(w, 14 | ` 15 | 16 | Hopwatch Debugger 17 | 18 | 19 | 20 | 21 | 22 | 23 | 35 |
36 |
37 |
38 |
39 |
40 |
somefile.go
41 |
42 |
43 |
44 |
45 |
46 | 49 | 50 | 51 | `) 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /hopwatch_javascript.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012+ ernestmicklei.com. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package hopwatch 6 | 7 | import ( 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | func js(w http.ResponseWriter, req *http.Request) { 13 | w.Header().Set("Content-Type", "text/javascript") 14 | io.WriteString(w, ` 15 | var wsUri = "ws://" + window.location.hostname + ":" + window.location.port + "/hopwatch"; 16 | var output; 17 | var websocket = null; // create at init 18 | var connected = false; 19 | var suspended = false; 20 | var golineSize = 18; 21 | 22 | function init() { 23 | output = document.getElementById("output"); 24 | websocket = new WebSocket(wsUri); 25 | setupWebSocket(); 26 | } 27 | function setupWebSocket() { 28 | websocket.addEventListener('open', onOpen); 29 | websocket.addEventListener('close', onClose); 30 | websocket.addEventListener('message', onMessage); 31 | websocket.addEventListener('error', onError); 32 | } 33 | function onOpen(evt) { 34 | connected = true; 35 | document.getElementById("disconnect").className = "buttonEnabled"; 36 | writeToScreen("<-> connected","info mono"); 37 | sendConnected(); 38 | } 39 | function onClose(evt) { 40 | console.log("onClose:",evt); 41 | handleDisconnected(); 42 | } 43 | function onMessage(evt) { 44 | try { 45 | var cmd = JSON.parse(evt.data); 46 | } catch (e) { 47 | console.log('[hopwatch] failed to read valid JSON: ', message.data); 48 | return; 49 | } 50 | // console.log("[hopwatch] received: " + evt.data); 51 | if (cmd.Action == "display") { 52 | var logdiv = document.createElement("div"); 53 | logdiv.className = "logline" 54 | addTime(logdiv); 55 | addGoline(logdiv,cmd); 56 | addMessage(logdiv,watchParametersToHtml(cmd.Parameters),"watch mono"); 57 | output.appendChild(logdiv); 58 | logdiv.scrollIntoView(); 59 | sendResume(); 60 | return; 61 | } 62 | if (cmd.Action == "print") { 63 | var logdiv = document.createElement("div"); 64 | logdiv.className = "logline" 65 | addTime(logdiv); 66 | addGoline(logdiv,cmd); 67 | addMessage(logdiv,cmd.Parameters["line"],"watch mono"); 68 | output.appendChild(logdiv); 69 | logdiv.scrollIntoView(); 70 | sendResume(); 71 | return; 72 | } 73 | if (cmd.Action == "break") { 74 | handleSuspended(cmd); 75 | return; 76 | } 77 | } 78 | function onError(evt) { 79 | writeToScreen(evt,"err mono"); 80 | } 81 | function handleSuspended(cmd) { 82 | suspended = true; 83 | document.getElementById("resume").className = "buttonEnabled"; 84 | var logdiv = document.createElement("div"); 85 | logdiv.className = "logline" 86 | addTime(logdiv); 87 | addGoline(logdiv,cmd); 88 | var celldiv = addMessage(logdiv,"--> program suspended", "suspend mono"); 89 | addStack(celldiv,cmd); 90 | output.appendChild(logdiv); 91 | logdiv.scrollIntoView(); 92 | handleSourceUpdate(cmd); 93 | } 94 | function handleSourceUpdate(cmd) { 95 | loadSource(cmd.Parameters["go.file"], cmd.Parameters["go.line"]); 96 | } 97 | function writeToScreen(text,cls) { 98 | var logdiv = document.createElement("div"); 99 | logdiv.className = "logline" 100 | addTime(logdiv); 101 | addEmptiness(logdiv); 102 | addMessage(logdiv,text,cls); 103 | logdiv.scrollIntoView(); 104 | output.appendChild(logdiv); 105 | } 106 | function addTime(logdiv) { 107 | var stamp = document.createElement("span"); 108 | stamp.innerHTML = timeHHMMSS(); 109 | stamp.className = "time mono" 110 | logdiv.appendChild(stamp); 111 | } 112 | function addMessage(logdiv,msg,msgcls) { 113 | var txt = document.createElement("span"); 114 | txt.className = msgcls 115 | var escaped = ""; 116 | var arr = safe_tags(msg).split('\n'); 117 | for (var i = 0; i < arr.length; i++) { 118 | if (i > 0) escaped += "\n        "; // TODO remove hack 119 | escaped += arr[i]; 120 | } 121 | txt.innerHTML = escaped; 122 | logdiv.appendChild(txt); 123 | return txt; 124 | } 125 | function safe_tags(str) { 126 | return str.replace(/&/g,'&').replace(//g,'>') ; 127 | } 128 | function addEmptiness(logdiv) { 129 | var empt = document.createElement("span"); 130 | empt.className = "goline" 131 | empt.innerHTML = " "; 132 | logdiv.appendChild(empt); 133 | } 134 | function addGoline(logdiv,cmd) { 135 | var where = document.createElement("span"); 136 | where.className = "srcline" 137 | var link = document.createElement("a"); 138 | link.href = "#"; 139 | link.className = "goline mono"; 140 | link.onclick = function() { 141 | loadSource(cmd.Parameters["go.file"], cmd.Parameters["go.line"]); 142 | }; 143 | link.innerHTML = goline(cmd.Parameters); 144 | where.appendChild(link); 145 | logdiv.appendChild(where); 146 | } 147 | function loadSource(fileName, nr) { 148 | $("#gofile").html(shortenFileName(fileName)); 149 | $("#gosource-pane").show(); 150 | $.ajax({ 151 | url:"/gosource?file="+fileName 152 | }).done( 153 | function(responseText,status,xhr) { 154 | handleSourceLoaded(responseText,nr); 155 | } 156 | ); 157 | } 158 | function handleSourceLoaded(responseText,line) { 159 | gosource = $("#gosource"); 160 | gosource.empty(); 161 | var breakElm 162 | // Insert line numbers 163 | var arr = responseText.split('\n'); 164 | for (var i = 0; i < arr.length; i++) { 165 | var nr = i + 1 166 | var buf = space_padded(nr) + arr[i]; 167 | var elm = document.createElement("div"); 168 | elm.innerHTML = buf; 169 | if (line == nr) { 170 | elm.className = "break"; 171 | breakElm = elm 172 | } 173 | gosource.append(elm) 174 | } 175 | breakElm.scrollIntoView(); 176 | } 177 | function space_padded(i) { 178 | var buf = "" + i 179 | if (i<1000) { buf += " " } 180 | if (i<100) { buf += " " } 181 | if (i<10) { buf += " " } 182 | return buf 183 | } 184 | function shortenFileName(fileName) { 185 | return fileName.length > 48 ? "..." + fileName.substring(fileName.length - 48) : fileName; 186 | } 187 | function addStack(celldiv,cmd) { 188 | var stack = cmd.Parameters["go.stack"]; 189 | if (stack != null && stack.length > 0) { 190 | addNonEmptyStackTo(stack,celldiv); 191 | } 192 | } 193 | function addNonEmptyStackTo(stack,celldiv) { 194 | var toggle = document.createElement("a"); 195 | toggle.href = "#"; 196 | toggle.className = "toggle"; 197 | toggle.onclick = function() { toggleStack(toggle); }; 198 | toggle.innerHTML="stack ▶"; 199 | celldiv.appendChild(toggle); 200 | 201 | var stk = document.createElement("div"); 202 | stk.style.display = "none"; 203 | var lines = document.createElement("pre"); 204 | lines.innerHTML = stack 205 | lines.className = "stack mono" 206 | stk.appendChild(lines) 207 | celldiv.appendChild(stk) 208 | } 209 | function toggleStack(link) { 210 | var stack = link.nextSibling; 211 | if (stack.style.display == "none") { 212 | link.innerHTML = "stack ▼"; 213 | stack.style.display = "block" 214 | stack.scrollIntoView(); 215 | } else { 216 | link.innerHTML = "stack ▶"; 217 | stack.style.display = "none"; 218 | } 219 | } 220 | // http://www.quirksmode.org/js/keys.html 221 | function handleKeyDown(event) { 222 | if (event.keyCode == 119) { 223 | actionResume(); 224 | } 225 | } 226 | function watchParametersToHtml(parameters) { 227 | var line = ""; 228 | var multiline = false; 229 | for (var prop in parameters) { 230 | if (prop.slice(0,3) != "go.") { 231 | if (multiline) { line = line + ", "; } 232 | line = line + prop + "=" + parameters[prop]; 233 | multiline = true; 234 | } 235 | } 236 | return line 237 | } 238 | function goline(parameters) { 239 | var f = parameters["go.file"] 240 | f = f.substr(f.lastIndexOf("/")+1) 241 | var padded = f + ":" + parameters["go.line"] 242 | while (padded.length > golineSize) { 243 | golineSize += 4 244 | } 245 | for (i=padded.length;i-< disconnected","info mono"); 269 | } 270 | function timeHHMMSS() { return new Date().toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1"); } 271 | function sendConnected() { doSend('{"Action":"connected"}'); } 272 | function sendResume() { doSend('{"Action":"resume"}'); } 273 | function sendQuit() { doSend('{"Action":"quit"}'); } 274 | function doSend(message) { 275 | // console.log("[hopwatch] send: " + message); 276 | websocket.send(message); 277 | } 278 | window.addEventListener("load", init, false); 279 | window.addEventListener("keydown", handleKeyDown, true); `) 280 | return 281 | } 282 | -------------------------------------------------------------------------------- /spew.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2022 Ernest Micklei. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package hopwatch 6 | 7 | import ( 8 | "bytes" 9 | "github.com/davecgh/go-spew/spew" 10 | "strings" 11 | ) 12 | 13 | // Dump displays the passed parameters with newlines and additional debug information such as complete types and all pointer addresses used to indirect to the final value. 14 | // Delegates to spew.Fdump, see http://godoc.org/github.com/davecgh/go-spew/spew#Dump 15 | func Dump(a ...interface{}) *Watchpoint { 16 | wp := &Watchpoint{offset: 3} 17 | wp.Dump(a...) 18 | wp.offset -= 1 19 | return wp 20 | } 21 | 22 | // Dumpf formats and displays the passed parameters with newlines and additional debug information such as complete types and all pointer addresses used to indirect to the final value. 23 | // delegates to spew.Fprintf, see http://godoc.org/github.com/davecgh/go-spew/spew#Dump 24 | func Dumpf(format string, a ...interface{}) *Watchpoint { 25 | wp := &Watchpoint{offset: 3} 26 | wp.Dumpf(format, a...) 27 | wp.offset -= 1 28 | return wp 29 | } 30 | 31 | // Dump displays the passed parameters with newlines and additional debug information such as complete types and all pointer addresses used to indirect to the final value. 32 | // Delegates to spew.Fdump, see http://godoc.org/github.com/davecgh/go-spew/spew#Dump 33 | func (w *Watchpoint) Dump(a ...interface{}) *Watchpoint { 34 | writer := new(bytes.Buffer) 35 | spew.Fdump(writer, a...) 36 | return w.printcontent(strings.TrimRight(string(writer.Bytes()), "\n")) 37 | } 38 | 39 | // Dumpf formats and displays the passed parameters with newlines and additional debug information such as complete types and all pointer addresses used to indirect to the final value. 40 | // Delegates to spew.Fprintf, see http://godoc.org/github.com/davecgh/go-spew/spew#Dump 41 | func (w *Watchpoint) Dumpf(format string, a ...interface{}) *Watchpoint { 42 | writer := new(bytes.Buffer) 43 | _, err := spew.Fprintf(writer, format, a...) 44 | if err != nil { 45 | return Printf("[hopwatch] error in spew.Fprintf:%v", err) 46 | } 47 | return w.printcontent(string(writer.Bytes())) 48 | } 49 | -------------------------------------------------------------------------------- /watchpoint.go: -------------------------------------------------------------------------------- 1 | package hopwatch 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "runtime" 7 | ) 8 | 9 | // watchpoint is a helper to provide a fluent style api. 10 | // This allows for statements like hopwatch.Display("var",value).Break() 11 | type Watchpoint struct { 12 | disabled bool 13 | offset int // offset in the caller stack for highlighting source 14 | } 15 | 16 | // CallerOffset (default=2) allows you to change the file indicator in hopwatch. 17 | func (w *Watchpoint) CallerOffset(offset int) *Watchpoint { 18 | if hopwatchEnabled && (offset < 0) { 19 | log.Panicf("[hopwatch] ERROR: illegal caller offset:%v . watchpoint is disabled.\n", offset) 20 | w.disabled = true 21 | } 22 | w.offset = offset 23 | return w 24 | } 25 | 26 | // Printf formats according to a format specifier and writes to the debugger screen. 27 | func (w *Watchpoint) Printf(format string, params ...interface{}) *Watchpoint { 28 | w.offset += 1 29 | var content string 30 | if len(params) == 0 { 31 | content = format 32 | } else { 33 | content = fmt.Sprintf(format, params...) 34 | } 35 | return w.printcontent(content) 36 | } 37 | 38 | // Printf formats according to a format specifier and writes to the debugger screen. 39 | func (w *Watchpoint) printcontent(content string) *Watchpoint { 40 | _, file, line, ok := runtime.Caller(w.offset) 41 | cmd := command{Action: "print"} 42 | if ok { 43 | cmd.addParam("go.file", file) 44 | cmd.addParam("go.line", fmt.Sprint(line)) 45 | } 46 | cmd.addParam("line", content) 47 | channelExchangeCommands(cmd) 48 | return w 49 | } 50 | 51 | // Display sends variable name,value pairs to the debugger. Values are formatted using %#v. 52 | // The parameter nameValuePairs must be even sized. 53 | func (w *Watchpoint) Display(nameValuePairs ...interface{}) *Watchpoint { 54 | _, file, line, ok := runtime.Caller(w.offset) 55 | cmd := command{Action: "display"} 56 | if ok { 57 | cmd.addParam("go.file", file) 58 | cmd.addParam("go.line", fmt.Sprint(line)) 59 | } 60 | if len(nameValuePairs)%2 == 0 { 61 | for i := 0; i < len(nameValuePairs); i += 2 { 62 | k := nameValuePairs[i] 63 | v := nameValuePairs[i+1] 64 | cmd.addParam(fmt.Sprint(k), fmt.Sprintf("%#v", v)) 65 | } 66 | } else { 67 | log.Printf("[hopwatch] WARN: missing variable for Display(...) in: %v:%v\n", file, line) 68 | w.disabled = true 69 | return w 70 | } 71 | channelExchangeCommands(cmd) 72 | return w 73 | } 74 | 75 | // Break halts the execution of the program and waits for an instruction from the debugger (e.g. Resume). 76 | // Break is only effective if all (if any) conditions are true. The program will resume otherwise. 77 | func (w Watchpoint) Break(conditions ...bool) { 78 | suspend(w.offset, conditions...) 79 | } 80 | --------------------------------------------------------------------------------