core/run_dev.go
Rama dev · réplica de lectura del repositorio original.
package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"github.com/fsnotify/fsnotify"
)
var (
isDev = false
ROOT_PATH = GetCwdPath()
)
// FileWatcher structure
type FileWatcher struct {
watcher *fsnotify.Watcher // File system watcher
rootPath string // Root directory to monitor
scriptPath string // Script path to execute
extensions []string // File extensions to monitor
mu sync.Mutex // Mutex to prevent concurrent script execution
isExecuting bool // Flag indicating if script is executing
debounceMap map[string]int64 // Debounce map, recording last modification time of each file
debounceMs int64 // Debounce time interval (milliseconds)
logger *log.Logger // Logger
delay time.Duration // Delay before executing the script
timer *time.Timer // Timer for debouncing
}
// NewFileWatcher creates a new file watcher
func NewFileWatcher(rootPath string, scriptPath string, extensions []string) (*FileWatcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, fmt.Errorf("failed to create file watcher: %w", err)
}
return &FileWatcher{
watcher: watcher,
rootPath: rootPath,
scriptPath: scriptPath,
extensions: extensions,
debounceMap: make(map[string]int64),
debounceMs: 500, // Default debounce time: 500 milliseconds
logger: log.New(os.Stdout, "[FileWatcher] ", log.LstdFlags),
delay: time.Second,
timer: time.NewTimer(time.Second),
}, nil
}
// Start begins monitoring file changes
func (fw *FileWatcher) Start(ctx context.Context) error {
// Add all directories to the watcher
if err := fw.addDirsRecursively(fw.rootPath); err != nil {
return err
}
fw.logger.Printf("Started monitoring directory: %s", fw.rootPath)
fw.logger.Printf("Script to execute: %s", fw.scriptPath)
fw.logger.Printf("Monitoring file types: %v", fw.extensions)
// Start the monitoring loop
go fw.watchLoop(ctx)
return nil
}
// Stop stops monitoring
func (fw *FileWatcher) Stop() {
fw.watcher.Close()
}
// addDirsRecursively recursively adds directories to the watcher
func (fw *FileWatcher) addDirsRecursively(root string) error {
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip hidden directories and files
if strings.HasPrefix(filepath.Base(path), ".") {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
// Skip vendor and node_modules directories
if info.IsDir() && (filepath.Base(path) == "vendor" || filepath.Base(path) == "node_modules") {
return filepath.SkipDir
}
// Only watch directory changes
if info.IsDir() {
if err := fw.watcher.Add(path); err != nil {
return fmt.Errorf("failed to add directory %s to watcher: %w", path, err)
}
}
return nil
})
}
// watchLoop monitoring loop
func (fw *FileWatcher) watchLoop(ctx context.Context) {
for {
select {
case event, ok := <-fw.watcher.Events:
if !ok {
return
}
// Only focus on write and create events
if event.Op&(fsnotify.Write|fsnotify.Create) == 0 {
continue
}
// If a new directory is created, add it to the watcher
if fi, err := os.Stat(event.Name); err == nil && fi.IsDir() {
fw.watcher.Add(event.Name)
continue
}
// Check if file extension matches
if !fw.hasMatchingExtension(event.Name) {
continue
}
// Apply debounce logic
if fw.shouldDebounce(event.Name) {
continue
}
fw.timer.Reset(fw.delay)
// Execute script
fw.logger.Printf("Detected file change: %s", event.Name)
go fw.executeScript(ctx)
case err, ok := <-fw.watcher.Errors:
if !ok {
return
}
fw.logger.Printf("Watcher error: %v", err)
}
}
}
// hasMatchingExtension checks if file has a matching extension
func (fw *FileWatcher) hasMatchingExtension(filename string) bool {
ext := strings.ToLower(filepath.Ext(filename))
for _, e := range fw.extensions {
if ext == e {
return true
}
// Check if the extension is a directory (e.g., "public/dist/")
if strings.HasSuffix(e, "/") && strings.HasPrefix(filename, filepath.Join(ROOT_PATH, e)) {
return true
}
}
return false
}
// shouldDebounce checks if debouncing should be applied (avoid multiple executions in short time)
func (fw *FileWatcher) shouldDebounce(filename string) bool {
fw.mu.Lock()
defer fw.mu.Unlock()
now := time.Now().UnixMilli()
lastTime, exists := fw.debounceMap[filename]
// Update last modification time
fw.debounceMap[filename] = now
// If it's the first modification or beyond debounce time, don't debounce
if !exists || now-lastTime > fw.debounceMs {
return false
}
return true
}
// executeScript executes the script
func (fw *FileWatcher) executeScript(ctx context.Context) {
<-fw.timer.C
fw.mu.Lock()
if fw.isExecuting {
fw.mu.Unlock()
return
}
fw.isExecuting = true
fw.mu.Unlock()
defer func() {
fw.mu.Lock()
fw.isExecuting = false
fw.mu.Unlock()
}()
// Check if script exists
if _, err := os.Stat(fw.scriptPath); os.IsNotExist(err) {
fw.logger.Printf("Script does not exist: %s", fw.scriptPath)
return
}
// Create command
cmd := exec.Command("/bin/bash", fw.scriptPath)
cmd.Env = os.Environ()
// Get standard output and error output
stdout, err := cmd.StdoutPipe()
if err != nil {
fw.logger.Printf("Failed to get standard output: %v", err)
return
}
stderr, err := cmd.StderrPipe()
if err != nil {
fw.logger.Printf("Failed to get error output: %v", err)
return
}
// Start command
fw.logger.Printf("Executing script: %s", fw.scriptPath)
if err := cmd.Start(); err != nil {
fw.logger.Printf("Failed to start script: %v", err)
return
}
// Real-time reading and output of standard output
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
fmt.Println("►", scanner.Text())
}
}()
// Real-time reading and output of error output
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
fmt.Println("⚠", scanner.Text())
}
}()
// Wait for command to complete
if err := cmd.Wait(); err != nil {
fw.logger.Printf("Script execution failed: %v", err)
return
}
fw.logger.Printf("Script execution completed")
}
// GetCwdPath Get current working directory
func GetCwdPath() string {
if len(os.Args) == 0 {
p, _ := os.Getwd()
return p
}
// Get executable path
exePath := filepath.Dir(os.Args[0])
// Check if it is a development environment
if isDev || strings.Contains(filepath.ToSlash(exePath), "/go-build") {
isDev = true
p, _ := os.Getwd()
return p
}
// Get absolute path
p, _ := filepath.Abs(exePath)
return p
}
// AbsPath Get absolute path
func AbsPath(p string) string {
// In Linux, if the path starts with "/", it is an absolute path
if strings.HasPrefix(p, "/") {
return p
}
// In Windows, if the path starts with "C:", it is an absolute path
if len(p) > 1 && p[1] == ':' {
return p
}
p, _ = filepath.Abs(filepath.Join(ROOT_PATH, p))
return p
}
func main() {
ctx := context.Background()
logger := log.New(os.Stdout, "[Main] ", log.LstdFlags)
// Arguments for root path and script path
var rootPath, scriptPath string
// Get command line arguments
args := os.Args
if len(args) < 3 {
rootPath = "." // Default root path
scriptPath = "./run_dev.sh" // Default script path
} else {
rootPath = args[1]
scriptPath = args[2]
}
// Create file watcher
watcher, err := NewFileWatcher(AbsPath(rootPath), AbsPath(scriptPath), []string{".go", ".html", "public/dist/"})
if err != nil {
logger.Fatalf("Failed to create file watcher: %v", err)
}
defer watcher.Stop()
// Start monitoring
if err := watcher.Start(ctx); err != nil {
logger.Fatalf("Failed to start monitoring: %v", err)
}
logger.Printf("File watcher started, press Ctrl+C to stop")
// Keep the main goroutine running
select {}
}