package pipelines
import (
"html/template"
"path"
"regexp"
"strings"
terminal "github.com/buildkite/terminal-to-html/v3"
"github.com/gorilla/websocket"
"tangled.org/core/appview/pages/markup/sanitizer"
"tangled.org/core/hostutil"
)
// matches any ANSI escape sequence: ESC [ m
var sequenceRe = regexp.MustCompile(`\x1b\[([\d;]*)m`)
// ansiState tracks the active stack across log lines
// each non-reset SGR code is pushed onto the stack; a reset clears it.
//
// the stack contents are prepended to each new line so colours carry over.
type ansiState struct {
stack []string
}
func NewAnsiState() *ansiState {
return &ansiState{
stack: []string{},
}
}
func (a *ansiState) Render(line string) template.HTML {
// prepend whatever sequences are still open from the previous line
prefix := strings.Join(a.stack, "")
// render current line with the existing prefix
rendered := terminal.Render([]byte(prefix + line))
// sanitize
sanitized := sanitizer.SanitizeLogs(rendered)
// update the stack with sequences from current line
for _, m := range sequenceRe.FindAllStringSubmatch(line, -1) {
params := m[1]
if params == "" || params == "0" || params == "00" {
a.stack = a.stack[:0]
} else {
a.stack = append(a.stack, m[0])
}
}
return template.HTML(sanitized)
}
type LogEvent struct {
Msg []byte
Err error
}
func (ev *LogEvent) IsCloseError() bool {
return websocket.IsCloseError(
ev.Err,
websocket.CloseNormalClosure,
websocket.CloseGoingAway,
websocket.CloseAbnormalClosure,
)
}
func ReadLogs(conn *websocket.Conn, ch chan LogEvent) {
defer close(ch)
for {
if conn == nil {
return
}
_, msg, err := conn.ReadMessage()
if err != nil {
ch <- LogEvent{Err: err}
return
}
ch <- LogEvent{Msg: msg}
}
}
func SpindleURL(spindle, knot, rkey, workflow string) string {
url, err := hostutil.EnsureWsScheme(spindle)
if err != nil {
return ""
}
return url + path.Join("/logs", knot, rkey, workflow)
}