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