github.com/bilpay-tech/air@v0.0.0-20230514155040-b55f770a4ac6/runner/engine.go (about) 1 package runner 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strings" 10 "sync" 11 "sync/atomic" 12 "time" 13 14 "github.com/gohugoio/hugo/watcher/filenotify" 15 ) 16 17 // Engine ... 18 type Engine struct { 19 config *Config 20 logger *logger 21 watcher filenotify.FileWatcher 22 debugMode bool 23 runArgs []string 24 running bool 25 26 eventCh chan string 27 watcherStopCh chan bool 28 buildRunCh chan bool 29 buildRunStopCh chan bool 30 canExit chan bool 31 binStopCh chan bool 32 exitCh chan bool 33 34 mu sync.RWMutex 35 watchers uint 36 round uint64 37 fileChecksums *checksumMap 38 39 ll sync.Mutex // lock for logger 40 } 41 42 // NewEngineWithConfig ... 43 func NewEngineWithConfig(cfg *Config, debugMode bool) (*Engine, error) { 44 logger := newLogger(cfg) 45 watcher, err := newWatcher(cfg) 46 if err != nil { 47 return nil, err 48 } 49 e := Engine{ 50 config: cfg, 51 logger: logger, 52 watcher: watcher, 53 debugMode: debugMode, 54 runArgs: cfg.Build.ArgsBin, 55 eventCh: make(chan string, 1000), 56 watcherStopCh: make(chan bool, 10), 57 buildRunCh: make(chan bool, 1), 58 buildRunStopCh: make(chan bool, 1), 59 canExit: make(chan bool, 1), 60 binStopCh: make(chan bool), 61 exitCh: make(chan bool), 62 fileChecksums: &checksumMap{m: make(map[string]string)}, 63 watchers: 0, 64 } 65 66 return &e, nil 67 } 68 69 // NewEngine ... 70 func NewEngine(cfgPath string, debugMode bool) (*Engine, error) { 71 var err error 72 cfg, err := InitConfig(cfgPath) 73 if err != nil { 74 return nil, err 75 } 76 return NewEngineWithConfig(cfg, debugMode) 77 } 78 79 // Run run run 80 func (e *Engine) Run() { 81 if len(os.Args) > 1 && os.Args[1] == "init" { 82 writeDefaultConfig() 83 return 84 } 85 86 e.mainDebug("CWD: %s", e.config.Root) 87 88 var err error 89 if err = e.checkRunEnv(); err != nil { 90 os.Exit(1) 91 } 92 if err = e.watching(e.config.Root); err != nil { 93 os.Exit(1) 94 } 95 96 e.start() 97 e.cleanup() 98 } 99 100 func (e *Engine) checkRunEnv() error { 101 p := e.config.tmpPath() 102 if _, err := os.Stat(p); os.IsNotExist(err) { 103 e.runnerLog("mkdir %s", p) 104 if err := os.Mkdir(p, 0o755); err != nil { 105 e.runnerLog("failed to mkdir, error: %s", err.Error()) 106 return err 107 } 108 } 109 return nil 110 } 111 112 func (e *Engine) watching(root string) error { 113 return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 114 // NOTE: path is absolute 115 if info != nil && !info.IsDir() { 116 if e.checkIncludeFile(path) { 117 return e.watchPath(path) 118 } 119 return nil 120 } 121 // exclude tmp dir 122 if e.isTmpDir(path) { 123 e.watcherLog("!exclude %s", e.config.rel(path)) 124 return filepath.SkipDir 125 } 126 // exclude testdata dir 127 if e.isTestDataDir(path) { 128 e.watcherLog("!exclude %s", e.config.rel(path)) 129 return filepath.SkipDir 130 } 131 // exclude hidden directories like .git, .idea, etc. 132 if isHiddenDirectory(path) { 133 return filepath.SkipDir 134 } 135 // exclude user specified directories 136 if e.isExcludeDir(path) { 137 e.watcherLog("!exclude %s", e.config.rel(path)) 138 return filepath.SkipDir 139 } 140 isIn, walkDir := e.checkIncludeDir(path) 141 if !walkDir { 142 e.watcherLog("!exclude %s", e.config.rel(path)) 143 return filepath.SkipDir 144 } 145 if isIn { 146 return e.watchPath(path) 147 } 148 return nil 149 }) 150 } 151 152 // cacheFileChecksums calculates and stores checksums for each non-excluded file it finds from root. 153 func (e *Engine) cacheFileChecksums(root string) error { 154 return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 155 if err != nil { 156 if info == nil { 157 return err 158 } 159 if info.IsDir() { 160 return filepath.SkipDir 161 } 162 return err 163 } 164 165 if !info.Mode().IsRegular() { 166 if e.isTmpDir(path) || e.isTestDataDir(path) || isHiddenDirectory(path) || e.isExcludeDir(path) { 167 e.watcherDebug("!exclude checksum %s", e.config.rel(path)) 168 return filepath.SkipDir 169 } 170 171 // Follow symbolic link 172 if e.config.Build.FollowSymlink && (info.Mode()&os.ModeSymlink) > 0 { 173 link, err := filepath.EvalSymlinks(path) 174 if err != nil { 175 return err 176 } 177 linkInfo, err := os.Stat(link) 178 if err != nil { 179 return err 180 } 181 if linkInfo.IsDir() { 182 err = e.watchPath(link) 183 if err != nil { 184 return err 185 } 186 } 187 return nil 188 } 189 } 190 191 if e.isExcludeFile(path) || !e.isIncludeExt(path) { 192 e.watcherDebug("!exclude checksum %s", e.config.rel(path)) 193 return nil 194 } 195 196 excludeRegex, err := e.isExcludeRegex(path) 197 if err != nil { 198 return err 199 } 200 if excludeRegex { 201 e.watcherDebug("!exclude checksum %s", e.config.rel(path)) 202 return nil 203 } 204 205 // update the checksum cache for the current file 206 _ = e.isModified(path) 207 208 return nil 209 }) 210 } 211 212 func (e *Engine) watchPath(path string) error { 213 if err := e.watcher.Add(path); err != nil { 214 e.watcherLog("failed to watch %s, error: %s", path, err.Error()) 215 return err 216 } 217 e.watcherLog("watching %s", e.config.rel(path)) 218 219 go func() { 220 e.withLock(func() { 221 e.watchers++ 222 }) 223 defer func() { 224 e.withLock(func() { 225 e.watchers-- 226 }) 227 }() 228 229 if e.config.Build.ExcludeUnchanged { 230 err := e.cacheFileChecksums(path) 231 if err != nil { 232 e.watcherLog("error building checksum cache: %v", err) 233 } 234 } 235 236 for { 237 select { 238 case <-e.watcherStopCh: 239 return 240 case ev := <-e.watcher.Events(): 241 e.mainDebug("event: %+v", ev) 242 if isDir(ev.Name) { 243 e.watchNewDir(ev.Name, removeEvent(ev)) 244 break 245 } 246 if e.isExcludeFile(ev.Name) { 247 break 248 } 249 excludeRegex, _ := e.isExcludeRegex(ev.Name) 250 if excludeRegex { 251 break 252 } 253 if !e.isIncludeExt(ev.Name) { 254 break 255 } 256 e.watcherDebug("%s has changed", e.config.rel(ev.Name)) 257 e.eventCh <- ev.Name 258 case err := <-e.watcher.Errors(): 259 e.watcherLog("error: %s", err.Error()) 260 } 261 } 262 }() 263 return nil 264 } 265 266 func (e *Engine) watchNewDir(dir string, removeDir bool) { 267 if e.isTmpDir(dir) { 268 return 269 } 270 if e.isTestDataDir(dir) { 271 return 272 } 273 if isHiddenDirectory(dir) || e.isExcludeDir(dir) { 274 e.watcherLog("!exclude %s", e.config.rel(dir)) 275 return 276 } 277 if removeDir { 278 if err := e.watcher.Remove(dir); err != nil { 279 e.watcherLog("failed to stop watching %s, error: %s", dir, err.Error()) 280 } 281 return 282 } 283 go func(dir string) { 284 if err := e.watching(dir); err != nil { 285 e.watcherLog("failed to watching %s, error: %s", dir, err.Error()) 286 } 287 }(dir) 288 } 289 290 func (e *Engine) isModified(filename string) bool { 291 newChecksum, err := fileChecksum(filename) 292 if err != nil { 293 e.watcherDebug("can't determine if file was changed: %v - assuming it did without updating cache", err) 294 return true 295 } 296 297 if e.fileChecksums.updateFileChecksum(filename, newChecksum) { 298 e.watcherDebug("stored checksum for %s: %s", e.config.rel(filename), newChecksum) 299 return true 300 } 301 302 return false 303 } 304 305 // Endless loop and never return 306 func (e *Engine) start() { 307 e.running = true 308 firstRunCh := make(chan bool, 1) 309 firstRunCh <- true 310 311 for { 312 var filename string 313 314 select { 315 case <-e.exitCh: 316 e.mainDebug("exit in start") 317 return 318 case filename = <-e.eventCh: 319 if !e.isIncludeExt(filename) { 320 continue 321 } 322 if e.config.Build.ExcludeUnchanged { 323 if !e.isModified(filename) { 324 e.mainLog("skipping %s because contents unchanged", e.config.rel(filename)) 325 continue 326 } 327 } 328 329 time.Sleep(e.config.buildDelay()) 330 e.flushEvents() 331 332 if e.config.Screen.ClearOnRebuild { 333 if e.config.Screen.KeepScroll { 334 // https://stackoverflow.com/questions/22891644/how-can-i-clear-the-terminal-screen-in-go 335 fmt.Print("\033[2J") 336 } else { 337 // https://stackoverflow.com/questions/5367068/clear-a-terminal-screen-for-real/5367075#5367075 338 fmt.Print("\033c") 339 } 340 } 341 342 e.mainLog("%s has changed", e.config.rel(filename)) 343 case <-firstRunCh: 344 // go down 345 } 346 347 // already build and run now 348 select { 349 case <-e.buildRunCh: 350 e.buildRunStopCh <- true 351 default: 352 } 353 354 // if current app is running, stop it 355 e.withLock(func() { 356 close(e.binStopCh) 357 e.binStopCh = make(chan bool) 358 }) 359 go e.buildRun() 360 } 361 } 362 363 func (e *Engine) buildRun() { 364 e.buildRunCh <- true 365 defer func() { 366 <-e.buildRunCh 367 }() 368 369 select { 370 case <-e.buildRunStopCh: 371 return 372 case <-e.canExit: 373 default: 374 } 375 var err error 376 if err = e.building(); err != nil { 377 e.canExit <- true 378 e.buildLog("failed to build, error: %s", err.Error()) 379 _ = e.writeBuildErrorLog(err.Error()) 380 if e.config.Build.StopOnError { 381 return 382 } 383 } 384 385 select { 386 case <-e.buildRunStopCh: 387 return 388 case <-e.exitCh: 389 e.mainDebug("exit in buildRun") 390 close(e.canExit) 391 return 392 default: 393 } 394 if err = e.runBin(); err != nil { 395 e.runnerLog("failed to run, error: %s", err.Error()) 396 } 397 } 398 399 func (e *Engine) flushEvents() { 400 for { 401 select { 402 case <-e.eventCh: 403 e.mainDebug("flushing events") 404 default: 405 return 406 } 407 } 408 } 409 410 func (e *Engine) building() error { 411 var err error 412 e.buildLog("building...") 413 cmd, stdout, stderr, err := e.startCmd(e.config.Build.Cmd) 414 if err != nil { 415 return err 416 } 417 defer func() { 418 stdout.Close() 419 stderr.Close() 420 }() 421 _, _ = io.Copy(os.Stdout, stdout) 422 _, _ = io.Copy(os.Stderr, stderr) 423 // wait for building 424 err = cmd.Wait() 425 if err != nil { 426 return err 427 } 428 return nil 429 } 430 431 func (e *Engine) runBin() error { 432 // control killFunc should be kill or not 433 killCh := make(chan struct{}) 434 wg := sync.WaitGroup{} 435 go func() { 436 // listen to binStopCh 437 // cleanup() will close binStopCh when engine stop 438 // start() will close binStopCh when file changed 439 <-e.binStopCh 440 close(killCh) 441 442 select { 443 case <-e.exitCh: 444 wg.Wait() 445 close(e.canExit) 446 default: 447 } 448 }() 449 450 killFunc := func(cmd *exec.Cmd, stdout io.ReadCloser, stderr io.ReadCloser, killCh chan struct{}, processExit chan struct{}, wg *sync.WaitGroup) { 451 defer wg.Done() 452 select { 453 // the process haven't exited yet, kill it 454 case <-killCh: 455 break 456 457 // the process is exited, return 458 case <-processExit: 459 return 460 } 461 462 e.mainDebug("trying to kill pid %d, cmd %+v", cmd.Process.Pid, cmd.Args) 463 defer func() { 464 stdout.Close() 465 stderr.Close() 466 }() 467 pid, err := e.killCmd(cmd) 468 if err != nil { 469 e.mainDebug("failed to kill PID %d, error: %s", pid, err.Error()) 470 if cmd.ProcessState != nil && !cmd.ProcessState.Exited() { 471 os.Exit(1) 472 } 473 } else { 474 e.mainDebug("cmd killed, pid: %d", pid) 475 } 476 cmdBinPath := cmdPath(e.config.rel(e.config.binPath())) 477 if _, err = os.Stat(cmdBinPath); os.IsNotExist(err) { 478 return 479 } 480 if err = os.Remove(cmdBinPath); err != nil { 481 e.mainLog("failed to remove %s, error: %s", e.config.rel(e.config.binPath()), err) 482 } 483 } 484 485 e.runnerLog("running...") 486 go func() { 487 for { 488 select { 489 case <-killCh: 490 return 491 default: 492 command := strings.Join(append([]string{e.config.Build.Bin}, e.runArgs...), " ") 493 cmd, stdout, stderr, _ := e.startCmd(command) 494 processExit := make(chan struct{}) 495 e.mainDebug("running process pid %v", cmd.Process.Pid) 496 497 wg.Add(1) 498 atomic.AddUint64(&e.round, 1) 499 go killFunc(cmd, stdout, stderr, killCh, processExit, &wg) 500 501 _, _ = io.Copy(os.Stdout, stdout) 502 _, _ = io.Copy(os.Stderr, stderr) 503 _, _ = cmd.Process.Wait() 504 close(processExit) 505 506 if !e.config.Build.Rerun { 507 return 508 } 509 time.Sleep(e.config.rerunDelay()) 510 } 511 } 512 }() 513 514 return nil 515 } 516 517 func (e *Engine) cleanup() { 518 e.mainLog("cleaning...") 519 defer e.mainLog("see you again~") 520 521 e.withLock(func() { 522 close(e.binStopCh) 523 e.binStopCh = make(chan bool) 524 }) 525 e.mainDebug("wating for close watchers..") 526 527 e.withLock(func() { 528 for i := 0; i < int(e.watchers); i++ { 529 e.watcherStopCh <- true 530 } 531 }) 532 533 e.mainDebug("waiting for buildRun...") 534 var err error 535 if err = e.watcher.Close(); err != nil { 536 e.mainLog("failed to close watcher, error: %s", err.Error()) 537 } 538 539 e.mainDebug("waiting for clean ...") 540 541 if e.config.Misc.CleanOnExit { 542 e.mainLog("deleting %s", e.config.tmpPath()) 543 if err = os.RemoveAll(e.config.tmpPath()); err != nil { 544 e.mainLog("failed to delete tmp dir, err: %+v", err) 545 } 546 } 547 548 e.mainDebug("waiting for exit...") 549 550 <-e.canExit 551 e.running = false 552 e.mainDebug("exited") 553 } 554 555 // Stop the air 556 func (e *Engine) Stop() { 557 close(e.exitCh) 558 }