diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a6410b6 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +GO_CMD=go +GOLINT_CMD=golint +GO_TEST=$(GO_CMD) test -v ./... +GO_VET=$(GO_CMD) vet ./... +GO_LINT=$(GOLINT_CMD) . + +all: + $(GO_VET) + $(GO_LINT) + $(GO_TEST) diff --git a/_examples/gocraft-web/main.go b/_examples/gocraft-web/main.go deleted file mode 100644 index a549397..0000000 --- a/_examples/gocraft-web/main.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "fmt" - "github.com/gocraft/web" - "github.com/pilu/fresh/runner/runnerutils" - "net/http" -) - -func runnerMiddleware(rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { - if runnerutils.HasErrors() { - runnerutils.RenderError(rw) - return - } - - next(rw, req) -} - -type Context struct{} - -func (c *Context) SayHello(rw web.ResponseWriter, req *web.Request) { - fmt.Fprint(rw, "Hello World") -} - -func main() { - router := web.New(Context{}). - Middleware(web.LoggerMiddleware). - Middleware(runnerMiddleware). - Get("/", (*Context).SayHello) - http.ListenAndServe("localhost:3000", router) -} diff --git a/_examples/martini/main.go b/_examples/martini/main.go deleted file mode 100644 index 9cd80ec..0000000 --- a/_examples/martini/main.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "github.com/codegangsta/martini" - "github.com/pilu/fresh/runner/runnerutils" - "net/http" - "os" -) - -func runnerMiddleware(w http.ResponseWriter, r *http.Request) { - if runnerutils.HasErrors() { - runnerutils.RenderError(w) - } -} - -func main() { - m := martini.Classic() - - if os.Getenv("DEV_RUNNER") == "1" { - m.Use(runnerMiddleware) - } - - m.Get("/", func() string { - return "Hello world - Martini" - }) - m.Run() -} diff --git a/_examples/pilu-martini/main.go b/_examples/pilu-martini/main.go deleted file mode 100644 index 9846b75..0000000 --- a/_examples/pilu-martini/main.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "github.com/pilu/martini" -) - -func main() { - m := martini.Classic() - m.Get("/", func() string { - return "Hello world - Martini" - }) - m.Run() -} diff --git a/_examples/traffic/main.go b/_examples/traffic/main.go deleted file mode 100644 index 7ce40af..0000000 --- a/_examples/traffic/main.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "github.com/pilu/traffic" -) - -func rootHandler(w traffic.ResponseWriter, r *traffic.Request) { - w.WriteText("Hello World - Traffic") -} - -func main() { - router := traffic.New() - router.Get("/", rootHandler) - router.Run() -} diff --git a/_test_fixtures/Freshfile b/_test_fixtures/Freshfile new file mode 100644 index 0000000..c0f0f79 --- /dev/null +++ b/_test_fixtures/Freshfile @@ -0,0 +1,6 @@ +[.go, .tpl, .tmpl, .html] +test: go test +build: go build -o hello + +[stylesheets: .less] +less-compiler: lessc less/application.less > css/apllication.css diff --git a/command.go b/command.go new file mode 100644 index 0000000..f7d8d91 --- /dev/null +++ b/command.go @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "io" + "log" + "os/exec" + "strings" +) + +type command struct { + Section *section + Name string + CmdString string + Cmd *exec.Cmd + Stdout io.ReadCloser + Stderr io.ReadCloser + Logger *customLogger +} + +func newCommand(section *section, cmd string) *command { + var name string + parts := strings.Split(cmd, " ") + + if len(parts) > 0 { + name = parts[0] + } + + loggerPrefix := fmt.Sprintf("%s - %s", section.Name, name) + c := &command{ + Section: section, + Name: name, + CmdString: cmd, + Logger: newLogger(loggerPrefix), + } + + return c +} + +func (c *command) build() error { + options := strings.Split(c.CmdString, " ") + c.Cmd = exec.Command(options[0], options[1:]...) + + var err error + c.Stdout, err = c.Cmd.StdoutPipe() + if err != nil { + return err + } + + c.Stderr, err = c.Cmd.StderrPipe() + if err != nil { + return err + } + + return nil +} + +func (c *command) Run() error { + c.Logger.log(c.CmdString) + + err := c.build() + if err != nil { + log.Fatal(err) + } + + go io.Copy(c.Logger, c.Stdout) + go io.Copy(c.Logger, c.Stderr) + + err = c.Cmd.Start() + if err != nil { + logger.log("Errors on `%s - %s`: %v\n", c.Section.Name, c.Name, err) + } + + logger.log(fmt.Sprintf("`%s - %s` started with pid %d", c.Section.Name, c.Name, c.Cmd.Process.Pid)) + + err = c.Cmd.Wait() + if err != nil { + logger.log("Errors on `%s - %s`: %v\n", c.Section.Name, c.Name, err) + } + + logger.log("`%s - %s` ended\n", c.Section.Name, c.Name) + + return err +} + +func (c *command) Stop() { + if c.Cmd != nil && c.Cmd.Process != nil { + logger.log("Killing process `%s`\n", c.Name) + c.Cmd.Process.Kill() + } +} diff --git a/command_test.go b/command_test.go new file mode 100644 index 0000000..3ac7be1 --- /dev/null +++ b/command_test.go @@ -0,0 +1,19 @@ +package main + +// func TestNewCommand(t *testing.T) { +// s := newSection("foo") +// c := newCommand(s, "build", "./build all -o foo") +// assert.Equal(t, "build", c.Name) +// assert.Equal(t, "./build all -o foo", c.CmdString) +// } + +// func TestCommand_Build(t *testing.T) { +// s := newSection("foo") +// c := newCommand(s, "build", "./build all -o foo") +// assert.Nil(t, c.Cmd) + +// c.build() +// assert.NotNil(t, c.Cmd) +// assert.Equal(t, "./build", c.Cmd.Path) +// assert.Equal(t, []string{"./build", "all", "-o", "foo"}, c.Cmd.Args) +// } diff --git a/config.go b/config.go new file mode 100644 index 0000000..0b8bc02 --- /dev/null +++ b/config.go @@ -0,0 +1,162 @@ +package main + +import ( + "bufio" + "fmt" + "text/scanner" +) + +type config struct { + sections []*section +} + +type stateFunc func(*config, *section) (stateFunc, *section, error) + +type configScanner struct { + s scanner.Scanner + state stateFunc + commands map[string]stateFunc +} + +func newConfigScanner(r *bufio.Reader) *configScanner { + var sc scanner.Scanner + sc.Init(r) + + s := &configScanner{s: sc} + s.init() + + return s +} + +func (s *configScanner) init() { + s.commands = map[string]stateFunc{ + "RUN": s.scanCMDRun, + "WATCH": s.scanCMDWatch, + } +} + +func (s *configScanner) next() rune { + r := s.s.Next() + if r != '#' { + return r + } + + for r != '\n' && r != scanner.EOF { + r = s.s.Next() + } + + return r +} + +func (s *configScanner) scan(c *config) error { + var err error + + sec := §ion{Name: "MAIN"} + c.sections = append(c.sections, sec) + + for s.state = s.scanLine; s.state != nil; { + s.state, sec, err = s.state(c, sec) + if err != nil { + break + } + } + + return err +} + +func (s *configScanner) scanLine(c *config, sec *section) (stateFunc, *section, error) { + r := s.s.Peek() + for r != scanner.EOF { + if r != ' ' && r != '\t' && r != '\n' && r != '#' { + if r == '[' { + return s.scanSection, sec, nil + } + + return s.scanCMD, sec, nil + } + + s.next() + r = s.s.Peek() + + } + + return nil, sec, nil +} + +func (s *configScanner) scanSection(c *config, sec *section) (stateFunc, *section, error) { + r := s.next() + if r != '[' { + return nil, sec, s.errorExpectedRune("[") + } + + sec = §ion{} + c.sections = append(c.sections, sec) + + return s.scanSectionName, sec, nil +} + +func (s *configScanner) scanSectionName(c *config, sec *section) (stateFunc, *section, error) { + var name string + r := s.s.Peek() + for r != ']' { + if r == scanner.EOF || r == '\n' || r == '#' { + return nil, sec, s.errorExpectedRune("]") + } + + r = s.s.Next() + name += string(r) + r = s.s.Peek() + } + s.next() + sec.Name = name + + return s.scanLine, sec, nil +} + +func (s *configScanner) scanCMD(c *config, sec *section) (stateFunc, *section, error) { + var name string + r := s.next() + for r != scanner.EOF { + if r == ' ' || r == '\t' || r == '\n' { + break + } + + name += string(r) + r = s.next() + } + + if cmd, ok := s.commands[name]; ok { + return cmd, sec, nil + } + + return nil, sec, fmt.Errorf("Unknown command `%s`", name) +} + +func (s *configScanner) scanCMDRun(c *config, sec *section) (stateFunc, *section, error) { + var cmdString string + r := s.next() + for r != scanner.EOF && r != '\n' { + cmdString = cmdString + string(r) + r = s.next() + } + + sec.NewCommand(cmdString) + + return s.scanLine, sec, nil +} + +func (s *configScanner) scanCMDWatch(c *config, sec *section) (stateFunc, *section, error) { + var cmd string + r := s.next() + for r != scanner.EOF && r != '\n' { + cmd = cmd + string(r) + r = s.next() + } + + return s.scanLine, sec, nil +} + +func (s *configScanner) errorExpectedRune(c string) error { + p := s.s.Pos() + return fmt.Errorf("Expected `%s` at line %d, col %d", c, p.Line, p.Column) +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..eb4bd61 --- /dev/null +++ b/config_test.go @@ -0,0 +1,47 @@ +package main + +import ( + "bufio" + "strings" + "testing" + + assert "github.com/pilu/miniassert" +) + +func TestParseConfig(t *testing.T) { + content := ` + # comment + [section 1] # aslid las dlkj s + WATCH ./public/js + RUN + RUN + [section 2] + [section 3] + #WATCH .` + + reader := bufio.NewReader(strings.NewReader(content)) + + cs := newConfigScanner(reader) + + config := &config{} + err := cs.scan(config) + assert.Nil(t, err) + assert.Equal(t, 4, len(config.sections)) + + s := config.sections[0] + assert.Equal(t, "MAIN", s.Name) + + s = config.sections[1] + assert.Equal(t, "section 1", s.Name) + assert.Equal(t, 2, len(s.Commands)) + assert.Equal(t, "compile-js", s.Commands[0].Name) + // assert.Equal(t, "compile-js -w ./public/javascripts", s.Commands[0].CmdString) + // assert.Equal(t, "minify-js", s.Commands[1].Name) + // assert.Equal(t, "minify-js ./public/javascripts/app.js", s.Commands[1].CmdString) + + s = config.sections[2] + assert.Equal(t, "section 2", s.Name) + + s = config.sections[3] + assert.Equal(t, "section 3", s.Name) +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..e8667dc --- /dev/null +++ b/logger.go @@ -0,0 +1,80 @@ +package main + +import ( + "fmt" + "log" + "math" + "os" + "time" +) + +var loggerColors = map[string]int{ + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, +} + +var loggerAvailableColors = []string{ + "cyan", + "yellow", + "green", + "magenta", + "red", + "blue", +} + +type customLogger struct { + Name string + Verbose bool + Color int + *log.Logger +} + +func (logger *customLogger) Write(p []byte) (n int, err error) { + logger.log(string(p)) + return len(p), nil +} + +var ( + loggerColorIndex int + loggerMaxNameLength int +) + +func newLogger(name string) *customLogger { + colorIndex := int(math.Mod(float64(loggerColorIndex), float64(len(loggerAvailableColors)))) + colorName := loggerAvailableColors[colorIndex] + + loggerColorIndex++ + + if length := len(name); length > loggerMaxNameLength { + loggerMaxNameLength = length + } + + return newLoggerWithColor(name, colorName) +} + +func newLoggerWithColor(name, colorName string) *customLogger { + return &customLogger{ + Name: name, + Logger: log.New(os.Stderr, "", 0), + Verbose: true, + Color: loggerColors[colorName], + } +} + +func (logger *customLogger) log(format string, v ...interface{}) { + if !logger.Verbose { + return + } + now := time.Now() + timeString := fmt.Sprintf("%02d:%02d:%02d", now.Hour(), now.Minute(), now.Second()) + formatPadding := fmt.Sprintf("%%-%ds", loggerMaxNameLength) + prefix := fmt.Sprintf(formatPadding, logger.Name) + format = fmt.Sprintf("\033[%dm%s %s | \033[0m%s", logger.Color, timeString, prefix, format) + logger.Logger.Printf(format, v...) +} diff --git a/main.go b/main.go index 1dea9dd..f21b477 100644 --- a/main.go +++ b/main.go @@ -1,36 +1,34 @@ -/* -Fresh is a command line tool that builds and (re)starts your web application everytime you save a go or template file. - -If the web framework you are using supports the Fresh runner, it will show build errors on your browser. - -It currently works with Traffic (https://github.com/pilu/traffic), Martini (https://github.com/codegangsta/martini) and gocraft/web (https://github.com/gocraft/web). - -Fresh will watch for file events, and every time you create/modifiy/delete a file it will build and restart the application. -If `go build` returns an error, it will logs it in the tmp folder. - -Traffic (https://github.com/pilu/traffic) already has a middleware that shows the content of that file if it is present. This middleware is automatically added if you run a Traffic web app in dev mode with Fresh. -*/ package main import ( "flag" "fmt" - "github.com/pilu/fresh/runner" "os" ) +const defaultConfigFilename = "Freshfile" + +var logger *customLogger + +func init() { + logger = newLoggerWithColor("fresh", "white") +} + func main() { - configPath := flag.String("c", "", "config file path") + var freshfilePath string + + flag.BoolVar(&logger.Verbose, "v", false, "verbose") + flag.StringVar(&freshfilePath, "f", defaultConfigFilename, "Freshfile path") flag.Parse() - if *configPath != "" { - if _, err := os.Stat(*configPath); err != nil { - fmt.Printf("Can't find config file `%s`\n", *configPath) - os.Exit(1) - } else { - os.Setenv("RUNNER_CONFIG_PATH", *configPath) - } + r, err := newRunnerWithFreshfile(freshfilePath) + if err != nil { + fmt.Printf("%s\n", err) + os.Exit(1) } - runner.Start() + logger.log("started with pid %d", os.Getpid()) + r.Run() + <-r.DoneChan + println("the end") } diff --git a/runner.conf.sample b/runner.conf.sample deleted file mode 100644 index 5ac3c05..0000000 --- a/runner.conf.sample +++ /dev/null @@ -1,12 +0,0 @@ -root: . -tmp_path: ./tmp -build_name: runner-build -build_log: runner-build-errors.log -valid_ext: .go, .tpl, .tmpl, .html -build_delay: 600 -colors: 1 -log_color_main: cyan -log_color_build: yellow -log_color_runner: green -log_color_watcher: magenta -log_color_app: diff --git a/runner.go b/runner.go new file mode 100644 index 0000000..806185a --- /dev/null +++ b/runner.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "runtime" + "time" +) + +type runner struct { + Sections []*section + DoneChan chan bool + SigChan chan os.Signal +} + +func newRunner() *runner { + r := &runner{ + DoneChan: make(chan bool), + SigChan: make(chan os.Signal), + } + + signal.Notify(r.SigChan, os.Interrupt) + signal.Notify(r.SigChan, os.Kill) + + return r +} + +func newRunnerWithFreshfile(freshfilePath string) (*runner, error) { + r := newRunner() + + return r, nil + + // sections, err := parseConfigFile(freshfilePath, "main: *") + // if err != nil { + // return r, err + // } + + // r.Sections = sections + + // return r, nil +} + +func (r *runner) Run() { + logger.log("Running...") + logger.log("%d goroutines", runtime.NumGoroutine()) + go r.ListenForSignals() + + for _, s := range r.Sections { + go func(s *section) { + s.Run() + }(s) + } +} + +func (r *runner) Stop() { + logger.log("Stopping all sections") + for _, s := range r.Sections { + s.Stop() + } +} + +func (r *runner) ListenForSignals() { + logger.log("Listening for signals") + <-r.SigChan + fmt.Printf("Interrupt a second time to quit\n") + logger.log("Waiting for a second signal") + select { + case <-r.SigChan: + logger.log("Second signal received") + r.DoneChan <- true + case <-time.After(1 * time.Second): + logger.log("Timeout") + logger.log("Stopping...") + r.Stop() + logger.log("Calling Run...") + r.Run() + } +} diff --git a/runner/build.go b/runner/build.go deleted file mode 100644 index 7f0242f..0000000 --- a/runner/build.go +++ /dev/null @@ -1,39 +0,0 @@ -package runner - -import ( - "io" - "io/ioutil" - "os" - "os/exec" -) - -func build() (string, bool) { - buildLog("Building...") - - cmd := exec.Command("go", "build", "-o", buildPath(), root()) - - stderr, err := cmd.StderrPipe() - if err != nil { - fatal(err) - } - - stdout, err := cmd.StdoutPipe() - if err != nil { - fatal(err) - } - - err = cmd.Start() - if err != nil { - fatal(err) - } - - io.Copy(os.Stdout, stdout) - errBuf, _ := ioutil.ReadAll(stderr) - - err = cmd.Wait() - if err != nil { - return string(errBuf), false - } - - return "", true -} diff --git a/runner/logger.go b/runner/logger.go deleted file mode 100644 index 4ccc0c3..0000000 --- a/runner/logger.go +++ /dev/null @@ -1,40 +0,0 @@ -package runner - -import ( - "fmt" - logPkg "log" - "os" - "time" -) - -type logFunc func(string, ...interface{}) - -var logger = logPkg.New(os.Stderr, "", 0) - -func newLogFunc(prefix string) func(string, ...interface{}) { - color, clear := "", "" - if settings["colors"] == "1" { - color = fmt.Sprintf("\033[%sm", logColor(prefix)) - clear = fmt.Sprintf("\033[%sm", colors["reset"]) - } - prefix = fmt.Sprintf("%-11s", prefix) - - return func(format string, v ...interface{}) { - now := time.Now() - timeString := fmt.Sprintf("%d:%d:%02d", now.Hour(), now.Minute(), now.Second()) - format = fmt.Sprintf("%s%s %s |%s %s", color, timeString, prefix, clear, format) - logger.Printf(format, v...) - } -} - -func fatal(err error) { - logger.Fatal(err) -} - -type appLogWriter struct{} - -func (a appLogWriter) Write(p []byte) (n int, err error) { - appLog(string(p)) - - return len(p), nil -} diff --git a/runner/runner.go b/runner/runner.go deleted file mode 100644 index f15f89a..0000000 --- a/runner/runner.go +++ /dev/null @@ -1,39 +0,0 @@ -package runner - -import ( - "io" - "os/exec" -) - -func run() bool { - runnerLog("Running...") - - cmd := exec.Command(buildPath()) - - stderr, err := cmd.StderrPipe() - if err != nil { - fatal(err) - } - - stdout, err := cmd.StdoutPipe() - if err != nil { - fatal(err) - } - - err = cmd.Start() - if err != nil { - fatal(err) - } - - go io.Copy(appLogWriter{}, stderr) - go io.Copy(appLogWriter{}, stdout) - - go func() { - <-stopChannel - pid := cmd.Process.Pid - runnerLog("Killing PID %d", pid) - cmd.Process.Kill() - }() - - return true -} diff --git a/runner/runnerutils/utils.go b/runner/runnerutils/utils.go deleted file mode 100644 index 3c05c1b..0000000 --- a/runner/runnerutils/utils.go +++ /dev/null @@ -1,80 +0,0 @@ -package runnerutils - -import ( - "bufio" - "html/template" - "io/ioutil" - "net/http" - "os" - "path/filepath" -) - -var logFilePath string - -func init() { - root := os.Getenv("RUNNER_WD") - tmpPath := os.Getenv("RUNNER_TMP_PATH") - fileName := os.Getenv("RUNNER_BUILD_LOG") - logFilePath = filepath.Join(root, tmpPath, fileName) -} - -// Returns true if a build error file exists in the tmp folder. -func HasErrors() bool { - if _, err := os.Stat(logFilePath); err == nil { - return true - } - - return false -} - -// It renders an error page with the build error message. -func RenderError(w http.ResponseWriter) { - data := map[string]interface{}{ - "Output": readErrorFile(), - } - - w.Header().Set("Content-Type", "text/html") - tpl := template.Must(template.New("ErrorPage").Parse(buildPageTpl)) - tpl.Execute(w, data) -} - -func readErrorFile() string { - file, err := os.Open(logFilePath) - if err != nil { - return "" - } - - defer file.Close() - - reader := bufio.NewReader(file) - bytes, _ := ioutil.ReadAll(reader) - - return string(bytes) -} - -const buildPageTpl string = ` - - - Traffic Panic - - - - -
-
-

