package builder import ( "os/exec" "io" "os" "time" "io/fs" "path/filepath" "errors" "github.com/fsnotify/fsnotify" "bufio" "regexp" "strconv" "lcthw.dev/go/ttarpit/config" "lcthw.dev/go/ttarpit/data" . "lcthw.dev/go/ttarpit/debug" ) type ErrorHandler func (data.ErrInfo) const ( START=0 RUNNING=1 FAILED=2 PASSED=3 ) type Builder struct { settings config.Config OnError ErrorHandler BuildState int HadErrors bool } func New(settings config.Config, handler ErrorHandler) (*Builder) { builder := new(Builder) builder.settings = settings builder.OnError = handler return builder } func (build *Builder) ParseErrInfo(line string, reg *regexp.Regexp) (data.ErrInfo, bool) { var info data.ErrInfo matches := reg.FindStringSubmatch(line) if matches == nil { return info, false } et_index := reg.SubexpIndex("ErrType") if et_index != -1 { info.ErrType = matches[et_index] } // BUG: use reflect info.File = matches[reg.SubexpIndex("File")] info.Line, _ = strconv.Atoi(matches[reg.SubexpIndex("Line")]) info.Col, _ = strconv.Atoi(matches[reg.SubexpIndex("Col")]) info.Message = matches[reg.SubexpIndex("Message")] return info, true } func (build *Builder) LaunchLogger(in io.Reader, out io.Writer, err error) { if err != nil { Log.Fatal(err) } go func() { scan := bufio.NewScanner(in) for scan.Scan() { for _, reg := range build.settings.TriggerRegex { line := scan.Text() errinfo, ok := build.ParseErrInfo(line, reg) if ok { build.OnError(errinfo) build.HadErrors = true } } } }() } func (build *Builder) LaunchProcess(proc *config.Process) { proc.ExecCmd = exec.Command(proc.Command, proc.Args...) if errors.Is(proc.ExecCmd.Err, exec.ErrDot) { proc.ExecCmd.Err = nil } Log.Println("STARTING", proc.Command) stderr, err := proc.ExecCmd.StderrPipe(); build.LaunchLogger(stderr, os.Stdout, err) stdout, err := proc.ExecCmd.StdoutPipe(); build.LaunchLogger(stdout, os.Stdout, err) err = proc.ExecCmd.Start() if err != nil { Log.Fatalf("FAIL %s %s err=%v", proc.Command, proc.Args, err) } Log.Println("WAITING for", proc.Command) proc.ExecCmd.Wait() Log.Println("PROCESS", proc.Command, "EXITED") } func (build *Builder) MatchesPath(fp string) bool { fp = filepath.ToSlash(fp) for _, reg := range build.settings.IncludeRegex { matches := reg.MatchString(fp) if matches { return true } } return false } func (build *Builder) BeginBuild() bool { if build.BuildState != RUNNING { build.BuildState = RUNNING build.HadErrors = false return true } else { return false } } func (build *Builder) EndBuild() { if build.HadErrors { build.BuildState = FAILED } else { build.BuildState = PASSED } } func (build *Builder) RunBuild() { if build.BeginBuild() { defer build.EndBuild() Log.Println("CONFIG:", build.settings.ConfigPath) Log.Println("COMMANDs:", build.settings.Processes) for name, proc := range build.settings.Processes { Log.Println("PROCESS:", name) build.LaunchProcess(&proc) } time.Sleep(1000 * time.Millisecond) } else { Log.Println("!!!! BUILD SKIP, already running") } } func (build *Builder) AddWatchDir(watcher *fsnotify.Watcher, name string) error { return filepath.WalkDir(name, func(path string, d fs.DirEntry, err error) error { if err != nil { Log.Printf("WATCH ERROR! walking=%s path=%s: err=%v", name, path, err) return err } if d.IsDir() { Log.Println("WATCHING: ", path) err = watcher.Add(path) if err != nil { Log.Printf("failed to watch %s", path) return err } } return nil }) } func (build *Builder) WatchDir() { watcher, err := fsnotify.NewWatcher() if err != nil { Log.Fatal("Failed to start fsnotify", err) } defer watcher.Close() go func() { for { select { case event, ok := <-watcher.Events: if !ok { return } if event.Has(fsnotify.Create) { Log.Println("---> CREATE", event.Name) build.AddWatchDir(watcher, event.Name) } else if event.Has(fsnotify.Write) { // check if match then do thing if build.MatchesPath(event.Name) { go build.RunBuild() } } else { Log.Println("event:", event) } case err, ok := <-watcher.Errors: if !ok { return } Log.Println("error: ", err) } } }() err = build.AddWatchDir(watcher, ".") if err != nil { Log.Fatal(err, "Failed to watch .") } <-make(chan struct{}) }