summaryrefslogtreecommitdiff
path: root/cmd/tealdbg/server.go
blob: b6ccb0524caa9e52fe2a861734eaee2f3848f425 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
// Copyright (C) 2019-2022 Algorand, Inc.
// This file is part of go-algorand
//
// go-algorand is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// go-algorand is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand.  If not, see <https://www.gnu.org/licenses/>.

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/algorand/go-algorand/data/transactions/logic"
	"github.com/algorand/websocket"
	"github.com/gorilla/mux"
)

const (
	// WebSocketReadBufferSize is the size of the ReadBuffer for the
	// tealdbg/cdt websocket session.
	// A buffer that is too small will cause the session to choke
	// during the `getScriptSource` call and the session cannot recover.
	WebSocketReadBufferSize = 81920

	// WebSocketWriteBufferSize is the size of the WriteBuffer for the
	// tealdbg/cdt websocket session.
	// The reasoning for the size is the same as above
	WebSocketWriteBufferSize = 81920
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  WebSocketReadBufferSize,
	WriteBufferSize: WebSocketWriteBufferSize,
	CheckOrigin: func(r *http.Request) bool {
		if len(r.Header.Get("Origin")) == 0 {
			return true
		}
		if strings.HasPrefix(r.Header.Get("Origin"), "devtools://") {
			return true
		}
		if strings.HasPrefix(r.Header.Get("Origin"), "http://localhost") {
			return true
		}
		if strings.HasPrefix(r.Header.Get("Origin"), "http://127.0.0.1") {
			return true
		}
		return false
	},
}

// DebugServer is Debugger + HTTP/WS handlers for frontends
type DebugServer struct {
	debugger  *Debugger
	frontend  DebugAdapter
	router    *mux.Router
	server    *http.Server
	remote    *RemoteHookAdapter
	params    *DebugParams
	spinoffCh chan spinoffMsg
}

type spinoffMsg struct {
	err  error
	data []byte
}

// DebugParams is a container for debug parameters
type DebugParams struct {
	ProgramNames     []string
	ProgramBlobs     [][]byte
	Proto            string
	TxnBlob          []byte
	GroupIndex       int
	PastSideEffects  []logic.EvalSideEffects
	BalanceBlob      []byte
	DdrBlob          []byte
	IndexerURL       string
	IndexerToken     string
	Round            uint64
	LatestTimestamp  int64
	RunMode          string
	DisableSourceMap bool
	AppID            uint64
	Painless         bool
	ListenForDrReq   bool
}

// FrontendFactory interface for attaching debug frontends
type FrontendFactory interface {
	Make(router *mux.Router, appAddress string) (da DebugAdapter)
}

func makeDebugServer(iface string, port int, ff FrontendFactory, dp *DebugParams) DebugServer {
	debugger := MakeDebugger()

	router := mux.NewRouter()
	appAddress := fmt.Sprintf("%s:%d", iface, port)

	da := ff.Make(router, appAddress)
	debugger.AddAdapter(da)

	server := &http.Server{
		Handler:      router,
		Addr:         appAddress,
		WriteTimeout: time.Duration(0),
		ReadTimeout:  time.Duration(0),
	}

	return DebugServer{
		debugger:  debugger,
		frontend:  da,
		router:    router,
		server:    server,
		params:    dp,
		spinoffCh: make(chan spinoffMsg),
	}
}

func (ds *DebugServer) startRemote() error {
	remote := MakeRemoteHook(ds.debugger)
	remote.Setup(ds.router)
	ds.remote = remote

	log.Printf("starting server on %s", ds.server.Addr)
	err := ds.server.ListenAndServe()
	if err != nil && err != http.ErrServerClosed {
		return err
	}
	return nil
}

// DebugServer in local evaluator mode either accepts Dryrun Request obj if ListenForDrReq is set
// or works with existing args set via command line.
// So that for ListenForDrReq case a new endpoint is created and incoming data is await first.
// Then execution is set up and program(s) run with stage-by-stage sync with ListenForDrReq's handler.
func (ds *DebugServer) startDebug() (err error) {
	local := MakeLocalRunner(ds.debugger)

	if ds.params.ListenForDrReq {
		path := "/spinoff"
		ds.router.HandleFunc(path, ds.dryrunReqHander).Methods("POST")
		log.Printf("listening for upcoming dryrun requests at http://%s%s", ds.server.Addr, path)
	}

	go func() {
		err := ds.server.ListenAndServe()
		if err != nil && err != http.ErrServerClosed {
			log.Panicf("failed to listen: %v", err)
		}
	}()
	defer ds.server.Shutdown(context.Background())

	urlFetcherCh := make(chan struct{})
	if ds.params.ListenForDrReq {
		msg := <-ds.spinoffCh
		ds.params.DdrBlob = msg.data
		go func() {
			for {
				url := ds.frontend.URL()
				if len(url) > 0 {
					ds.spinoffCh <- spinoffMsg{nil, []byte(url)}
					break
				}
				time.Sleep(100 * time.Millisecond)
			}
			urlFetcherCh <- struct{}{}
		}()
	}

	if err = local.Setup(ds.params); err != nil {
		if ds.params.ListenForDrReq {
			ds.spinoffCh <- spinoffMsg{err, []byte(err.Error())}
			<-ds.spinoffCh // wait handler to complete
		}
		return
	}

	if err = local.RunAll(); err != nil {
		if ds.params.ListenForDrReq {
			ds.spinoffCh <- spinoffMsg{err, []byte(err.Error())}
			<-ds.spinoffCh // wait handler to complete
		}
		return
	}

	ds.frontend.WaitForCompletion()

	if ds.params.ListenForDrReq {
		// It is possible URL fetcher routine does not return anything and stuck in the loop.
		// By this point all executions are done and a message urlFetcherCh must be available.
		// If not then URL fetcher stuck and special handling is needed: send an error message
		// to the network handler in order to unblock it.
		select {
		case <-urlFetcherCh:
		default:
			err = fmt.Errorf("no URL from frontend")
			ds.spinoffCh <- spinoffMsg{err, []byte(err.Error())}
		}
		<-ds.spinoffCh // wait handler to complete
	}
	return
}

func (ds *DebugServer) dryrunReqHander(w http.ResponseWriter, r *http.Request) {
	blob := make([]byte, 0, 4096)
	buf := make([]byte, 1024)
	n, err := r.Body.Read(buf)
	for n > 0 {
		blob = append(blob, buf...)
		n, err = r.Body.Read(buf)
	}
	if err != nil && err != io.EOF {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	// send data
	ds.spinoffCh <- spinoffMsg{nil, blob}
	// wait for confirmation message
	msg := <-ds.spinoffCh

	w.Header().Set("Content-Type", "text/plain")
	if msg.err == nil {
		w.WriteHeader(http.StatusOK)
	} else {
		w.WriteHeader(http.StatusBadRequest)
	}
	w.Write(msg.data)

	// let the main thread to exit
	close(ds.spinoffCh)
	return
}