// 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//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" }