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  }