github.com/zhb127/air@v0.0.2-0.20231109030911-fb911e430cdd/runner/engine.go (about)

     1  package runner
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"log"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  	"sync"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	"github.com/gohugoio/hugo/watcher/filenotify"
    16  )
    17  
    18  // Engine ...
    19  type Engine struct {
    20  	config    *Config
    21  	logger    *logger
    22  	watcher   filenotify.FileWatcher
    23  	debugMode bool
    24  	runArgs   []string
    25  	running   bool
    26  
    27  	eventCh        chan string
    28  	watcherStopCh  chan bool
    29  	buildRunCh     chan bool
    30  	buildRunStopCh 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  		binStopCh:      make(chan bool),
    60  		exitCh:         make(chan bool),
    61  		fileChecksums:  &checksumMap{m: make(map[string]string)},
    62  		watchers:       0,
    63  	}
    64  
    65  	return &e, nil
    66  }
    67  
    68  // NewEngine ...
    69  func NewEngine(cfgPath string, debugMode bool) (*Engine, error) {
    70  	var err error
    71  	cfg, err := InitConfig(cfgPath)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  	return NewEngineWithConfig(cfg, debugMode)
    76  }
    77  
    78  // Run run run
    79  func (e *Engine) Run() {
    80  	if len(os.Args) > 1 && os.Args[1] == "init" {
    81  		configName, err := writeDefaultConfig()
    82  		if err != nil {
    83  			log.Fatalf("Failed writing default config: %+v", err)
    84  		}
    85  		fmt.Printf("%s file created to the current directory with the default settings\n", configName)
    86  		return
    87  	}
    88  
    89  	e.mainDebug("CWD: %s", e.config.Root)
    90  
    91  	var err error
    92  	if err = e.checkRunEnv(); err != nil {
    93  		os.Exit(1)
    94  	}
    95  	if err = e.watching(e.config.Root); err != nil {
    96  		os.Exit(1)
    97  	}
    98  
    99  	e.start()
   100  	e.cleanup()
   101  }
   102  
   103  func (e *Engine) checkRunEnv() error {
   104  	p := e.config.tmpPath()
   105  	if _, err := os.Stat(p); os.IsNotExist(err) {
   106  		e.runnerLog("mkdir %s", p)
   107  		if err := os.Mkdir(p, 0o755); err != nil {
   108  			e.runnerLog("failed to mkdir, error: %s", err.Error())
   109  			return err
   110  		}
   111  	}
   112  	return nil
   113  }
   114  
   115  func (e *Engine) watching(root string) error {
   116  	return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
   117  		// NOTE: path is absolute
   118  		if info != nil && !info.IsDir() {
   119  			if e.checkIncludeFile(path) {
   120  				return e.watchPath(path)
   121  			}
   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.watchPath(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.watchPath(link)
   186  					if err != nil {
   187  						return err
   188  					}
   189  				}
   190  				return nil
   191  			}
   192  		}
   193  
   194  		if e.isExcludeFile(path) || !e.isIncludeExt(path) && !e.checkIncludeFile(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) watchPath(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) && !e.checkIncludeFile(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) && !e.checkIncludeFile(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  			// cannot set buldDelay to 0, because when the write mutiple events received in short time
   336  			// it will start Multiple buildRuns: https://github.com/cosmtrek/air/issues/473
   337  			time.Sleep(e.config.buildDelay())
   338  			e.flushEvents()
   339  
   340  			if e.config.Screen.ClearOnRebuild {
   341  				if e.config.Screen.KeepScroll {
   342  					// https://stackoverflow.com/questions/22891644/how-can-i-clear-the-terminal-screen-in-go
   343  					fmt.Print("\033[2J")
   344  				} else {
   345  					// https://stackoverflow.com/questions/5367068/clear-a-terminal-screen-for-real/5367075#5367075
   346  					fmt.Print("\033c")
   347  				}
   348  			}
   349  
   350  			e.mainLog("%s has changed", e.config.rel(filename))
   351  		case <-firstRunCh:
   352  			// go down
   353  		}
   354  
   355  		// already build and run now
   356  		select {
   357  		case <-e.buildRunCh:
   358  			e.buildRunStopCh <- true
   359  		default:
   360  		}
   361  
   362  		// if current app is running, stop it
   363  		e.withLock(func() {
   364  			close(e.binStopCh)
   365  			e.binStopCh = make(chan bool)
   366  		})
   367  		go e.buildRun()
   368  	}
   369  }
   370  
   371  func (e *Engine) buildRun() {
   372  	e.buildRunCh <- true
   373  	defer func() {
   374  		<-e.buildRunCh
   375  	}()
   376  
   377  	select {
   378  	case <-e.buildRunStopCh:
   379  		return
   380  	default:
   381  	}
   382  	var err error
   383  	if err = e.runPreCmd(); err != nil {
   384  		e.runnerLog("failed to execute pre_cmd: %s", err.Error())
   385  		if e.config.Build.StopOnError {
   386  			return
   387  		}
   388  	}
   389  	if err = e.building(); err != nil {
   390  		e.buildLog("failed to build, error: %s", err.Error())
   391  		_ = e.writeBuildErrorLog(err.Error())
   392  		if e.config.Build.StopOnError {
   393  			return
   394  		}
   395  	}
   396  
   397  	select {
   398  	case <-e.buildRunStopCh:
   399  		return
   400  	case <-e.exitCh:
   401  		e.mainDebug("exit in buildRun")
   402  		return
   403  	default:
   404  	}
   405  	if err = e.runBin(); err != nil {
   406  		e.runnerLog("failed to run, error: %s", err.Error())
   407  	}
   408  }
   409  
   410  func (e *Engine) flushEvents() {
   411  	for {
   412  		select {
   413  		case <-e.eventCh:
   414  			e.mainDebug("flushing events")
   415  		default:
   416  			return
   417  		}
   418  	}
   419  }
   420  
   421  // utility to execute commands, such as cmd & pre_cmd
   422  func (e *Engine) runCommand(command string) error {
   423  	cmd, stdout, stderr, err := e.startCmd(command)
   424  	if err != nil {
   425  		return err
   426  	}
   427  	defer func() {
   428  		stdout.Close()
   429  		stderr.Close()
   430  	}()
   431  	_, _ = io.Copy(os.Stdout, stdout)
   432  	_, _ = io.Copy(os.Stderr, stderr)
   433  	// wait for command to finish
   434  	err = cmd.Wait()
   435  	if err != nil {
   436  		return err
   437  	}
   438  	return nil
   439  }
   440  
   441  // run cmd option in .air.toml
   442  func (e *Engine) building() error {
   443  	e.buildLog("building...")
   444  	err := e.runCommand(e.config.Build.Cmd)
   445  	if err != nil {
   446  		return err
   447  	}
   448  	return nil
   449  }
   450  
   451  // run pre_cmd option in .air.toml
   452  func (e *Engine) runPreCmd() error {
   453  	for _, command := range e.config.Build.PreCmd {
   454  		e.runnerLog("> %s", command)
   455  		err := e.runCommand(command)
   456  		if err != nil {
   457  			return err
   458  		}
   459  	}
   460  	return nil
   461  }
   462  
   463  // run post_cmd option in .air.toml
   464  func (e *Engine) runPostCmd() error {
   465  	for _, command := range e.config.Build.PostCmd {
   466  		e.runnerLog("> %s", command)
   467  		err := e.runCommand(command)
   468  		if err != nil {
   469  			return err
   470  		}
   471  	}
   472  	return nil
   473  }
   474  
   475  func (e *Engine) runBin() error {
   476  	killFunc := func(cmd *exec.Cmd, stdout io.ReadCloser, stderr io.ReadCloser, killCh chan struct{}, processExit chan struct{}, wg *sync.WaitGroup) {
   477  		defer wg.Done()
   478  		select {
   479  		// listen to binStopCh
   480  		// cleanup() will close binStopCh when engine stop
   481  		// start() will close binStopCh when file changed
   482  		case <-e.binStopCh:
   483  			close(killCh)
   484  			break
   485  
   486  		// the process is exited, return
   487  		case <-processExit:
   488  			return
   489  		}
   490  
   491  		e.mainDebug("trying to kill pid %d, cmd %+v", cmd.Process.Pid, cmd.Args)
   492  		defer func() {
   493  			stdout.Close()
   494  			stderr.Close()
   495  		}()
   496  		pid, err := e.killCmd(cmd)
   497  		if err != nil {
   498  			e.mainDebug("failed to kill PID %d, error: %s", pid, err.Error())
   499  			if cmd.ProcessState != nil && !cmd.ProcessState.Exited() {
   500  				os.Exit(1)
   501  			}
   502  		} else {
   503  			e.mainDebug("cmd killed, pid: %d", pid)
   504  		}
   505  		cmdBinPath := cmdPath(e.config.rel(e.config.binPath()))
   506  		if _, err = os.Stat(cmdBinPath); os.IsNotExist(err) {
   507  			return
   508  		}
   509  		if err = os.Remove(cmdBinPath); err != nil {
   510  			e.mainLog("failed to remove %s, error: %s", e.config.rel(e.config.binPath()), err)
   511  		}
   512  	}
   513  
   514  	e.runnerLog("running...")
   515  	go func() {
   516  		wg := sync.WaitGroup{}
   517  
   518  		defer func() {
   519  			select {
   520  			case <-e.exitCh:
   521  				e.mainDebug("exit in runBin")
   522  				wg.Wait()
   523  			default:
   524  			}
   525  		}()
   526  
   527  		// control killFunc should be kill or not
   528  		killCh := make(chan struct{})
   529  		for {
   530  			select {
   531  			case <-killCh:
   532  				return
   533  			default:
   534  				command := strings.Join(append([]string{e.config.Build.Bin}, e.runArgs...), " ")
   535  				cmd, stdout, stderr, _ := e.startCmd(command)
   536  				processExit := make(chan struct{})
   537  				e.mainDebug("running process pid %v", cmd.Process.Pid)
   538  
   539  				wg.Add(1)
   540  				atomic.AddUint64(&e.round, 1)
   541  				e.withLock(func() {
   542  					close(e.binStopCh)
   543  					e.binStopCh = make(chan bool)
   544  					go killFunc(cmd, stdout, stderr, killCh, processExit, &wg)
   545  				})
   546  
   547  				go func() {
   548  					_, _ = io.Copy(os.Stdout, stdout)
   549  					_, _ = cmd.Process.Wait()
   550  				}()
   551  
   552  				go func() {
   553  					_, _ = io.Copy(os.Stderr, stderr)
   554  					_, _ = cmd.Process.Wait()
   555  				}()
   556  				state, _ := cmd.Process.Wait()
   557  				close(processExit)
   558  				switch state.ExitCode() {
   559  				case 0:
   560  					e.runnerLog("Process Exit with Code 0")
   561  				case -1:
   562  					// because when we use ctrl + c to stop will return -1
   563  				default:
   564  					e.runnerLog("Process Exit with Code: %v", state.ExitCode())
   565  				}
   566  
   567  				if !e.config.Build.Rerun {
   568  					return
   569  				}
   570  				time.Sleep(e.config.rerunDelay())
   571  			}
   572  		}
   573  	}()
   574  
   575  	return nil
   576  }
   577  
   578  func (e *Engine) cleanup() {
   579  	e.mainLog("cleaning...")
   580  	defer e.mainLog("see you again~")
   581  
   582  	e.withLock(func() {
   583  		close(e.binStopCh)
   584  		e.binStopCh = make(chan bool)
   585  	})
   586  	e.mainDebug("wating for	close watchers..")
   587  
   588  	e.withLock(func() {
   589  		for i := 0; i < int(e.watchers); i++ {
   590  			e.watcherStopCh <- true
   591  		}
   592  	})
   593  
   594  	e.mainDebug("waiting for buildRun...")
   595  	var err error
   596  	if err = e.watcher.Close(); err != nil {
   597  		e.mainLog("failed to close watcher, error: %s", err.Error())
   598  	}
   599  
   600  	e.mainDebug("waiting for clean ...")
   601  
   602  	if e.config.Misc.CleanOnExit {
   603  		e.mainLog("deleting %s", e.config.tmpPath())
   604  		if err = os.RemoveAll(e.config.tmpPath()); err != nil {
   605  			e.mainLog("failed to delete tmp dir, err: %+v", err)
   606  		}
   607  	}
   608  
   609  	e.mainDebug("waiting for exit...")
   610  
   611  	e.running = false
   612  	e.mainDebug("exited")
   613  }
   614  
   615  // Stop the air
   616  func (e *Engine) Stop() {
   617  	if err := e.runPostCmd(); err != nil {
   618  		e.runnerLog("failed to execute post_cmd, error: %s", err.Error())
   619  	}
   620  	close(e.exitCh)
   621  }