From d5134de05655da8c4e8d1f2e5626d46b8b9e160e Mon Sep 17 00:00:00 2001 From: Federico Pedemonte Date: Fri, 29 Aug 2025 14:49:30 +0200 Subject: [PATCH] add optional -r flag to report overall prep status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an opt-in reporting mode for external tooling. When run with -r, modd writes a .modd_report snapshot containing the overall status of all blocks’ prep commands. Semantics: - Status 0 -> every prep command exited 0 - Non-zero -> at least one prep command failed Format: one line per update, "RFC3339,status", e.g.: 2025-08-29T14:35:07Z,0 A single Reporter implementation (FileReporter) is provided --- cmd/modd/main.go | 16 +++++++++++++++- conf/conf.go | 2 ++ modd.go | 37 +++++++++++++++++++++++++++++++------ report/reporter.go | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 report/reporter.go diff --git a/cmd/modd/main.go b/cmd/modd/main.go index e1c16d8..d99e834 100644 --- a/cmd/modd/main.go +++ b/cmd/modd/main.go @@ -8,6 +8,7 @@ import ( "github.com/cortesi/modd" "github.com/cortesi/modd/notify" + "github.com/cortesi/modd/report" "github.com/cortesi/termlog" "gopkg.in/alecthomas/kingpin.v2" "mvdan.cc/sh/v3/interp" @@ -15,6 +16,7 @@ import ( ) const modfile = "./modd.conf" +const reportFile = ".modd_report" var file = kingpin.Flag( "file", @@ -41,6 +43,10 @@ var doNotify = kingpin.Flag("notify", "Send stderr to system notification if com Short('n'). Bool() +var doReport = kingpin.Flag("report", "Report last prep status to file"). + Short('r'). + Bool() + var prep = kingpin.Flag("prep", "Run prep commands and exit"). Short('p'). Bool() @@ -109,7 +115,15 @@ func main() { notifiers = append(notifiers, ¬ify.BeepNotifier{}) } - mr, err := modd.NewModRunner(*file, log, notifiers, !(*noconf)) + reporters := []report.Reporter{} + if *doReport { + r := report.FileReporter{ + Filename: reportFile, + } + reporters = append(reporters, r) + } + + mr, err := modd.NewModRunner(*file, log, notifiers, reporters, !(*noconf)) if err != nil { log.Shout("%s", err) return diff --git a/conf/conf.go b/conf/conf.go index 6142f89..3852218 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -27,6 +27,8 @@ type Block struct { Daemons []Daemon Preps []Prep + + Status int } func (b *Block) addPrep(command string, options []string) error { diff --git a/modd.go b/modd.go index 8eb1409..5f6557f 100644 --- a/modd.go +++ b/modd.go @@ -9,6 +9,7 @@ import ( "github.com/cortesi/modd/conf" "github.com/cortesi/modd/notify" + "github.com/cortesi/modd/report" "github.com/cortesi/modd/shell" "github.com/cortesi/moddwatch" "github.com/cortesi/termlog" @@ -58,15 +59,17 @@ type ModRunner struct { ConfPath string ConfReload bool Notifiers []notify.Notifier + Reporters []report.Reporter } // NewModRunner constructs a new ModRunner -func NewModRunner(confPath string, log termlog.TermLog, notifiers []notify.Notifier, confreload bool) (*ModRunner, error) { +func NewModRunner(confPath string, log termlog.TermLog, notifiers []notify.Notifier, reporters []report.Reporter, confreload bool) (*ModRunner, error) { mr := &ModRunner{ Log: log, ConfPath: confPath, ConfReload: confreload, Notifiers: notifiers, + Reporters: reporters, } err := mr.ReadConfig() if err != nil { @@ -106,12 +109,12 @@ func (mr *ModRunner) PrepOnly(initial bool) error { return nil } -func (mr *ModRunner) runBlock(b conf.Block, mod *moddwatch.Mod, dpen *DaemonPen) { +func (mr *ModRunner) runBlock(b conf.Block, mod *moddwatch.Mod, dpen *DaemonPen) error { if b.InDir != "" { currentDir, err := os.Getwd() if err != nil { mr.Log.Shout("Error getting current working directory: %s", err) - return + return err } err = os.Chdir(b.InDir) if err != nil { @@ -120,7 +123,7 @@ func (mr *ModRunner) runBlock(b conf.Block, mod *moddwatch.Mod, dpen *DaemonPen) b.InDir, err, ) - return + return err } defer func() { err := os.Chdir(currentDir) @@ -140,9 +143,10 @@ func (mr *ModRunner) runBlock(b conf.Block, mod *moddwatch.Mod, dpen *DaemonPen) if _, ok := err.(ProcError); !ok { mr.Log.Shout("Error running prep: %s", err) } - return + return err } dpen.Restart() + return nil } func (mr *ModRunner) trigger(root string, mod *moddwatch.Mod, dworld *DaemonWorld) { @@ -159,7 +163,28 @@ func (mr *ModRunner) trigger(root string, mod *moddwatch.Mod, dworld *DaemonWorl continue } } - mr.runBlock(b, lmod, dworld.DaemonPens[i]) + + err := mr.runBlock(b, lmod, dworld.DaemonPens[i]) + + if err != nil { + mr.Config.Blocks[i].Status = 1 + } else { + mr.Config.Blocks[i].Status = 0 + } + } + + // Compute overall status: != 0 if any block has Status != 0, otherwise 0. + status := 0 + + for _, b := range mr.Config.Blocks { + if status == 0 { + status = b.Status + } + } + + // Report the overall status to all reporters. + for _, r := range mr.Reporters { + r.Report(time.Now(), status) } } diff --git a/report/reporter.go b/report/reporter.go new file mode 100644 index 0000000..7d2982d --- /dev/null +++ b/report/reporter.go @@ -0,0 +1,38 @@ +package report + +import ( + "fmt" + "os" + "time" +) + +// Reporter publishes status updates. +type Reporter interface { + Report(timestamp time.Time, status int) +} + +// FileReporter writes status updates to a file. +type FileReporter struct { + Filename string +} + +// Report implements the Reporter interface. +func (fs FileReporter) Report(timestamp time.Time, status int) { + ts := time.Now().UTC().Format(time.RFC3339) + msg := fmt.Sprintf("%s,%d\n", ts, status) + + // Open for write, create if missing, truncate to zero length. + f, err := os.OpenFile(fs.Filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return + } + defer f.Close() + + if _, err := f.Write([]byte(msg)); err != nil { + return + } + + if err := f.Sync(); err != nil { + return + } +}