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.

🛟 Updated. 28d5985 · on v1.0.0 · k33g · 15h ago
trace.go · 120 lines · 3.3 KBGo Blame HistoryRaw
  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
package acp

import (
	"bytes"
	"io"
	"os"
	"sync"
	"time"
)

// TraceEnv names the file every message to and from an agent is appended to,
// when it is set. It exists for one question — "what did the agent actually
// send?" — which nothing on the screen can answer: an update this client
// cannot read is counted, not shown, and an agent that never sent one looks
// exactly like an agent whose message was dropped.
//
//	TURBO_ACP_TRACE=/tmp/acp.log turbo-go
//
// The trace is never allowed to break the editor: a file that cannot be
// opened or written means no trace, and nothing else.
const TraceEnv = "TURBO_ACP_TRACE"

// traceStream wraps a stream so that each line read from and written to it is
// also appended to the trace file, when TraceEnv names one.
func traceStream(stream io.ReadWriteCloser) io.ReadWriteCloser {
	path := os.Getenv(TraceEnv)
	if path == "" {
		return stream
	}
	file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
	if err != nil {
		return stream
	}
	return &tracer{stream: stream, file: file, now: time.Now}
}

// tracer is the wrapping stream. Each direction keeps its own partial line,
// because a read may end anywhere and the protocol's unit is the line.
type tracer struct {
	stream io.ReadWriteCloser
	file   *os.File
	now    func() time.Time

	mu      sync.Mutex
	partial [2]bytes.Buffer // 0 read, 1 written
}

// The two directions, as the trace marks them.
const (
	fromAgent = 0
	toAgent   = 1
)

var directionMarks = [2]string{"<- ", "-> "}

// Read reads from the agent, and traces the lines that completes.
func (t *tracer) Read(p []byte) (int, error) {
	n, err := t.stream.Read(p)
	if n > 0 {
		t.trace(fromAgent, p[:n])
	}
	return n, err
}

// Write writes to the agent, and traces the lines that completes.
func (t *tracer) Write(p []byte) (int, error) {
	n, err := t.stream.Write(p)
	if n > 0 {
		t.trace(toAgent, p[:n])
	}
	return n, err
}

// Close closes the stream, flushes whatever partial line either direction
// still held, and closes the file.
func (t *tracer) Close() error {
	err := t.stream.Close()

	t.mu.Lock()
	defer t.mu.Unlock()
	for direction := range t.partial {
		if t.partial[direction].Len() > 0 {
			t.emit(direction, t.partial[direction].Bytes())
			t.partial[direction].Reset()
		}
	}
	_ = t.file.Close()
	return err
}

// trace appends data to the direction's partial line and emits every complete
// line it now holds.
func (t *tracer) trace(direction int, data []byte) {
	t.mu.Lock()
	defer t.mu.Unlock()

	buffer := &t.partial[direction]
	buffer.Write(data)
	for {
		line, rest, found := bytes.Cut(buffer.Bytes(), []byte{'\n'})
		if !found {
			return
		}
		t.emit(direction, line)
		remaining := append([]byte{}, rest...)
		buffer.Reset()
		buffer.Write(remaining)
	}
}

// emit writes one traced line: the time, the direction, the message.
func (t *tracer) emit(direction int, line []byte) {
	stamp := t.now().Format("15:04:05.000")
	_, _ = t.file.WriteString(stamp + " " + directionMarks[direction] + string(line) + "\n")
}

// TraceStreamForTest wraps a stream the way Start wraps an agent's pipes, so
// that the trace can be tested over an in-process pipe. It reads TraceEnv
// like the real thing, and returns the stream untouched when it is unset.
func TraceStreamForTest(stream io.ReadWriteCloser) io.ReadWriteCloser { return traceStream(stream) }