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