all repos — searchix @ eeac61c274b3ee615aedf3318c97715d70915a72

Search engine for NixOS, nix-darwin, home-manager and NUR users

cmd/searchix-web/serve.go (view raw)

package main

import (
	"context"
	"errors"
	"os"
	"os/signal"
	"sync"
	"syscall"

	"github.com/Southclaws/fault"
	"github.com/Southclaws/fault/fmsg"
	"golang.org/x/term"

	"alin.ovh/searchix/internal/file"
	"alin.ovh/searchix/internal/importer"
	"alin.ovh/searchix/internal/index"
	"alin.ovh/searchix/internal/manpages"
	"alin.ovh/searchix/internal/server"
	"alin.ovh/searchix/internal/storage"
	"alin.ovh/searchix/web"
)

type ServeOptions struct{}

func (opts *ServeOptions) Execute(_ []string) (err error) {
	signals := []os.Signal{os.Interrupt, syscall.SIGTERM}
	if term.IsTerminal(int(os.Stdout.Fd())) {
		signals = append(signals, syscall.SIGHUP)
	}

	ctx, cancel := signal.NotifyContext(context.Background(), signals...)
	defer cancel()

	root, err := file.CreateAndOpenRoot(cfg.DataPath)
	if err != nil {
		return fault.Wrap(err, fmsg.With("Failed to open data root"))
	}
	defer root.Close()

	store, err := storage.New(&storage.Options{
		LowMemory: cfg.Importer.LowMemory,
		Root:      root,
		Logger:    logger.Named("store"),
	})
	if err != nil {
		return fault.Wrap(err, fmsg.With("Failed to create store"))
	}
	defer store.Close()

	read, write, err := index.OpenOrCreate(
		&index.Options{
			Config:    cfg,
			LowMemory: cfg.Importer.LowMemory,
			BatchSize: cfg.Importer.BatchSize,
			Logger:    logger.Named("index"),
			Root:      root,
			Store:     store,
		},
	)
	if err != nil {
		return fault.Wrap(err, fmsg.With("Failed to open or create index"))
	}

	mdb := manpages.New(&manpages.Options{
		Logger: logger.Named("manpages"),
		Root:   root,
	})

	s, err := web.New(cfg, logger, &server.Options{
		ReadIndex:      read,
		ManpagesURLMap: mdb,
		Store:          store,
	})
	if err != nil {
		return fault.Wrap(err, fmsg.With("Failed to initialise searchix-web"))
	}

	imp, err := importer.New(cfg, &importer.Options{
		Storage:    store,
		WriteIndex: write,
		LowMemory:  cfg.Importer.LowMemory,
		Logger:     logger.Named("importer"),
		Manpages:   mdb,
		Root:       root,
	})
	if err != nil {
		return fault.Wrap(err, fmsg.With("Failed to create importer"))
	}

	if store.IsNew() {
		err = imp.Fetch(ctx, true, false, nil)
		if err != nil {
			return fault.Wrap(err, fmsg.With("Failed to start importer"))
		}
	}

	if store.IsNew() || !write.Exists() {
		err = imp.Index(ctx)
		if err != nil {
			return fault.Wrap(err, fmsg.With("Failed to index data"))
		}
	}

	err = imp.EnsureSourcesIndexed(ctx, read)
	if err != nil {
		return fault.Wrap(err, fmsg.With("Failed to ensure sources indexed"))
	}

	wg := &sync.WaitGroup{}
	wg.Add(1)
	go func() {
		sCtx, cancel := context.WithCancel(ctx)
		defer cancel()
		defer wg.Done()
		err := s.Start(sCtx, globalOptions.Dev)
		if err != nil && !errors.Is(err, context.Canceled) {
			// Error starting or closing listener:
			logger.Fatal("error", "error", err)
		}
	}()

	reimport := make(chan os.Signal, 1)
	go func() {
		for sig := range reimport {
			if sig == syscall.SIGUSR1 {
				logger.Info("manual fetch on SIGUSR1")
				err := imp.Fetch(ctx, true, false, nil)
				if err != nil {
					logger.Warn("manual fetch error", "error", err)
				}
				logger.Info("manual fetch succeeded")
			}

			logger.Info("manual re-index", "signal", sig.String())
			err := imp.Index(ctx)
			if err != nil {
				logger.Error("manual index error", "error", err)
			}
			logger.Info("manual re-index completed")

			if sig == syscall.SIGUSR1 {
				logger.Info("manual prune")
				err = imp.Prune(ctx)
				if err != nil {
					logger.Error("manual prune error", "error", err)
				}
				logger.Info("manual prune completed")
			}
		}
	}()
	signal.Notify(reimport, syscall.SIGUSR1, syscall.SIGUSR2)

	wg.Add(1)
	go func() {
		defer wg.Done()
		imp.StartUpdateTimer(ctx)
	}()

	<-ctx.Done()
	s.Stop()
	wg.Wait()

	return
}

func init() {
	var opts ServeOptions

	cmd, err := parser.AddCommand("serve", "run server", "Serve web interface", &opts)
	if err != nil {
		panic(err)
	}

	cmd.Aliases = []string{"run"}
}