diff --git a/cmd.go b/cmd.go index 69e7933..8a71296 100644 --- a/cmd.go +++ b/cmd.go @@ -9,12 +9,9 @@ import ( // Tool options customizable, should be moved in Cmd type tool struct { - dir bool - status bool - name string - err string - cmd []string - options []string + name, err, out string + cmd, options []string + dir, status bool } // Cmds list of go commands @@ -32,11 +29,11 @@ type Cmds struct { // Cmd single command fields and options type Cmd struct { - Status bool `yaml:"status,omitempty" json:"status,omitempty"` Method string `yaml:"method,omitempty" json:"method,omitempty"` Args []string `yaml:"args,omitempty" json:"args,omitempty"` - method []string + Status bool `yaml:"status,omitempty" json:"status,omitempty"` tool bool + method []string name, startTxt, endTxt string } @@ -46,6 +43,8 @@ func (r *realize) clean() error { arr := r.Schema for key, val := range arr { if _, err := duplicates(val, arr[key+1:]); err != nil { + // path validation + r.Schema = append(arr[:key], arr[key+1:]...) break } @@ -56,14 +55,15 @@ func (r *realize) clean() error { } // Add a new project -func (r *realize) add(p *cli.Context) error { - path, err := filepath.Abs(p.String("path")) - if err != nil { - return err +func (r *realize) add(p *cli.Context) (err error) { + // project init + name := filepath.Base(p.String("path")) + if name == "." { + name = filepath.Base(wdir()) } project := Project{ - Name: filepath.Base(filepath.Clean(p.String("path"))), - Path: path, + Name: name, + Path: p.String("path"), Cmds: Cmds{ Vet: Cmd{ Status: p.Bool("vet"), diff --git a/exec.go b/exec.go index 82984ce..09c4615 100644 --- a/exec.go +++ b/exec.go @@ -39,6 +39,7 @@ func (p *Project) goCompile(stop <-chan bool, method []string, args []string) (s if err != nil { return stderr.String(), err } + return "", nil } return "", nil } @@ -47,7 +48,6 @@ func (p *Project) goCompile(stop <-chan bool, method []string, args []string) (s func (p *Project) goRun(stop <-chan bool, runner chan bool) { var build *exec.Cmd var args []string - // custom error pattern isErrorText := func(string) bool { return false @@ -72,13 +72,16 @@ func (p *Project) goRun(stop <-chan bool, runner chan bool) { } gobin := os.Getenv("GOBIN") - path := filepath.Join(gobin, p.name) + dirPath := filepath.Base(p.Path) + if p.Path == "." { + dirPath = filepath.Base(wdir()) + } + path := filepath.Join(gobin, dirPath) if _, err := os.Stat(path); err == nil { build = exec.Command(path, args...) } else if _, err := os.Stat(path + extWindows); err == nil { build = exec.Command(path+extWindows, args...) } else { - path := filepath.Join(p.Path, p.name) if _, err = os.Stat(path); err == nil { build = exec.Command(path, args...) } else if _, err = os.Stat(path + extWindows); err == nil { @@ -148,26 +151,26 @@ func (p *Project) command(stop <-chan bool, cmd Command) (string, string) { var stderr bytes.Buffer done := make(chan error) args := strings.Split(strings.Replace(strings.Replace(cmd.Command, "'", "", -1), "\"", "", -1), " ") - exec := exec.Command(args[0], args[1:]...) - exec.Dir = p.Path + ex := exec.Command(args[0], args[1:]...) + ex.Dir = p.Path // make cmd path if cmd.Path != "" { if strings.Contains(cmd.Path, p.Path) { - exec.Dir = cmd.Path + ex.Dir = cmd.Path } else { - exec.Dir = filepath.Join(p.Path, cmd.Path) + ex.Dir = filepath.Join(p.Path, cmd.Path) } } - exec.Stdout = &stdout - exec.Stderr = &stderr + ex.Stdout = &stdout + ex.Stderr = &stderr // Start command - exec.Start() - go func() { done <- exec.Wait() }() + ex.Start() + go func() { done <- ex.Wait() }() // Wait a result select { case <-stop: // Stop running command - exec.Process.Kill() + ex.Process.Kill() return "", "" case err := <-done: // Command completed @@ -206,15 +209,17 @@ func (p *Project) goTool(wg *sync.WaitGroup, stop <-chan bool, result chan<- too case <-stop: // Stop running command cmd.Process.Kill() - break + return case err := <-done: // Command completed if err != nil { tool.err = stderr.String() + out.String() // send command result result <- tool + } else { + tool.out = out.String() } - break + return } } diff --git a/notify.go b/notify.go index 19eac5a..47cb785 100644 --- a/notify.go +++ b/notify.go @@ -95,6 +95,7 @@ func (w *fsNotifyWatcher) Events() <-chan fsnotify.Event { return w.Watcher.Events } +// Walk fsnotify func (w *fsNotifyWatcher) Walk(path string, init bool) string { if err := w.Add(path); err != nil { return "" @@ -106,9 +107,8 @@ func (w *fsNotifyWatcher) Walk(path string, init bool) string { // All watches are stopped, removed, and the poller cannot be added to func (w *filePoller) Close() error { w.mu.Lock() - defer w.mu.Unlock() - if w.closed { + w.mu.Unlock() return nil } @@ -117,6 +117,7 @@ func (w *filePoller) Close() error { w.remove(name) delete(w.watches, name) } + w.mu.Unlock() return nil } @@ -157,6 +158,7 @@ func (w *filePoller) Add(name string) error { return nil } +// Remove poller func (w *filePoller) remove(name string) error { if w.closed { return errPollerClosed @@ -184,6 +186,7 @@ func (w *filePoller) Events() <-chan fsnotify.Event { return w.events } +// Walk poller func (w *filePoller) Walk(path string, init bool) string { check := w.watches[path] if err := w.Add(path); err != nil { @@ -232,12 +235,8 @@ func (w *filePoller) watch(f *os.File, lastFi os.FileInfo, chClose chan struct{} } fi, err := os.Stat(f.Name()) - if err != nil { - // if we got an error here and lastFi is not set, we can presume that nothing has changed - // This should be safe since before `watch()` is called, a stat is performed, there is any error `watch` is not called - if lastFi == nil { - continue - } + switch { + case err != nil && lastFi != nil: // If it doesn't exist at this point, it must have been removed // no need to send the error here since this is a valid operation if os.IsNotExist(err) { @@ -245,37 +244,25 @@ func (w *filePoller) watch(f *os.File, lastFi os.FileInfo, chClose chan struct{} return } lastFi = nil - continue } // at this point, send the error - if err := w.sendErr(err, chClose); err != nil { - return - } - continue - } - - if lastFi == nil { + w.sendErr(err, chClose) + return + case lastFi == nil: if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Create, Name: f.Name()}, chClose); err != nil { return } lastFi = fi - continue - } - - if fi.Mode() != lastFi.Mode() { + case fi.Mode() != lastFi.Mode(): if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Chmod, Name: f.Name()}, chClose); err != nil { return } lastFi = fi - continue - } - - if fi.ModTime() != lastFi.ModTime() || fi.Size() != lastFi.Size() { + case fi.ModTime() != lastFi.ModTime() || fi.Size() != lastFi.Size(): if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Write, Name: f.Name()}, chClose); err != nil { return } lastFi = fi - continue } } } diff --git a/realize.go b/realize.go index 27fff9b..5e33324 100644 --- a/realize.go +++ b/realize.go @@ -15,7 +15,7 @@ import ( ) const ( - version = "1.5.1r2" + version = "1.5.2" ) // New realize instance @@ -35,26 +35,16 @@ type realize struct { // Cli commands func main() { app := &cli.App{ - Name: "Realize", - Version: version, - Authors: []*cli.Author{ - { - Name: "Alessio Pracchia", - Email: "pracchia@hastega.it", - }, - { - Name: "Daniele Conventi", - Email: "conventi@hastega.it", - }, - }, + Name: "Realize", + Version: version, Description: "Go build system with file watchers, output streams and live reload. Run, build and watch file changes with custom paths", Commands: []*cli.Command{ { Name: "start", - Aliases: []string{"r"}, + Aliases: []string{"s"}, Description: "Start a toolchain on a project or a list of projects. If not exist a config file it creates a new one", Flags: []cli.Flag{ - &cli.StringFlag{Name: "path", Aliases: []string{"p"}, Value: wdir(), Usage: "Project base path"}, + &cli.StringFlag{Name: "path", Aliases: []string{"p"}, Value: ".", Usage: "Project base path"}, &cli.StringFlag{Name: "name", Aliases: []string{"n"}, Value: "", Usage: "Run a project by its name"}, &cli.BoolFlag{Name: "fmt", Aliases: []string{"f"}, Value: false, Usage: "Enable go fmt"}, &cli.BoolFlag{Name: "vet", Aliases: []string{"v"}, Value: false, Usage: "Enable go vet"}, @@ -172,7 +162,7 @@ func main() { if err != nil { return d.Err() } - r.Settings.FileLimit = val + r.Settings.FileLimit = int32(val) return nil }, }, @@ -1217,11 +1207,11 @@ func prefix(s string) string { if s != "" { return fmt.Sprint(yellow.bold("["), "REALIZE", yellow.bold("]"), " : ", s) } - return "" + return s } // Before is launched before each command -func before(*cli.Context) error { +func before(*cli.Context) (err error) { // custom log log.SetFlags(0) log.SetOutput(logWriter{}) @@ -1230,7 +1220,7 @@ func before(*cli.Context) error { if gopath == "" { return errors.New("$GOPATH isn't set properly") } - if err := os.Setenv("GOPATH", gopath); err != nil { + if err = os.Setenv("GOPATH", gopath); err != nil { return err } // new realize instance @@ -1239,11 +1229,11 @@ func before(*cli.Context) error { r.Settings.read(&r) // increase the file limit if r.Settings.FileLimit != 0 { - if err := r.Settings.flimit(); err != nil { + if err = r.Settings.flimit(); err != nil { return err } } - return nil + return } // Rewrite the layout of the log timestamp diff --git a/server.go b/server.go index f5c6d29..98fd7ed 100644 --- a/server.go +++ b/server.go @@ -26,16 +26,15 @@ type Server struct { parent *realize Status bool `yaml:"status" json:"status"` Open bool `yaml:"open" json:"open"` - Host string `yaml:"host" json:"host"` Port int `yaml:"port" json:"port"` + Host string `yaml:"host" json:"host"` } // Websocket projects -func (s *Server) projects(c echo.Context) error { +func (s *Server) projects(c echo.Context) (err error) { websocket.Handler(func(ws *websocket.Conn) { - defer ws.Close() msg, _ := json.Marshal(s.parent) - err := websocket.Message.Send(ws, string(msg)) + err = websocket.Message.Send(ws, string(msg)) go func() { for { select { @@ -51,7 +50,7 @@ func (s *Server) projects(c echo.Context) error { for { // Read text := "" - err := websocket.Message.Receive(ws, &text) + err = websocket.Message.Receive(ws, &text) if err != nil { break } else { @@ -62,6 +61,7 @@ func (s *Server) projects(c echo.Context) error { } } } + ws.Close() }).ServeHTTP(c.Response(), c.Request()) return nil } @@ -69,7 +69,9 @@ func (s *Server) projects(c echo.Context) error { // Start the web server func (s *Server) start(p *cli.Context) (err error) { if p.Bool("server") { - s.parent.Server.Status = p.Bool("server") + s.parent.Server.Status = true + } + if p.Bool("open") { s.parent.Server.Open = true } diff --git a/settings.go b/settings.go index a712153..e941874 100644 --- a/settings.go +++ b/settings.go @@ -32,8 +32,8 @@ const ( type Settings struct { file string Files `yaml:"files,omitempty" json:"files,omitempty"` - FileLimit int64 `yaml:"flimit,omitempty" json:"flimit,omitempty"` Legacy Legacy `yaml:"legacy" json:"legacy"` + FileLimit int32 `yaml:"flimit,omitempty" json:"flimit,omitempty"` Recovery bool `yaml:"recovery,omitempty" json:"recovery,omitempty"` } diff --git a/watcher.go b/watcher.go index 8c5cfe7..a53ba20 100644 --- a/watcher.go +++ b/watcher.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/fsnotify/fsnotify" "log" "math/big" "os" @@ -12,8 +13,6 @@ import ( "sync" "syscall" "time" - - "github.com/fsnotify/fsnotify" ) var ( @@ -54,12 +53,12 @@ type Project struct { parent *realize watcher FileWatcher init bool + Settings `yaml:"-" json:"-"` files, folders int64 name, lastFile string tools []tool paths []string lastTime time.Time - Settings `yaml:"-" json:"-"` Name string `yaml:"name" json:"name"` Path string `yaml:"path" json:"path"` Environment map[string]string `yaml:"environment,omitempty" json:"environment,omitempty"` @@ -119,16 +118,27 @@ L: select { case event := <-p.watcher.Events(): if time.Now().Truncate(time.Second).After(p.lastTime) || event.Name != p.lastFile { + // event time + eventTime := time.Now() + // file extension + ext := ext(event.Name) + if ext == "" { + ext = "DIR" + } + // change message + msg = fmt.Sprintln(p.pname(p.Name, 4), ":", magenta.bold(strings.ToUpper(ext)), "changed", magenta.bold(event.Name)) + out = BufferOut{Time: time.Now(), Text: ext + " changed " + event.Name} + // switch event type switch event.Op { case fsnotify.Chmod: case fsnotify.Remove: - ext := ext(event.Name) + p.watcher.Remove(event.Name) if !strings.Contains(ext, "_") && !strings.Contains(ext, ".") && array(ext, p.Watcher.Exts) { close(stop) stop = make(chan bool) - p.changed(event, stop) // stop + p.stamp("log", out, msg, "") + go p.routines(stop, p.watcher, "") } - p.watcher.Remove(event.Name) default: file, err := os.Stat(event.Name) if err != nil { @@ -137,15 +147,20 @@ L: if file.IsDir() { filepath.Walk(event.Name, p.walk) } else if file.Size() > 0 { + // used only for test and debug if p.parent.Settings.Recovery { log.Println(event) } - ext := ext(event.Name) if !strings.Contains(ext, "_") && !strings.Contains(ext, ".") && array(ext, p.Watcher.Exts) { // change watched - close(stop) - stop = make(chan bool) - p.changed(event, stop) + // check if a file is still writing #119 + if event.Op != fsnotify.Write || eventTime.Truncate(time.Millisecond).After(file.ModTime().Truncate(time.Millisecond)) { + close(stop) + stop = make(chan bool) + // stop and start again + p.stamp("log", out, msg, "") + go p.routines(stop, p.watcher, event.Name) + } } p.lastTime = time.Now().Truncate(time.Second) p.lastFile = event.Name @@ -172,12 +187,6 @@ func (p *Project) err(err error) { // Config project init func (p *Project) config(r *realize) { - // validate project path, if invalid get wdir or clean current - if !filepath.IsAbs(p.Path) { - p.Path = wdir() - } else { - p.Path = filepath.Clean(p.Path) - } // get basepath name p.name = filepath.Base(p.Path) // env variables @@ -190,24 +199,12 @@ func (p *Project) config(r *realize) { if len(p.Cmds.Fmt.Args) == 0 { p.Cmds.Fmt.Args = []string{"-s", "-w", "-e", "./"} } - p.tools = append(p.tools, tool{ - status: p.Cmds.Fix.Status, - cmd: replace([]string{"go fix"}, p.Cmds.Fix.Method), - options: split([]string{}, p.Cmds.Fix.Args), - name: "Fix", - }) p.tools = append(p.tools, tool{ status: p.Cmds.Clean.Status, cmd: replace([]string{"go clean"}, p.Cmds.Clean.Method), options: split([]string{}, p.Cmds.Clean.Args), name: "Clean", }) - p.tools = append(p.tools, tool{ - status: p.Cmds.Fmt.Status, - cmd: replace([]string{"gofmt"}, p.Cmds.Fmt.Method), - options: split([]string{}, p.Cmds.Fmt.Args), - name: "Fmt", - }) p.tools = append(p.tools, tool{ status: p.Cmds.Generate.Status, cmd: replace([]string{"go", "generate"}, p.Cmds.Generate.Method), @@ -216,11 +213,16 @@ func (p *Project) config(r *realize) { dir: true, }) p.tools = append(p.tools, tool{ - status: p.Cmds.Test.Status, - cmd: replace([]string{"go", "test"}, p.Cmds.Test.Method), - options: split([]string{}, p.Cmds.Test.Args), - name: "Test", - dir: true, + status: p.Cmds.Fix.Status, + cmd: replace([]string{"go fix"}, p.Cmds.Fix.Method), + options: split([]string{}, p.Cmds.Fix.Args), + name: "Fix", + }) + p.tools = append(p.tools, tool{ + status: p.Cmds.Fmt.Status, + cmd: replace([]string{"gofmt"}, p.Cmds.Fmt.Method), + options: split([]string{}, p.Cmds.Fmt.Args), + name: "Fmt", }) p.tools = append(p.tools, tool{ status: p.Cmds.Vet.Status, @@ -229,6 +231,13 @@ func (p *Project) config(r *realize) { name: "Vet", dir: true, }) + p.tools = append(p.tools, tool{ + status: p.Cmds.Test.Status, + cmd: replace([]string{"go", "test"}, p.Cmds.Test.Method), + options: split([]string{}, p.Cmds.Test.Args), + name: "Test", + dir: true, + }) p.Cmds.Install = Cmd{ Status: p.Cmds.Install.Status, Args: append([]string{}, p.Cmds.Install.Args...), @@ -292,10 +301,11 @@ func (p *Project) cmd(stop <-chan bool, flag string, global bool) { // Compile is used for run and display the result of a compiling func (p *Project) compile(stop <-chan bool, cmd Cmd) error { if cmd.Status { - start := time.Now() + var start time.Time channel := make(chan Result) go func() { log.Println(p.pname(p.Name, 1), ":", cmd.startTxt) + start = time.Now() stream, err := p.goCompile(stop, cmd.method, cmd.Args) if stream != msgStop { channel <- Result{stream, err} @@ -349,10 +359,12 @@ func (p *Project) tool(stop <-chan bool, path string) error { result := make(chan tool) go func() { var wg sync.WaitGroup - wg.Add(len(p.tools)) for _, element := range p.tools { // no need a sequence, these commands can be asynchronous - go p.goTool(&wg, stop, result, path, element) + if element.status { + wg.Add(1) + p.goTool(&wg, stop, result, path, element) + } } wg.Wait() close(done) @@ -361,9 +373,15 @@ func (p *Project) tool(stop <-chan bool, path string) error { for { select { case tool := <-result: - msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.bold(tool.name), red.regular("there are some errors in"), ":", magenta.bold(path)) - buff := BufferOut{Time: time.Now(), Text: "there are some errors in", Path: path, Type: tool.name, Stream: tool.err} - p.stamp("error", buff, msg, tool.err) + if tool.err != "" { + msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.bold(tool.name), red.regular("there are some errors in"), ":", magenta.bold(path)) + buff := BufferOut{Time: time.Now(), Text: "there are some errors in", Path: path, Type: tool.name, Stream: tool.err} + p.stamp("error", buff, msg, tool.err) + } else if tool.out != "" { + msg = fmt.Sprintln(p.pname(p.Name, 3), ":", red.bold(tool.name), red.regular("outputs"), ":", blue.bold(path)) + buff := BufferOut{Time: time.Now(), Text: "outputs", Path: path, Type: tool.name, Stream: tool.out} + p.stamp("out", buff, msg, tool.out) + } case <-done: break loop case <-stop: @@ -374,19 +392,6 @@ func (p *Project) tool(stop <-chan bool, path string) error { return nil } -// Changed detect a file/directory change -func (p *Project) changed(event fsnotify.Event, stop chan bool) { - e := ext(event.Name) - if e == "" { - e = "DIR" - } - msg = fmt.Sprintln(p.pname(p.Name, 4), ":", magenta.bold(strings.ToUpper(e)), "changed", magenta.bold(event.Name)) - out = BufferOut{Time: time.Now(), Text: ext(event.Name) + " changed " + event.Name} - p.stamp("log", out, msg, "") - //stop running process - go p.routines(stop, p.watcher, event.Name) -} - // Watch the files tree of a project func (p *Project) walk(path string, info os.FileInfo, err error) error { for _, v := range p.Watcher.Ignore { @@ -475,45 +480,53 @@ func (p *Project) routines(stop <-chan bool, watcher FileWatcher, path string) { } } }() - if !done { - // before command - p.cmd(stop, "before", false) + if done { + return } - if !done { - // Go supported tools - p.tool(stop, path) - // Prevent fake events on polling startup - p.init = true + // before command + p.cmd(stop, "before", false) + if done { + return } + // Go supported tools + p.tool(stop, path) + // Prevent fake events on polling startup + p.init = true // prevent errors using realize without config with only run flag if p.Cmds.Run && !p.Cmds.Install.Status && !p.Cmds.Build.Status { p.Cmds.Install.Status = true } - if !done { - install = p.compile(stop, p.Cmds.Install) + if done { + return } - if !done { - build = p.compile(stop, p.Cmds.Build) + install = p.compile(stop, p.Cmds.Install) + if done { + return } - if !done && (install == nil && build == nil) { - if p.Cmds.Run { - start := time.Now() - runner := make(chan bool, 1) - go func() { - log.Println(p.pname(p.Name, 1), ":", "Running..") - p.goRun(stop, runner) - }() - select { - case <-runner: - msg = fmt.Sprintln(p.pname(p.Name, 5), ":", green.regular("Started"), "in", magenta.regular(big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3), " s")) - out = BufferOut{Time: time.Now(), Text: "Started in " + big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3) + " s"} - p.stamp("log", out, msg, "") - case <-stop: - return - } + build = p.compile(stop, p.Cmds.Build) + if done { + return + } + if install == nil && build == nil && p.Cmds.Run { + var start time.Time + runner := make(chan bool, 1) + go func() { + log.Println(p.pname(p.Name, 1), ":", "Running..") + start = time.Now() + p.goRun(stop, runner) + }() + select { + case <-runner: + msg = fmt.Sprintln(p.pname(p.Name, 5), ":", green.regular("Started"), "in", magenta.regular(big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3), " s")) + out = BufferOut{Time: time.Now(), Text: "Started in " + big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3) + " s"} + p.stamp("log", out, msg, "") + case <-stop: + return } } - if !done { - p.cmd(stop, "after", false) + if done { + return } + p.cmd(stop, "after", false) + }