202 lines
4.8 KiB
Go
202 lines
4.8 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 }
|
|
|
|
// statsMsg / statsTickMsg — snapshot metrik kontainer (tab Statistik).
|
|
type statsMsg struct {
|
|
id int64
|
|
st docker.Stats
|
|
err error
|
|
}
|
|
type statsTickMsg struct{ id int64 }
|
|
|
|
const statsInterval = time.Second
|
|
|
|
func fetchStats(id int64, cl *docker.Client, cid string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
st, err := cl.StatsOnce(ctx, cid)
|
|
return statsMsg{id: id, st: st, err: err}
|
|
}
|
|
}
|
|
|
|
func statsNext(id int64) tea.Cmd {
|
|
return tea.Tick(statsInterval, func(time.Time) tea.Msg { return statsTickMsg{id: id} })
|
|
}
|
|
|
|
// inspectMsg / envMsg / procsMsg — data tab Konfigurasi / Env / Proses
|
|
// (one-shot saat tab dibuka atau `r`).
|
|
type inspectMsg struct {
|
|
id int64
|
|
data []byte
|
|
err error
|
|
}
|
|
type envMsg struct {
|
|
id int64
|
|
env []string
|
|
err error
|
|
}
|
|
type procsMsg struct {
|
|
id int64
|
|
titles []string
|
|
rows [][]string
|
|
err error
|
|
}
|
|
|
|
func fetchInspect(id int64, cl *docker.Client, cid string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
b, err := cl.InspectJSON(ctx, cid)
|
|
return inspectMsg{id: id, data: b, err: err}
|
|
}
|
|
}
|
|
|
|
func fetchEnv(id int64, cl *docker.Client, cid string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
env, err := cl.InspectEnv(ctx, cid)
|
|
return envMsg{id: id, env: env, err: err}
|
|
}
|
|
}
|
|
|
|
func fetchProcs(id int64, cl *docker.Client, cid string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
titles, rows, err := cl.TopProcesses(ctx, cid)
|
|
return procsMsg{id: id, titles: titles, rows: rows, err: err}
|
|
}
|
|
}
|
|
|
|
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}
|
|
}
|
|
}
|