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