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