217 lines
6.1 KiB
Go
217 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/lixenwraith/logwisp/internal/authz"
|
|
"github.com/lixenwraith/logwisp/internal/compose"
|
|
"github.com/lixenwraith/logwisp/internal/config"
|
|
"github.com/lixenwraith/logwisp/internal/core"
|
|
"github.com/lixenwraith/logwisp/internal/pipeline"
|
|
"github.com/lixenwraith/logwisp/internal/service"
|
|
"github.com/lixenwraith/logwisp/internal/sink/console"
|
|
"github.com/lixenwraith/logwisp/internal/tui"
|
|
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// The records a tap holds for the screen between ticks: a run's, lw's log's
|
|
const (
|
|
runTapRecords = 1024
|
|
logTapRecords = 256
|
|
)
|
|
|
|
// screenRunner runs lw --tui's pipelines while the screen owns the terminal.
|
|
// Start loads them as r would, through the file, environment and overrides,
|
|
// and replaces what runs as a reload does.
|
|
type screenRunner struct {
|
|
load config.Args
|
|
mu sync.Mutex // svc, tap and log, which State reads from another goroutine
|
|
svc *service.Service
|
|
tap *core.Tap
|
|
log *core.Tap
|
|
}
|
|
|
|
func (r *screenRunner) Start(specs []config.Spec, ask tui.Ask) (*core.Tap, error) {
|
|
cfg, err := r.config(specs)
|
|
if err == nil {
|
|
err = r.startLogger(cfg)
|
|
}
|
|
if err != nil {
|
|
return r.tap, err
|
|
}
|
|
authz.SetAsker(ask)
|
|
tap := core.NewTap(runTapRecords)
|
|
tap.All.Store(true) // until the view's first draw says what its point shows
|
|
if r.svc == nil {
|
|
svc, err := bootstrapService(context.Background(), cfg, tap)
|
|
if err != nil {
|
|
return nil, placed(cfg, err)
|
|
}
|
|
if err := svc.Start(); err != nil {
|
|
svc.Shutdown()
|
|
return nil, placed(cfg, err)
|
|
}
|
|
r.set(svc, tap)
|
|
return tap, nil
|
|
}
|
|
svc, _, _, err := handleReload(context.Background(), cfg, r.svc, nil, tap)
|
|
switch {
|
|
case errors.Is(err, errServiceStopped):
|
|
r.set(nil, nil)
|
|
return nil, placed(cfg, err)
|
|
case err != nil:
|
|
return r.tap, placed(cfg, err)
|
|
}
|
|
r.set(svc, tap)
|
|
return tap, nil
|
|
}
|
|
|
|
// placed addresses a start error to the part it names, by the path the
|
|
// screen places a configuration problem by
|
|
func placed(cfg *config.Config, err error) error {
|
|
part, ok := errors.AsType[*pipeline.PartError](err)
|
|
if !ok {
|
|
return err
|
|
}
|
|
pi := slices.IndexFunc(cfg.Pipelines, func(p config.PipelineConfig) bool { return p.Name == part.Pipeline })
|
|
if pi < 0 {
|
|
return err
|
|
}
|
|
return config.At(compose.Node{Role: part.Role, ID: part.ID}.Path(pi), err)
|
|
}
|
|
|
|
func (r *screenRunner) set(svc *service.Service, tap *core.Tap) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.svc, r.tap = svc, tap
|
|
}
|
|
|
|
func (r *screenRunner) Stop() {
|
|
r.mu.Lock()
|
|
svc := r.svc
|
|
r.svc, r.tap = nil, nil
|
|
r.mu.Unlock()
|
|
if svc != nil {
|
|
svc.Shutdown()
|
|
}
|
|
}
|
|
|
|
func (r *screenRunner) State() tui.RunState {
|
|
r.mu.Lock()
|
|
svc, st := r.svc, tui.RunState{Log: r.log}
|
|
r.mu.Unlock()
|
|
if svc == nil {
|
|
return st
|
|
}
|
|
select {
|
|
case <-svc.Done():
|
|
st.Ended = true
|
|
default:
|
|
}
|
|
st.Counts = map[string]tui.Counts{}
|
|
pipelines, _ := svc.GetGlobalStats()["pipelines"].(map[string]any)
|
|
for name, v := range pipelines {
|
|
p, _ := v.(map[string]any)
|
|
flow, _ := p["flow"].(map[string]any)
|
|
var c tui.Counts
|
|
c.In, _ = flow["total_processed"].(uint64)
|
|
c.Filtered, _ = flow["total_dropped"].(uint64)
|
|
c.SinksDropped, _ = p["total_dropped_by_sink"].(uint64)
|
|
st.Counts[name] = c
|
|
}
|
|
return st
|
|
}
|
|
|
|
// close ends view mode before lw runs on or exits: nothing runs, lw's log is
|
|
// shut down, and terminals answer passwords again
|
|
func (r *screenRunner) close() {
|
|
r.Stop()
|
|
authz.SetAsker(nil)
|
|
shutdownLogger()
|
|
logger = nil
|
|
}
|
|
|
|
// config is specs as they would run on screen: no status reporter, as the
|
|
// screen shows the counts, and the terminal's streams the screen's
|
|
func (r *screenRunner) config(specs []config.Spec) (*config.Config, error) {
|
|
load := r.load
|
|
load.Specs = specs
|
|
m, err := config.Load(load)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer m.Close()
|
|
cfg, err := m.Snapshot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.StatusReporter = false
|
|
return cfg, onScreen(cfg)
|
|
}
|
|
|
|
// onScreen gives the screen the streams that are the terminal: a console sink
|
|
// writing to it runs as a null sink with its id (the tap shows what it would
|
|
// print; both make the pipeline wait, not drop), and a console source would
|
|
// read the screen's keys
|
|
func onScreen(cfg *config.Config) error {
|
|
for i := range cfg.Pipelines {
|
|
p := &cfg.Pipelines[i]
|
|
for _, s := range p.PluginSources {
|
|
if s.Type == "console" && term.IsTerminal(int(os.Stdin.Fd())) {
|
|
return config.At(compose.Node{Role: "source", ID: s.ID}.Path(i),
|
|
errors.New("console reads the keyboard this screen uses: pipe input in (cmd | lw -T), or run with r"))
|
|
}
|
|
}
|
|
for j, s := range p.PluginSinks {
|
|
target, _ := s.Config["target"].(string)
|
|
if s.Type == "console" && term.IsTerminal(int(console.Stream(target).Fd())) {
|
|
p.PluginSinks[j] = config.PluginSinkConfig{ID: s.ID, Type: "null"}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// startLogger starts lw's log on the first run. While its console is the
|
|
// terminal, its records and the logger's own diagnostics go to the screen's
|
|
// lw stream instead: the one place that routes lw's log on screen.
|
|
func (r *screenRunner) startLogger(cfg *config.Config) error {
|
|
if logger != nil {
|
|
return nil
|
|
}
|
|
if err := initializeLogger(cfg); err != nil {
|
|
logger = nil
|
|
return err
|
|
}
|
|
c := logger.GetConfig()
|
|
streams := map[string][]*os.File{"stdout": {os.Stdout}, "stderr": {os.Stderr}, "split": {os.Stdout, os.Stderr}}[c.ConsoleTarget]
|
|
if c.EnableConsole && slices.ContainsFunc(streams, func(f *os.File) bool { return term.IsTerminal(int(f.Fd())) }) {
|
|
w := lwStream{core.NewTap(logTapRecords)}
|
|
logger.SetConsoleWriter(w)
|
|
logger.SetErrorHandler(func(msg string) { w.Write([]byte(msg)) })
|
|
r.mu.Lock()
|
|
r.log = w.tap
|
|
r.mu.Unlock()
|
|
}
|
|
return logger.Start()
|
|
}
|
|
|
|
// lwStream offers each record lw's log writes as an entry of source "lw"
|
|
type lwStream struct{ tap *core.Tap }
|
|
|
|
func (w lwStream) Write(p []byte) (int, error) {
|
|
now := time.Now()
|
|
line := strings.TrimRight(string(p), "\r\n") // a copy: the logger reuses p
|
|
level, _, _ := core.LevelWord(line, "")
|
|
w.tap.Offer(core.Tapped{Source: "lw", Event: core.TransportEvent{Time: now,
|
|
Entry: core.LogEntry{Time: now, Source: "lw", Level: level, Message: line, RawSize: int64(len(p))}}})
|
|
return len(p), nil
|
|
}
|