From 29d742d97ee0e44e6d1c52c8771bda48e087aee3 Mon Sep 17 00:00:00 2001 From: Patrick Koperwas Date: Mon, 29 Jan 2018 17:32:15 -0800 Subject: [PATCH] refactor: Move to CLI only configuration --- Gopkg.lock | 16 ++--- Gopkg.toml | 12 +--- README.md | 113 ++++++++++++++++++++-------------- cmd/init.go | 130 ---------------------------------------- cmd/root.go | 103 +++++++++++++++++++++++++++++-- cmd/run.go | 77 ------------------------ cmd/version.go | 21 ------- tychus/configuration.go | 25 -------- tychus/logger.go | 13 +--- tychus/orchestrator.go | 17 ++++++ tychus/runner.go | 8 +-- tychus/watcher.go | 7 ++- 12 files changed, 199 insertions(+), 343 deletions(-) delete mode 100644 cmd/init.go delete mode 100644 cmd/run.go delete mode 100644 cmd/version.go diff --git a/Gopkg.lock b/Gopkg.lock index 4c44699..f337040 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -4,8 +4,8 @@ [[projects]] name = "github.com/devlocker/devproxy" packages = ["devproxy"] - revision = "9492883c201c9b27979e7c8c64994add763d74d0" - version = "0.2.1" + revision = "c5444b29b6e6db1308ed9ad4b175b0303ffd5737" + version = "0.2.2" [[projects]] name = "github.com/fatih/color" @@ -38,10 +38,10 @@ version = "v0.0.3" [[projects]] + branch = "master" name = "github.com/spf13/cobra" packages = ["."] - revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" - version = "v0.0.1" + revision = "f91529fc609202eededff4de2dc0ba2f662240a3" [[projects]] name = "github.com/spf13/pflag" @@ -55,15 +55,9 @@ packages = ["unix"] revision = "ff2a66f350cefa5c93a634eadb5d25bb60c85a9c" -[[projects]] - branch = "v2" - name = "gopkg.in/yaml.v2" - packages = ["."] - revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4" - [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "e595b55d436a5778bcf05f96420141d3d8dd9b2123864f3469dd7896c3699de9" + inputs-digest = "c5a9fb7c0f7c220499ac2425585aaa1b366e40e74c38e14eb5d9211db31dc61f" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 248aa0f..aa0ad9a 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,10 +1,6 @@ [[constraint]] name = "github.com/devlocker/devproxy" - version = "0.2.1" - -[[constraint]] - name = "github.com/fatih/color" - version = "1.5.0" + version = "0.2.2" [[constraint]] name = "github.com/fsnotify/fsnotify" @@ -12,11 +8,7 @@ [[constraint]] name = "github.com/spf13/cobra" - version = "0.0.1" - -[[constraint]] - branch = "v2" - name = "gopkg.in/yaml.v2" + branch = "master" [prune] go-tests = true diff --git a/README.md b/README.md index 7f0764b..ebd32e0 100644 --- a/README.md +++ b/README.md @@ -31,76 +31,101 @@ go get github.com/devlocker/tychus ### Windows Currently isn't supported :( -## Getting Started -You will need to create a `.tychus.yml` file configuration file. Easiest way is -to generate one is with: - -``` -$ tychus init -``` - -Double check your generated `.tychus.yml` config to make sure it knows which -file extensions to watch. - ## Usage -Usage is simple, `tychus run` and then your command. On a filesystem change that +Usage is simple, `tychus` and then your command. On a filesystem change that command will be rerun. ``` // Go -tychus run go run main.go +tychus go run main.go // Rust -tychus run cargo run +tychus cargo run // Ruby tychus ruby myapp.rb // Shell Commands -tychus run ls +tychus ls ``` Need to pass flags? Stick the command in quotes ``` -tychus run "ruby myapp.rb -e development" +tychus "ruby myapp.rb -e development" ``` Complicated command? Stick it in quotes ``` -tychus run "go build -o my-bin && echo 'Built Binary' && ./my-bin" +tychus "go build -o my-bin && echo 'Built Binary' && ./my-bin" ``` -## Configuration +## Options +Tychus has a few options. In most cases the defaults should be sufficient. See +below for a few examples. ```yaml -# List of extentions to watch. A change to a file with one of these extensions -# will trigger a fresh of your application. -extensions: -- .go - -# List of folders to not watch. Too many watched files / folders can slow things -down, so try and ignore as much as possible. -ignore: -- node_modules -- tmp -- log -- vendor - -# If not enabled, proxy will not start. -proxy_enabled: true - -# Port proxy runs on. -proxy_port: 4000 - -# Port your application runs on. NOTE: a PORT environment will take overwrite -# whatever you put here. -app_port: 3000 - -# In seconds, how long the proxy will attempt to proxy a request until it -# gives up and returns a 502. -timeout: 10 + -a, --app-port int port your application runs on, overwritten by ENV['PORT'] (default 3000) + -p, --proxy-port int proxy port (default 4000) + -w, --watch stringSlice comma separated list of extensions that will trigger a reload. If not set, will reload on any file change. + -x, --ignore stringSlice comma separated list of directories to ignore file changes in. (default [node_modules,log,tmp,vendor]) + -t, --timeout int timeout for proxied requests (default 10) + + -h, --help help for tychus + --debug print debug output + --no-proxy will not start proxy if set + --version version for tychus +``` + +Note: If you do not specify any file extensions in `--watch`, Tychus will +trigger a reload on any file change, except for files inside directories listed +in `--ignore` + +Note: Tychus will not watch any hidden directories (those beginning with `.`). + +## Examples + +### Sinatra +By default, Sinatra runs on port `4567`. Only want to watch `ruby` and +`erb` files. Default ignore list is sufficient. The following are equivalent. + +``` +tychus ruby myapp.rb -w .rb,.erb -a 4567 +tychus ruby myapp.rb --watch=.rb,.erb --app-port=4567 +``` + +Visit http://localhost:4000 (4000 is the default proxy host) and to view your +app. + + +### Foreman / Procfile +Similar to the previous example, except this time running inside of +[foreman](https://github.com/ddollar/foreman) (or someother Procfile runner). + +``` +# Procfile +web: tychus "rackup -p $PORT -s puma" -w rb,erb +``` + +Note: If you need to pass flags to your command (like `-p` & `-s` in this case), +wrap your entire command in quotes. + +We don't need to explicitly add a `-a $PORT` flag, because `tychus` will +automatically pick up the $PORT and automatically set `app-port` for you. + + +### Kitchen Sink Example +Running a Go program, separate build and run steps, with some logging thrown in, +only watching `.go` files, running a server on port `5000`, running proxy on +`8080`, ignoring just `tmp` and `vendor`, with a timeout of 5 seconds. + +``` +tychus "echo 'Building...' && go build -o tmp/my-bin && echo 'Built' && ./tmp/my-bin some args -e development" --app-port=5000 --proxy-port=8080 --watch=.go --ignore=tmp,vendor --timeout=5 + +# Or, using short flags + +tychus "echo 'Building...' && go build -o tmp/my-bin && echo 'Built' && ./tmp/my-bin some args -e development" -a 5000 -p 8080 -w .go -x tmp,vendor -t 5 ``` diff --git a/cmd/init.go b/cmd/init.go deleted file mode 100644 index bd14c0a..0000000 --- a/cmd/init.go +++ /dev/null @@ -1,130 +0,0 @@ -package cmd - -import ( - "bufio" - "fmt" - "io/ioutil" - "log" - "os" - "path/filepath" - "strings" - - "github.com/devlocker/tychus/tychus" - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(initCmd) -} - -var initCmd = &cobra.Command{ - Use: "init", - Short: "Initialize Tychus with a configuration file", - Run: func(cmd *cobra.Command, args []string) { - // Get the current working directory - wd, err := os.Getwd() - if err != nil { - fmt.Println("Cant get working dir") - return - } - - // Generate a configuration based on project. - c, err := detectLangauge(wd) - if err != nil { - fmt.Println(err) - return - } - - // Conig already exist? Ask to overwrite if so. - if _, err = os.Stat(configFile); !os.IsNotExist(err) { - fmt.Println("Config file already exists") - reader := bufio.NewReader(os.Stdin) - - for { - fmt.Print("Replace configuration file? [y/n]: ") - - response, err := reader.ReadString('\n') - if err != nil { - log.Fatal(err) - } - - response = strings.ToLower(strings.TrimSpace(response)) - if response == "y" || response == "yes" { - break - } else if response == "n" || response == "no" { - return - } - } - } - - // Write config file to disk. - fmt.Printf("Creating %v\n", configFile) - c.Write(configFile) - }, -} - -// Naive project language detection. Scans the top level files to determine -// project type. Return a configuration with sensible defaults. -func detectLangauge(dir string) (*tychus.Configuration, error) { - files, err := ioutil.ReadDir("./") - if err != nil { - return nil, err - } - - c := &tychus.Configuration{ - Extensions: []string{".go"}, - Ignore: []string{"node_modules", "tmp", "log", "vendor"}, - ProxyEnabled: true, - ProxyPort: 4000, - AppPort: 3000, - Timeout: 10, - } - - // Go Project? - for _, f := range files { - ext := filepath.Ext(f.Name()) - if ext == ".go" { - // Already configured for Go - return c, nil - } - } - - // Ruby Project? - for _, f := range files { - ext := filepath.Ext(f.Name()) - if f.Name() == "Gemfile" || ext == ".rb" { - c.Extensions = []string{".rb"} - return c, nil - } - } - - // Python Project? - for _, f := range files { - ext := filepath.Ext(f.Name()) - if ext == ".py" { - c.Extensions = []string{".py"} - return c, nil - } - } - - // Rust Project? - for _, f := range files { - ext := filepath.Ext(f.Name()) - if f.Name() == "Cargo.toml" || ext == ".rs" { - c.Extensions = []string{".rs"} - return c, nil - } - } - - // JS Project? - for _, f := range files { - ext := filepath.Ext(f.Name()) - if f.Name() == "package.json" || ext == ".js" { - c.Extensions = []string{".js"} - return c, nil - } - } - - // Something else, return default - return c, nil -} diff --git a/cmd/root.go b/cmd/root.go index 004de9a..86def9c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,20 +3,50 @@ package cmd import ( "fmt" "os" + "os/signal" + "strconv" + "strings" + "syscall" + "github.com/devlocker/tychus/tychus" "github.com/spf13/cobra" ) -var configFile string -var port string +var version = "master" + +var appPort int var debug bool +var ignored []string +var noProxy bool +var proxyPort int +var timeout int +var watch []string var rootCmd = &cobra.Command{ Use: "tychus", - Short: "Restart and your application as you make changes", + Short: "Starts and reloads your application as you make changes to source files.", + Long: `tychus is a command line utility to live-reload your application. tychus will +watch your filesystem for changes and automatically recompile and restart code +on change. + +Example: + tychus go run main.go -w .go + tychus ruby myapp.rb --app-port=4567 --proxy-port=4000 --watch .rb,.erb --ignore node_modules + +Example: No Proxy + tychus ls --no-proxy + +Example: Flags - use quotes + tychus "ruby myapp.rb -p 5000 -e development" -a 5000 -p 4000 -w .rb,.erb + +Example: Multiple Commands - use quotes + tychus "go build -o my-bin && echo 'Done Building' && ./my-bin" + `, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { start(args) }, + Version: version, } func Execute() { @@ -27,7 +57,68 @@ func Execute() { } func init() { - rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", ".tychus.yml", "path to tychus config") - rootCmd.Flags().StringVarP(&port, "port", "p", "", "Port to run on") - rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "print debug output") + rootCmd.Flags().IntVarP(&appPort, "app-port", "a", 3000, "port your application runs on, overwritten by ENV['PORT']") + rootCmd.Flags().BoolVar(&debug, "debug", false, "print debug output") + rootCmd.Flags().StringSliceVarP(&ignored, "ignore", "x", []string{"node_modules", "log", "tmp", "vendor"}, "comma separated list of directories to ignore file changes in.") + rootCmd.Flags().BoolVar(&noProxy, "no-proxy", false, "will not start proxy if set") + rootCmd.Flags().IntVarP(&proxyPort, "proxy-port", "p", 4000, "proxy port") + rootCmd.Flags().IntVarP(&timeout, "timeout", "t", 10, "timeout for proxied requests") + rootCmd.Flags().StringSliceVarP(&watch, "watch", "w", []string{}, "comma separated list of extensions that will trigger a reload. If not set, will reload on any file change.") +} + +func start(args []string) { + // Catch signals, need to do make sure to stop any executing commands. + // Otherwise they become orphan proccesses. + stop := make(chan os.Signal, 1) + signal.Notify( + stop, + os.Interrupt, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + + // Clean up watched file extensions + for i, ext := range watch { + ext = strings.TrimSpace(ext) + if !strings.HasPrefix(ext, ".") { + ext = "." + ext + } + + watch[i] = ext + } + + // If PORT is set, use that instead of AppPort. For things like foreman + // where ports are automatically assigned. + envPort, ok := os.LookupEnv("PORT") + if ok { + if envPort, err := strconv.Atoi(envPort); err == nil { + appPort = envPort + } + } + + // Create a configuration + c := &tychus.Configuration{ + Extensions: watch, + Ignore: ignored, + ProxyEnabled: !noProxy, + ProxyPort: proxyPort, + AppPort: appPort, + Timeout: timeout, + Logger: tychus.NewLogger(debug), + } + + // Run tychus + t := tychus.New(args, c) + go func() { + err := t.Start() + if err != nil { + t.Stop() + c.Logger.Fatal(err.Error()) + } + }() + + <-stop + t.Stop() } diff --git a/cmd/run.go b/cmd/run.go deleted file mode 100644 index d9aa6b3..0000000 --- a/cmd/run.go +++ /dev/null @@ -1,77 +0,0 @@ -package cmd - -import ( - "os" - "os/signal" - "strconv" - "syscall" - - "github.com/devlocker/tychus/tychus" - "github.com/fatih/color" - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(runCmd) -} - -var runCmd = &cobra.Command{ - Use: "run", - Short: "Reloads your application as you make changes to source files.", - Run: func(cmd *cobra.Command, args []string) { - start(args) - }, -} - -func start(args []string) { - stop := make(chan os.Signal, 1) - signal.Notify( - stop, - os.Interrupt, - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - - // Load configuration and use default logger - c := &tychus.Configuration{} - err := c.Load(configFile) - if err != nil { - c.Logger.Fatal(err.Error()) - } - - c.Logger = tychus.NewLogger(debug) - - // If PORT is set, use that instead of AppPort. For things like foreman - // where ports are automatically assigned. - port, ok := os.LookupEnv("PORT") - if ok { - if appPort, err := strconv.Atoi(port); err == nil { - c.AppPort = appPort - } - } - - o := tychus.New(args, c) - - // Run tychus - go func() { - err = o.Start() - if err != nil { - o.Stop() - c.Logger.Fatal(err.Error()) - } - }() - - <-stop - // Have to call `Stop` - o.Stop() -} - -func isEnabledStr(b bool) string { - if b { - return color.GreenString("enabled") - } - - return color.YellowString("disabled") -} diff --git a/cmd/version.go b/cmd/version.go deleted file mode 100644 index 3081b7e..0000000 --- a/cmd/version.go +++ /dev/null @@ -1,21 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -var version = "master" - -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Print the version number of Tychus", - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Tychus Version: %v\n", version) - }, -} - -func init() { - rootCmd.AddCommand(versionCmd) -} diff --git a/tychus/configuration.go b/tychus/configuration.go index 256e89a..421d545 100644 --- a/tychus/configuration.go +++ b/tychus/configuration.go @@ -1,12 +1,5 @@ package tychus -import ( - "fmt" - "io/ioutil" - - yaml "gopkg.in/yaml.v2" -) - type Configuration struct { Extensions []string `yaml:"extensions"` Ignore []string `yaml:"ignore"` @@ -16,21 +9,3 @@ type Configuration struct { Timeout int `yaml:"timeout"` Logger Logger `yaml:"-"` } - -func (c *Configuration) Write(path string) error { - data, err := yaml.Marshal(c) - if err != nil { - return err - } - - return ioutil.WriteFile(path, data, 0666) -} - -func (c *Configuration) Load(path string) error { - data, err := ioutil.ReadFile(path) - if err != nil { - return fmt.Errorf("%v\nHave you run 'tychus init'? Run 'tychus help' for more information.", err) - } - - return yaml.Unmarshal(data, c) -} diff --git a/tychus/logger.go b/tychus/logger.go index 976462b..f3b107e 100644 --- a/tychus/logger.go +++ b/tychus/logger.go @@ -3,17 +3,14 @@ package tychus import ( "log" "os" - - "github.com/fatih/color" ) type Logger interface { Debug(interface{}) Debugf(string, ...interface{}) - Error(string) Fatal(...interface{}) Printf(string, ...interface{}) - Success(string) + Print(...interface{}) } func NewLogger(debug bool) Logger { @@ -41,11 +38,3 @@ func (l *logger) Debugf(format string, args ...interface{}) { l.Printf("DEBUG: "+format, args...) } } - -func (l *logger) Error(msg string) { - l.Print(color.RedString(msg)) -} - -func (l *logger) Success(msg string) { - l.Print(color.GreenString(msg)) -} diff --git a/tychus/orchestrator.go b/tychus/orchestrator.go index 63506bf..e281b12 100644 --- a/tychus/orchestrator.go +++ b/tychus/orchestrator.go @@ -12,6 +12,8 @@ package tychus import ( + "strings" + "github.com/devlocker/devproxy/devproxy" ) @@ -39,6 +41,8 @@ func New(args []string, c *Configuration) *Orchestrator { // Starts Tychus. Any filesystem changes will cause the command passed in to be // rerun. To avoid orphaning processes, make sure to call Stop before exiting. func (o *Orchestrator) Start() error { + o.printStartMessage() + stop := make(chan error, 1) go o.watcher.start(o.config) @@ -92,3 +96,16 @@ func (o *Orchestrator) Start() error { func (o *Orchestrator) Stop() error { return o.runner.kill() } + +func (o *Orchestrator) printStartMessage() { + exts := o.config.Extensions + if len(exts) == 0 { + exts = []string{"all"} + } + + o.config.Logger.Printf( + "Starting: watching extensions: [%v], ignoring dirs: [%v]", + strings.Join(exts, ", "), + strings.Join(o.config.Ignore, ", "), + ) +} diff --git a/tychus/runner.go b/tychus/runner.go index 8479efa..38c1d5b 100644 --- a/tychus/runner.go +++ b/tychus/runner.go @@ -68,7 +68,7 @@ func (r *runner) rerun() error { if err := r.cmd.Wait(); err != nil { // Program errored. Only log it if it exit status is postive, as status - // code -1 is when the process was killed. + // code -1 is returned when the process was killed by kill(). if exiterr, ok := err.(*exec.ExitError); ok { ws := exiterr.Sys().(syscall.WaitStatus) if ws.ExitStatus() > 0 { @@ -80,12 +80,10 @@ func (r *runner) rerun() error { return nil } -// Kill the existing process +// Kill the existing process & process group func (r *runner) kill() error { if r.cmd != nil && r.cmd.Process != nil { - // Kill the process group - pgid, err := syscall.Getpgid(r.cmd.Process.Pid) - if err == nil { + if pgid, err := syscall.Getpgid(r.cmd.Process.Pid); err == nil { syscall.Kill(-pgid, syscall.SIGKILL) } diff --git a/tychus/watcher.go b/tychus/watcher.go index e22b51f..c6cc886 100644 --- a/tychus/watcher.go +++ b/tychus/watcher.go @@ -113,10 +113,13 @@ func (w *watcher) shouldSkipDir(path string, c *Configuration) bool { // Checks to see if path matches a configured extension. func (w *watcher) isWatchedFile(path string, c *Configuration) bool { - ext := filepath.Ext(path) + if len(c.Extensions) == 0 { + return true + } + ext := filepath.Ext(path) for _, e := range c.Extensions { - if strings.TrimSpace(e) == ext { + if e == ext { return true } }