github.com/gofiber/fiber-cli@v0.0.3/cmd/dev.go (about)

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"io/ioutil"
     7  	"log"
     8  	"os"
     9  	"os/exec"
    10  	"os/signal"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strconv"
    14  	"strings"
    15  	"sync/atomic"
    16  	"syscall"
    17  	"time"
    18  
    19  	"github.com/fsnotify/fsnotify"
    20  	"github.com/spf13/cobra"
    21  )
    22  
    23  var c config
    24  
    25  func init() {
    26  	devCmd.PersistentFlags().StringVarP(&c.root, "root", "r", ".",
    27  		"root path for watch, all files must be under root")
    28  	devCmd.PersistentFlags().StringVarP(&c.target, "target", "t", ".",
    29  		"target path for go build")
    30  	devCmd.PersistentFlags().StringSliceVarP(&c.extensions, "extensions", "e",
    31  		[]string{"go", "tmpl", "tpl", "html"}, "file extensions to watch")
    32  	devCmd.PersistentFlags().StringSliceVarP(&c.excludeDirs, "exclude_dirs", "D",
    33  		[]string{"assets", "tmp", "vendor", "node_modules"}, "ignore these directories")
    34  	devCmd.PersistentFlags().StringSliceVarP(&c.excludeFiles, "exclude_files", "F", nil, "ignore these files")
    35  	devCmd.PersistentFlags().DurationVarP(&c.delay, "delay", "d", time.Second,
    36  		"delay to trigger rerun")
    37  }
    38  
    39  // devCmd reruns the fiber project if watched files changed
    40  var devCmd = &cobra.Command{
    41  	Use:   "dev",
    42  	Short: "Rerun the fiber project if watched files changed",
    43  	RunE:  devRunE,
    44  }
    45  
    46  func devRunE(_ *cobra.Command, _ []string) error {
    47  	return newEscort(c).run()
    48  }
    49  
    50  type config struct {
    51  	root         string
    52  	target       string
    53  	binPath      string
    54  	extensions   []string
    55  	excludeDirs  []string
    56  	excludeFiles []string
    57  	delay        time.Duration
    58  }
    59  
    60  type escort struct {
    61  	config
    62  
    63  	ctx       context.Context
    64  	terminate context.CancelFunc
    65  
    66  	w             *fsnotify.Watcher
    67  	watcherEvents chan fsnotify.Event
    68  	watcherErrors chan error
    69  	sig           chan os.Signal
    70  
    71  	binPath    string
    72  	bin        *exec.Cmd
    73  	stdoutPipe io.ReadCloser
    74  	stderrPipe io.ReadCloser
    75  	hitCh      chan struct{}
    76  	hitFunc    func()
    77  	compiling  atomic.Value
    78  }
    79  
    80  func newEscort(c config) *escort {
    81  	return &escort{
    82  		config: c,
    83  		hitCh:  make(chan struct{}, 1),
    84  		sig:    make(chan os.Signal, 1),
    85  	}
    86  }
    87  
    88  func (e *escort) run() (err error) {
    89  	if err = e.init(); err != nil {
    90  		return
    91  	}
    92  
    93  	log.Println("Welcome to fiber dev 👋")
    94  
    95  	defer func() {
    96  		_ = e.w.Close()
    97  		_ = os.Remove(e.binPath)
    98  	}()
    99  
   100  	go e.runBin()
   101  	go e.watchingBin()
   102  	go e.watchingFiles()
   103  
   104  	signal.Notify(e.sig, syscall.SIGTERM, syscall.SIGINT, os.Interrupt)
   105  	<-e.sig
   106  
   107  	e.terminate()
   108  
   109  	log.Println("See you next time 👋")
   110  
   111  	return nil
   112  }
   113  
   114  func (e *escort) init() (err error) {
   115  	if e.w, err = fsnotify.NewWatcher(); err != nil {
   116  		return
   117  	}
   118  
   119  	e.watcherEvents = e.w.Events
   120  	e.watcherErrors = e.w.Errors
   121  
   122  	e.ctx, e.terminate = context.WithCancel(context.Background())
   123  
   124  	// normalize root
   125  	if e.root, err = filepath.Abs(e.root); err != nil {
   126  		return
   127  	}
   128  
   129  	// create bin target
   130  	var f *os.File
   131  	if f, err = ioutil.TempFile("", ""); err != nil {
   132  		return
   133  	}
   134  	defer func() {
   135  		if e := f.Close(); e != nil {
   136  			err = e
   137  		}
   138  	}()
   139  
   140  	e.binPath = f.Name()
   141  	if runtime.GOOS == "windows" {
   142  		e.binPath += ".exe"
   143  	}
   144  
   145  	e.hitFunc = e.runBin
   146  
   147  	return
   148  }
   149  
   150  func (e *escort) watchingFiles() {
   151  	// walk root and add all dirs
   152  	e.walkForWatcher(e.root)
   153  
   154  	var (
   155  		info os.FileInfo
   156  		err  error
   157  	)
   158  
   159  	for {
   160  		select {
   161  		case <-e.ctx.Done():
   162  			return
   163  		case event := <-e.watcherEvents:
   164  			p, op := event.Name, event.Op
   165  
   166  			// ignore chmod
   167  			if isChmoded(op) {
   168  				continue
   169  			}
   170  
   171  			if isRemoved(op) {
   172  				e.tryRemoveWatch(p)
   173  				continue
   174  			}
   175  
   176  			if info, err = os.Stat(p); err != nil {
   177  				log.Printf("Failed to get info of %s: %s\n", p, err)
   178  				continue
   179  			}
   180  
   181  			base := filepath.Base(p)
   182  
   183  			if info.IsDir() && isCreated(op) {
   184  				e.walkForWatcher(p)
   185  				e.hitCh <- struct{}{}
   186  				continue
   187  			}
   188  
   189  			if e.ignoredFiles(base) {
   190  				continue
   191  			}
   192  
   193  			if e.hitExtension(filepath.Ext(base)) {
   194  				e.hitCh <- struct{}{}
   195  			}
   196  		case err := <-e.watcherErrors:
   197  			log.Printf("Watcher error: %v\n", err)
   198  		}
   199  	}
   200  }
   201  
   202  func (e *escort) watchingBin() {
   203  	var timer *time.Timer
   204  	for range e.hitCh {
   205  		// reset timer
   206  		if timer != nil && !timer.Stop() {
   207  			select {
   208  			case <-timer.C:
   209  			default:
   210  			}
   211  		}
   212  		timer = time.AfterFunc(e.delay, e.hitFunc)
   213  	}
   214  }
   215  
   216  func (e *escort) runBin() {
   217  	if ok := e.compiling.Load(); ok != nil && ok.(bool) {
   218  		return
   219  	}
   220  
   221  	e.compiling.Store(true)
   222  	defer e.compiling.Store(false)
   223  
   224  	if e.bin != nil {
   225  		e.cleanOldBin()
   226  		log.Println("Recompiling...")
   227  	} else {
   228  		log.Println("Compiling...")
   229  	}
   230  
   231  	start := time.Now()
   232  
   233  	// build target
   234  	compile := execCommand("go", "build", "-o", e.binPath, e.target)
   235  	if out, err := compile.CombinedOutput(); err != nil {
   236  		log.Printf("Failed to compile %s: %s\n", e.target, out)
   237  		return
   238  	}
   239  
   240  	log.Printf("Compile done in %s!\n", formatLatency(time.Since(start)))
   241  
   242  	e.bin = execCommand(e.binPath)
   243  
   244  	e.bin.Env = os.Environ()
   245  
   246  	e.watchingPipes()
   247  
   248  	if err := e.bin.Start(); err != nil {
   249  		log.Printf("Failed to start bin: %s\n", err)
   250  		e.bin = nil
   251  		return
   252  	}
   253  
   254  	log.Println("New pid is", e.bin.Process.Pid)
   255  }
   256  
   257  func (e *escort) cleanOldBin() {
   258  	defer func() {
   259  		if e.stdoutPipe != nil {
   260  			_ = e.stdoutPipe.Close()
   261  		}
   262  		if e.stderrPipe != nil {
   263  			_ = e.stderrPipe.Close()
   264  		}
   265  	}()
   266  
   267  	pid := e.bin.Process.Pid
   268  	log.Println("Killing old pid", pid)
   269  
   270  	var err error
   271  	if runtime.GOOS == "windows" {
   272  		err = execCommand("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(pid)).Run()
   273  	} else {
   274  		err = e.bin.Process.Kill()
   275  		_, _ = e.bin.Process.Wait()
   276  	}
   277  
   278  	if err != nil {
   279  		log.Printf("Failed to kill old pid %d: %s\n", pid, err)
   280  	}
   281  
   282  	e.bin = nil
   283  }
   284  
   285  func (e *escort) watchingPipes() {
   286  	var err error
   287  	if e.stdoutPipe, err = e.bin.StdoutPipe(); err != nil {
   288  		log.Printf("Failed to get stdout pipe: %s", err)
   289  	} else {
   290  		go func() { _, _ = io.Copy(os.Stdout, e.stdoutPipe) }()
   291  	}
   292  
   293  	if e.stderrPipe, err = e.bin.StderrPipe(); err != nil {
   294  		log.Printf("Failed to get stderr pipe: %s", err)
   295  	} else {
   296  		go func() { _, _ = io.Copy(os.Stderr, e.stderrPipe) }()
   297  	}
   298  }
   299  
   300  func (e *escort) walkForWatcher(root string) {
   301  	if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
   302  		if err != nil {
   303  			return err
   304  		}
   305  
   306  		if info != nil && !info.IsDir() {
   307  			return nil
   308  		}
   309  
   310  		base := filepath.Base(path)
   311  
   312  		if e.ignoredDirs(base) {
   313  			return filepath.SkipDir
   314  		}
   315  
   316  		log.Println("Add", path, "to watch")
   317  		return e.w.Add(path)
   318  	}); err != nil {
   319  		log.Printf("Failed to walk root %s: %s\n", e.root, err)
   320  	}
   321  }
   322  
   323  func (e *escort) tryRemoveWatch(p string) {
   324  	if err := e.w.Remove(p); err != nil && !strings.Contains(err.Error(), "non-existent") {
   325  		log.Printf("Failed to remove %s from watch: %s\n", p, err)
   326  	}
   327  }
   328  
   329  func (e *escort) hitExtension(ext string) bool {
   330  	if ext == "" {
   331  		return false
   332  	}
   333  	// remove '.'
   334  	ext = ext[1:]
   335  	for _, e := range e.extensions {
   336  		if ext == e {
   337  			return true
   338  		}
   339  	}
   340  
   341  	return false
   342  }
   343  
   344  func (e *escort) ignoredDirs(dir string) bool {
   345  	// exclude hidden directories like .git, .idea, etc.
   346  	if len(dir) > 1 && dir[0] == '.' {
   347  		return true
   348  	}
   349  
   350  	for _, d := range e.excludeDirs {
   351  		if dir == d {
   352  			return true
   353  		}
   354  	}
   355  
   356  	return false
   357  }
   358  
   359  func (e *escort) ignoredFiles(filename string) bool {
   360  	for _, f := range e.excludeFiles {
   361  		if filename == f {
   362  			return true
   363  		}
   364  	}
   365  
   366  	return false
   367  }
   368  
   369  func isRemoved(op fsnotify.Op) bool {
   370  	return op&fsnotify.Remove != 0
   371  }
   372  
   373  func isCreated(op fsnotify.Op) bool {
   374  	return op&fsnotify.Create != 0
   375  }
   376  
   377  func isChmoded(op fsnotify.Op) bool {
   378  	return op&fsnotify.Chmod != 0
   379  }