Skip to content

Commit

Permalink
Complete tool with working config example.
Browse files Browse the repository at this point in the history
Signed-off-by: Vaibhav <[email protected]>
  • Loading branch information
vrongmeal committed Feb 23, 2020
1 parent a4da64a commit 73857c8
Show file tree
Hide file tree
Showing 183 changed files with 45,738 additions and 66 deletions.
34 changes: 34 additions & 0 deletions .leaf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Leaf configuration file.

# Root directory to watch.
# Defaults to current working directory.
root: "."

# Exclude directories while watching.
# If certain directories are not excluded, it might reach a limitation where watcher doesn't start.
exclude:
- ".git/"
- "vendor/"
- "build/"

# Filters to apply on the watch.
# Filters starting with '+' are includent and then with '-' are excluded.
# This is not like exclude, these are still being watched yet can be excluded from the execution.
# These can include any filepath regex supported by "filepath".Match method or even a directory.
filters:
- "- .git*"
- "- .go*"
- "- .golangci.yml"
- "- go.*"
- "- Makefile"
- "- LICENSE"
- "- README.md"

# Commands to be executed.
# These are run in the provided order.
exec:
- ["make", "format"]
- ["make", "build"]

# Delay after which commands are executed.
delay: '1s'
54 changes: 53 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
package main

import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/vrongmeal/leaf/pkg/engine"
"github.com/vrongmeal/leaf/pkg/utils"

prefixed "github.com/x-cray/logrus-prefixed-formatter"
)

var (
confPath string

rootCmd = &cobra.Command{
Use: "leaf",
Short: "General purpose hot-reloader for all projects",
Long: `Given a set of commands, leaf watches the filtered paths in the project directory for any changes and runs the commands in
order so you don't have to yourself`,

Run: func(*cobra.Command, []string) {
conf, err := utils.GetConfig(confPath)
if err != nil {
logrus.Fatalf("Error getting config: %s", err.Error())
}

isdir, err := utils.IsDir(conf.Root)
if err != nil || !isdir {
conf.Root = utils.CWD
}

logrus.Infof("Starting to watch: %s", conf.Root)
logrus.Infoln("Excluded paths:")
for i, e := range conf.Exclude {
logrus.Infof("%d. %s", i, e)
}

if err := engine.Start(&conf); err != nil {
logrus.Fatalf("Cannot start leaf: %s", err.Error())
}
},
}
)

func init() {
logrus.SetLevel(logrus.TraceLevel)
logrus.SetFormatter(new(prefixed.TextFormatter))

rootCmd.PersistentFlags().StringVarP(&confPath, "config", "c", utils.DefaultConfPath, "Config path for leaf configuration file")
}

func main() {
println("Hello, World!")
if err := rootCmd.Execute(); err != nil {
logrus.Fatalf("Cannot start leaf: %s", err.Error())
}
}
11 changes: 10 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@ module github.com/vrongmeal/leaf
go 1.13

require (
github.com/BurntSushi/toml v0.3.1
github.com/fsnotify/fsnotify v1.4.7
github.com/golangci/golangci-lint v1.23.6 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kr/pretty v0.2.0 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/onsi/ginkgo v1.12.0 // indirect
github.com/onsi/gomega v1.9.0 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.3.0 // indirect
github.com/spf13/cobra v0.0.6
github.com/x-cray/logrus-prefixed-formatter v0.5.2
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 // indirect
gopkg.in/yaml.v2 v2.2.8
)
321 changes: 321 additions & 0 deletions go.sum

Large diffs are not rendered by default.

113 changes: 113 additions & 0 deletions pkg/commander/commander.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package commander

import (
"fmt"
"os"
"os/exec"
"sync"
"syscall"

"github.com/sirupsen/logrus"
)

type pipeLogger struct{}

func (pl pipeLogger) Write(p []byte) (int, error) {
fmt.Print(string(p))
return len(p), nil
}

// Commander is a type with multiple commands and runs them in order.
type Commander struct {
index int
cmds [][]string
cmd *exec.Cmd
kill chan bool
wg *sync.WaitGroup
}

// NewCommander returns a Commander with given commands.
func NewCommander(cmds [][]string) *Commander {
return &Commander{
cmds: cmds,
index: 0,
kill: make(chan bool),
wg: &sync.WaitGroup{},
}
}

func newCmd(cmd []string) (*exec.Cmd, error) {
if len(cmd) == 0 {
return nil, fmt.Errorf("command cannot be empty")
}

var c *exec.Cmd

if len(cmd) == 1 {
c = exec.Command(cmd[0]) // nolint:gosec
} else {
name := cmd[0]
args := cmd[1:]
c = exec.Command(name, args...) // nolint:gosec
}

c.Stdout = os.Stdout
c.Stderr = os.Stderr
c.Stdin = os.Stdin
c.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

return c, nil
}

