-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmonitor.go
More file actions
219 lines (195 loc) · 5.37 KB
/
monitor.go
File metadata and controls
219 lines (195 loc) · 5.37 KB
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
package main
// taken from: https://github.com/tinygo-org/tinygo/blob/release/monitor.go
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/mattn/go-tty"
"go.bug.st/serial"
)
// openSerial tries to open `port` at `baud`. It retries for up to ~3
// seconds (300 attempts at 10ms each), which lets you start the
// terminal before the device has finished enumerating, and lets the
// reconnect loop wait for a brief device reset / replug.
//
// If exitCh is closed (ctrl+] pressed) during the retry loop, the
// function returns immediately with (nil, nil) so the caller can exit
// without waiting for the full timeout.
func openSerial(port string, baud int, exitCh <-chan struct{}) (serial.Port, error) {
const wait = 300
var p serial.Port
var err error
for i := 0; i <= wait; i++ {
select {
case <-exitCh:
return nil, nil
default:
}
p, err = serial.Open(port, &serial.Mode{BaudRate: baud})
if err == nil {
p.ResetInputBuffer()
p.ResetOutputBuffer()
return p, nil
}
if i < wait {
time.Sleep(10 * time.Millisecond)
}
}
return nil, err
}
func Monitor(port string, baud int, lineDelay time.Duration) error {
if baud < 1 {
baud = 115200
}
fmt.Printf("connecting %s %d\n", port, baud)
tty, err := tty.Open()
if err != nil {
return err
}
defer tty.Close()
// Undo any visual state the remote device may have left on the
// host terminal. tty.Close() restores termios (raw -> cooked) but
// not escape-sequence state like cursor visibility or colors.
// This matches what picocom and tio do on exit.
defer fmt.Print("\x1b[?25h\x1b[0m\n")
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGQUIT)
defer signal.Stop(sig)
// currentPort is the serial.Port the reconnect loop currently has
// open, or nil between connections. The signal-forwarding and
// input-forwarding goroutines write through writePort, which
// silently drops bytes if no port is open.
var (
portMu sync.Mutex
currentPort serial.Port
)
setPort := func(p serial.Port) {
portMu.Lock()
currentPort = p
portMu.Unlock()
}
writePort := func(data []byte) {
portMu.Lock()
if currentPort != nil {
currentPort.Write(data)
}
portMu.Unlock()
}
// exitCh is closed by the input goroutine on ctrl+] or any tty
// read error. The reconnect loop selects on it for clean shutdown
// so the deferred tty.Close() and signal.Stop() can run.
exitCh := make(chan struct{})
// Signal forwarder: turn host ^C / ^\ into wire bytes for the
// remote, regardless of which port (if any) is currently open.
go func() {
for s := range sig {
switch s {
case os.Interrupt:
writePort([]byte{0x1b, 0x03}) // send ctrl+c to tty
case syscall.SIGQUIT:
writePort([]byte{0x1b, 0x1c}) // send ctrl+\ to tty
}
}
}()
// Input forwarder: host tty -> currentPort. Closes exitCh on
// ctrl+] or any tty read error. When pasted text arrives faster
// than lineDelay between consecutive \n characters, the goroutine
// sleeps the difference so the remote device has time to process
// each line before the next one arrives.
go func() {
var lastNewline time.Time
for {
r, err := tty.ReadRune()
if err != nil {
close(exitCh)
return
}
if r == 0 {
continue
}
if r == 29 { // ctrl+]
fmt.Println("ctrl+] received, exiting...")
close(exitCh)
return
}
if (r == '\n' || r == '\r') && lineDelay > 0 {
now := time.Now()
if !lastNewline.IsZero() {
if gap := now.Sub(lastNewline); gap < lineDelay {
time.Sleep(lineDelay - gap)
}
}
lastNewline = now
}
writePort([]byte(string(r)))
}
}()
// Reconnect loop. The first iteration is the initial connect; on
// a read error we close the port, print a status line, and try
// the open again with the same retry budget. On exitCh close we
// return nil and let the deferred Closes run.
first := true
for {
// Check for clean exit before any blocking call.
select {
case <-exitCh:
return nil
default:
}
p, err := openSerial(port, baud, exitCh)
if p == nil && err == nil {
return nil // exitCh closed during open retry
}
if err != nil {
if first {
return err // initial connect failed -- real error
}
// Reconnect failed -- device still gone. openSerial
// already waited ~3s; loop back to try again.
continue
}
setPort(p)
if first {
fmt.Printf("%s connected, use ctrl+] to exit\n", port)
first = false
} else {
fmt.Printf("%s reconnected\n", port)
}
// Per-iteration read goroutine + per-iteration error channel
// so an old goroutine can never race with the next iteration.
// We pass p as a parameter so the goroutine references the
// specific port for this iteration, not the shared variable.
readErr := make(chan error, 1)
go func(p serial.Port) {
buf := make([]byte, 100*1024)
for {
n, err := p.Read(buf)
if err != nil {
readErr <- err
return
}
if n == 0 {
continue
}
fmt.Printf("%v", string(buf[:n]))
}
}(p)
select {
case <-exitCh:
// Clean exit (ctrl+] or tty error) -- close this
// iteration's port and let the deferred cleanup run.
setPort(nil)
p.Close()
return nil
case err := <-readErr:
// Device disappeared. Close the dead port and loop
// back to openSerial for another retry budget.
setPort(nil)
p.Close()
fmt.Printf("\nlost connection (%v); reconnecting...\n", err)
}
}
}