Build Error

-
-
- -
-
{{ .Output }}
-
- - - ` diff --git a/runner/settings.go b/runner/settings.go deleted file mode 100644 index 61e2412..0000000 --- a/runner/settings.go +++ /dev/null @@ -1,138 +0,0 @@ -package runner - -import ( - "fmt" - "github.com/pilu/config" - "os" - "path/filepath" - "strconv" - "strings" - "time" -) - -const ( - envSettingsPrefix = "RUNNER_" - mainSettingsSection = "Settings" -) - -var settings = map[string]string{ - "config_path": "./runner.conf", - "root": ".", - "tmp_path": "./tmp", - "build_name": "runner-build", - "build_log": "runner-build-errors.log", - "valid_ext": ".go, .tpl, .tmpl, .html", - "build_delay": "600", - "colors": "1", - "log_color_main": "cyan", - "log_color_build": "yellow", - "log_color_runner": "green", - "log_color_watcher": "magenta", - "log_color_app": "", -} - -var colors = map[string]string{ - "reset": "0", - "black": "30", - "red": "31", - "green": "32", - "yellow": "33", - "blue": "34", - "magenta": "35", - "cyan": "36", - "white": "37", - "bold_black": "30;1", - "bold_red": "31;1", - "bold_green": "32;1", - "bold_yellow": "33;1", - "bold_blue": "34;1", - "bold_magenta": "35;1", - "bold_cyan": "36;1", - "bold_white": "37;1", - "bright_black": "30;2", - "bright_red": "31;2", - "bright_green": "32;2", - "bright_yellow": "33;2", - "bright_blue": "34;2", - "bright_magenta": "35;2", - "bright_cyan": "36;2", - "bright_white": "37;2", -} - -func logColor(logName string) string { - settingsKey := fmt.Sprintf("log_color_%s", logName) - colorName := settings[settingsKey] - - return colors[colorName] -} - -func loadEnvSettings() { - for key, _ := range settings { - envKey := fmt.Sprintf("%s%s", envSettingsPrefix, strings.ToUpper(key)) - if value := os.Getenv(envKey); value != "" { - settings[key] = value - } - } -} - -func loadRunnerConfigSettings() { - if _, err := os.Stat(configPath()); err != nil { - return - } - - logger.Printf("Loading settings from %s", configPath()) - sections, err := config.ParseFile(configPath(), mainSettingsSection) - if err != nil { - return - } - - for key, value := range sections[mainSettingsSection] { - settings[key] = value - } -} - -func initSettings() { - loadEnvSettings() - loadRunnerConfigSettings() -} - -func getenv(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - - return defaultValue -} - -func root() string { - return settings["root"] -} - -func tmpPath() string { - return settings["tmp_path"] -} - -func buildName() string { - return settings["build_name"] -} -func buildPath() string { - return filepath.Join(tmpPath(), buildName()) -} - -func buildErrorsFileName() string { - return settings["build_log"] -} - -func buildErrorsFilePath() string { - return filepath.Join(tmpPath(), buildErrorsFileName()) -} - -func configPath() string { - return settings["config_path"] -} - -func buildDelay() time.Duration { - value, _ := strconv.Atoi(settings["build_delay"]) - - return time.Duration(value) -} diff --git a/runner/start.go b/runner/start.go deleted file mode 100644 index b382dc3..0000000 --- a/runner/start.go +++ /dev/null @@ -1,127 +0,0 @@ -package runner - -import ( - "fmt" - "os" - "runtime" - "strings" - "syscall" - "time" -) - -var ( - startChannel chan string - stopChannel chan bool - mainLog logFunc - watcherLog logFunc - runnerLog logFunc - buildLog logFunc - appLog logFunc -) - -func flushEvents() { - for { - select { - case eventName := <-startChannel: - mainLog("receiving event %s", eventName) - default: - return - } - } -} - -func start() { - loopIndex := 0 - buildDelay := buildDelay() - - started := false - - go func() { - for { - loopIndex++ - mainLog("Waiting (loop %d)...", loopIndex) - eventName := <-startChannel - - mainLog("receiving first event %s", eventName) - mainLog("sleeping for %d milliseconds", buildDelay) - time.Sleep(buildDelay * time.Millisecond) - mainLog("flushing events") - - flushEvents() - - mainLog("Started! (%d Goroutines)", runtime.NumGoroutine()) - err := removeBuildErrorsLog() - if err != nil { - mainLog(err.Error()) - } - - errorMessage, ok := build() - if !ok { - mainLog("Build Failed: \n %s", errorMessage) - if !started { - os.Exit(1) - } - createBuildErrorsLog(errorMessage) - } else { - if started { - stopChannel <- true - } - run() - } - - started = true - mainLog(strings.Repeat("-", 20)) - } - }() -} - -func init() { - startChannel = make(chan string, 1000) - stopChannel = make(chan bool) -} - -func initLogFuncs() { - mainLog = newLogFunc("main") - watcherLog = newLogFunc("watcher") - runnerLog = newLogFunc("runner") - buildLog = newLogFunc("build") - appLog = newLogFunc("app") -} - -func initLimit() { - var rLimit syscall.Rlimit - rLimit.Max = 10000 - rLimit.Cur = 10000 - err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) - if err != nil { - fmt.Println("Error Setting Rlimit ", err) - } -} - -func setEnvVars() { - os.Setenv("DEV_RUNNER", "1") - wd, err := os.Getwd() - if err == nil { - os.Setenv("RUNNER_WD", wd) - } - - for k, v := range settings { - key := strings.ToUpper(fmt.Sprintf("%s%s", envSettingsPrefix, k)) - os.Setenv(key, v) - } -} - -// Watches for file changes in the root directory. -// After each file system event it builds and (re)starts the application. -func Start() { - initLimit() - initSettings() - initLogFuncs() - initFolders() - setEnvVars() - watch() - start() - startChannel <- "/" - - <-make(chan int) -} diff --git a/runner/utils.go b/runner/utils.go deleted file mode 100644 index 93096ff..0000000 --- a/runner/utils.go +++ /dev/null @@ -1,63 +0,0 @@ -package runner - -import ( - "os" - "path/filepath" - "strings" -) - -func initFolders() { - runnerLog("InitFolders") - path := tmpPath() - runnerLog("mkdir %s", path) - err := os.Mkdir(path, 0755) - if err != nil { - runnerLog(err.Error()) - } -} - -func isTmpDir(path string) bool { - absolutePath, _ := filepath.Abs(path) - absoluteTmpPath, _ := filepath.Abs(tmpPath()) - - return absolutePath == absoluteTmpPath -} - -func isWatchedFile(path string) bool { - absolutePath, _ := filepath.Abs(path) - absoluteTmpPath, _ := filepath.Abs(tmpPath()) - - if strings.HasPrefix(absolutePath, absoluteTmpPath) { - return false - } - - ext := filepath.Ext(path) - - for _, e := range strings.Split(settings["valid_ext"], ",") { - if strings.TrimSpace(e) == ext { - return true - } - } - - return false -} - -func createBuildErrorsLog(message string) bool { - file, err := os.Create(buildErrorsFilePath()) - if err != nil { - return false - } - - _, err = file.WriteString(message) - if err != nil { - return false - } - - return true -} - -func removeBuildErrorsLog() error { - err := os.Remove(buildErrorsFilePath()) - - return err -} diff --git a/runner/utils_test.go b/runner/utils_test.go deleted file mode 100644 index d759a20..0000000 --- a/runner/utils_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package runner - -import ( - assert "github.com/pilu/miniassert" - "testing" -) - -func TestIsWatchedFile(t *testing.T) { - // valid extensions - assert.True(t, isWatchedFile("test.go")) - assert.True(t, isWatchedFile("test.tpl")) - assert.True(t, isWatchedFile("test.tmpl")) - assert.True(t, isWatchedFile("test.html")) - - /* // invalid extensions */ - assert.False(t, isWatchedFile("test.css")) - assert.False(t, isWatchedFile("test-executable")) - - // files in tmp - assert.False(t, isWatchedFile("./tmp/test.go")) -} diff --git a/runner/watcher.go b/runner/watcher.go deleted file mode 100644 index 7df2706..0000000 --- a/runner/watcher.go +++ /dev/null @@ -1,51 +0,0 @@ -package runner - -import ( - "github.com/howeyc/fsnotify" - "os" - "path/filepath" - "strings" -) - -func watchFolder(path string) { - watcher, err := fsnotify.NewWatcher() - if err != nil { - fatal(err) - } - - go func() { - for { - select { - case ev := <-watcher.Event: - if isWatchedFile(ev.Name) { - watcherLog("sending event %s", ev) - startChannel <- ev.String() - } - case err := <-watcher.Error: - watcherLog("error: %s", err) - } - } - }() - - watcherLog("Watching %s", path) - err = watcher.Watch(path) - - if err != nil { - fatal(err) - } -} - -func watch() { - root := root() - filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if info.IsDir() && !isTmpDir(path) { - if len(path) > 1 && strings.HasPrefix(filepath.Base(path), ".") { - return filepath.SkipDir - } - - watchFolder(path) - } - - return err - }) -} diff --git a/runner_test.go b/runner_test.go new file mode 100644 index 0000000..70f12dc --- /dev/null +++ b/runner_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "testing" + + assert "github.com/pilu/miniassert" +) + +func TestNewRunner(t *testing.T) { + r := newRunner() + assert.Equal(t, 0, len(r.Sections)) +} + +func TestNewRunnerWithFreshfile_WithFileNotFound(t *testing.T) { + _, err := newRunnerWithFreshfile("./_test_fixtures/file-not-found") + assert.NotNil(t, err) +} + +func TestNewRunnerWithFreshfile_WithValidFile(t *testing.T) { + r, err := newRunnerWithFreshfile("./_test_fixtures/Freshfile") + assert.Nil(t, err) + assert.Equal(t, 3, len(r.Sections)) +} diff --git a/section.go b/section.go new file mode 100644 index 0000000..5a69e80 --- /dev/null +++ b/section.go @@ -0,0 +1,61 @@ +package main + +import ( + "regexp" + "strings" +) + +type section struct { + Name string + Globs []string + Commands []*command +} + +func newSection(description string) *section { + var globsString string + name := description + parts := strings.SplitN(description, ":", 2) + if len(parts) > 1 { + name = parts[0] + globsString = parts[1] + } + + extRe := regexp.MustCompile(`\s*,\s*`) + globs := []string{} + for _, rawGlob := range extRe.Split(globsString, -1) { + glob := strings.TrimSpace(rawGlob) + if len(glob) > 0 { + globs = append(globs, glob) + } + } + + return §ion{ + Name: name, + Globs: globs, + } +} + +func (s *section) NewCommand(cmd string) *command { + c := newCommand(s, cmd) + s.Commands = append(s.Commands, c) + + return c +} + +func (s *section) Run() { + logger.log("Running section `%s`", s.Name) + for _, c := range s.Commands { + err := c.Run() + if err != nil { + break + } + } + logger.log("Section ended `%s`", s.Name) +} + +func (s *section) Stop() { + logger.log("Stopping section %s", s.Name) + for _, c := range s.Commands { + c.Stop() + } +} diff --git a/section_test.go b/section_test.go new file mode 100644 index 0000000..b13cff9 --- /dev/null +++ b/section_test.go @@ -0,0 +1,24 @@ +package main + +// func TestNewSection(t *testing.T) { +// var s *section + +// s = newSection("stylesheets: *.css, *.less, , , ") +// assert.Equal(t, "stylesheets", s.Name) +// assert.Equal(t, []string{"*.css", "*.less"}, s.Globs) +// assert.Equal(t, 0, len(s.Commands)) + +// // only name, without globs +// s = newSection("foo-section") +// assert.Equal(t, "foo-section", s.Name) +// assert.Equal(t, 0, len(s.Globs)) +// assert.Equal(t, 0, len(s.Commands)) +// } + +// func TestSection_NewCommand(t *testing.T) { +// s := newSection("go") +// assert.Equal(t, 0, len(s.Commands)) +// c := s.NewCommand("build", "./build") +// assert.Equal(t, 1, len(s.Commands)) +// assert.Equal(t, c, s.Commands[0]) +// }