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