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 = ` - -
-{{ .Output }}
-