GitRoot
craft your forge, build your project, grow your community freely
1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5package exec
6
7import (
8 "bytes"
9 "os/exec"
10 "path/filepath"
11 "sync"
12 "time"
13
14 "github.com/samber/oops"
15 "github.com/shirou/gopsutil/v4/process"
16 pluginLib "gitroot.dev/libs/golang/plugin/model"
17 "gitroot.dev/server/logger"
18)
19
20const watchTime = 100 * time.Millisecond
21
22type BareMetal struct {
23 logger *logger.Logger
24}
25
26func NewBareMetal(log *logger.Logger) *BareMetal {
27 return &BareMetal{
28 logger: log.NewSubLogger("bareMetal"),
29 }
30}
31
32func (e *BareMetal) name() string {
33 return "bareMetal"
34}
35
36func (e *BareMetal) exec(dir string, exe pluginLib.Exec) (*pluginLib.ExecStatus, error) {
37 status := &pluginLib.ExecStatus{
38 CmdsExec: make([]string, len(exe.Cmds)),
39 CmdsStatus: make([]int, len(exe.Cmds)),
40 CmdsLogs: make([]string, len(exe.Cmds)),
41 CmdsStats: make([]pluginLib.CmdStats, len(exe.Cmds)),
42 }
43
44 for i, cmd := range exe.Cmds {
45 var logs bytes.Buffer
46 c := exec.Command(cmd.Cmd, cmd.Args...)
47 e.logger.Debug("exec a cmd", logger.NewLoggerPair("cmd", c.String()))
48 c.Env = exe.Env
49 c.Dir = filepath.Join(dir, APP_DIR)
50 c.Stdout = &logs
51 c.Stderr = &logs
52 stats := e.start(c)
53 status.CmdsExec[i] = c.String()
54 status.CmdsLogs[i] = logs.String()
55 status.CmdsStatus[i] = c.ProcessState.ExitCode()
56 status.CmdsStats[i] = *stats
57 }
58 return status, nil
59}
60
61func (e *BareMetal) start(cmd *exec.Cmd) *pluginLib.CmdStats {
62 stats := &pluginLib.CmdStats{}
63 var wg sync.WaitGroup
64
65 if err := cmd.Start(); err != nil {
66 e.logger.Error("can't start process", err)
67 return stats
68 }
69 proc, err := process.NewProcess(int32(cmd.Process.Pid))
70 if err != nil {
71 e.logger.Error("can't get process", err)
72 return stats
73 }
74
75 wg.Go(func() {
76 if err := e.monitor(proc, stats); err != nil {
77 e.logger.Error("monitor not started", err)
78 }
79 })
80
81 cmd.Wait() //don't look at error because it will incorpored in statusCode and logs
82 wg.Wait()
83
84 return stats
85}
86
87func (e *BareMetal) monitor(proc *process.Process, stats *pluginLib.CmdStats) error {
88 ticker := time.NewTicker(watchTime)
89 defer ticker.Stop()
90
91 initialIOCounters, err := proc.IOCounters()
92 if err != nil {
93 return oops.Wrapf(err, "can't read first io counter")
94 }
95 e.updateStats(proc, stats, initialIOCounters)
96
97 for range ticker.C {
98 isRunning, err := proc.IsRunning()
99 if err != nil || !isRunning {
100 e.updateStats(proc, stats, initialIOCounters)
101 return err
102 }
103 e.updateStats(proc, stats, initialIOCounters)
104 }
105
106 e.updateStats(proc, stats, initialIOCounters)
107 return nil
108}
109
110func (e *BareMetal) updateStats(proc *process.Process, stats *pluginLib.CmdStats, initialIOCounters *process.IOCountersStat) {
111 if memInfo, err := proc.MemoryInfo(); err == nil {
112 if memInfo.RSS > stats.MaxMemoryBytes {
113 stats.MaxMemoryBytes = memInfo.RSS
114 }
115 }
116
117 if threads, err := proc.NumThreads(); err == nil {
118 if threads > stats.MaxThreads {
119 stats.MaxThreads = threads
120 }
121 }
122
123 if currentIOCounters, err := proc.IOCounters(); err == nil {
124 stats.ReadBytesTotal = currentIOCounters.ReadBytes - initialIOCounters.ReadBytes
125 stats.WriteBytesTotal = currentIOCounters.WriteBytes - initialIOCounters.WriteBytes
126 }
127
128 if cpuTimes, err := proc.Times(); err == nil {
129 stats.TotalCPUTimeMs = uint64((cpuTimes.User + cpuTimes.System) * 1000)
130 }
131}