2023-03-14 00:39:15 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"os"
|
2023-12-16 12:39:15 +01:00
|
|
|
"os/exec"
|
2023-03-14 00:39:15 +01:00
|
|
|
"path/filepath"
|
|
|
|
"somegit.dev/ALHP/ALHP.GO/ent"
|
|
|
|
"somegit.dev/ALHP/ALHP.GO/ent/dbpackage"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
func housekeeping(repo, march string, wg *sync.WaitGroup) error {
|
|
|
|
defer wg.Done()
|
|
|
|
fullRepo := repo + "-" + march
|
2023-05-21 20:28:23 +02:00
|
|
|
log.Debugf("[%s] start housekeeping", fullRepo)
|
2023-03-14 00:39:15 +01:00
|
|
|
packages, err := Glob(filepath.Join(conf.Basedir.Repo, fullRepo, "/**/*.pkg.tar.zst"))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("[HK/%s] removing orphans, signature check", fullRepo)
|
|
|
|
for _, path := range packages {
|
|
|
|
mPackage := Package(path)
|
|
|
|
|
|
|
|
dbPkg, err := mPackage.DBPackage(db)
|
|
|
|
if ent.IsNotFound(err) {
|
2023-05-15 15:56:34 +02:00
|
|
|
log.Infof("[HK] removing orphan %s->%s", fullRepo, filepath.Base(path))
|
2023-03-14 00:39:15 +01:00
|
|
|
pkg := &ProtoPackage{
|
2023-05-21 20:28:23 +02:00
|
|
|
FullRepo: *mPackage.FullRepo(),
|
2023-03-14 00:39:15 +01:00
|
|
|
PkgFiles: []string{path},
|
2023-05-21 20:28:23 +02:00
|
|
|
March: *mPackage.MArch(),
|
2023-03-14 00:39:15 +01:00
|
|
|
}
|
|
|
|
buildManager.repoPurge[pkg.FullRepo] <- []*ProtoPackage{pkg}
|
|
|
|
continue
|
|
|
|
} else if err != nil {
|
2023-06-21 12:54:45 +02:00
|
|
|
log.Warningf("[HK] error fetching %s->%q from db: %v", fullRepo, path, err)
|
2023-03-14 00:39:15 +01:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
pkg := &ProtoPackage{
|
|
|
|
Pkgbase: dbPkg.Pkgbase,
|
|
|
|
Repo: mPackage.Repo(),
|
2023-05-21 20:28:23 +02:00
|
|
|
FullRepo: *mPackage.FullRepo(),
|
2023-03-14 00:39:15 +01:00
|
|
|
DBPackage: dbPkg,
|
2023-05-21 20:28:23 +02:00
|
|
|
March: *mPackage.MArch(),
|
|
|
|
Arch: *mPackage.Arch(),
|
2023-03-14 00:39:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// check if package is still part of repo
|
|
|
|
dbs, err := alpmHandle.SyncDBs()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
buildManager.alpmMutex.Lock()
|
|
|
|
pkgResolved, err := dbs.FindSatisfier(mPackage.Name())
|
|
|
|
buildManager.alpmMutex.Unlock()
|
2023-12-13 03:23:56 +01:00
|
|
|
if err != nil ||
|
|
|
|
pkgResolved.DB().Name() != pkg.DBPackage.Repository.String() ||
|
|
|
|
pkgResolved.DB().Name() != pkg.Repo.String() ||
|
|
|
|
pkgResolved.Architecture() != pkg.Arch ||
|
|
|
|
pkgResolved.Name() != mPackage.Name() ||
|
2025-01-26 20:14:16 +01:00
|
|
|
MatchGlobList(pkg.Pkgbase, conf.Blacklist.Packages) {
|
2023-12-13 02:26:03 +01:00
|
|
|
switch {
|
2023-12-14 19:03:55 +01:00
|
|
|
case err != nil:
|
|
|
|
log.Infof("[HK] %s->%s not included in repo (resolve error: %v)", pkg.FullRepo, mPackage.Name(), err)
|
2023-12-13 02:26:03 +01:00
|
|
|
case pkgResolved.DB().Name() != pkg.DBPackage.Repository.String():
|
2023-12-14 19:03:55 +01:00
|
|
|
log.Infof("[HK] %s->%s not included in repo (repo mismatch: repo:%s != db:%s)", pkg.FullRepo,
|
|
|
|
mPackage.Name(), pkgResolved.DB().Name(), pkg.DBPackage.Repository.String())
|
2023-12-13 02:26:03 +01:00
|
|
|
case pkgResolved.DB().Name() != pkg.Repo.String():
|
2023-12-14 19:03:55 +01:00
|
|
|
log.Infof("[HK] %s->%s not included in repo (repo mismatch: repo:%s != pkg:%s)", pkg.FullRepo,
|
|
|
|
mPackage.Name(), pkgResolved.DB().Name(), pkg.Repo.String())
|
2023-12-13 02:26:03 +01:00
|
|
|
case pkgResolved.Architecture() != pkg.Arch:
|
2023-12-14 19:03:55 +01:00
|
|
|
log.Infof("[HK] %s->%s not included in repo (arch mismatch: repo:%s != pkg:%s)", pkg.FullRepo,
|
|
|
|
mPackage.Name(), pkgResolved.Architecture(), pkg.Arch)
|
2023-12-13 02:26:03 +01:00
|
|
|
case pkgResolved.Name() != mPackage.Name():
|
2023-12-14 19:03:55 +01:00
|
|
|
log.Infof("[HK] %s->%s not included in repo (name mismatch: repo:%s != pkg:%s)", pkg.FullRepo,
|
|
|
|
mPackage.Name(), pkgResolved.Name(), mPackage.Name())
|
2025-01-26 20:14:16 +01:00
|
|
|
case MatchGlobList(pkg.Pkgbase, conf.Blacklist.Packages):
|
2023-12-13 03:23:56 +01:00
|
|
|
log.Infof("[HK] %s->%s not included in repo (blacklisted pkgbase %s)", pkg.FullRepo, mPackage.Name(), pkg.Pkgbase)
|
2023-12-13 02:26:03 +01:00
|
|
|
}
|
|
|
|
|
2023-03-14 00:39:15 +01:00
|
|
|
// package not found on mirror/db -> not part of any repo anymore
|
2023-09-23 12:52:51 +02:00
|
|
|
err = pkg.findPkgFiles()
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("[HK] %s->%s unable to get pkg-files: %v", pkg.FullRepo, mPackage.Name(), err)
|
|
|
|
continue
|
|
|
|
}
|
2023-05-21 20:28:23 +02:00
|
|
|
err = db.DBPackage.DeleteOne(pkg.DBPackage).Exec(context.Background())
|
2023-09-23 12:52:51 +02:00
|
|
|
pkg.DBPackage = nil
|
|
|
|
buildManager.repoPurge[pkg.FullRepo] <- []*ProtoPackage{pkg}
|
2023-03-14 00:39:15 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if pkg.DBPackage.LastVerified.Before(pkg.DBPackage.BuildTimeStart) {
|
|
|
|
err := pkg.DBPackage.Update().SetLastVerified(time.Now().UTC()).Exec(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// check if pkg signature is valid
|
|
|
|
valid, err := mPackage.HasValidSignature()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !valid {
|
2023-05-15 15:56:34 +02:00
|
|
|
log.Infof("[HK] %s->%s invalid package signature", pkg.FullRepo, pkg.Pkgbase)
|
2023-03-14 00:39:15 +01:00
|
|
|
buildManager.repoPurge[pkg.FullRepo] <- []*ProtoPackage{pkg}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// compare db-version with repo version
|
|
|
|
repoVer, err := pkg.repoVersion()
|
|
|
|
if err == nil && repoVer != dbPkg.RepoVersion {
|
2023-05-15 15:56:34 +02:00
|
|
|
log.Infof("[HK] %s->%s update repoVersion %s->%s", pkg.FullRepo, pkg.Pkgbase, dbPkg.RepoVersion, repoVer)
|
2023-05-21 20:28:23 +02:00
|
|
|
pkg.DBPackage, err = pkg.DBPackage.Update().SetRepoVersion(repoVer).ClearTagRev().Save(context.Background())
|
2023-03-14 00:39:15 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// check all packages from db for existence
|
2023-05-21 20:28:23 +02:00
|
|
|
dbPackages, err := db.DBPackage.Query().Where(
|
2023-03-14 00:39:15 +01:00
|
|
|
dbpackage.And(
|
|
|
|
dbpackage.RepositoryEQ(dbpackage.Repository(repo)),
|
|
|
|
dbpackage.March(march),
|
|
|
|
)).All(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-05-26 12:59:59 +02:00
|
|
|
log.Debugf("[HK/%s] checking %d packages from database", fullRepo, len(dbPackages))
|
2023-03-14 00:39:15 +01:00
|
|
|
|
|
|
|
for _, dbPkg := range dbPackages {
|
|
|
|
pkg := &ProtoPackage{
|
|
|
|
Pkgbase: dbPkg.Pkgbase,
|
|
|
|
Repo: dbPkg.Repository,
|
|
|
|
March: dbPkg.March,
|
|
|
|
FullRepo: dbPkg.Repository.String() + "-" + dbPkg.March,
|
|
|
|
DBPackage: dbPkg,
|
|
|
|
}
|
|
|
|
|
|
|
|
if !pkg.isAvailable(alpmHandle) {
|
2023-05-15 15:56:34 +02:00
|
|
|
log.Infof("[HK] %s->%s not found on mirror, removing", pkg.FullRepo, pkg.Pkgbase)
|
2023-05-21 20:28:23 +02:00
|
|
|
err = db.DBPackage.DeleteOne(dbPkg).Exec(context.Background())
|
2023-03-14 00:39:15 +01:00
|
|
|
if err != nil {
|
2023-05-21 20:28:23 +02:00
|
|
|
log.Errorf("[HK] error deleting package %s->%s: %v", pkg.FullRepo, dbPkg.Pkgbase, err)
|
2023-03-14 00:39:15 +01:00
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case dbPkg.Status == dbpackage.StatusLatest && dbPkg.RepoVersion != "":
|
2023-05-13 18:16:17 +02:00
|
|
|
// check lastVersionBuild
|
|
|
|
if dbPkg.LastVersionBuild != dbPkg.RepoVersion {
|
2023-05-15 15:56:34 +02:00
|
|
|
log.Infof("[HK] %s->%s updating lastVersionBuild %s -> %s", fullRepo, dbPkg.Pkgbase, dbPkg.LastVersionBuild, dbPkg.RepoVersion)
|
2023-05-13 18:17:46 +02:00
|
|
|
dbPkg, err = dbPkg.Update().SetLastVersionBuild(dbPkg.RepoVersion).Save(context.Background())
|
2023-05-13 18:16:17 +02:00
|
|
|
if err != nil {
|
2023-05-15 15:56:34 +02:00
|
|
|
log.Warningf("[HK] error updating lastVersionBuild for %s->%s: %v", fullRepo, dbPkg.Pkgbase, err)
|
2023-05-13 18:16:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-14 00:39:15 +01:00
|
|
|
var existingSplits []string
|
|
|
|
var missingSplits []string
|
|
|
|
for _, splitPkg := range dbPkg.Packages {
|
|
|
|
pkgFile := filepath.Join(conf.Basedir.Repo, fullRepo, "os", conf.Arch,
|
|
|
|
splitPkg+"-"+dbPkg.RepoVersion+"-"+conf.Arch+".pkg.tar.zst")
|
|
|
|
_, err = os.Stat(pkgFile)
|
|
|
|
switch {
|
|
|
|
case os.IsNotExist(err):
|
|
|
|
missingSplits = append(missingSplits, splitPkg)
|
|
|
|
case err != nil:
|
|
|
|
log.Warningf("[HK] error reading package-file %s: %v", splitPkg, err)
|
|
|
|
default:
|
|
|
|
existingSplits = append(existingSplits, pkgFile)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(missingSplits) > 0 {
|
2023-05-15 15:56:34 +02:00
|
|
|
log.Infof("[HK] %s->%s missing split-package(s): %s", fullRepo, dbPkg.Pkgbase, missingSplits)
|
2023-06-05 15:33:02 +02:00
|
|
|
pkg.DBPackage, err = pkg.DBPackage.Update().
|
|
|
|
ClearRepoVersion().
|
|
|
|
ClearTagRev().
|
|
|
|
SetStatus(dbpackage.StatusQueued).
|
|
|
|
Save(context.Background())
|
2023-03-14 00:39:15 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
pkg := &ProtoPackage{
|
|
|
|
FullRepo: fullRepo,
|
|
|
|
PkgFiles: existingSplits,
|
|
|
|
March: march,
|
|
|
|
DBPackage: dbPkg,
|
|
|
|
}
|
|
|
|
buildManager.repoPurge[fullRepo] <- []*ProtoPackage{pkg}
|
|
|
|
}
|
2023-12-20 09:16:51 +01:00
|
|
|
|
|
|
|
rawState, err := os.ReadFile(filepath.Join(conf.Basedir.Work, stateDir, dbPkg.Repository.String()+"-"+conf.Arch, dbPkg.Pkgbase))
|
|
|
|
if err != nil {
|
2024-03-10 13:15:18 +01:00
|
|
|
log.Infof("[HK] state not found for %s->%s: %v, removing package", fullRepo, dbPkg.Pkgbase, err)
|
|
|
|
pkg := &ProtoPackage{
|
|
|
|
FullRepo: fullRepo,
|
|
|
|
PkgFiles: existingSplits,
|
|
|
|
March: march,
|
|
|
|
DBPackage: dbPkg,
|
|
|
|
}
|
|
|
|
buildManager.repoPurge[fullRepo] <- []*ProtoPackage{pkg}
|
2023-12-20 09:16:51 +01:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
state, err := parseState(string(rawState))
|
|
|
|
if err != nil {
|
|
|
|
log.Warningf("[HK] error parsing state file for %s->%s: %v", fullRepo, dbPkg.Pkgbase, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if dbPkg.TagRev != nil && state.TagRev == *dbPkg.TagRev && state.PkgVer != dbPkg.Version {
|
|
|
|
log.Infof("[HK] reseting package %s->%s with mismatched state information (%s!=%s)",
|
|
|
|
fullRepo, dbPkg.Pkgbase, state.PkgVer, dbPkg.Version)
|
|
|
|
err = dbPkg.Update().SetStatus(dbpackage.StatusQueued).ClearTagRev().Exec(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2023-03-14 00:39:15 +01:00
|
|
|
case dbPkg.Status == dbpackage.StatusLatest && dbPkg.RepoVersion == "":
|
2023-05-15 15:56:34 +02:00
|
|
|
log.Infof("[HK] reseting missing package %s->%s with no repo version", fullRepo, dbPkg.Pkgbase)
|
2023-05-21 20:28:23 +02:00
|
|
|
err = dbPkg.Update().SetStatus(dbpackage.StatusQueued).ClearTagRev().ClearRepoVersion().Exec(context.Background())
|
2023-03-14 00:39:15 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-09-01 15:12:10 +02:00
|
|
|
case dbPkg.Status == dbpackage.StatusSkipped && dbPkg.RepoVersion != "" && !strings.HasPrefix(dbPkg.SkipReason, "delayed"):
|
|
|
|
log.Infof("[HK] delete skipped package %s->%s", fullRepo, dbPkg.Pkgbase)
|
2023-03-14 00:39:15 +01:00
|
|
|
pkg := &ProtoPackage{
|
|
|
|
FullRepo: fullRepo,
|
|
|
|
March: march,
|
|
|
|
DBPackage: dbPkg,
|
|
|
|
}
|
|
|
|
buildManager.repoPurge[fullRepo] <- []*ProtoPackage{pkg}
|
2025-01-26 20:14:16 +01:00
|
|
|
case dbPkg.Status == dbpackage.StatusSkipped && dbPkg.SkipReason == "blacklisted" && !MatchGlobList(pkg.Pkgbase, conf.Blacklist.Packages):
|
2025-01-26 13:33:28 +01:00
|
|
|
log.Infof("[HK] requeue previously blacklisted package %s->%s", fullRepo, dbPkg.Pkgbase)
|
|
|
|
err = dbPkg.Update().SetStatus(dbpackage.StatusQueued).ClearSkipReason().ClearTagRev().Exec(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-09-04 18:26:17 +02:00
|
|
|
case dbPkg.Status == dbpackage.StatusFailed && dbPkg.RepoVersion != "":
|
|
|
|
log.Infof("[HK] package %s->%s failed but still present in repo, removing", fullRepo, dbPkg.Pkgbase)
|
|
|
|
pkg := &ProtoPackage{
|
|
|
|
FullRepo: fullRepo,
|
|
|
|
March: march,
|
|
|
|
DBPackage: dbPkg,
|
|
|
|
}
|
|
|
|
buildManager.repoPurge[fullRepo] <- []*ProtoPackage{pkg}
|
2023-03-14 00:39:15 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("[HK/%s] all tasks finished", fullRepo)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func logHK() error {
|
|
|
|
// check if package for log exists and if error can be fixed by rebuild
|
|
|
|
logFiles, err := Glob(filepath.Join(conf.Basedir.Repo, logDir, "/**/*.log"))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, logFile := range logFiles {
|
|
|
|
pathSplit := strings.Split(logFile, string(filepath.Separator))
|
|
|
|
extSplit := strings.Split(filepath.Base(logFile), ".")
|
|
|
|
pkgbase := strings.Join(extSplit[:len(extSplit)-1], ".")
|
|
|
|
march := pathSplit[len(pathSplit)-2]
|
|
|
|
|
|
|
|
pkg := ProtoPackage{
|
|
|
|
Pkgbase: pkgbase,
|
|
|
|
March: march,
|
|
|
|
}
|
|
|
|
|
|
|
|
if exists, err := pkg.exists(); err != nil {
|
|
|
|
return err
|
|
|
|
} else if !exists {
|
|
|
|
_ = os.Remove(logFile)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-05-21 20:28:23 +02:00
|
|
|
pkgSkipped, err := db.DBPackage.Query().Where(
|
2023-03-14 00:39:15 +01:00
|
|
|
dbpackage.Pkgbase(pkg.Pkgbase),
|
|
|
|
dbpackage.March(pkg.March),
|
|
|
|
dbpackage.StatusEQ(dbpackage.StatusSkipped),
|
|
|
|
).Exist(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if pkgSkipped {
|
|
|
|
_ = os.Remove(logFile)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
logContent, err := os.ReadFile(logFile)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
sLogContent := string(logContent)
|
|
|
|
|
2023-05-28 13:08:01 +02:00
|
|
|
if rePortError.MatchString(sLogContent) || reSigError.MatchString(sLogContent) || reDownloadError.MatchString(sLogContent) ||
|
|
|
|
reDownloadError2.MatchString(sLogContent) {
|
|
|
|
rows, err := db.DBPackage.Update().Where(dbpackage.Pkgbase(pkg.Pkgbase), dbpackage.March(pkg.March),
|
|
|
|
dbpackage.StatusEQ(dbpackage.StatusFailed)).ClearTagRev().SetStatus(dbpackage.StatusQueued).Save(context.Background())
|
2023-03-14 00:39:15 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if rows > 0 {
|
|
|
|
log.Infof("[HK/%s/%s] fixable build-error detected, requeueing package (%d)", pkg.March, pkg.Pkgbase, rows)
|
|
|
|
}
|
|
|
|
} else if reLdError.MatchString(sLogContent) || reRustLTOError.MatchString(sLogContent) {
|
2023-05-21 20:28:23 +02:00
|
|
|
rows, err := db.DBPackage.Update().Where(
|
2023-03-14 00:39:15 +01:00
|
|
|
dbpackage.Pkgbase(pkg.Pkgbase),
|
|
|
|
dbpackage.March(pkg.March),
|
|
|
|
dbpackage.StatusEQ(dbpackage.StatusFailed),
|
|
|
|
dbpackage.LtoNotIn(dbpackage.LtoAutoDisabled, dbpackage.LtoDisabled),
|
2023-05-21 20:28:23 +02:00
|
|
|
).ClearTagRev().SetStatus(dbpackage.StatusQueued).SetLto(dbpackage.LtoAutoDisabled).Save(context.Background())
|
2023-03-14 00:39:15 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if rows > 0 {
|
|
|
|
log.Infof("[HK/%s/%s] fixable build-error detected (linker-error), requeueing package (%d)", pkg.March, pkg.Pkgbase, rows)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2023-12-16 12:39:15 +01:00
|
|
|
|
|
|
|
func debugHK() {
|
|
|
|
for _, march := range conf.March {
|
|
|
|
if _, err := os.Stat(filepath.Join(conf.Basedir.Debug, march)); err == nil {
|
|
|
|
log.Debugf("[DHK/%s] start cleanup debug packages", march)
|
2023-12-17 21:02:44 +01:00
|
|
|
cleanCmd := exec.Command("paccache", "-rc", filepath.Join(conf.Basedir.Debug, march), "-k", "1") //nolint:gosec
|
2023-12-16 12:39:15 +01:00
|
|
|
res, err := cleanCmd.CombinedOutput()
|
|
|
|
if err != nil {
|
|
|
|
log.Warningf("[DHK/%s] cleanup debug packages failed: %v (%s)", march, err, string(res))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|