File: timer.go 1 /* 2 The MIT License (MIT) 3 4 Copyright © 2020-2025 pacman64 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy of 7 this software and associated documentation files (the “Software”), to deal 8 in the Software without restriction, including without limitation the rights to 9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 of the Software, and to permit persons to whom the Software is furnished to do 11 so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in all 14 copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 SOFTWARE. 23 */ 24 25 /* 26 Single-file source-code for timer, where the unit-tests from the original app 27 are omitted. 28 29 To compile a smaller-sized command-line app, you can use the `go` command as 30 follows: 31 32 go build -ldflags "-s -w" -trimpath timer.go 33 */ 34 35 package main 36 37 import ( 38 "bufio" 39 "io" 40 "os" 41 "os/exec" 42 "os/signal" 43 "strconv" 44 "sync" 45 "time" 46 ) 47 48 // Note: the code is avoiding using the fmt package to save hundreds of 49 // kilobytes on the resulting executable, which is a noticeable difference. 50 51 const ( 52 spaces = ` ` 53 54 // clear has enough spaces in it to cover any chronograph output 55 clear = "\r" + spaces + spaces + spaces + "\r" 56 ) 57 58 const info = ` 59 timer [command...] [args...] 60 61 62 Run a live timer/chronograph on stderr, always showing below all lines 63 from stdin, which update stdout as they come. 64 65 When stdin is over, it simply quits showing how long it ran, by extending 66 its last visible timer line on stderr. 67 68 You can also use this app without piping anything to its stdin, acting as 69 a simple live clock until you force-quit it. 70 71 To ensure any stderr lines don't clash with the chronograph output line, 72 you can also run this app with arguments to have it run a command, thus 73 guaranteeing the subtask's stderr lines show without interference. 74 ` 75 76 func main() { 77 if len(os.Args) > 1 { 78 switch os.Args[1] { 79 case `-h`, `--h`, `-help`, `--help`: 80 os.Stderr.WriteString(info[1:]) 81 return 82 } 83 } 84 85 if len(os.Args) > 1 { 86 os.Exit(runTask(os.Args[1], os.Args[2:])) 87 } 88 89 err := chronograph(os.Stdin, nil) 90 if quit, ok := err.(justQuit); ok { 91 os.Exit(quit.exitCode) 92 } 93 } 94 95 // justQuit is a custom error-type which isn't for showing, but quitting the 96 // app right away instead 97 type justQuit struct { 98 exitCode int 99 } 100 101 // Error is only to satisfy the error interface, and not for showing 102 func (justQuit) Error() string { 103 return `quitting right away` 104 } 105 106 // runTask handles running the app in `subtask-mode` 107 func runTask(name string, args []string) (exitCode int) { 108 cmd := exec.Command(name, args...) 109 cmd.Stdin = os.Stdin 110 111 stdout, err := cmd.StdoutPipe() 112 if err != nil { 113 showError(err) 114 return 1 115 } 116 defer stdout.Close() 117 118 stderr, err := cmd.StderrPipe() 119 if err != nil { 120 showError(err) 121 return 1 122 } 123 defer stderr.Close() 124 125 if err := cmd.Start(); err != nil { 126 showError(err) 127 return 1 128 } 129 130 err = chronograph(stdout, stderr) 131 if quit, ok := err.(justQuit); ok { 132 return quit.exitCode 133 } 134 if err != nil { 135 showError(err) 136 } 137 138 if err := cmd.Wait(); err != nil { 139 showError(err) 140 } 141 return cmd.ProcessState.ExitCode() 142 } 143 144 // showError gives a consistent style/look to any of the app's own errors 145 func showError(err error) { 146 if err == nil { 147 return 148 } 149 150 os.Stderr.WriteString("\x1b[31m") 151 os.Stderr.WriteString(err.Error()) 152 os.Stderr.WriteString("\x1b[0m\n") 153 } 154 155 // readLines is run twice asynchronously, so that both stdin and stderr lines 156 // are handled independently, which matters when running a subtask 157 func readLines(r io.Reader, lines chan []byte) error { 158 defer close(lines) 159 160 // when not handling a subtask, this func will be called with a nil 161 // reader, since without a subtask, there's no stderr to read from 162 if r == nil { 163 return nil 164 } 165 166 const gb = 1024 * 1024 * 1024 167 sc := bufio.NewScanner(r) 168 sc.Buffer(nil, 8*gb) 169 170 for sc.Scan() { 171 lines <- sc.Bytes() 172 } 173 return sc.Err() 174 } 175 176 // chronograph runs a live chronograph, showing the time elapsed: 2 input 177 // sources for lines are handled concurrently, one destined for the app's 178 // stdout, the other for the app's stderr, without interfering with the 179 // chronograph lines, which also show on stderr 180 func chronograph(stdout io.Reader, stderr io.Reader) error { 181 start := time.Now() 182 t := time.NewTicker(100 * time.Millisecond) 183 startChronoLine(start, start) 184 185 stopped := make(chan os.Signal, 1) 186 defer close(stopped) 187 signal.Notify(stopped, os.Interrupt) 188 189 errors := make(chan error) 190 var waitAllLines sync.WaitGroup 191 waitAllLines.Add(2) 192 193 outLines := make(chan []byte) 194 go func() { 195 defer waitAllLines.Done() 196 if stdout != nil { 197 errors <- readLines(stdout, outLines) 198 } 199 }() 200 201 errLines := make(chan []byte) 202 go func() { 203 defer waitAllLines.Done() 204 if stderr != nil { 205 errors <- readLines(stderr, errLines) 206 } 207 }() 208 209 quit := make(chan struct{}) 210 defer close(quit) 211 212 go func() { 213 waitAllLines.Wait() 214 close(errors) 215 quit <- struct{}{} 216 }() 217 218 for { 219 select { 220 case now := <-t.C: 221 os.Stderr.WriteString(clear) 222 startChronoLine(start, now) 223 224 case line := <-outLines: 225 if line == nil { 226 // filter out junk for needlessly-chatty pipes, while still 227 // keeping actual empty lines 228 continue 229 } 230 231 // write-order of the next 3 steps matters, to avoid mixing 232 // up lines since stdout and stderr lines can show up together 233 os.Stderr.WriteString(clear) 234 os.Stdout.Write(line) 235 _, err := os.Stdout.WriteString("\n") 236 startChronoLine(start, time.Now()) 237 if err != nil { 238 endChronoLine(start) 239 return nil 240 } 241 242 case line := <-errLines: 243 if line == nil { 244 // filter out junk for needlessly-chatty pipes, while still 245 // keeping actual empty lines 246 continue 247 } 248 249 // write-order of the next 3 steps matters, to avoid mixing 250 // up lines since stdout and stderr lines can show up together 251 os.Stderr.WriteString(clear) 252 os.Stderr.Write(line) 253 _, err := os.Stderr.WriteString("\n") 254 startChronoLine(start, time.Now()) 255 if err != nil { 256 endChronoLine(start) 257 return nil 258 } 259 260 case err := <-errors: 261 if err == nil { 262 continue 263 } 264 os.Stderr.WriteString(clear) 265 showError(err) 266 startChronoLine(start, time.Now()) 267 268 case <-quit: 269 endChronoLine(start) 270 return justQuit{0} 271 272 case <-stopped: 273 t.Stop() 274 endChronoLine(start) 275 return justQuit{255} 276 } 277 } 278 } 279 280 // func startChronoLine(start, now time.Time) { 281 // var buf [64]byte 282 // dt := now.Sub(start) 283 // 284 // os.Stderr.Write(time.Time{}.Add(dt).AppendFormat(buf[:0], `15:04:05.0`)) 285 // os.Stderr.WriteString(` `) 286 // os.Stderr.Write(now.AppendFormat(buf[:0], `2006-01-02 15:04:05 Jan Mon`)) 287 // } 288 289 func startChronoLine(start, now time.Time) { 290 var buf [64]byte 291 dt := now.Sub(start) 292 293 s := buf[:0] 294 s = time.Time{}.Add(dt).AppendFormat(s, `15:04:05.0`) 295 s = append(s, ` `...) 296 s = now.AppendFormat(s, `2006-01-02 15:04:05 Jan Mon`) 297 os.Stderr.Write(s) 298 } 299 300 func endChronoLine(start time.Time) { 301 var buf [64]byte 302 secs := time.Since(start).Seconds() 303 304 os.Stderr.Write([]byte(` `)) 305 os.Stderr.Write(strconv.AppendFloat(buf[:0], secs, 'f', 4, 64)) 306 os.Stderr.Write([]byte(" seconds\n")) 307 }