turbo-editors/turbo-corepublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

trace.go · 120 lines · 3.3 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 17h ago1package acp
2
3import (
4 "bytes"
5 "io"
6 "os"
7 "sync"
8 "time"
9)
10
11// TraceEnv names the file every message to and from an agent is appended to,
12// when it is set. It exists for one question — "what did the agent actually
13// send?" — which nothing on the screen can answer: an update this client
14// cannot read is counted, not shown, and an agent that never sent one looks
15// exactly like an agent whose message was dropped.
16//
17// TURBO_ACP_TRACE=/tmp/acp.log turbo-go
18//
19// The trace is never allowed to break the editor: a file that cannot be
20// opened or written means no trace, and nothing else.
21const TraceEnv = "TURBO_ACP_TRACE"
22
23// traceStream wraps a stream so that each line read from and written to it is
24// also appended to the trace file, when TraceEnv names one.
25func traceStream(stream io.ReadWriteCloser) io.ReadWriteCloser {
26 path := os.Getenv(TraceEnv)
27 if path == "" {
28 return stream
29 }
30 file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
31 if err != nil {
32 return stream
33 }
34 return &tracer{stream: stream, file: file, now: time.Now}
35}
36
37// tracer is the wrapping stream. Each direction keeps its own partial line,
38// because a read may end anywhere and the protocol's unit is the line.
39type tracer struct {
40 stream io.ReadWriteCloser
41 file *os.File
42 now func() time.Time
43
44 mu sync.Mutex
45 partial [2]bytes.Buffer // 0 read, 1 written
46}
47
48// The two directions, as the trace marks them.
49const (
50 fromAgent = 0
51 toAgent = 1
52)
53
54var directionMarks = [2]string{"<- ", "-> "}
55
56// Read reads from the agent, and traces the lines that completes.
57func (t *tracer) Read(p []byte) (int, error) {
58 n, err := t.stream.Read(p)
59 if n > 0 {
60 t.trace(fromAgent, p[:n])
61 }
62 return n, err
63}
64
65// Write writes to the agent, and traces the lines that completes.
66func (t *tracer) Write(p []byte) (int, error) {
67 n, err := t.stream.Write(p)
68 if n > 0 {
69 t.trace(toAgent, p[:n])
70 }
71 return n, err
72}
73
74// Close closes the stream, flushes whatever partial line either direction
75// still held, and closes the file.
76func (t *tracer) Close() error {
77 err := t.stream.Close()
78
79 t.mu.Lock()
80 defer t.mu.Unlock()
81 for direction := range t.partial {
82 if t.partial[direction].Len() > 0 {
83 t.emit(direction, t.partial[direction].Bytes())
84 t.partial[direction].Reset()
85 }
86 }
87 _ = t.file.Close()
88 return err
89}
90
91// trace appends data to the direction's partial line and emits every complete
92// line it now holds.
93func (t *tracer) trace(direction int, data []byte) {
94 t.mu.Lock()
95 defer t.mu.Unlock()
96
97 buffer := &t.partial[direction]
98 buffer.Write(data)
99 for {
100 line, rest, found := bytes.Cut(buffer.Bytes(), []byte{'\n'})
101 if !found {
102 return
103 }
104 t.emit(direction, line)
105 remaining := append([]byte{}, rest...)
106 buffer.Reset()
107 buffer.Write(remaining)
108 }
109}
110
111// emit writes one traced line: the time, the direction, the message.
112func (t *tracer) emit(direction int, line []byte) {
113 stamp := t.now().Format("15:04:05.000")
114 _, _ = t.file.WriteString(stamp + " " + directionMarks[direction] + string(line) + "\n")
115}
116
117// TraceStreamForTest wraps a stream the way Start wraps an agent's pipes, so
118// that the trace can be tested over an in-process pipe. It reads TraceEnv
119// like the real thing, and returns the stream untouched when it is unset.
120func TraceStreamForTest(stream io.ReadWriteCloser) io.ReadWriteCloser { return traceStream(stream) }