package buildqueue import ( "brunel/config" "brunel/domain" "brunel/packages" "context" "fmt" "log/slog" "net/http" "strings" "time" "golang.org/x/net/html" ) func StartPackageQueueWorker(ctx context.Context) { go func() { for { select { case <-ctx.Done(): return default: err := packages.ProcessPackages() if err != nil { slog.Error("unable to process packages: " + err.Error()) } packs := packages.GetPackages() packs.Iter(func(k string, v domain.SourcePackage) bool { needsBuild := false buildVersion := "" buildAttempt := 0 errPreviously := false v.Packages.Iter(func(k string, v domain.PackageInfo) bool { if v.Status == domain.Current { return true } if v.LastBuildStatus == domain.Error { errPreviously = true buildAttempt = 1 } if v.Status == domain.Missing { needsBuild = true buildVersion = v.NewVersion return false } if v.Status == domain.Stale { needsBuild = true buildVersion = v.NewVersion return false } return true }) if needsBuild { typ := domain.BuildTypeNormal if errPreviously { typ = domain.BuildTypeLTO } buildItem := domain.BuildQueueItem{ Source: v, Status: domain.Queued, Type: typ, Patch: false, Rebuild: false, BuildNumber: buildAttempt, BuildVersion: buildVersion, } err := Add(buildItem) if err != nil { slog.Info("unable to add package to queue: " + err.Error()) } } return true }) time.Sleep(1 * time.Hour) } } }() } func StartQueueWorker(ctx context.Context) { go processQueue(ctx) } func StartStatusWorker(ctx context.Context) { go processStatus(ctx) } func processStatus(ctx context.Context) { for { select { case <-ctx.Done(): return default: q := GetBuildingQueue() itemsToRemove := make([]string, 0) q.Iter(func(k string, item domain.BuildQueueItem) bool { complete, err := CheckIfBuildComplete(ctx, item) if err != nil && !complete { slog.Error("unable to check if build is complete: " + err.Error()) } if complete { if err != nil { item.Source.Packages.Iter(func(k string, v domain.PackageInfo) bool { v.Status = domain.Error v.LastBuildStatus = domain.Error item.Source.Packages.Set(k, v) return true }) packages.UpdateSourcePackage(item.Source) itemsToRemove = append(itemsToRemove, k) return true } item.Source.Packages.Iter(func(k string, v domain.PackageInfo) bool { v.Status = domain.Built v.LastBuildStatus = domain.Built v.Version = item.BuildVersion v.NewVersion = "" item.Source.Packages.Set(k, v) return true }) packages.UpdateSourcePackage(item.Source) itemsToRemove = append(itemsToRemove, k) return true } return true }) for _, item := range itemsToRemove { Remove(item) } time.Sleep(10 * time.Second) } } } func processQueue(ctx context.Context) { for { select { case <-ctx.Done(): return default: err := ProcessNext() if err != nil { slog.Error("unable to process queue: " + err.Error()) } } time.Sleep(30 * time.Second) } } func CheckIfBuildComplete(ctx context.Context, item domain.BuildQueueItem) (bool, error) { resp, err := http.Get(config.Configs.ActionsUrl) if err != nil { return false, err } defer resp.Body.Close() doc, err := html.Parse(resp.Body) if err != nil { return false, err } buildName := item.Source.Name + "=" + item.BuildVersion var f func(*html.Node) (bool, error) f = func(n *html.Node) (bool, error) { if n.Type == html.ElementNode && n.Data == "div" { for _, a := range n.Attr { if a.Key == "class" && strings.Contains(a.Val, "flex-item") { isMatch, status := checkBuildBlock(n, buildName) if isMatch { switch status { case "Success": return true, nil case "Failure": return true, fmt.Errorf("build failed") default: // Build found but status unknown, keep searching return false, nil } } } } } for c := n.FirstChild; c != nil; c = c.NextSibling { isComplete, err := f(c) if isComplete || err != nil { return isComplete, err } } return false, nil } return f(doc) } func checkBuildBlock(n *html.Node, buildName string) (bool, string) { var title string var status string var f func(*html.Node) f = func(n *html.Node) { if n.Type == html.ElementNode { switch n.Data { case "a": for _, a := range n.Attr { if a.Key == "class" && a.Val == "flex-item-title" { for _, attr := range n.Attr { if attr.Key == "title" { title = attr.Val } } } } case "span": for _, a := range n.Attr { if a.Key == "data-tooltip-content" { if a.Val == "Success" || a.Val == "Failure" { status = a.Val } } } } } for c := n.FirstChild; c != nil; c = c.NextSibling { f(c) } } f(n) return title == buildName, status }