From 66de71f560cd772e7ef9e9cf1182ce67e495b1ec Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sun, 22 Nov 2020 21:36:52 +0300 Subject: [PATCH] Simple real-time status messages --- README | 1 + js.go | 3 +- log.go | 17 ++++++--- main.go | 1 + run.go | 33 +++++++++++++--- status.go | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 status.go diff --git a/README b/README index edb5b76..f8b2873 100644 --- a/README +++ b/README @@ -52,6 +52,7 @@ FEATURES *goredo-features* lock and jobserver acquirings/releases * displaying of each target's execution time * each target's stderr can be displayed with the PID +* optional statusline with currently running/waiting/done jobs * target's stderr can be stored on the disk with TAI64N timestamp prefixes for each line. To convert them to localtime you can use either tai64nlocal utility from daemontools (http://cr.yp.to/daemontools.html) diff --git a/js.go b/js.go index ccc9fe1..d160c10 100644 --- a/js.go +++ b/js.go @@ -21,6 +21,7 @@ package main import ( "flag" + "fmt" "log" "os" "os/signal" @@ -41,7 +42,7 @@ var ( jsTokens int jsTokensM sync.Mutex - flagJobs = flag.Uint64("j", 1, "number of parallel jobs (0=inf) (REDO_JOBS)") + flagJobs = flag.Uint64("j", 1, fmt.Sprintf("number of parallel jobs (0=inf) (%s)", EnvJobs)) ) func jsInit() { diff --git a/log.go b/log.go index f4fa2ca..ebfb394 100644 --- a/log.go +++ b/log.go @@ -24,6 +24,7 @@ import ( "fmt" "os" "strings" + "sync" ) const ( @@ -65,11 +66,13 @@ var ( LogJS bool MyPid int - flagDebug = flag.Bool("debug", false, "enable debug logging (REDO_DEBUG=1)") - flagLogWait = flag.Bool("log-wait", false, "enable wait messages logging (REDO_LOG_WAIT=1)") - flagLogLock = flag.Bool("log-lock", false, "enable lock messages logging (REDO_LOG_LOCK=1)") - flagLogPid = flag.Bool("log-pid", false, "append PIDs (REDO_LOG_PID=1)") - flagLogJS = flag.Bool("log-js", false, "enable jobserver messages logging (REDO_LOG_JS=1)") + flagDebug = flag.Bool("debug", false, fmt.Sprintf("enable debug logging (%s=1)", EnvDebug)) + flagLogWait = flag.Bool("log-wait", false, fmt.Sprintf("enable wait messages logging (%s=1)", EnvLogWait)) + flagLogLock = flag.Bool("log-lock", false, fmt.Sprintf("enable lock messages logging (%s=1)", EnvLogLock)) + flagLogPid = flag.Bool("log-pid", false, fmt.Sprintf("append PIDs (%s=1)", EnvLogPid)) + flagLogJS = flag.Bool("log-js", false, fmt.Sprintf("enable jobserver messages logging (%s=1)", EnvLogJS)) + + LogMutex sync.Mutex ) func trace(level, format string, args ...interface{}) { @@ -79,7 +82,9 @@ func trace(level, format string, args ...interface{}) { } switch level { case CNone: + LogMutex.Lock() os.Stderr.WriteString(StderrPrefix + p + fmt.Sprintf(format+"\n", args...)) + LogMutex.Unlock() return case CDebug: if !Debug { @@ -110,7 +115,9 @@ func trace(level, format string, args ...interface{}) { } msg := fmt.Sprintf(format, args...) msg = StderrPrefix + colourize(level, p+strings.Repeat(". ", Level)+msg) + LogMutex.Lock() os.Stderr.WriteString(msg + "\n") + LogMutex.Unlock() } func colourize(colour, s string) string { diff --git a/main.go b/main.go index afeb353..63f580a 100644 --- a/main.go +++ b/main.go @@ -177,6 +177,7 @@ func main() { raw[0:4], raw[4:6], raw[6:8], raw[8:10], raw[10:], ) } + statusInit() tgts := flag.Args() if len(tgts) == 0 { diff --git a/run.go b/run.go index bb455e8..93c837d 100644 --- a/run.go +++ b/run.go @@ -63,9 +63,9 @@ var ( StderrPrefix string Jobs sync.WaitGroup - flagTrace = flag.Bool("x", false, "trace current target (sh -x) (set REDO_TRACE=1 for others too)") - flagStderrKeep = flag.Bool("logs", false, "keep job's stderr (REDO_LOGS=1)") - flagStderrSilent = flag.Bool("silent", false, "do not print job's stderr (REDO_SILENT=1)") + flagTrace = flag.Bool("x", false, fmt.Sprintf("trace current target (sh -x) (set %s=1 for others too)", EnvTrace)) + flagStderrKeep = flag.Bool("logs", false, fmt.Sprintf("keep job's stderr (%s=1)", EnvStderrKeep)) + flagStderrSilent = flag.Bool("silent", false, fmt.Sprintf("do not print job's stderr (%s=1)", EnvStderrSilent)) ) type RunErr struct { @@ -193,14 +193,20 @@ func runScript(tgtOrig string, errs chan error, traced bool) error { fdLock.Close() return TgtErr{tgtOrig, err} } - trace(CDebug, "waiting: %s", tgtOrig) Jobs.Add(1) + trace(CDebug, "waiting: %s", tgtOrig) + if FdStatus != nil { + FdStatus.Write([]byte{StatusWait}) + } go func() { defer Jobs.Done() trace(CLock, "LOCK_EX: %s", fdLock.Name()) unix.Flock(int(fdLock.Fd()), unix.LOCK_EX) lockRelease() trace(CDebug, "waiting done: %s", tgtOrig) + if FdStatus != nil { + FdStatus.Write([]byte{StatusWaited}) + } var depInfo *DepInfo fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix)) if err != nil { @@ -354,10 +360,19 @@ func runScript(tgtOrig string, errs chan error, traced bool) error { "%s=%s", EnvStderrPrefix, childStderrPrefix, )) - cmd.ExtraFiles = append(cmd.ExtraFiles, fdDep) fdNum := 0 + cmd.ExtraFiles = append(cmd.ExtraFiles, fdDep) cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvDepFd, 3+fdNum)) fdNum++ + + if FdStatus == nil { + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", EnvStatusFd)) + } else { + cmd.ExtraFiles = append(cmd.ExtraFiles, FdStatus) + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvStatusFd, 3+fdNum)) + fdNum++ + } + if JSR == nil { // infinite jobs cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", EnvJSFd)) @@ -394,6 +409,9 @@ func runScript(tgtOrig string, errs chan error, traced bool) error { Jobs.Add(1) go func() { jsAcquire(shCtx) + if FdStatus != nil { + FdStatus.Write([]byte{StatusRun}) + } defer func() { jsRelease(shCtx) lockRelease() @@ -406,6 +424,9 @@ func runScript(tgtOrig string, errs chan error, traced bool) error { os.Remove(fdStdout.Name()) os.Remove(tmpPath) os.Remove(fdLock.Name()) + if FdStatus != nil { + FdStatus.Write([]byte{StatusDone}) + } Jobs.Done() }() started := time.Now() @@ -433,7 +454,9 @@ func runScript(tgtOrig string, errs chan error, traced bool) error { } if fdStderr != nil { tai64nNow(ts) + LogMutex.Lock() fmt.Fprintf(fdStderr, "@%s %s\n", hex.EncodeToString(ts[:]), line) + LogMutex.Unlock() } if StderrSilent { continue diff --git a/status.go b/status.go new file mode 100644 index 0000000..f203b74 --- /dev/null +++ b/status.go @@ -0,0 +1,111 @@ +/* +goredo -- redo implementation on pure Go +Copyright (C) 2020 Sergey Matveev + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package main + +import ( + "flag" + "fmt" + "os" + "strings" +) + +const ( + EnvStatusFd = "REDO_STATUS_FD" + EnvNoStatus = "REDO_NO_STATUS" + + StatusRun = iota + StatusDone + StatusWait + StatusWaited +) + +var ( + FdStatus *os.File + + flagNoStatus = flag.Bool("no-status", false, "disable statusline (REDO_NO_STATUS=1)") +) + +func statusInit() { + if *flagNoStatus { + return + } + if v := os.Getenv(EnvNoStatus); v == "1" { + return + } + if v := os.Getenv(EnvStatusFd); v != "" { + if v == "NO" { + return + } + FdStatus = mustParseFd(v, EnvStatusFd) + return + } + var r *os.File + var err error + r, FdStatus, err = os.Pipe() + if err != nil { + panic(err) + } + go func() { + running := 0 + waiting := 0 + done := 0 + var out string + outLenPrev := 0 + buf := make([]byte, 1) + var n int + for { + n, err = r.Read(buf) + if err != nil || n != 1 { + break + } + switch buf[0] { + case StatusRun: + running++ + case StatusDone: + running-- + done++ + case StatusWait: + waiting++ + case StatusWaited: + waiting-- + } + if NoColor { + out = fmt.Sprintf( + "\rrun: %d wait: %d done: %d\r", + running, waiting, done, + ) + } else { + out = fmt.Sprintf( + "\rrun: %s%d%s wait: %s%d%s done: %s%d%s\r", + CRedo, running, CReset, + CWait, waiting, CReset, + CJS, done, CReset, + ) + } + if len(out) < outLenPrev { + outLenPrev = len(out) + out += strings.Repeat(" ", outLenPrev-len(out)) + } else { + outLenPrev = len(out) + } + LogMutex.Lock() + os.Stderr.WriteString(out) + LogMutex.Unlock() + } + }() +} -- 2.44.0