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 }