From fd96b63c3eaf3a731f999fa56f4058628e9b7b38 Mon Sep 17 00:00:00 2001 From: nxtcoder17 Date: Sat, 21 Sep 2024 15:36:16 +0530 Subject: [PATCH 1/5] feat: nix flake based development setup --- .envrc | 1 + .gitignore | 1 + flake.lock | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 33 ++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..a5dbbcb --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake . diff --git a/.gitignore b/.gitignore index 16af516..7478b4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea bin/ +.direnv diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1426038 --- /dev/null +++ b/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "id": "flake-utils", + "type": "indirect" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1726243404, + "narHash": "sha256-sjiGsMh+1cWXb53Tecsm4skyFNag33GPbVgCdfj3n9I=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "345c263f2f53a3710abe117f28a5cb86d0ba4059", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..3edc589 --- /dev/null +++ b/flake.nix @@ -0,0 +1,33 @@ +{ + description = "fwatcher dev workspace"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + devShells.default = pkgs.mkShell { + # hardeningDisable = [ "all" ]; + + buildInputs = with pkgs; [ + # source version control + git + pre-commit + + go_1_22 + upx + ]; + + shellHook = '' + # ''; + }; + } + ); +} + + From 0a6166f3d43c79847389b0eab24d33f47cee6e9c Mon Sep 17 00:00:00 2001 From: nxtcoder17 Date: Sat, 21 Sep 2024 15:39:20 +0530 Subject: [PATCH 2/5] chore: migrating logger from zerolog to `log/slog` --- pkg/logging/log-levels.go | 10 +++++ pkg/logging/logger.go | 86 +++++++++++++++++++++++++++++++-------- pkg/logging/zerolog.go | 53 ------------------------ 3 files changed, 78 insertions(+), 71 deletions(-) create mode 100644 pkg/logging/log-levels.go delete mode 100644 pkg/logging/zerolog.go diff --git a/pkg/logging/log-levels.go b/pkg/logging/log-levels.go new file mode 100644 index 0000000..32d1ed0 --- /dev/null +++ b/pkg/logging/log-levels.go @@ -0,0 +1,10 @@ +package logging + +import "log/slog" + +// Define a custom Trace level +const ( + DebugVerbose1Level = slog.Level(-4) + DebugVerbose2Level = slog.Level(-8) + DebugVerbose3Level = slog.Level(-12) +) diff --git a/pkg/logging/logger.go b/pkg/logging/logger.go index bdf6e02..8072f3c 100644 --- a/pkg/logging/logger.go +++ b/pkg/logging/logger.go @@ -1,25 +1,75 @@ package logging -type LogLevel string - -const ( - TraceLevel = "TRACE" - DebugLevel = "DEBUG" - InfoLevel = "INFO" - WarnLevel = "WARN" - ErrorLevel = "ERROR" - PanicLevel = "PANIC" +import ( + "io" + "log/slog" + "os" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/log" ) -type Logger interface { - SetLogLevel(lvl LogLevel) - Info(msg string) - Error(err error) - ErrorStack(err error) - ErrorWrap(err error, msg string) - Debug(msg string) +type SlogOptions struct { + Writer io.Writer + Prefix string + + ShowTimestamp bool + ShowCaller bool + ShowDebugLogs bool + + SetAsDefaultLogger bool } -func NewLogger() Logger { - return newZeroLogger() +func NewSlogLogger(opts SlogOptions) *slog.Logger { + // INFO: force colored output, otherwise honor the env-var `CLICOLOR_FORCE` + if _, ok := os.LookupEnv("CLICOLOR_FORCE"); !ok { + os.Setenv("CLICOLOR_FORCE", "1") + } + + if opts.Writer == nil { + opts.Writer = os.Stderr + } + + level := log.InfoLevel + if opts.ShowDebugLogs { + level = log.DebugLevel + } + + logger := log.NewWithOptions(opts.Writer, log.Options{ + ReportCaller: opts.ShowCaller, + ReportTimestamp: opts.ShowTimestamp, + Prefix: opts.Prefix, + Level: level, + }) + + styles := log.DefaultStyles() + // styles.Caller = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Dark: "#5b717f", Light: "#36cbfa"}).Faint(true) + styles.Caller = lipgloss.NewStyle().Foreground(lipgloss.Color("#878a8a")) + + styles.Levels[log.DebugLevel] = styles.Levels[log.DebugLevel].Foreground(lipgloss.Color("#5b717f")) + + styles.Levels[log.InfoLevel] = styles.Levels[log.InfoLevel].Foreground(lipgloss.Color("#36cbfa")) + + // BUG: due to a bug in termenv, adaptive colors don't work within tmux + // it always selects the dark variant + + // styles.Levels[log.InfoLevel] = styles.Levels[log.InfoLevel].Foreground(lipgloss.AdaptiveColor{ + // Light: string(lipgloss.Color("#36cbfa")), + // Dark: string(lipgloss.Color("#608798")), + // }) + + styles.Key = lipgloss.NewStyle().Foreground(lipgloss.Color("#36cbfa")).Bold(true) + + logger.SetStyles(styles) + + // output := termenv.NewOutput(os.Stdout, termenv.WithProfile(termenv.TrueColor)) + // logger.Info("theme", "fg", output.ForegroundColor(), "bg", output.BackgroundColor(), "has-dark", output.HasDarkBackground()) + + l := slog.New(logger) + + if opts.SetAsDefaultLogger { + slog.SetDefault(l) + } + + return l } diff --git a/pkg/logging/zerolog.go b/pkg/logging/zerolog.go deleted file mode 100644 index 9fb8b39..0000000 --- a/pkg/logging/zerolog.go +++ /dev/null @@ -1,53 +0,0 @@ -package logging - -import ( - "github.com/rs/zerolog" - "os" -) - -type lgr struct { - logger zerolog.Logger - lvlMap map[LogLevel]zerolog.Level -} - -func (l lgr) SetLogLevel(lvl LogLevel) { - zerolog.SetGlobalLevel(l.lvlMap[lvl]) -} - -func (l lgr) ErrorWrap(err error, msg string) { - l.logger.Err(err).Msg(msg) -} - -func (l lgr) Debug(msg string) { - l.logger.Debug().Msg(msg) -} - -func (l lgr) ErrorStack(err error) { - l.logger.Error().Stack().Err(err).Msg("") -} - -func (l lgr) Error(err error) { - l.logger.Err(err).Msg("") -} - -func (l lgr) Info(msg string) { - l.logger.Info().Msgf(msg) -} - -func newZeroLogger() Logger { - zerolog.TimeFieldFormat = zerolog.TimeFormatUnix - zerolog.SetGlobalLevel(zerolog.DebugLevel) - consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout} - logger := zerolog.New(consoleWriter).With().Timestamp().Logger() - - lvlMap := map[LogLevel]zerolog.Level{ - TraceLevel: zerolog.TraceLevel, - DebugLevel: zerolog.DebugLevel, - InfoLevel: zerolog.InfoLevel, - WarnLevel: zerolog.WarnLevel, - ErrorLevel: zerolog.ErrorLevel, - PanicLevel: zerolog.PanicLevel, - } - - return &lgr{logger: logger, lvlMap: lvlMap} -} From 61bca9cdbdb2f85b887d6397c47a175eb5683fec Mon Sep 17 00:00:00 2001 From: nxtcoder17 Date: Sat, 21 Sep 2024 15:41:03 +0530 Subject: [PATCH 3/5] feat: global ignore dirs, and refactors to watcher, improved logging --- pkg/fs-watcher/watcher.go | 199 ------------------------------ pkg/watcher/global-ignore-dirs.go | 10 ++ pkg/watcher/watcher.go | 197 +++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 199 deletions(-) delete mode 100644 pkg/fs-watcher/watcher.go create mode 100644 pkg/watcher/global-ignore-dirs.go create mode 100644 pkg/watcher/watcher.go diff --git a/pkg/fs-watcher/watcher.go b/pkg/fs-watcher/watcher.go deleted file mode 100644 index 6d37f4a..0000000 --- a/pkg/fs-watcher/watcher.go +++ /dev/null @@ -1,199 +0,0 @@ -package fs_watcher - -import ( - "fmt" - "github.com/fsnotify/fsnotify" - "github.com/nxtcoder17/fwatcher/pkg/logging" - "io/fs" - "log" - "os" - "path/filepath" - "strings" - "time" -) - -type Watcher interface { - Close() error - Add(dir ...string) error - RecursiveAdd(dir ...string) error - WatchEvents(func(event Event, fp string) error) -} - -type eventInfo struct { - Time time.Time - FileInfo os.FileInfo - Counter int -} - -type fsnWatcher struct { - watcher *fsnotify.Watcher - eventMap map[string]eventInfo - logger logging.Logger - watchExtensions []string - ignoreExtensions []string - excludeDirs []string -} - -type Event fsnotify.Event - -var ( - Create = fsnotify.Create - Delete = fsnotify.Remove - Update = fsnotify.Write - Rename = fsnotify.Rename - Chmod = fsnotify.Chmod -) - -func (f fsnWatcher) WatchEvents(watcherFunc func(event Event, fp string) error) { - f.eventMap = map[string]eventInfo{} - for { - select { - case event, ok := <-f.watcher.Events: - { - if !ok { - return - } - - t := time.Now() - f.logger.Debug(fmt.Sprintf("event %+v received", event)) - - shouldIgnore := false - - for _, v := range f.ignoreExtensions { - if strings.HasSuffix(event.Name, v) { - f.logger.Debug(fmt.Sprintf("event occured on file %q, ignoring due to ignored extension: %q", event.Name, v)) - shouldIgnore = true - break - } - } - - for _, v := range f.excludeDirs { - absV := v - if !filepath.IsAbs(v) { - cwd, _ := os.Getwd() - absV = filepath.Join(cwd, v) - } - - if strings.HasPrefix(event.Name, absV) { - f.logger.Debug(fmt.Sprintf("event occured on file %q, ignoring due to excluded directory: %q", event.Name, v)) - shouldIgnore = true - } - } - - if shouldIgnore { - continue - } - - shouldWatch := true - if len(f.watchExtensions) > 0 { - shouldWatch = false - } - - for i := range f.watchExtensions { - if strings.HasSuffix(event.Name, f.watchExtensions[i]) { - shouldWatch = true - break - } - } - - if !shouldWatch { - continue - } - - eInfo, ok := f.eventMap[event.Name] - if !ok { - eInfo = eventInfo{Time: time.Time{}, FileInfo: nil, Counter: 0} - } - eInfo.Counter += 1 - f.eventMap[event.Name] = eInfo - - if time.Now().Sub(eInfo.Time) < 1*time.Second { - f.logger.Debug(fmt.Sprintf("too many events (%d) under 1s ... ignoring", eInfo.Counter)) - continue - } - - //lstat, err := os.Lstat(event.Name) - //if err != nil { - // f.logger.Error(err) - // return - //} - //f.eventMap[event.Name] = eventInfo{Time: time.Now(), FileInfo: lstat} - // f.eventMap[event.Name] = eInfo - - //if eInfo.FileInfo != nil && lstat.Size() == eInfo.FileInfo.Size() { - // f.logger.Debug(fmt.Sprintf("%s has not changed", event.Name)) - // continue - //} - - if err := watcherFunc(Event(event), event.Name); err != nil { - f.logger.Error(err) - return - } - eInfo.Time = time.Now() - eInfo.Counter = 0 - f.eventMap[event.Name] = eInfo - - tDiff := time.Now().Sub(t).Milliseconds() - f.logger.Debug(fmt.Sprintf("[TIME TAKEN] to process watch loop: %dms", tDiff)) - } - case err, ok := <-f.watcher.Errors: - if !ok { - return - } - f.logger.Error(err) - } - } -} - -func (f fsnWatcher) RecursiveAdd(dirs ...string) error { - for i := range dirs { - if err := filepath.WalkDir(dirs[i], func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { - return f.watcher.Add(path) - } - return nil - }); err != nil { - return err - } - } - return nil -} - -func (f fsnWatcher) Add(dir ...string) error { - for i := range dir { - err := f.watcher.Add(dir[i]) - if err != nil { - log.Println(err) - return err - } - } - return nil -} - -func (f fsnWatcher) Close() error { - return f.watcher.Close() -} - -type WatcherCtx struct { - Logger logging.Logger - WatchExtensions []string - IgnoreExtensions []string - ExcludeDirs []string -} - -func NewWatcher(ctx WatcherCtx) Watcher { - watcher, err := fsnotify.NewWatcher() - if err != nil { - log.Fatal(err) - } - return &fsnWatcher{ - watcher: watcher, - logger: ctx.Logger, - watchExtensions: ctx.WatchExtensions, - ignoreExtensions: ctx.IgnoreExtensions, - excludeDirs: ctx.ExcludeDirs, - } -} diff --git a/pkg/watcher/global-ignore-dirs.go b/pkg/watcher/global-ignore-dirs.go new file mode 100644 index 0000000..48997ea --- /dev/null +++ b/pkg/watcher/global-ignore-dirs.go @@ -0,0 +1,10 @@ +package watcher + +var globalExcludeDirs = []string{ + ".git", ".svn", ".hg", // version control + ".idea", ".vscode", // IDEs + ".direnv", // direnv nix guys + "node_modules", // node + ".DS_Store", // macOS + ".log", // logs +} diff --git a/pkg/watcher/watcher.go b/pkg/watcher/watcher.go new file mode 100644 index 0000000..cae3022 --- /dev/null +++ b/pkg/watcher/watcher.go @@ -0,0 +1,197 @@ +package watcher + +import ( + "fmt" + "log/slog" + "os" + "path/filepath" + "strings" + "time" + + "github.com/fsnotify/fsnotify" +) + +type Watcher interface { + Close() error + RecursiveAdd(dir ...string) error + WatchEvents(func(event Event, fp string) error) +} + +type eventInfo struct { + Time time.Time + FileInfo os.FileInfo + Counter int +} + +type fsnWatcher struct { + watcher *fsnotify.Watcher + eventMap map[string]eventInfo + + directoryCount int + + Logger *slog.Logger + IgnoreSuffixes []string + ExcludeDirs map[string]struct{} +} + +type Event fsnotify.Event + +var ( + Create = fsnotify.Create + Delete = fsnotify.Remove + Update = fsnotify.Write + Rename = fsnotify.Rename + Chmod = fsnotify.Chmod +) + +func (f fsnWatcher) ignoreEvent(event fsnotify.Event) bool { + // Vim/Neovim creates this temporary file to see whether it can write + // into a target directory. It screws up our watching algorithm, + // so ignore it. + // [source](https://brandur.org/live-reload) + if filepath.Base(event.Name) == "4913" { + return true + } + + // Special case for Vim: + // vim creates files with ~ suffixes, which we don't want to watch. + if strings.HasSuffix(event.Name, "~") { + return true + } + + for _, suffix := range f.IgnoreSuffixes { + if strings.HasSuffix(event.Name, suffix) { + f.Logger.Debug("file is ignored", "file", event.Name) + return true + } + } + return false +} + +func (f *fsnWatcher) WatchEvents(watcherFunc func(event Event, fp string) error) { + f.eventMap = map[string]eventInfo{} + for { + select { + case event, ok := <-f.watcher.Events: + { + if !ok { + return + } + + t := time.Now() + f.Logger.Debug(fmt.Sprintf("event %+v received", event)) + + if f.ignoreEvent(event) { + continue + } + + eInfo, ok := f.eventMap[event.Name] + if !ok { + eInfo = eventInfo{Time: time.Time{}, FileInfo: nil, Counter: 0} + } + eInfo.Counter += 1 + f.eventMap[event.Name] = eInfo + + if time.Since(eInfo.Time) < 1*time.Second { + f.Logger.Debug("too many events under 1s, ignoring...", "counter", eInfo.Counter) + continue + } + + if err := watcherFunc(Event(event), event.Name); err != nil { + f.Logger.Error("while processing event, got", "err", err) + return + } + eInfo.Time = time.Now() + eInfo.Counter = 0 + f.eventMap[event.Name] = eInfo + + f.Logger.Debug("watch loop completed", "took", fmt.Sprintf("%dms", time.Since(t).Milliseconds())) + } + case err, ok := <-f.watcher.Errors: + if !ok { + return + } + f.Logger.Error("watcher error", "err", err) + } + } +} + +func (f *fsnWatcher) RecursiveAdd(dirs ...string) error { + for _, dir := range dirs { + fi, err := os.Lstat(dir) + if err != nil { + return err + } + + if !fi.IsDir() { + continue + } + + if _, ok := f.ExcludeDirs[filepath.Base(dir)]; ok { + f.Logger.Debug("EXCLUDED from watchlist", "dir", dir) + continue + } + + f.addToWatchList(dir) + + ls, err := os.ReadDir(dir) + if err != nil { + return err + } + + de := make([]string, 0, len(ls)) + for _, l := range ls { // TODO: use filepath.WalkDir + if !l.IsDir() { + continue + } + de = append(de, filepath.Join(dir, l.Name())) + } + + f.RecursiveAdd(de...) + } + + return nil +} + +func (f *fsnWatcher) addToWatchList(dir string) error { + if err := f.watcher.Add(dir); err != nil { + f.Logger.Error("failed to add directory", "dir", dir, "err", err) + return err + } + f.directoryCount++ + f.Logger.Debug("ADDED to watchlist", "dir", dir, "count", f.directoryCount) + return nil +} + +func (f *fsnWatcher) Close() error { + return f.watcher.Close() +} + +type WatcherArgs struct { + Logger *slog.Logger + IgnoreSuffixes []string + ExcludeDirs []string + UseDefaultIgnoreList bool +} + +func NewWatcher(args WatcherArgs) (Watcher, error) { + if args.Logger == nil { + args.Logger = slog.Default() + } + + if args.UseDefaultIgnoreList { + args.ExcludeDirs = append(args.ExcludeDirs, globalExcludeDirs...) + } + + excludeDirs := map[string]struct{}{} + for _, dir := range args.ExcludeDirs { + excludeDirs[dir] = struct{}{} + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + args.Logger.Error("failed to create watcher, got", "err", err) + return nil, err + } + return &fsnWatcher{watcher: watcher, Logger: args.Logger, ExcludeDirs: excludeDirs, IgnoreSuffixes: args.IgnoreSuffixes}, nil +} From 66bc7578cb50808283248cee700dc939a3bce633 Mon Sep 17 00:00:00 2001 From: nxtcoder17 Date: Sat, 21 Sep 2024 15:42:03 +0530 Subject: [PATCH 4/5] feat: program executor extracted out to a separate package --- main.go | 194 ++++++++++++++++++++----------------------- pkg/executor/exec.go | 86 +++++++++++++++++++ 2 files changed, 177 insertions(+), 103 deletions(-) create mode 100644 pkg/executor/exec.go diff --git a/main.go b/main.go index d845bf3..4aa2089 100644 --- a/main.go +++ b/main.go @@ -3,72 +3,22 @@ package main import ( "context" "fmt" - "log" "os" "os/exec" "os/signal" + "path/filepath" "strings" - "syscall" "time" - fswatcher "github.com/nxtcoder17/fwatcher/pkg/fs-watcher" + "github.com/nxtcoder17/fwatcher/pkg/executor" "github.com/nxtcoder17/fwatcher/pkg/logging" + fswatcher "github.com/nxtcoder17/fwatcher/pkg/watcher" "github.com/urfave/cli/v2" ) -type ProgramManager struct { - done chan os.Signal - logger logging.Logger -} - -func NewProgramManager(logger logging.Logger) *ProgramManager { - done := make(chan os.Signal, 1) - signal.Notify(done, syscall.SIGTERM) - - return &ProgramManager{ - done: done, - logger: logger, - } -} - -func (pm *ProgramManager) Exec(execCmd string) error { - ctx, cancelFunc := context.WithCancel(context.Background()) - defer cancelFunc() - - cmd := exec.CommandContext(ctx, "sh", "-c", execCmd) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - - if err := cmd.Start(); err != nil { - return err - } - - defer func() { - syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) - }() - - go func() { - select { - case <-pm.done: - cancelFunc() - } - }() - - if err := cmd.Wait(); err != nil { - if strings.HasPrefix(err.Error(), "signal:") { - pm.logger.Debug(fmt.Sprintf("wait terminated as %s received", err.Error())) - } - return err - } - - return nil -} - func main() { - logger := logging.NewLogger() + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) + defer stop() app := &cli.App{ Name: "fwatcher", @@ -80,11 +30,18 @@ func main() { Required: false, Value: false, }, + &cli.BoolFlag{ + Name: "no-default-ignore", + Usage: "disables ignoring from default ignore list", + Required: false, + Aliases: []string{"I"}, + Value: false, + }, &cli.StringFlag{ - Name: "exec", + Name: "command", Usage: "specifies command to execute on file change", - Required: true, - // Aliases: []string{"exec"}, + Required: false, + Aliases: []string{"c"}, }, &cli.PathFlag{ Name: "dir", @@ -101,69 +58,103 @@ func main() { EnvVars: nil, }, &cli.StringSliceFlag{ - Name: "extensions", - Usage: "file extensions to watch on", + Name: "ignore-suffixes", + Usage: "files suffixes to ignore", Required: false, - Aliases: []string{"ext"}, - }, - &cli.StringSliceFlag{ - Name: "ignore-extensions", - Usage: "file extensions to ignore watching on", - Required: false, - Aliases: []string{"iext"}, + Aliases: []string{"i"}, }, &cli.StringSliceFlag{ Name: "exclude-dir", Usage: "directory to exclude from watching", Required: false, - Aliases: []string{"exclude"}, + Aliases: []string{"x", "e"}, + }, + &cli.BoolFlag{ + Name: "no-default-ignore", + Usage: "disables ignoring from default ignore list", + Required: false, + Aliases: []string{"I"}, + Value: false, }, }, - Action: func(ctx *cli.Context) error { - logger.SetLogLevel(logging.InfoLevel) - isDebugMode := ctx.Bool("debug") - if isDebugMode { - logger.SetLogLevel(logging.DebugLevel) - } + Action: func(cctx *cli.Context) error { + logger := logging.NewSlogLogger(logging.SlogOptions{ + ShowTimestamp: false, + ShowCaller: false, + ShowDebugLogs: cctx.Bool("debug"), + SetAsDefaultLogger: true, + }) - pm := NewProgramManager(logger) + var execCmd string + var execArgs []string + + if cctx.String("command") != "" { + s := strings.SplitN(cctx.String("command"), " ", 2) + + switch len(s) { + case 0: + return fmt.Errorf("invalid command") + case 1: + execCmd = s[0] + execArgs = nil + case 2: + execCmd = s[0] + execArgs = strings.Split(s[1], " ") + } + } else { + logger.Debug("no command specified, using args") + if cctx.Args().Len() == 0 { + return fmt.Errorf("no command specified") + } - execCmd := ctx.String("exec") + execCmd = cctx.Args().First() + execArgs = cctx.Args().Tail() + } - watchExt := ctx.StringSlice("extensions") - ignoreExt := ctx.StringSlice("ignore-extensions") - excludeDirs := ctx.StringSlice("exclude-dir") + ex := executor.NewExecutor(ctx, executor.ExecutorArgs{ + Logger: logger, + Command: func(context.Context) *exec.Cmd { + cmd := exec.Command(execCmd, execArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + return cmd + }, + }) - watcher := fswatcher.NewWatcher(fswatcher.WatcherCtx{ - Logger: logger, - WatchExtensions: watchExt, - IgnoreExtensions: ignoreExt, - ExcludeDirs: excludeDirs, + watcher, err := fswatcher.NewWatcher(fswatcher.WatcherArgs{ + Logger: logger, + IgnoreSuffixes: cctx.StringSlice("ignore-suffixes"), + ExcludeDirs: cctx.StringSlice("exclude-dir"), + UseDefaultIgnoreList: !cctx.Bool("no-global-ignore"), }) - if err := watcher.RecursiveAdd(ctx.String("dir")); err != nil { + if err != nil { panic(err) } - go pm.Exec(execCmd) - defer func() { - pm.done <- syscall.SIGKILL - }() + if err := watcher.RecursiveAdd(cctx.String("dir")); err != nil { + panic(err) + } + + go ex.Exec() go func() { - select { - case <-ctx.Done(): - print("ending ...") - pm.done <- syscall.SIGKILL - time.Sleep(100 * time.Millisecond) - os.Exit(0) - } + <-ctx.Done() + logger.Debug("fwatcher is closing ...") + <-time.After(200 * time.Millisecond) + os.Exit(0) }() watcher.WatchEvents(func(event fswatcher.Event, fp string) error { - pm.done <- syscall.SIGKILL - logger.Info(fmt.Sprintf("[RELOADING] due changes in %s", fp)) - go pm.Exec(execCmd) + relPath, err := filepath.Rel(cctx.String("dir"), fp) + if err != nil { + return err + } + logger.Info(fmt.Sprintf("[RELOADING] due changes in %s", relPath)) + ex.Kill() + <-time.After(100 * time.Millisecond) + go ex.Exec() return nil }) @@ -171,10 +162,7 @@ func main() { }, } - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) - defer stop() - if err := app.RunContext(ctx, os.Args); err != nil { - log.Fatal(err) + os.Exit(1) } } diff --git a/pkg/executor/exec.go b/pkg/executor/exec.go new file mode 100644 index 0000000..6ffae6b --- /dev/null +++ b/pkg/executor/exec.go @@ -0,0 +1,86 @@ +package executor + +import ( + "context" + "log/slog" + "os" + "os/exec" + "os/signal" + "strings" + "sync" + "syscall" +) + +type Executor struct { + ctx context.Context + done chan os.Signal + logger *slog.Logger + + newCmd func(context.Context) *exec.Cmd + mu sync.Mutex +} + +type ExecutorArgs struct { + Logger *slog.Logger + Command func(context.Context) *exec.Cmd +} + +func NewExecutor(ctx context.Context, args ExecutorArgs) *Executor { + done := make(chan os.Signal, 1) + signal.Notify(done, syscall.SIGTERM) + + if args.Logger == nil { + args.Logger = slog.Default() + } + + return &Executor{ + done: done, + logger: args.Logger, + ctx: ctx, + newCmd: args.Command, + mu: sync.Mutex{}, + } +} + +func (ex *Executor) Exec() error { + ex.mu.Lock() + defer ex.mu.Unlock() + + ex.logger.Debug("[exec] starting process") + + ctx, cf := context.WithCancel(ex.ctx) + defer cf() + + cmd := ex.newCmd(ctx) + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + if err := cmd.Start(); err != nil { + return err + } + + go func() { + select { + case <-ctx.Done(): + ex.logger.Debug("process terminated", "pid", cmd.Process.Pid) + case <-ex.done: + ex.logger.Debug("executor terminated", "pid", cmd.Process.Pid) + } + ex.logger.Debug("[exec] killing process", "pid", cmd.Process.Pid) + if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); err != nil { + ex.logger.Error("failed to kill process", "pid", cmd.Process.Pid, "err", err) + } + }() + + if err := cmd.Wait(); err != nil { + if strings.HasPrefix(err.Error(), "signal:") { + ex.logger.Debug("wait terminated, received", "signal", err.Error()) + } + return err + } + + return nil +} + +func (ex *Executor) Kill() { + ex.done <- os.Signal(syscall.SIGTERM) +} From 446dff82e58c2cd29dc7e1c727037a44ac413e44 Mon Sep 17 00:00:00 2001 From: nxtcoder17 Date: Sat, 21 Sep 2024 15:43:12 +0530 Subject: [PATCH 5/5] chore: go mod tidy --- go.mod | 16 ++++++++++++---- go.sum | 43 ++++++++++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 9549c83..dd43907 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,24 @@ module github.com/nxtcoder17/fwatcher go 1.20 require ( + github.com/charmbracelet/lipgloss v0.13.0 + github.com/charmbracelet/log v0.4.0 github.com/fsnotify/fsnotify v1.6.0 - github.com/rs/zerolog v1.29.0 github.com/urfave/cli/v2 v2.24.3 ) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/x/ansi v0.1.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/sys v0.19.0 // indirect ) diff --git a/go.sum b/go.sum index 14766ca..c45d7f5 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,41 @@ -github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= +github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= +github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= +github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= -github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0= github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=