Builds: design 47 G §262, §245, §440, §447 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
141 lines
4.6 KiB
Go
141 lines
4.6 KiB
Go
// Package processbuilds reads the build each process of this node was started with (design 47, *The fingerprint handover*):
|
|
// the value MESH_FINGERPRINT the node-engine handed a daemon or a container at its start, read from the running process
|
|
// itself, never from what the declaration would hand it now. A process started before the node-engine handed the value,
|
|
// or one whose value cannot be read, says why its build is unknown instead of a fingerprint.
|
|
package processbuilds
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
)
|
|
|
|
// Var is the environment value the build is handed in.
|
|
const Var = "MESH_FINGERPRINT"
|
|
|
|
// Runner runs a command and answers its output.
|
|
type Runner func(ctx context.Context, name string, args ...string) (string, error)
|
|
|
|
// Reader reads the running processes of a node.
|
|
type Reader struct {
|
|
Node string
|
|
Run Runner
|
|
// Environ reads a process's environment by its pid; nil reads /proc/<pid>/environ.
|
|
Environ func(pid int) ([]byte, error)
|
|
}
|
|
|
|
func (r Reader) environ(pid int) ([]byte, error) {
|
|
if r.Environ != nil {
|
|
return r.Environ(pid)
|
|
}
|
|
return os.ReadFile(fmt.Sprintf("/proc/%d/environ", pid))
|
|
}
|
|
|
|
// Read answers the build of every daemon and container the declaration holds, narrowed to module when it is not empty,
|
|
// ordered by module and process.
|
|
func (r Reader) Read(ctx context.Context, d *declaration.Declaration, module string) nodeengine.ProcessBuilds {
|
|
out := nodeengine.ProcessBuilds{Node: r.Node, Processes: []nodeengine.ProcessBuild{}}
|
|
if d == nil {
|
|
out.Error = "this node keeps no declaration of what it runs"
|
|
return out
|
|
}
|
|
for _, res := range d.Resources {
|
|
owner := res.DeclaredModule()
|
|
if owner == "" {
|
|
owner, _ = declaration.ModuleGuess(res.Identity())
|
|
}
|
|
if module != "" && owner != module {
|
|
continue
|
|
}
|
|
switch p := res.(type) {
|
|
case *declaration.Process:
|
|
out.Processes = append(out.Processes, r.daemon(ctx, owner, p.Name))
|
|
case *declaration.Container:
|
|
out.Processes = append(out.Processes, r.container(ctx, owner, p.Name))
|
|
}
|
|
}
|
|
sort.SliceStable(out.Processes, func(i, j int) bool {
|
|
a, b := out.Processes[i], out.Processes[j]
|
|
if a.Module != b.Module {
|
|
return a.Module < b.Module
|
|
}
|
|
return a.Process < b.Process
|
|
})
|
|
return out
|
|
}
|
|
|
|
func (r Reader) daemon(ctx context.Context, module, name string) nodeengine.ProcessBuild {
|
|
unit := name + ".service"
|
|
b := nodeengine.ProcessBuild{Module: module, Process: unit, Kind: nodeengine.ProcessBuildKindService}
|
|
said, err := r.Run(ctx, "systemctl", "show", "--property=MainPID,ActiveEnterTimestamp", "--", unit)
|
|
if err != nil {
|
|
b.Unknown = "the unit cannot be read: " + err.Error()
|
|
return b
|
|
}
|
|
props := map[string]string{}
|
|
for _, line := range strings.Split(said, "\n") {
|
|
if k, v, ok := strings.Cut(strings.TrimSpace(line), "="); ok {
|
|
props[k] = v
|
|
}
|
|
}
|
|
pid, _ := strconv.Atoi(props["MainPID"])
|
|
if pid <= 0 {
|
|
b.Unknown = "the unit runs no process now"
|
|
return b
|
|
}
|
|
if t, err := time.Parse("Mon 2006-01-02 15:04:05 MST", props["ActiveEnterTimestamp"]); err == nil {
|
|
b.Started = &t
|
|
}
|
|
raw, err := r.environ(pid)
|
|
if err != nil {
|
|
b.Unknown = "its environment cannot be read: " + err.Error()
|
|
return b
|
|
}
|
|
b.Fingerprint, b.Unknown = fingerprintIn(strings.Split(string(raw), "\x00"))
|
|
return b
|
|
}
|
|
|
|
func (r Reader) container(ctx context.Context, module, name string) nodeengine.ProcessBuild {
|
|
b := nodeengine.ProcessBuild{Module: module, Process: name, Kind: nodeengine.ProcessBuildKindContainer}
|
|
var said string
|
|
var err error
|
|
for _, cri := range []string{"docker", "podman"} {
|
|
if said, err = r.Run(ctx, cri, "container", "inspect", "--format", "{{json .Config.Env}}\t{{.State.StartedAt}}", name); err == nil {
|
|
break
|
|
}
|
|
}
|
|
if err != nil {
|
|
b.Unknown = "the container cannot be read: " + err.Error()
|
|
return b
|
|
}
|
|
env, started, _ := strings.Cut(strings.TrimSpace(said), "\t")
|
|
var vars []string
|
|
if err := json.Unmarshal([]byte(env), &vars); err != nil {
|
|
b.Unknown = "its environment cannot be read: " + err.Error()
|
|
return b
|
|
}
|
|
if t, err := time.Parse(time.RFC3339Nano, started); err == nil {
|
|
b.Started = &t
|
|
}
|
|
b.Fingerprint, b.Unknown = fingerprintIn(vars)
|
|
return b
|
|
}
|
|
|
|
// fingerprintIn is the value of the handed variable in a process's environment, or why there is none.
|
|
func fingerprintIn(vars []string) (fingerprint, unknown string) {
|
|
for _, v := range vars {
|
|
if value, ok := strings.CutPrefix(v, Var+"="); ok && value != "" {
|
|
return value, ""
|
|
}
|
|
}
|
|
return "", "started before the node-engine handed " + Var + ", or without it"
|
|
}
|