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  }