~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
103 lines · 3.6 KB Raw History
package bootstrap

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/hostos"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/hostsvc"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kcpserver"
)

type SlotDrift struct {
	Slot        string   `json:"slot"`
	Unit        string   `json:"unit"`
	PID         int      `json:"pid"`
	Running     []string `json:"running,omitempty"`
	Want        []string `json:"want,omitempty"`
	RunningExe  string   `json:"runningExe,omitempty"`
	WantExe     string   `json:"wantExe,omitempty"`
	Refused     string   `json:"refused,omitempty"`
	ArgvChanged bool     `json:"argvChanged"`
	ExeChanged  bool     `json:"exeChanged"`
}

func (d SlotDrift) Drifted() bool { return d.ArgvChanged || d.ExeChanged }

func ActiveSlotDrift(ctx context.Context, m hostsvc.Manager, p Params) (SlotDrift, bool, error) {
	slots, err := kcpserver.ReadSlots(p.SlotsFile())
	if errors.Is(err, os.ErrNotExist) {
		return SlotDrift{}, false, nil
	}
	if err != nil {
		return SlotDrift{}, false, err
	}
	d := SlotDrift{Slot: slots.Active, Unit: p.App.Names().Slot(slots.Active)}
	st, err := m.Inspect(ctx, d.Unit)
	if err != nil || st.State != hostsvc.Active || st.MainPID <= 0 {
		return d, false, err
	}
	d.PID = st.MainPID
	raw, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(d.PID), "cmdline"))
	if err != nil {
		return d, false, err
	}
	d.Running = strings.Split(strings.TrimRight(string(raw), "\x00"), "\x00")
	spec, _, err := p.slotSpec(slots.Active, slots.Front.URL())
	if err != nil {
		return d, false, err
	}
	d.Want = spec.Exec
	d.ArgvChanged = !slices.Equal(d.Running, d.Want)
	if err := kcpserver.CheckArgv(d.Running, slots.Shape); err != nil {
		d.Refused = err.Error()
	}
	d.RunningExe, _ = os.Readlink(filepath.Join("/proc", strconv.Itoa(d.PID), "exe"))
	d.WantExe, _ = filepath.EvalSymlinks(p.KcpPath)
	d.ExeChanged = d.RunningExe != "" && d.WantExe != "" && strings.TrimSuffix(d.RunningExe, " (deleted)") != d.WantExe
	return d, true, nil
}

func RollDrifted(ctx context.Context, m hostsvc.Manager, p Params, o InstalledRoll) (SlotDrift, *InstalledRollReport, error) {
	log := o.Log
	if log == nil {
		log = slog.Default()
	}
	d, ok, err := ActiveSlotDrift(ctx, m, p)
	if err != nil || !ok || !d.Drifted() {
		return d, nil, err
	}
	if d.ExeChanged {
		from, err := kcpserver.ReadAsset(ctx, strings.TrimSuffix(d.RunningExe, " (deleted)"))
		if err != nil {
			return d, nil, fmt.Errorf("bootstrap: the running kcp %s of %s: %w", d.RunningExe, d.Unit, err)
		}
		to, err := kcpserver.ReadAsset(ctx, p.KcpPath)
		if err != nil {
			return d, nil, err
		}
		if _, err := kcpserver.CheckPair(from, to, o.Compat, o.PlannedOutage); err != nil {
			return d, nil, fmt.Errorf("bootstrap: %s runs %s and the linked kcp is %s; not rolled: %w", d.Unit, from.Version, to.Version, err)
		}
	}
	lf, err := os.OpenFile(p.RollLock(), os.O_CREATE|os.O_RDWR, 0o600)
	if err != nil {
		return d, nil, err
	}
	defer lf.Close()
	if err := hostos.Lock(lf, hostos.LockExclusive|hostos.LockNonBlocking); err != nil {
		return d, nil, fmt.Errorf("bootstrap: %s is held by another roll; %s keeps its old argv until that roll ends: %w", p.RollLock(), d.Unit, err)
	}
	log.Warn("the running kcp slot was started with another argv or kcp than this binary renders; rolling it",
		"unit", d.Unit, "pid", d.PID, "argvChanged", d.ArgvChanged, "exeChanged", d.ExeChanged, "refused", d.Refused,
		"running", strings.Join(d.Running, " "), "want", strings.Join(d.Want, " "))
	rep, err := RollInstalled(ctx, m, p, o)
	return d, &rep, err
}