// Run executes the commands in order.
func (c *Commander) Run() error {
c.wg.Add(1)
defer c.wg.Done()

var err error

for _, command := range c.cmds {
c.cmd, err = newCmd(command)
if err != nil {
c.reset()
continue
}

logrus.Debugln("Running:", c.cmd.String())
if err := c.cmd.Start(); err != nil {
c.reset()
return err
}

c.cmd.Wait() // nolint:errcheck,gosec
select {
case <-c.kill:
goto killRun
default:
continue
}
}

killRun:
c.reset()
return nil
}

func (c *Commander) reset() {
c.cmd = nil
}

// Kill stops the execution of current command and terminates the Run.
func (c *Commander) Kill() error {
if c.cmd == nil {
return nil
}

if err := syscall.Kill(-c.cmd.Process.Pid, syscall.SIGKILL); err != nil {
return err
}
c.kill <- true
c.wg.Wait()
c.reset()
return nil
}
83 changes: 83 additions & 0 deletions pkg/engine/engine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package engine

import (
"os"
"os/signal"
"time"

"github.com/sirupsen/logrus"

"github.com/vrongmeal/leaf/pkg/commander"
"github.com/vrongmeal/leaf/pkg/utils"
"github.com/vrongmeal/leaf/pkg/watcher"
)

// Start runs the watcher and executes the commands from the config on file change.
func Start(conf *utils.Config) error {
cmdr := commander.NewCommander(conf.Exec)

opts := watcher.WatchOpts{
Root: conf.Root,
Exclude: conf.Exclude,
Filters: conf.Filters,
}

wchr, err := watcher.NewWatcher(&opts)
if err != nil {
return err
}

go func() {
if err := wchr.Watch(); err != nil {
logrus.Errorf("Failed to setup watcher: %s", err.Error())
}
}()

interrupt := make(chan os.Signal)
signal.Notify(interrupt, os.Interrupt)

exit := make(chan bool, 1)

runCmd(cmdr)

go func() {
<-interrupt
logrus.Infoln("Terminating after cleanup")
if err := cmdr.Kill(); err != nil {
logrus.Errorf("Error while stopping command: %s", err.Error())
}
wchr.Close()
exit <- true
}()

for {
select {
case file := <-wchr.File:
if err := cmdr.Kill(); err != nil {
logrus.Errorf("Error while stopping command: %s", err.Error())
wchr.Close()
return err
}
logrus.Infof("File modified! Reloading... (%s)", file)

// Sleep for conf.Delay duration amount of time.
time.Sleep(conf.Delay)

runCmd(cmdr)

case err := <-wchr.Err:
logrus.Errorf("Error while watching: %s", err.Error())

case <-exit:
return nil
}
}
}

func runCmd(cmdr *commander.Commander) {
go func() {
if err := cmdr.Run(); err != nil {
logrus.Warnf("Error while running command: %s", err.Error())
}
}()
}
52 changes: 52 additions & 0 deletions pkg/utils/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package utils

import (
"encoding/json"
"io/ioutil"
"path/filepath"
"time"

"github.com/BurntSushi/toml"
"gopkg.in/yaml.v2"
)

// Config represents the conf file for the runner.
type Config struct {
Root string `json:"root" yaml:"root" toml:"root"`
Exclude []string `json:"exclude" yaml:"exclude" toml:"exclude"`
Filters []string `json:"filters" yaml:"filters" toml:"filters"`
Exec [][]string `json:"exec" yaml:"exec" toml:"exec"`
Delay time.Duration `json:"delay" yaml:"delay" toml:"delay"`
}

// GetConfig returns config from the filepath.
func GetConfig(path string) (Config, error) {
config := Config{}

isdir, err := IsDir(path)
if err != nil || isdir {
return config, err
}

content, err := ioutil.ReadFile(filepath.Clean(path))
if err != nil {
return config, err
}

switch filepath.Ext(path) {
case ".json":
if err := json.Unmarshal(content, &config); err != nil {
return config, err
}
case ".toml":
if _, err := toml.Decode(string(content), &config); err != nil {
return config, err
}
default:
if err := yaml.Unmarshal(content, &config); err != nil {
return config, err
}
}

return config, nil
}
26 changes: 26 additions & 0 deletions pkg/utils/defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package utils

import (
"os"
"path/filepath"

"github.com/sirupsen/logrus"
)

var (
// CWD is the current working directory or "."
CWD string

// DefaultConfPath is the default path for app config.
DefaultConfPath string
)

func init() {
var err error
CWD, err = os.Getwd()
if err != nil {
logrus.Fatalln(err)
}

DefaultConfPath = filepath.Join(CWD, ".leaf.yml")
}
Loading

0 comments on commit 73857c8

Please sign in to comment.