github.com/fairyhunter13/air@v1.40.5/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 "time" 12 13 "github.com/gohugoio/hugo/watcher/filenotify" 14 ) 15 16 // Engine ... 17 type Engine struct { 18 config *Config 19 logger *logger 20 watcher filenotify.FileWatcher 21 debugMode bool 22 runArgs []string 23 running bool 24 25 eventCh chan string 26 watcherStopCh chan bool 27 buildRunCh chan bool 28 buildRunStopCh chan bool 29 canExit chan bool 30 binStopCh chan bool 31 exitCh chan bool 32 33 mu sync.RWMutex 34 watchers uint 35 fileChecksums *checksumMap 36 37 ll sync.Mutex // lock for logger 38 } 39 40 // NewEngineWithConfig ... 41 func NewEngineWithConfig(cfg *Config, debugMode bool) (*Engine, error) { 42 var ( 43 logger = newLogger(cfg) 44 watcher filenotify.FileWatcher 45 err error 46 ) 47 if cfg != nil && cfg.Polling.Enabled { 48 watcher = filenotify.NewPollingWatcher(cfg.GetPollingInterval()) 49 } else { 50 watcher, err = filenotify.NewEventWatcher() 51 } 52 if err != nil { 53 return nil, err 54 } 55 e := Engine{ 56 config: cfg, 57 logger: logger, 58 watcher: watcher, 59 debugMode: debugMode, 60 runArgs: cfg.Build.ArgsBin, 61 eventCh: make(chan string, 1000), 62 watcherStopCh: make(chan bool, 10), 63 buildRunCh: make(chan bool, 1), 64 buildRunStopCh: make(chan bool, 1), 65 canExit: make(chan bool, 1), 66 binStopCh: make(chan bool), 67 exitCh: make(chan bool), 68 fileChecksums: &checksumMap{m: make(map[string]string)}, 69 watchers: 0, 70 } 71 72 return &e, nil 73 } 74 75 // NewEngine ... 76 func NewEngine(cfgPath string, debugMode bool) (*Engine, error) { 77 var err error 78 cfg, err := InitConfig(cfgPath) 79 if err != nil { 80 return nil, err 81 } 82 return NewEngineWithConfig(cfg, debugMode) 83 } 84 85 // Run run run 86 func (e *Engine) Run() { 87 if len(os.Args) > 1 && os.Args[1] == "init" { 88 writeDefaultConfig() 89 return 90 } 91 92 e.mainDebug("CWD: %s", e.config.Root) 93 94 var err error 95 if err = e.checkRunEnv(); err != nil { 96 os.Exit(1) 97 } 98 if err = e.watching(e.config.Root); err != nil { 99 os.Exit(1) 100 } 101 102 e.start() 103 e.cleanup() 104 } 105 106 func (e *Engine) checkRunEnv() error { 107 p := e.config.tmpPath() 108 if _, err := os.Stat(p); os.IsNotExist(err) { 109 e.runnerLog("mkdir %s", p) 110 if err := os.Mkdir(p, 0o755); err != nil { 111 e.runnerLog("failed to mkdir, error: %s", err.Error()) 112 return err 113 } 114 } 115 return nil 116 } 117 118 func (e *Engine) watching(root string) error { 119 return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 120 // NOTE: path is absolute 121 if info != nil && !info.IsDir() { 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.watchDir(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.watchDir(link) 186 if err != nil { 187 return err 188 } 189 } 190 return nil 191 } 192 } 193 194 if e.isExcludeFile(path) || !e.isIncludeExt(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) watchDir(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) { 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) { 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 time.Sleep(e.config.buildDelay()) 336 e.flushEvents() 337 338 // clean on rebuild https://stackoverflow.com/questions/22891644/how-can-i-clear-the-terminal-screen-in-go 339 if e.config.Screen.ClearOnRebuild { 340 fmt.Println("\033[2J") 341 } 342 343 e.mainLog("%s has changed", e.config.rel(filename)) 344 case <-firstRunCh: 345 // go down 346 break 347 } 348 349 // already build and run now 350 select { 351 case <-e.buildRunCh: 352 e.buildRunStopCh <- true 353 default: 354 } 355 356 // if current app is running, stop it 357 e.withLock(func() { 358 close(e.binStopCh) 359 e.binStopCh = make(chan bool) 360 }) 361 go e.buildRun() 362 } 363 } 364 365 func (e *Engine) buildRun() { 366 e.buildRunCh <- true 367 defer func() { 368 <-e.buildRunCh 369 }() 370 371 select { 372 case <-e.buildRunStopCh: 373 return 374 case <-e.canExit: 375 default: 376 } 377 var err error 378 if err = e.building(); err != nil { 379 e.canExit <- true 380 e.buildLog("failed to build, error: %s", err.Error()) 381 _ = e.writeBuildErrorLog(err.Error()) 382 if e.config.Build.StopOnError { 383 return 384 } 385 } 386 387 select { 388 case <-e.buildRunStopCh: 389 return 390 case <-e.exitCh: 391 e.mainDebug("exit in buildRun") 392 close(e.canExit) 393 return 394 default: 395 } 396 if err = e.runBin(); err != nil { 397 e.runnerLog("failed to run, error: %s", err.Error()) 398 } 399 } 400 401 func (e *Engine) flushEvents() { 402 for { 403 select { 404 case <-e.eventCh: 405 e.mainDebug("flushing events") 406 default: 407 return 408 } 409 } 410 } 411 412 func (e *Engine) building() error { 413 var err error 414 e.buildLog("building...") 415 cmd, stdout, stderr, err := e.startCmd(e.config.Build.Cmd) 416 if err != nil { 417 return err 418 } 419 defer func() { 420 _ = stdout.Close() 421 _ = stderr.Close() 422 }() 423 _, _ = io.Copy(os.Stdout, stdout) 424 _, _ = io.Copy(os.Stderr, stderr) 425 // wait for building 426 err = cmd.Wait() 427 if err != nil { 428 return err 429 } 430 return nil 431 } 432 433 func (e *Engine) runBin() error { 434 var err error 435 e.runnerLog("running...") 436 437 command := strings.Join(append([]string{e.config.Build.Bin}, e.runArgs...), " ") 438 cmd, stdout, stderr, err := e.startCmd(command) 439 if err != nil { 440 return err 441 } 442 go func() { 443 _, _ = io.Copy(os.Stdout, stdout) 444 _, _ = io.Copy(os.Stderr, stderr) 445 _, _ = cmd.Process.Wait() 446 }() 447 448 killFunc := func(cmd *exec.Cmd, stdout io.ReadCloser, stderr io.ReadCloser) { 449 defer func() { 450 select { 451 case <-e.exitCh: 452 e.mainDebug("exit in killFunc") 453 close(e.canExit) 454 default: 455 } 456 }() 457 // when invoke close() it will return 458 <-e.binStopCh 459 e.mainDebug("trying to kill pid %d, cmd %+v", cmd.Process.Pid, cmd.Args) 460 defer func() { 461 _ = stdout.Close() 462 _ = stderr.Close() 463 }() 464 pid, err := e.killCmd(cmd) 465 if err != nil { 466 e.mainDebug("failed to kill PID %d, error: %s", pid, err.Error()) 467 if cmd.ProcessState != nil && !cmd.ProcessState.Exited() { 468 os.Exit(1) 469 } 470 } else { 471 e.mainDebug("cmd killed, pid: %d", pid) 472 } 473 cmdBinPath := cmdPath(e.config.rel(e.config.binPath())) 474 if _, err = os.Stat(cmdBinPath); os.IsNotExist(err) { 475 return 476 } 477 if err = os.Remove(cmdBinPath); err != nil { 478 e.mainLog("failed to remove %s, error: %s", e.config.rel(e.config.binPath()), err) 479 } 480 } 481 e.withLock(func() { 482 close(e.binStopCh) 483 e.binStopCh = make(chan bool) 484 go killFunc(cmd, stdout, stderr) 485 }) 486 e.mainDebug("running process pid %v", cmd.Process.Pid) 487 return nil 488 } 489 490 func (e *Engine) cleanup() { 491 e.mainLog("cleaning...") 492 defer e.mainLog("see you again~") 493 494 e.withLock(func() { 495 close(e.binStopCh) 496 e.binStopCh = make(chan bool) 497 }) 498 e.mainDebug("wating for close watchers..") 499 500 e.withLock(func() { 501 for i := 0; i < int(e.watchers); i++ { 502 e.watcherStopCh <- true 503 } 504 }) 505 506 e.mainDebug("waiting for buildRun...") 507 var err error 508 if err = e.watcher.Close(); err != nil { 509 e.mainLog("failed to close watcher, error: %s", err.Error()) 510 } 511 512 e.mainDebug("waiting for clean ...") 513 514 if e.config.Misc.CleanOnExit { 515 e.mainLog("deleting %s", e.config.tmpPath()) 516 if err = os.RemoveAll(e.config.tmpPath()); err != nil { 517 e.mainLog("failed to delete tmp dir, err: %+v", err) 518 } 519 } 520 521 e.mainDebug("waiting for exit...") 522 523 <-e.canExit 524 e.running = false 525 e.mainDebug("exited") 526 } 527 528 // Stop the air 529 func (e *Engine) Stop() { 530 close(e.exitCh) 531 }