133 lines
3.1 KiB
Go
133 lines
3.1 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
"github.com/docker/docker/pkg/stdcopy"
|
|
|
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/docker"
|
|
)
|
|
|
|
// --- pesan ---
|
|
|
|
// logBatchMsg — sekumpulan baris log (dikirim per batch agar hemat render).
|
|
type logBatchMsg struct {
|
|
id int64
|
|
lines []string
|
|
eof bool
|
|
}
|
|
|
|
// logTickMsg — tidak ada data dalam window idle; re-arm tanpa state change.
|
|
type logTickMsg struct{ id int64 }
|
|
|
|
const (
|
|
logBatchMax = 200 // maksimum baris per batch
|
|
logIdle = 200 * time.Millisecond // tunggu baris pertama
|
|
logDrainWait = 50 * time.Millisecond // jendela serap baris siap berikutnya
|
|
)
|
|
|
|
// lineWriter — pecah byte stream jadi baris dan dorong ke channel.
|
|
// Bila konsumen lambat (buffer penuh), baris di-drop supaya UI tetap responsif
|
|
// (shortcut: log yang lewat di detik ekstrem bisa hilang — listener batch
|
|
// akan mengejar; upgrade bila perlu arsip log lengkap di file).
|
|
type lineWriter struct {
|
|
ch chan<- string
|
|
buf []byte
|
|
}
|
|
|
|
func newLineWriter(ch chan<- string) *lineWriter { return &lineWriter{ch: ch} }
|
|
|
|
func (w *lineWriter) Write(p []byte) (int, error) {
|
|
w.buf = append(w.buf, p...)
|
|
for {
|
|
i := bytes.IndexByte(w.buf, '\n')
|
|
if i < 0 {
|
|
break
|
|
}
|
|
line := strings.TrimRight(string(w.buf[:i]), "\r")
|
|
w.buf = w.buf[i+1:]
|
|
select {
|
|
case w.ch <- line:
|
|
default:
|
|
}
|
|
}
|
|
if len(w.buf) > 1<<20 { // baris raksasa tanpa newline: jaga memori
|
|
w.buf = nil
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (w *lineWriter) flush() {
|
|
if len(w.buf) == 0 {
|
|
return
|
|
}
|
|
select {
|
|
case w.ch <- string(w.buf):
|
|
default:
|
|
}
|
|
w.buf = nil
|
|
}
|
|
|
|
// streamContainerLogs — goroutine per stream: inspect (TTY) → docker logs →
|
|
// demux → lineWriter → channel. Menutup channel saat selesai/ctx dibatalkan.
|
|
func streamContainerLogs(ctx context.Context, cl *docker.Client, id string, tail int, timestamps bool, ch chan<- string) {
|
|
defer close(ch)
|
|
|
|
tty, err := cl.ContainerTty(ctx, id)
|
|
if err != nil {
|
|
ch <- "! " + err.Error()
|
|
return
|
|
}
|
|
rc, err := cl.StreamLogs(ctx, id, tail, timestamps)
|
|
if err != nil {
|
|
ch <- "! " + err.Error()
|
|
return
|
|
}
|
|
defer rc.Close()
|
|
|
|
lw := newLineWriter(ch)
|
|
if tty {
|
|
_, _ = io.Copy(lw, rc)
|
|
} else {
|
|
_, _ = stdcopy.StdCopy(lw, lw, rc) // stdout & stderr dicampur
|
|
}
|
|
lw.flush()
|
|
}
|
|
|
|
// waitLogBatch — tea.Cmd: tunggu baris pertama (maks logIdle), lalu serap
|
|
// sebanyak mungkin dalam jendela pendek supaya 1 render menampung banyak baris.
|
|
func waitLogBatch(id int64, ch <-chan string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
var lines []string
|
|
select {
|
|
case l, ok := <-ch:
|
|
if !ok {
|
|
return logBatchMsg{id: id, eof: true}
|
|
}
|
|
lines = append(lines, l)
|
|
case <-time.After(logIdle):
|
|
return logTickMsg{id: id}
|
|
}
|
|
|
|
timer := time.NewTimer(logDrainWait)
|
|
defer timer.Stop()
|
|
for len(lines) < logBatchMax {
|
|
select {
|
|
case l, ok := <-ch:
|
|
if !ok {
|
|
return logBatchMsg{id: id, lines: lines, eof: true}
|
|
}
|
|
lines = append(lines, l)
|
|
case <-timer.C:
|
|
return logBatchMsg{id: id, lines: lines}
|
|
}
|
|
}
|
|
return logBatchMsg{id: id, lines: lines}
|
|
}
|
|
}
|