218 lines
8.3 KiB
Go
218 lines
8.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
)
|
|
|
|
// CheckRound is how often the module reads the installed toolchain again, beside the check before every step and
|
|
// the one every health look makes.
|
|
const CheckRound = 10 * time.Minute
|
|
|
|
// Module is the Node build module's one running state: the node it runs on, its recipe, the toolchain guard, the
|
|
// toolchain directory and the states it writes (toolchains, steps).
|
|
type Module struct {
|
|
Node string
|
|
// Recipe is the recipe in force. Reload, when set, reads it again at every check (the lock file the mesh
|
|
// writes can change under a running module); a failure to read it is a refusal.
|
|
Recipe Recipe
|
|
Reload func() (Recipe, error)
|
|
// Probe reads the installed Node version (ProbeNode).
|
|
Probe func(ctx context.Context) (string, error)
|
|
// Toolchain installs the toolchain directory; nil in a module that only guards.
|
|
Toolchain *Toolchain
|
|
// Work is the work directory, outside the home: a workspace is <Work>/<step id>/tree. Run runs the commands of a
|
|
// step (a fake in a test). Clock is the time the steps are timed by.
|
|
Work string
|
|
Run forgekey.Runner
|
|
// RemoteBase is where repositories are fetched from, "ssh://mesh-forge/" when empty; FetchOptions are the options of
|
|
// the SDK's fetch and merge. A test points them at a local repository (buildstep.AllowFileRemote).
|
|
RemoteBase string
|
|
FetchOptions []buildstep.Option
|
|
Clock func() time.Time
|
|
// Toolchains and Steps are the Put of the states of those names.
|
|
Toolchains, Steps buildstep.Put
|
|
// ReadSteps and ReadToolchainsState read those two states back for the read tools (issue 1562).
|
|
ReadSteps, ReadToolchainsState StateRead
|
|
// Emit tells the bus an event (stdio.Emit); nil emits nothing. The guard emits toolchain.reported after each write of
|
|
// the toolchains key, so the indexer, which holds no timer, learns that the bucket exists (issue 1444).
|
|
Emit func(eventType string, body any) error
|
|
// Ctx is the process's context, which a step runs under (the tool call's is cut at 30 s, the step's is not).
|
|
Ctx context.Context
|
|
Say func(format string, a ...any)
|
|
|
|
mu sync.Mutex // guards Recipe
|
|
install sync.Mutex // one install at a time
|
|
}
|
|
|
|
func (m *Module) recipe() Recipe {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return m.Recipe
|
|
}
|
|
|
|
// guard is the SDK guard for the version the probe just read: the module names no Node version (ADR 0477 §2), so the
|
|
// version wanted is the one installed and the guard refuses only when node cannot be run, writing the installed
|
|
// version to <node>.typescript of the toolchains state every time.
|
|
func (m *Module) guard(r Recipe, installed string, probeErr error) buildstep.Guard {
|
|
return buildstep.Guard{Language: "typescript", Want: installed, Recipe: r.Version(), Node: m.Node, Put: m.reportingPut(),
|
|
Probe: func(context.Context) (string, error) { return installed, probeErr }}
|
|
}
|
|
|
|
// Check reads the installed toolchain, writes <node>.typescript of the toolchains state and answers nil, or the
|
|
// refusal when node cannot be run (buildstep.Guard.Check); another version than another node's is no refusal (ADR 0477
|
|
// §2). It runs at start, before every step, at every health look and every CheckRound.
|
|
func (m *Module) Check(ctx context.Context) error {
|
|
_, err := m.check(ctx)
|
|
return err
|
|
}
|
|
|
|
// check is Check that also answers the version it found installed.
|
|
func (m *Module) check(ctx context.Context) (string, error) {
|
|
if m.Reload != nil {
|
|
r, err := m.Reload()
|
|
if err != nil {
|
|
return "", &buildstep.Refusal{Text: err.Error()}
|
|
}
|
|
m.mu.Lock()
|
|
m.Recipe = r
|
|
m.mu.Unlock()
|
|
}
|
|
installed, err := m.Probe(ctx)
|
|
return installed, m.guard(m.recipe(), installed, err).Check(ctx)
|
|
}
|
|
|
|
// Prepare is Check and then the toolchain directory of the recipe in force: what every step runs behind.
|
|
func (m *Module) Prepare(ctx context.Context) error {
|
|
if err := m.Check(ctx); err != nil {
|
|
return err
|
|
}
|
|
if m.Toolchain == nil {
|
|
return nil
|
|
}
|
|
m.install.Lock()
|
|
defer m.install.Unlock()
|
|
return m.Toolchain.Ensure(ctx, m.recipe().Version())
|
|
}
|
|
|
|
// Start prepares once, as the module starts, and then every CheckRound until ctx ends.
|
|
func (m *Module) Start(ctx context.Context) {
|
|
for {
|
|
if err := m.Prepare(ctx); err != nil && m.Say != nil {
|
|
m.Say("the toolchain: %v", err)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(CheckRound):
|
|
}
|
|
}
|
|
}
|
|
|
|
// Health is the module's health (the runtime-health tool, ADR 0366): unhealthy only while node cannot be run (or the lock cannot
|
|
// be read), saying why, so the controller raises the node's condition; healthy it names the installed version. It looks again each time it is
|
|
// asked.
|
|
func (m *Module) Health(ctx context.Context) health.Answer {
|
|
installed, err := m.check(ctx)
|
|
if err != nil {
|
|
return health.Unhealthy(err.Error())
|
|
}
|
|
return health.Healthy(fmt.Sprintf("node %s installed, the version the module builds with (recipe %s)", installed, m.recipe().Version()))
|
|
}
|
|
|
|
// Long is a tool that runs a long step (ADR 0469 §2): it answers at once with the step's id, and the step ends on
|
|
// its key in the steps state. The guard runs inside the step, before run, so a node whose node cannot be run ends every
|
|
// step refused with the cause named.
|
|
func (m *Module) Long(tool string, run func(ctx context.Context, args map[string]any) (any, error)) func(map[string]any) (any, error) {
|
|
return func(args map[string]any) (any, error) {
|
|
id, err := buildstep.Start(m.Ctx, m.Steps, m.Node, tool, func(ctx context.Context) (any, error) {
|
|
if err := m.Prepare(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
return run(ctx, args)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
key, _ := delivery.StepKey(m.Node, id)
|
|
return map[string]any{"step": id, "key": key, "state": delivery.StepRunning}, nil
|
|
}
|
|
}
|
|
|
|
// Short is a tool that answers in its own call; the guard runs before it, and a refusal is the call's error.
|
|
func (m *Module) Short(run func(ctx context.Context, args map[string]any) (any, error)) func(map[string]any) (any, error) {
|
|
return func(args map[string]any) (any, error) {
|
|
if err := m.Prepare(m.Ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
return run(m.Ctx, args)
|
|
}
|
|
}
|
|
|
|
// Status is what the module reports of itself: the node, the recipe, the toolchain found and why it refuses, if it
|
|
// does.
|
|
func (m *Module) Status(ctx context.Context) map[string]any {
|
|
err := m.Check(ctx)
|
|
installed, _ := m.Probe(ctx)
|
|
r := m.recipe()
|
|
out := map[string]any{"node": m.Node, "recipe": r.Version(), "installed": installed, "refusing": err != nil}
|
|
if m.Toolchain != nil {
|
|
out["toolchain-dir"] = m.Toolchain.Paths.Dir(r.Version())
|
|
out["toolchain-installed"] = m.Toolchain.Present(r.Version())
|
|
}
|
|
if err != nil {
|
|
out["why"] = err.Error()
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (m *Module) now() time.Time {
|
|
if m.Clock != nil {
|
|
return m.Clock()
|
|
}
|
|
return time.Now()
|
|
}
|
|
|
|
// defaultPath is where a step looks for node, npm, git and ssh when this process was given no PATH.
|
|
const defaultPath = "/usr/local/bin:/usr/bin:/bin"
|
|
|
|
// baseEnv is the environment of a step's command beyond what the step adds: PATH, the account's own HOME (its ssh
|
|
// files and git configuration) and a fixed locale. A command is given these and its own words, never this process's
|
|
// environment, which holds the module's bus credentials (ADR 0466 §4).
|
|
func (m *Module) baseEnv() []string {
|
|
return []string{"PATH=" + pathOrDefault(), "HOME=" + m.Toolchain.homeOrEmpty(), "LC_ALL=C"}
|
|
}
|
|
|
|
func pathOrDefault() string {
|
|
if path := os.Getenv("PATH"); path != "" {
|
|
return path
|
|
}
|
|
return defaultPath
|
|
}
|
|
|
|
// ToolchainReported is the event the guard emits after it writes the toolchains key.
|
|
const ToolchainReported = "toolchain.reported"
|
|
|
|
// reportingPut is the toolchains Put that then emits toolchain.reported with the node and the language. A write that
|
|
// failed emits nothing; an emit that failed is said and does not refuse the step.
|
|
func (m *Module) reportingPut() buildstep.Put {
|
|
return func(key string, value any) (uint64, error) {
|
|
rev, err := m.Toolchains(key, value)
|
|
if err != nil || m.Emit == nil {
|
|
return rev, err
|
|
}
|
|
if eerr := m.Emit(ToolchainReported, map[string]string{"node": m.Node, "language": "typescript"}); eerr != nil && m.Say != nil {
|
|
m.Say("the event %s could not be emitted: %v", ToolchainReported, eerr)
|
|
}
|
|
return rev, nil
|
|
}
|
|
}
|