diff --git a/.travis.yml b/.travis.yml index 1d9f6af..f23aa8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: go go: - - 1.7 - 1.8 - 1.9 - tip diff --git a/Gopkg.lock b/Gopkg.lock index e271b81..eb40bb3 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,4 +1,10 @@ -memo = "4f11729a341d710751e002460e3767a15151cb5aee2e11ba6f70409107016f15" +memo = "26587affd3e0577e23a9c4afef3eaf279d9a013fe3d673c0dc1fd062f26d2d2e" + +[[projects]] + name = "github.com/Sirupsen/logrus" + packages = ["."] + revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e" + version = "v1.0.3" [[projects]] name = "github.com/dgrijalva/jwt-go" @@ -10,7 +16,7 @@ memo = "4f11729a341d710751e002460e3767a15151cb5aee2e11ba6f70409107016f15" branch = "master" name = "github.com/fatih/color" packages = ["."] - revision = "67c513e5729f918f5e69786686770c27141a4490" + revision = "1535ebc2637cc042c938f07fa26c6356ab8f8504" [[projects]] branch = "master" @@ -19,10 +25,10 @@ memo = "4f11729a341d710751e002460e3767a15151cb5aee2e11ba6f70409107016f15" revision = "4da3e2cfbabc9f751898f250b49f2439785783a1" [[projects]] + branch = "master" name = "github.com/labstack/echo" packages = [".","middleware"] - revision = "cec7629194fe4bf83b0c72d9a02d340c7a1468ac" - version = "3.2.3" + revision = "a625e589cffa33eac768ab98bc21518cef786e2d" [[projects]] name = "github.com/labstack/gommon" @@ -42,6 +48,12 @@ memo = "4f11729a341d710751e002460e3767a15151cb5aee2e11ba6f70409107016f15" revision = "fc9e8d8ef48496124e79ae0df75490096eccf6fe" version = "v0.0.2" +[[projects]] + name = "github.com/moby/moby" + packages = ["pkg/filenotify"] + revision = "092cba3727bb9b4a2f0e922cd6c0f93ea270e363" + version = "v1.13.1" + [[projects]] branch = "master" name = "github.com/tockins/interact" @@ -63,20 +75,20 @@ memo = "4f11729a341d710751e002460e3767a15151cb5aee2e11ba6f70409107016f15" [[projects]] branch = "master" name = "golang.org/x/crypto" - packages = ["acme","acme/autocert"] - revision = "81e90905daefcd6fd217b62423c0908922eadb30" + packages = ["acme","acme/autocert","ssh/terminal"] + revision = "7d9177d70076375b9a59c8fde23d52d9c4a7ecd5" [[projects]] branch = "master" name = "golang.org/x/net" packages = ["websocket"] - revision = "57efc9c3d9f91fb3277f8da1cff370539c4d3dc5" + revision = "0744d001aa8470aaa53df28d32e5ceeb8af9bd70" [[projects]] branch = "master" name = "golang.org/x/sys" - packages = ["unix"] - revision = "2d6f6f883a06fc0d5f4b14a81e4c28705ea64c15" + packages = ["unix","windows"] + revision = "429f518978ab01db8bb6f44b66785088e7fba58b" [[projects]] branch = "v2" diff --git a/README.md b/README.md index 44bcfbf..513c6b0 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Various operations can be programmed for each project, which can be executed at - Support for most go commands (install, build, run, vet, test, fmt and much more) - Web panel for a smart control of the workflow -v 1.5 +Next features and informations - [ ] Use cases - [ ] Tests @@ -82,15 +82,15 @@ $ go get github.com/tockins/realize ``` --name="name" -> Run by name on existing configuration --path="realize/server" -> Custom Path, if not specified takes the working directory name + --generate -> Enable go generate + --fmt -> Enable go fmt + --test -> Enable go test + --install -> Enable go install --build -> Enable go build - --no-run -> Disable go run - --no-install -> Disable go install - --no-config -> Ignore an existing config / skip the creation of a new one + --run -> Enable go run --server -> Enable the web server --legacy -> Enable legacy watch instead of Fsnotify watch - --generate -> Enable go generate - --test -> Enable go test - --open -> Open in default browser + --no-config -> Ignore an existing config / skip the creation of a new one ``` Examples: @@ -98,7 +98,8 @@ $ go get github.com/tockins/realize $ realize run $ realize run --path="mypath" $ realize run --name="My Project" --build - $ realize run --path="realize" --no-run --no-config + $ realize run --path="realize" --run --no-config + $ realize run --install --test --fmt --no-config $ realize run --path="/Users/alessio/go/src/github.com/tockins/realize-examples/coin/" ``` @@ -106,7 +107,7 @@ $ go get github.com/tockins/realize **The additional arguments must go after the params** - **Run can run a project from its working directory without make a config file (--no-config).** + **Run command can start a project from its working directory without make a config file (--no-config).** ``` $ realize run --path="/print/printer" --no-run yourParams --yourFlags // right diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..d82d3c8 --- /dev/null +++ b/cmd.go @@ -0,0 +1,220 @@ +package main + +import ( + "errors" + "gopkg.in/urfave/cli.v2" + "os" + "path/filepath" + "time" +) + +// Tool options customizable +type tool struct { + dir bool + status bool + cmd string + name string + err string + options []string +} + +// Cmds +type Cmds struct { + Vet Cmd `yaml:"vet,omitempty" json:"vet,omitempty"` + Fmt Cmd `yaml:"fmt,omitempty" json:"fmt,omitempty"` + Test Cmd `yaml:"test,omitempty" json:"test,omitempty"` + Generate Cmd `yaml:"generate,omitempty" json:"generate,omitempty"` + Install Cmd `yaml:"install" json:"install"` + Build Cmd `yaml:"build,omitempty" json:"build,omitempty"` + Run bool `yaml:"run,omitempty" json:"run,omitempty"` +} + +// Cmd +type Cmd struct { + Status bool `yaml:"status,omitempty" json:"status,omitempty"` + Args []string `yaml:"args,omitempty" json:"args,omitempty"` + name, startTxt, endTxt string +} + +// Clean duplicate projects +func (r *realize) clean() { + arr := r.Schema + for key, val := range arr { + if _, err := duplicates(val, arr[key+1:]); err != nil { + r.Schema = append(arr[:key], arr[key+1:]...) + break + } + } +} + +// Check whether there is a project +func (r *realize) check() error { + if len(r.Schema) > 0 { + r.clean() + return nil + } + return errors.New("there are no projects") +} + +// Add a new project +func (r *realize) add(p *cli.Context) error { + project := Project{ + Name: r.Settings.name(p.String("name"), p.String("path")), + Path: r.Settings.path(p.String("path")), + Cmds: Cmds{ + Vet: Cmd{ + Status: p.Bool("vet"), + }, + Fmt: Cmd{ + Status: p.Bool("fmt"), + }, + Test: Cmd{ + Status: p.Bool("test"), + }, + Generate: Cmd{ + Status: p.Bool("generate"), + }, + Build: Cmd{ + Status: p.Bool("build"), + }, + Install: Cmd{ + Status: p.Bool("install"), + }, + Run: p.Bool("run"), + }, + Args: params(p), + Watcher: Watch{ + Paths: []string{"/"}, + Ignore: []string{"vendor"}, + Exts: []string{"go"}, + }, + } + if _, err := duplicates(project, r.Schema); err != nil { + return err + } + r.Schema = append(r.Schema, project) + return nil +} + +// Run launches the toolchain for each project +func (r *realize) run(p *cli.Context) error { + err := r.check() + if err == nil { + // loop projects + if p.String("name") != "" { + wg.Add(1) + } else { + wg.Add(len(r.Schema)) + } + for k, elm := range r.Schema { + if p.String("name") != "" && r.Schema[k].Name != p.String("name") { + continue + } + if elm.Cmds.Fmt.Status { + if len(elm.Cmds.Fmt.Args) == 0 { + elm.Cmds.Fmt.Args = []string{"-s", "-w", "-e", "./.."} + } + r.Schema[k].tools = append(r.Schema[k].tools, tool{ + status: elm.Cmds.Fmt.Status, + cmd: "gofmt", + options: split([]string{}, elm.Cmds.Fmt.Args), + name: "Go Fmt", + }) + } + if elm.Cmds.Generate.Status { + r.Schema[k].tools = append(r.Schema[k].tools, tool{ + status: elm.Cmds.Generate.Status, + cmd: "go", + options: split([]string{"generate"}, elm.Cmds.Generate.Args), + name: "Go Generate", + dir: true, + }) + } + if elm.Cmds.Test.Status { + r.Schema[k].tools = append(r.Schema[k].tools, tool{ + status: elm.Cmds.Test.Status, + cmd: "go", + options: split([]string{"test"}, elm.Cmds.Test.Args), + name: "Go Test", + dir: true, + }) + } + if elm.Cmds.Vet.Status { + r.Schema[k].tools = append(r.Schema[k].tools, tool{ + status: elm.Cmds.Vet.Status, + cmd: "go", + options: split([]string{"vet"}, elm.Cmds.Vet.Args), + name: "Go Vet", + dir: true, + }) + } + // default settings + r.Schema[k].Cmds.Install = Cmd{ + Status: elm.Cmds.Install.Status, + Args: append([]string{"install"}, elm.Cmds.Install.Args...), + name: "Go Install", + startTxt: "Instaling...", + endTxt: "Installed", + } + r.Schema[k].Cmds.Build = Cmd{ + Status: elm.Cmds.Build.Status, + Args: append([]string{"build"}, elm.Cmds.Build.Args...), + name: "Go Build", + startTxt: "Bulding...", + endTxt: "Built", + } + + r.Schema[k].parent = r + r.Schema[k].path = r.Schema[k].Path + + // env variables + for key, item := range r.Schema[k].Environment { + if err := os.Setenv(key, item); err != nil { + r.Schema[k].Buffer.StdErr = append(r.Schema[k].Buffer.StdErr, BufferOut{Time: time.Now(), Text: err.Error(), Type: "Env error", Stream: ""}) + } + } + + // base path of the project + wd, err := os.Getwd() + if err != nil { + return err + } + if elm.path == "." || elm.path == "/" { + r.Schema[k].base = wd + r.Schema[k].path = elm.wdir() + } else if filepath.IsAbs(elm.path) { + r.Schema[k].base = elm.path + } else { + r.Schema[k].base = filepath.Join(wd, elm.path) + } + go r.Schema[k].watch() + } + wg.Wait() + return nil + } + return err +} + +// Remove a project +func (r *realize) remove(p *cli.Context) error { + for key, val := range r.Schema { + if p.String("name") == val.Name { + r.Schema = append(r.Schema[:key], r.Schema[key+1:]...) + return nil + } + } + return errors.New("no project found") +} + +// Insert a project if there isn't already one +func (r *realize) insert(c *cli.Context) error { + if !c.Bool("config") { + r.Schema = []Project{} + } + if len(r.Schema) <= 0 { + if err := r.add(c); err != nil { + return err + } + } + return nil +} diff --git a/cmd_test.go b/cmd_test.go new file mode 100644 index 0000000..cd973a3 --- /dev/null +++ b/cmd_test.go @@ -0,0 +1,141 @@ +package main + +import ( + "flag" + "gopkg.in/urfave/cli.v2" + "os" + "reflect" + "testing" + "time" +) + +func TestBlueprint_Clean(t *testing.T) { + blp := Blueprint{} + blp.Settings = &Settings{} + blp.Projects = append(blp.Projects, Project{Name: "test0"}) + blp.Projects = append(blp.Projects, Project{Name: "test0"}) + blp.clean() + if len(blp.Projects) > 1 { + t.Error("Expected only one project") + } + blp.Projects = append(blp.Projects, Project{Path: "test1"}) + blp.Projects = append(blp.Projects, Project{Path: "test1"}) + blp.clean() + if len(blp.Projects) > 2 { + t.Error("Expected only one project") + } + +} + +func TestBlueprint_Add(t *testing.T) { + blp := Blueprint{} + blp.Settings = &Settings{} + // add all flags, test with expected + set := flag.NewFlagSet("test", 0) + set.Bool("fmt", false, "") + set.Bool("vet", false, "") + set.Bool("test", false, "") + set.Bool("install", false, "") + set.Bool("run", false, "") + set.Bool("build", false, "") + set.Bool("generate", false, "") + set.String("path", "", "") + c := cli.NewContext(nil, set, nil) + set.Parse([]string{"--path=test_path", "--fmt", "--install", "--run", "--build", "--generate", "--test", "--vet"}) + blp.add(c) + expected := Project{ + Name: "test_path", + Path: "test_path", + Cmds: Cmds{ + Fmt: Cmd{ + Status: true, + }, + Install: Cmd{ + Status: true, + }, + Generate: Cmd{ + Status: true, + }, + Test: Cmd{ + Status: true, + }, + Build: Cmd{ + Status: true, + }, + Vet: Cmd{ + Status: true, + }, + Run: true, + }, + Watcher: Watch{ + Paths: []string{"/"}, + Ignore: []string{"vendor"}, + Exts: []string{"go"}, + }, + } + if !reflect.DeepEqual(blp.Projects[0], expected) { + t.Error("Expected equal struct") + } +} + +func TestBlueprint_Check(t *testing.T) { + blp := Blueprint{} + blp.Settings = &Settings{} + err := blp.check() + if err == nil { + t.Error("There is no project, error expected") + } + blp.Projects = append(blp.Projects, Project{Name: "test0"}) + err = blp.check() + if err != nil { + t.Error("There is a project, error unexpected", err) + } +} + +func TestBlueprint_Remove(t *testing.T) { + blp := Blueprint{} + blp.Settings = &Settings{} + set := flag.NewFlagSet("name", 0) + set.String("name", "", "") + c := cli.NewContext(nil, set, nil) + set.Parse([]string{"--name=test0"}) + err := blp.remove(c) + if err == nil { + t.Error("Expected an error, there are no projects") + } + // Append a new project + blp.Projects = append(blp.Projects, Project{Name: "test0"}) + err = blp.remove(c) + if err != nil { + t.Error("Error unexpected, the project should be remove", err) + } +} + +func TestBlueprint_Run(t *testing.T) { + set := flag.NewFlagSet("test", 0) + params := cli.NewContext(nil, set, nil) + m := make(map[string]string) + m["test"] = "test" + projects := Blueprint{} + projects.Settings = &Settings{} + projects.Projects = []Project{ + { + Name: "test0", + Path: ".", + Environment: m, + }, + { + Name: "test1", + Path: ".", + }, + { + Name: "test1", + Path: ".", + }, + } + go projects.run(params) + if os.Getenv("test") != "test" { + t.Error("Env variable seems different from that given", os.Getenv("test"), "expected", m["test"]) + } + time.Sleep(5 * time.Second) +} diff --git a/watcher/exec.go b/exec.go similarity index 50% rename from watcher/exec.go rename to exec.go index 7a08f08..0ca2e63 100644 --- a/watcher/exec.go +++ b/exec.go @@ -1,10 +1,9 @@ -package watcher +package main import ( "bufio" "bytes" "fmt" - "github.com/tockins/realize/style" "log" "os" "os/exec" @@ -15,90 +14,39 @@ import ( "time" ) -// GoBuild is an implementation of the "go build" -func (p *Project) goBuild() (string, error) { - var out bytes.Buffer - var stderr bytes.Buffer - args := []string{"build"} - args = arguments(args, p.Cmds.Build.Args) - build := exec.Command("go", args...) - build.Dir = p.base - build.Stdout = &out - build.Stderr = &stderr - if err := build.Run(); err != nil { - return stderr.String(), err - } - return "", nil -} - -// GoInstall is an implementation of the "go install" -func (p *Project) goInstall() (string, error) { +// GoCompile is used for compile a project +func (p *Project) goCompile(stop <-chan bool, args []string) (string, error) { var out bytes.Buffer var stderr bytes.Buffer + done := make(chan error) err := os.Setenv("GOBIN", filepath.Join(getEnvPath("GOPATH"), "bin")) if err != nil { return "", err } - args := []string{"install"} - for _, arg := range p.Cmds.Install.Args { - arr := strings.Fields(arg) - args = append(args, arr...) - } - build := exec.Command("go", args...) - build.Dir = p.base - build.Stdout = &out - build.Stderr = &stderr - if err := build.Run(); err != nil { - return stderr.String(), err - } - return "", nil -} - -// Exec an additional command from a defined path if specified -func (p *Project) command(cmd Command) (errors string, logs string) { - var stdout bytes.Buffer - var stderr bytes.Buffer - command := strings.Replace(strings.Replace(cmd.Command, "'", "", -1), "\"", "", -1) - c := strings.Split(command, " ") - build := exec.Command(c[0], c[1:]...) - build.Dir = p.base - if cmd.Path != "" { - if strings.Contains(cmd.Path, p.base) { - build.Dir = cmd.Path - } else { - build.Dir = filepath.Join(p.base, cmd.Path) + cmd := exec.Command("go", args...) + cmd.Dir = p.base + cmd.Stdout = &out + cmd.Stderr = &stderr + // Start command + cmd.Start() + go func() { done <- cmd.Wait() }() + // Wait a result + select { + case <-stop: + // Stop running command + cmd.Process.Kill() + return "killed", nil + case err := <-done: + // Command completed + if err != nil { + return stderr.String(), err } } - build.Stdout = &stdout - build.Stderr = &stderr - err := build.Run() - // check if log - logs = stdout.String() - if err != nil { - errors = stderr.String() - return errors, logs - } - return "", logs -} - -// GoTool is used for run go methods such as fmt, test, generate... -func (p *Project) goTool(dir string, name string, cmd ...string) (string, error) { - if s := filepath.Ext(dir); s != "" && s != ".go" { - return "", nil - } - var out, stderr bytes.Buffer - build := exec.Command(name, cmd...) - build.Dir = dir - build.Stdout = &out - build.Stderr = &stderr - if err := build.Run(); err != nil { - return stderr.String() + out.String(), err - } return "", nil } // GoRun is an implementation of the bin execution -func (p *Project) goRun(channel chan bool, runner chan bool, wr *sync.WaitGroup) error { +func (p *Project) goRun(stop <-chan bool, runner chan bool) { var build *exec.Cmd var args []string isErrorText := func(string) bool { @@ -106,7 +54,7 @@ func (p *Project) goRun(channel chan bool, runner chan bool, wr *sync.WaitGroup) } errRegexp, err := regexp.Compile(p.ErrorOutputPattern) if err != nil { - msg := fmt.Sprintln(p.pname(p.Name, 3), ":", style.Blue.Regular(err.Error())) + msg := fmt.Sprintln(p.pname(p.Name, 3), ":", blue.regular(err.Error())) out := BufferOut{Time: time.Now(), Text: err.Error(), Type: "Go Run"} p.stamp("error", out, msg, "") } else { @@ -132,29 +80,28 @@ func (p *Project) goRun(channel chan bool, runner chan bool, wr *sync.WaitGroup) build = exec.Command(filepath.Join(getEnvPath("GOBIN"), filepath.Base(p.path))+".exe", args...) } else { p.Buffer.StdLog = append(p.Buffer.StdLog, BufferOut{Time: time.Now(), Text: "Can't run a not compiled project"}) - p.Fatal(err, "Can't run a not compiled project", ":") + p.fatal(err, "Can't run a not compiled project", ":") } defer func() { if err := build.Process.Kill(); err != nil { p.Buffer.StdLog = append(p.Buffer.StdLog, BufferOut{Time: time.Now(), Text: "Failed to stop: " + err.Error()}) - p.Fatal(err, "Failed to stop", ":") + p.fatal(err, "Failed to stop", ":") } - msg := fmt.Sprintln(p.pname(p.Name, 2), ":", style.Red.Regular("Ended")) + msg := fmt.Sprintln(p.pname(p.Name, 2), ":", red.regular("Ended")) out := BufferOut{Time: time.Now(), Text: "Ended", Type: "Go Run"} p.stamp("log", out, msg, "") - wr.Done() }() stdout, err := build.StdoutPipe() stderr, err := build.StderrPipe() if err != nil { - log.Println(style.Red.Bold(err.Error())) - return err + log.Println(red.bold(err.Error())) + return } if err := build.Start(); err != nil { - log.Println(style.Red.Bold(err.Error())) - return err + log.Println(red.bold(err.Error())) + return } close(runner) @@ -163,7 +110,7 @@ func (p *Project) goRun(channel chan bool, runner chan bool, wr *sync.WaitGroup) scanner := func(stop chan bool, output *bufio.Scanner, isError bool) { for output.Scan() { text := output.Text() - msg := fmt.Sprintln(p.pname(p.Name, 3), ":", style.Blue.Regular(text)) + msg := fmt.Sprintln(p.pname(p.Name, 3), ":", blue.regular(text)) if isError && !isErrorText(text) { out := BufferOut{Time: time.Now(), Text: text, Type: "Go Run"} p.stamp("error", out, msg, "") @@ -178,12 +125,90 @@ func (p *Project) goRun(channel chan bool, runner chan bool, wr *sync.WaitGroup) go scanner(stopError, execError, true) for { select { - case <-channel: - return nil + case <-stop: + return case <-stopOutput: - return nil + return case <-stopError: - return nil + return + } + } +} + +// Exec an additional command from a defined path if specified +func (p *Project) command(stop <-chan bool, cmd Command) (string, string) { + var stdout bytes.Buffer + 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.base + if cmd.Path != "" { + if strings.Contains(cmd.Path, p.base) { + exec.Dir = cmd.Path + } else { + exec.Dir = filepath.Join(p.base, cmd.Path) + } + } + exec.Stdout = &stdout + exec.Stderr = &stderr + // Start command + exec.Start() + go func() { done <- exec.Wait() }() + // Wait a result + select { + case <-stop: + // Stop running command + exec.Process.Kill() + return "", "" + case err := <-done: + // Command completed + if err != nil { + return stderr.String(), stdout.String() + } + } + return "", stdout.String() +} + +// GoTool is used for run go tools methods such as fmt, test, generate and so on +func (p *Project) goTool(wg *sync.WaitGroup, stop <-chan bool, result chan<- tool, path string, tool tool) { + defer wg.Done() + if tool.status { + if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "") { + if strings.HasSuffix(path, ".go") { + tool.options = append(tool.options, path) + path = p.base + } + if s := ext(path); s == "" || s == "go" { + if tool.dir { + path = filepath.Dir(path) + } + var out, stderr bytes.Buffer + done := make(chan error) + cmd := exec.Command(tool.cmd, tool.options...) + cmd.Dir = path + cmd.Stdout = &out + cmd.Stderr = &stderr + // Start command + cmd.Start() + go func() { done <- cmd.Wait() }() + // Wait a result + select { + case <-stop: + // Stop running command + cmd.Process.Kill() + break + case err := <-done: + // Command completed + if err != nil { + tool.err = stderr.String() + out.String() + // send command result + result <- tool + } + break + } + + } } } } diff --git a/exec_test.go b/exec_test.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/exec_test.go @@ -0,0 +1 @@ +package main diff --git a/watcher/notify.go b/notify.go similarity index 99% rename from watcher/notify.go rename to notify.go index 8573be4..9296745 100644 --- a/watcher/notify.go +++ b/notify.go @@ -1,4 +1,4 @@ -package watcher +package main import ( "errors" @@ -54,14 +54,6 @@ type ( } ) -// New tries to use an fs-event watcher, and falls back to the poller if there is an error -func Watcher() (FileWatcher, error) { - if w, err := EventWatcher(); err == nil { - return w, nil - } - return PollingWatcher(), nil -} - // NewPollingWatcher returns a poll-based file watcher func PollingWatcher() FileWatcher { return &filePoller{ @@ -71,6 +63,14 @@ func PollingWatcher() FileWatcher { } } +// New tries to use an fs-event watcher, and falls back to the poller if there is an error +func Watcher() (FileWatcher, error) { + if w, err := EventWatcher(); err == nil { + return w, nil + } + return PollingWatcher(), nil +} + // NewEventWatcher returns an fs-event based file watcher func EventWatcher() (FileWatcher, error) { w, err := fsnotify.NewWatcher() @@ -80,16 +80,16 @@ func EventWatcher() (FileWatcher, error) { return &fsNotifyWatcher{Watcher: w}, nil } -// Events returns the fsnotify event channel receiver -func (w *fsNotifyWatcher) Events() <-chan fsnotify.Event { - return w.Watcher.Events -} - // Errors returns the fsnotify error channel receiver func (w *fsNotifyWatcher) Errors() <-chan error { return w.Watcher.Errors } +// Events returns the fsnotify event channel receiver +func (w *fsNotifyWatcher) Events() <-chan fsnotify.Event { + return w.Watcher.Events +} + func (w *fsNotifyWatcher) Walk(path string, init bool) string { if err := w.Add(path); err != nil { return "" @@ -97,18 +97,28 @@ func (w *fsNotifyWatcher) Walk(path string, init bool) string { return path } -func (w *filePoller) Walk(path string, init bool) string { - check := w.watches[path] - if err := w.Add(path); err != nil { - return "" +// Close closes the poller +// 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 { + return nil } - if check == nil && init { - _, err := os.Stat(path) - if err == nil { - go w.sendEvent(fsnotify.Event{Op: fsnotify.Create, Name: path}, w.watches[path]) - } + + w.closed = true + for name := range w.watches { + w.remove(name) + delete(w.watches, name) } - return path + return nil +} + +// Errors returns the errors channel +// This is used for notifications about errors on watched files +func (w *filePoller) Errors() <-chan error { + return w.errors } // Add adds a filename to the list of watches @@ -142,13 +152,6 @@ func (w *filePoller) Add(name string) error { return nil } -// Remove stops and removes watch with the specified name -func (w *filePoller) Remove(name string) error { - w.mu.Lock() - defer w.mu.Unlock() - return w.remove(name) -} - func (w *filePoller) remove(name string) error { if w.closed { return errPollerClosed @@ -163,32 +166,39 @@ func (w *filePoller) remove(name string) error { return nil } +// Remove stops and removes watch with the specified name +func (w *filePoller) Remove(name string) error { + w.mu.Lock() + defer w.mu.Unlock() + return w.remove(name) +} + // Events returns the event channel // This is used for notifications on events about watched files func (w *filePoller) Events() <-chan fsnotify.Event { return w.events } -// Errors returns the errors channel -// This is used for notifications about errors on watched files -func (w *filePoller) Errors() <-chan error { - return w.errors +func (w *filePoller) Walk(path string, init bool) string { + check := w.watches[path] + if err := w.Add(path); err != nil { + return "" + } + if check == nil && init { + _, err := os.Stat(path) + if err == nil { + go w.sendEvent(fsnotify.Event{Op: fsnotify.Create, Name: path}, w.watches[path]) + } + } + return path } -// Close closes the poller -// 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 { - return nil - } - - w.closed = true - for name := range w.watches { - w.remove(name) - delete(w.watches, name) +// sendErr publishes the specified error to the errors channel +func (w *filePoller) sendErr(e error, chClose <-chan struct{}) error { + select { + case w.errors <- e: + case <-chClose: + return fmt.Errorf("closed") } return nil } @@ -203,16 +213,6 @@ func (w *filePoller) sendEvent(e fsnotify.Event, chClose <-chan struct{}) error return nil } -// sendErr publishes the specified error to the errors channel -func (w *filePoller) sendErr(e error, chClose <-chan struct{}) error { - select { - case w.errors <- e: - case <-chClose: - return fmt.Errorf("closed") - } - return nil -} - // watch is responsible for polling the specified file for changes // upon finding changes to a file or errors, sendEvent/sendErr is called func (w *filePoller) watch(f *os.File, lastFi os.FileInfo, chClose chan struct{}) { diff --git a/notify_test.go b/notify_test.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/notify_test.go @@ -0,0 +1 @@ +package main diff --git a/realize.go b/realize.go index 9d1e8ae..d00d3f7 100644 --- a/realize.go +++ b/realize.go @@ -5,10 +5,8 @@ import ( "fmt" "github.com/fatih/color" "github.com/tockins/interact" - "github.com/tockins/realize/settings" - "github.com/tockins/realize/style" - "github.com/tockins/realize/watcher" - cli "gopkg.in/urfave/cli.v2" + "gopkg.in/urfave/cli.v2" + "log" "os" "strconv" "time" @@ -18,18 +16,20 @@ const ( version = "1.4.1" ) -// Realize struct contains the general app informations -type realize struct { - settings.Settings `yaml:"settings,omitempty"` - Sync chan string `yaml:"-"` - blueprint watcher.Blueprint `yaml:"-"` - server server `yaml:"-"` - projects *[]watcher.Project `yaml:"projects" json:"projects"` -} - // New realize instance var r realize +// Log struct +type logWriter struct{} + +// Realize struct contains the general app informations +type realize struct { + Settings Settings `yaml:"settings"` + Server Server `yaml:"server"` + Schema []Project `yaml:"schema"` + sync chan string +} + // Cli commands func main() { app := &cli.App{ @@ -48,36 +48,35 @@ func main() { Description: "A Go build system with file watchers, output streams and live reload. Run, build and watch file changes with custom paths", Commands: []*cli.Command{ { - Name: "run", + Name: "start", Aliases: []string{"r"}, - Description: "Run a toolchain on a project or a list of projects. If not exist a config file it creates a new one", + 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: "", Usage: "Project base path."}, &cli.StringFlag{Name: "name", Aliases: []string{"n"}, Value: "", Usage: "Run a project by its name."}, - &cli.BoolFlag{Name: "test", Aliases: []string{"t"}, Value: false, Usage: "Enable go test."}, &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."}, + &cli.BoolFlag{Name: "test", Aliases: []string{"t"}, Value: false, Usage: "Enable go test."}, &cli.BoolFlag{Name: "generate", Aliases: []string{"g"}, Value: false, Usage: "Enable go generate."}, - &cli.BoolFlag{Name: "legacy", Aliases: []string{"l"}, Value: false, Usage: "Watch by polling instead of watch by fsnotify."}, &cli.BoolFlag{Name: "server", Aliases: []string{"s"}, Value: false, Usage: "Enable server and open into the default browser."}, &cli.BoolFlag{Name: "install", Aliases: []string{"i"}, Value: false, Usage: "Enable go install."}, &cli.BoolFlag{Name: "build", Aliases: []string{"b"}, Value: false, Usage: "Enable go build."}, &cli.BoolFlag{Name: "run", Aliases: []string{"nr"}, Value: false, Usage: "Enable go run"}, - &cli.BoolFlag{Name: "no-config", Aliases: []string{"nc"}, Value: false, Usage: "Ignore existing configurations."}, + &cli.BoolFlag{Name: "config", Aliases: []string{"nc"}, Value: false, Usage: "Use a config if exist or save a new one"}, }, Action: func(p *cli.Context) error { - polling(p, &r.Legacy) - if err := insert(p, &r.blueprint); err != nil { + if err := r.insert(p); err != nil { return err } - if !p.Bool("no-config") { - if err := r.Record(r); err != nil { + if p.Bool("config") { + if err := r.Settings.record(r); err != nil { return err } } - if err := r.server.start(p); err != nil { + if err := r.Server.start(p); err != nil { return err } - if err := r.blueprint.Run(p); err != nil { + if err := r.run(p); err != nil { return err } return nil @@ -93,20 +92,19 @@ func main() { &cli.StringFlag{Name: "path", Aliases: []string{"p"}, Value: "", Usage: "Project base path."}, &cli.BoolFlag{Name: "test", Aliases: []string{"t"}, Value: false, Usage: "Enable go test."}, &cli.BoolFlag{Name: "generate", Aliases: []string{"g"}, Value: false, Usage: "Enable go generate."}, - &cli.BoolFlag{Name: "legacy", Aliases: []string{"l"}, Value: false, Usage: "Watch by polling instead of Watch by fsnotify."}, &cli.BoolFlag{Name: "server", Aliases: []string{"s"}, Value: false, Usage: "Enable server and open into the default browser."}, &cli.BoolFlag{Name: "install", Aliases: []string{"i"}, Value: false, Usage: "Enable go install"}, &cli.BoolFlag{Name: "build", Aliases: []string{"b"}, Value: false, Usage: "Enable go build"}, &cli.BoolFlag{Name: "run", Aliases: []string{"r"}, Value: false, Usage: "Enable go run"}, }, Action: func(p *cli.Context) error { - if err := r.blueprint.Add(p); err != nil { + if err := r.add(p); err != nil { return err } - if err := r.Record(r); err != nil { + if err := r.Settings.record(r); err != nil { return err } - fmt.Fprintln(style.Output, prefix(style.Green.Bold("Your project was successfully added."))) + fmt.Fprintln(output, prefix(green.bold("Your project was successfully added."))) return nil }, Before: before, @@ -119,49 +117,49 @@ func main() { Action: func(p *cli.Context) (actErr error) { interact.Run(&interact.Interact{ Before: func(context interact.Context) error { - context.SetErr(style.Red.Bold("INVALID INPUT")) - context.SetPrfx(color.Output, style.Yellow.Bold("[")+"REALIZE"+style.Yellow.Bold("]")) + context.SetErr(red.bold("INVALID INPUT")) + context.SetPrfx(color.Output, yellow.bold("[")+"REALIZE"+yellow.bold("]")) return nil }, Questions: []*interact.Question{ { Before: func(d interact.Context) error { - if _, err := os.Stat(settings.Directory + "/" + settings.File); err != nil { + if _, err := os.Stat(Directory + "/" + File); err != nil { d.Skip() } - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), - Msg: "Would you want to overwrite the existing " + style.Magenta.Bold("Realize") + " config?", + Options: yellow.regular("[y/n]"), + Msg: "Would you want to overwrite the existing " + magenta.bold("Realize") + " config?", }, Action: func(d interact.Context) interface{} { val, err := d.Ans().Bool() if err != nil { return d.Err() } else if val { - r.Settings = settings.Settings{ - File: settings.File, - Server: settings.Server{ - Status: false, - Open: false, - Host: host, - Port: port, - }, + r.Settings = Settings{ + file: File, } - r.blueprint.Projects = r.blueprint.Projects[len(r.blueprint.Projects):] + r.Server = Server{ + Status: false, + Open: false, + Host: host, + Port: port, + } + r.Schema = r.Schema[len(r.Schema):] } return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Would you want to customize the " + ("settings") + "?", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() @@ -171,11 +169,11 @@ func main() { Subs: []*interact.Question{ { Before: func(d interact.Context) error { - d.SetDef(0, style.Green.Regular("(os default)")) + d.SetDef(0, green.regular("(os default)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[int]"), + Options: yellow.regular("[int]"), Msg: "Max number of open files (root required)", }, Action: func(d interact.Context) interface{} { @@ -183,54 +181,17 @@ func main() { if err != nil { return d.Err() } - r.FileLimit = val + r.Settings.FileLimit = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), - Msg: "Enable legacy watch by polling", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef(1, style.Green.Regular("(1s)")) - return nil - }, - Quest: interact.Quest{ - Options: style.Yellow.Regular("[seconds]"), - Msg: "Set polling interval in seconds", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Int() - if err != nil { - return d.Err() - } - r.Legacy.Interval = time.Duration(val * 1000000000) - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Enable logging files", }, Action: func(d interact.Context) interface{} { @@ -238,19 +199,19 @@ func main() { if err != nil { return d.Err() } - r.Files.Errors = settings.Resource{Name: settings.FileErr, Status: val} - r.Files.Outputs = settings.Resource{Name: settings.FileOut, Status: val} - r.Files.Logs = settings.Resource{Name: settings.FileLog, Status: val} + r.Settings.Files.Errors = Resource{Name: FileErr, Status: val} + r.Settings.Files.Outputs = Resource{Name: FileOut, Status: val} + r.Settings.Files.Logs = Resource{Name: FileLog, Status: val} return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Enable web server", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() @@ -260,11 +221,11 @@ func main() { Subs: []*interact.Question{ { Before: func(d interact.Context) error { - d.SetDef(port, style.Green.Regular("("+strconv.Itoa(port)+")")) + d.SetDef(port, green.regular("("+strconv.Itoa(port)+")")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[int]"), + Options: yellow.regular("[int]"), Msg: "Server port", }, Action: func(d interact.Context) interface{} { @@ -278,11 +239,11 @@ func main() { }, { Before: func(d interact.Context) error { - d.SetDef(host, style.Green.Regular("("+host+")")) + d.SetDef(host, green.regular("("+host+")")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Server host", }, Action: func(d interact.Context) interface{} { @@ -296,11 +257,11 @@ func main() { }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Open in the current browser", }, Action: func(d interact.Context) interface{} { @@ -333,17 +294,17 @@ func main() { }, { Before: func(d interact.Context) error { - d.SetDef(true, style.Green.Regular("(y)")) + d.SetDef(true, green.regular("(y)")) d.SetEnd("!") return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), - Msg: "Would you want to " + style.Magenta.Regular("add a new project") + "? (insert '!' to stop)", + Options: yellow.regular("[y/n]"), + Msg: "Would you want to " + magenta.regular("add a new project") + "? (insert '!' to stop)", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() if val { - r.blueprint.Add(p) + r.add(p) } return val }, @@ -351,11 +312,11 @@ func main() { Subs: []*interact.Question{ { Before: func(d interact.Context) error { - d.SetDef(r.Settings.Wdir(), style.Green.Regular("("+r.Settings.Wdir()+")")) + d.SetDef(r.Settings.wdir(), green.regular("("+r.Settings.wdir()+")")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Project name", }, Action: func(d interact.Context) interface{} { @@ -363,18 +324,18 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Name = val + r.Schema[len(r.Schema)-1].Name = val return nil }, }, { Before: func(d interact.Context) error { dir, _ := os.Getwd() - d.SetDef(dir, style.Green.Regular("("+dir+")")) + d.SetDef(dir, green.regular("("+dir+")")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Project path", }, Action: func(d interact.Context) interface{} { @@ -382,36 +343,58 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Path = r.Settings.Path(val) + r.Schema[len(r.Schema)-1].Path = r.Settings.path(val) return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(true, style.Green.Regular("(y)")) + d.SetDef(true, green.regular("(y)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Enable go vet", }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef("", green.regular("(none)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Vet additional arguments", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + if val != "" { + r.Schema[len(r.Schema)-1].Cmds.Vet.Args = append(r.Schema[len(r.Schema)-1].Cmds.Vet.Args, val) + } + return nil + }, + }, + }, Action: func(d interact.Context) interface{} { val, err := d.Ans().Bool() if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Vet = val + r.Schema[len(r.Schema)-1].Cmds.Vet.Status = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(true, style.Green.Regular("(y)")) + d.SetDef(true, green.regular("(y)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Enable go fmt", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() @@ -421,11 +404,11 @@ func main() { Subs: []*interact.Question{ { Before: func(d interact.Context) error { - d.SetDef("", style.Green.Regular("(none)")) + d.SetDef("", green.regular("(none)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Fmt additional arguments", }, Action: func(d interact.Context) interface{} { @@ -434,7 +417,7 @@ func main() { return d.Err() } if val != "" { - r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Fmt.Args = append(r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Fmt.Args, val) + r.Schema[len(r.Schema)-1].Cmds.Fmt.Args = append(r.Schema[len(r.Schema)-1].Cmds.Fmt.Args, val) } return nil }, @@ -445,17 +428,17 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Fmt.Status = val + r.Schema[len(r.Schema)-1].Cmds.Fmt.Status = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(true, style.Green.Regular("(y)")) + d.SetDef(true, green.regular("(y)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Enable go test", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() @@ -465,11 +448,11 @@ func main() { Subs: []*interact.Question{ { Before: func(d interact.Context) error { - d.SetDef("", style.Green.Regular("(none)")) + d.SetDef("", green.regular("(none)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Test additional arguments", }, Action: func(d interact.Context) interface{} { @@ -478,7 +461,7 @@ func main() { return d.Err() } if val != "" { - r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Test.Args = append(r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Test.Args, val) + r.Schema[len(r.Schema)-1].Cmds.Test.Args = append(r.Schema[len(r.Schema)-1].Cmds.Test.Args, val) } return nil }, @@ -489,17 +472,17 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Test.Status = val + r.Schema[len(r.Schema)-1].Cmds.Test.Status = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Enable go generate", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() @@ -509,11 +492,11 @@ func main() { Subs: []*interact.Question{ { Before: func(d interact.Context) error { - d.SetDef("", style.Green.Regular("(none)")) + d.SetDef("", green.regular("(none)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Generate additional arguments", }, Action: func(d interact.Context) interface{} { @@ -522,7 +505,7 @@ func main() { return d.Err() } if val != "" { - r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Generate.Args = append(r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Generate.Args, val) + r.Schema[len(r.Schema)-1].Cmds.Generate.Args = append(r.Schema[len(r.Schema)-1].Cmds.Generate.Args, val) } return nil }, @@ -533,17 +516,17 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Generate.Status = val + r.Schema[len(r.Schema)-1].Cmds.Generate.Status = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(true, style.Green.Regular("(y)")) + d.SetDef(true, green.regular("(y)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Enable go install", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() @@ -553,11 +536,11 @@ func main() { Subs: []*interact.Question{ { Before: func(d interact.Context) error { - d.SetDef("", style.Green.Regular("(none)")) + d.SetDef("", green.regular("(none)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Install additional arguments", }, Action: func(d interact.Context) interface{} { @@ -566,7 +549,7 @@ func main() { return d.Err() } if val != "" { - r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Install.Args = append(r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Install.Args, val) + r.Schema[len(r.Schema)-1].Cmds.Install.Args = append(r.Schema[len(r.Schema)-1].Cmds.Install.Args, val) } return nil }, @@ -577,17 +560,17 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Install.Status = val + r.Schema[len(r.Schema)-1].Cmds.Install.Status = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Enable go build", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() @@ -597,11 +580,11 @@ func main() { Subs: []*interact.Question{ { Before: func(d interact.Context) error { - d.SetDef("", style.Green.Regular("(none)")) + d.SetDef("", green.regular("(none)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Build additional arguments", }, Action: func(d interact.Context) interface{} { @@ -610,7 +593,7 @@ func main() { return d.Err() } if val != "" { - r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Build.Args = append(r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Build.Args, val) + r.Schema[len(r.Schema)-1].Cmds.Build.Args = append(r.Schema[len(r.Schema)-1].Cmds.Build.Args, val) } return nil }, @@ -621,17 +604,17 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Build.Status = val + r.Schema[len(r.Schema)-1].Cmds.Build.Status = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(true, style.Green.Regular("(y)")) + d.SetDef(true, green.regular("(y)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Enable go run", }, Action: func(d interact.Context) interface{} { @@ -639,22 +622,22 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Cmds.Run = val + r.Schema[len(r.Schema)-1].Cmds.Run = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Customize watched paths", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() if val { - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Paths = r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Paths[:len(r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Paths)-1] + r.Schema[len(r.Schema)-1].Watcher.Paths = r.Schema[len(r.Schema)-1].Watcher.Paths[:len(r.Schema[len(r.Schema)-1].Watcher.Paths)-1] } return val }, @@ -666,7 +649,7 @@ func main() { return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Insert a path to watch (insert '!' to stop)", }, Action: func(d interact.Context) interface{} { @@ -674,7 +657,7 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Paths = append(r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Paths, val) + r.Schema[len(r.Schema)-1].Watcher.Paths = append(r.Schema[len(r.Schema)-1].Watcher.Paths, val) d.Reload() return nil }, @@ -690,16 +673,16 @@ func main() { }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Customize ignored paths", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() if val { - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Ignore = r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Ignore[:len(r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Ignore)-1] + r.Schema[len(r.Schema)-1].Watcher.Ignore = r.Schema[len(r.Schema)-1].Watcher.Ignore[:len(r.Schema[len(r.Schema)-1].Watcher.Ignore)-1] } return val }, @@ -711,7 +694,7 @@ func main() { return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Insert a path to ignore (insert '!' to stop)", }, Action: func(d interact.Context) interface{} { @@ -719,7 +702,7 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Ignore = append(r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Ignore, val) + r.Schema[len(r.Schema)-1].Watcher.Ignore = append(r.Schema[len(r.Schema)-1].Watcher.Ignore, val) d.Reload() return nil }, @@ -735,11 +718,11 @@ func main() { }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Add an additional argument", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() @@ -753,7 +736,7 @@ func main() { return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Add another argument (insert '!' to stop)", }, Action: func(d interact.Context) interface{} { @@ -761,7 +744,7 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Args = append(r.blueprint.Projects[len(r.blueprint.Projects)-1].Args, val) + r.Schema[len(r.Schema)-1].Args = append(r.Schema[len(r.Schema)-1].Args, val) d.Reload() return nil }, @@ -777,12 +760,12 @@ func main() { }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(none)")) + d.SetDef(false, green.regular("(none)")) d.SetEnd("!") return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Add a 'before' custom command (insert '!' to stop)", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() @@ -795,7 +778,7 @@ func main() { return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Insert a command", }, Action: func(d interact.Context) interface{} { @@ -803,17 +786,17 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts = append(r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts, watcher.Command{Type: "before", Command: val}) + r.Schema[len(r.Schema)-1].Watcher.Scripts = append(r.Schema[len(r.Schema)-1].Watcher.Scripts, Command{Type: "before", Command: val}) return nil }, }, { Before: func(d interact.Context) error { - d.SetDef("", style.Green.Regular("(n)")) + d.SetDef("", green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Launch from a specific path", }, Action: func(d interact.Context) interface{} { @@ -821,17 +804,17 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts[len(r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts)-1].Path = val + r.Schema[len(r.Schema)-1].Watcher.Scripts[len(r.Schema[len(r.Schema)-1].Watcher.Scripts)-1].Path = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Tag as global command", }, Action: func(d interact.Context) interface{} { @@ -839,17 +822,17 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts[len(r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts)-1].Global = val + r.Schema[len(r.Schema)-1].Watcher.Scripts[len(r.Schema[len(r.Schema)-1].Watcher.Scripts)-1].Global = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Display command output", }, Action: func(d interact.Context) interface{} { @@ -857,7 +840,7 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts[len(r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts)-1].Output = val + r.Schema[len(r.Schema)-1].Watcher.Scripts[len(r.Schema[len(r.Schema)-1].Watcher.Scripts)-1].Output = val return nil }, }, @@ -875,12 +858,12 @@ func main() { }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(none)")) + d.SetDef(false, green.regular("(none)")) d.SetEnd("!") return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Add an 'after' custom commands (insert '!' to stop)", Resolve: func(d interact.Context) bool { val, _ := d.Ans().Bool() @@ -893,7 +876,7 @@ func main() { return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Insert a command", }, Action: func(d interact.Context) interface{} { @@ -901,17 +884,17 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts = append(r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts, watcher.Command{Type: "after", Command: val}) + r.Schema[len(r.Schema)-1].Watcher.Scripts = append(r.Schema[len(r.Schema)-1].Watcher.Scripts, Command{Type: "after", Command: val}) return nil }, }, { Before: func(d interact.Context) error { - d.SetDef("", style.Green.Regular("(n)")) + d.SetDef("", green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Launch from a specific path", }, Action: func(d interact.Context) interface{} { @@ -919,17 +902,17 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts[len(r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts)-1].Path = val + r.Schema[len(r.Schema)-1].Watcher.Scripts[len(r.Schema[len(r.Schema)-1].Watcher.Scripts)-1].Path = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Tag as global command", }, Action: func(d interact.Context) interface{} { @@ -937,17 +920,17 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts[len(r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts)-1].Global = val + r.Schema[len(r.Schema)-1].Watcher.Scripts[len(r.Schema[len(r.Schema)-1].Watcher.Scripts)-1].Global = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Display command output", }, Action: func(d interact.Context) interface{} { @@ -955,7 +938,7 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts[len(r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Scripts)-1].Output = val + r.Schema[len(r.Schema)-1].Watcher.Scripts[len(r.Schema[len(r.Schema)-1].Watcher.Scripts)-1].Output = val return nil }, }, @@ -973,11 +956,11 @@ func main() { }, { Before: func(d interact.Context) error { - d.SetDef(false, style.Green.Regular("(n)")) + d.SetDef(false, green.regular("(n)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[y/n]"), + Options: yellow.regular("[y/n]"), Msg: "Enable watcher files preview", }, Action: func(d interact.Context) interface{} { @@ -985,17 +968,17 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].Watcher.Preview = val + r.Schema[len(r.Schema)-1].Watcher.Preview = val return nil }, }, { Before: func(d interact.Context) error { - d.SetDef("", style.Green.Regular("(none)")) + d.SetDef("", green.regular("(none)")) return nil }, Quest: interact.Quest{ - Options: style.Yellow.Regular("[string]"), + Options: yellow.regular("[string]"), Msg: "Set an error output pattern", }, Action: func(d interact.Context) interface{} { @@ -1003,7 +986,7 @@ func main() { if err != nil { return d.Err() } - r.blueprint.Projects[len(r.blueprint.Projects)-1].ErrorOutputPattern = val + r.Schema[len(r.Schema)-1].ErrorOutputPattern = val return nil }, }, @@ -1020,7 +1003,7 @@ func main() { }, After: func(d interact.Context) error { if val, _ := d.Qns().Get(0).Ans().Bool(); val { - actErr = r.Settings.Remove(settings.Directory) + actErr = r.Settings.del(Directory) if actErr != nil { return actErr } @@ -1028,10 +1011,10 @@ func main() { return nil }, }) - if err := r.Record(r); err != nil { + if err := r.Settings.record(r); err != nil { return err } - fmt.Fprintln(style.Output, prefix(style.Green.Bold(" Your configuration was successful."))) + fmt.Fprintln(output, prefix(green.bold(" Your configuration was successful."))) return nil }, Before: before, @@ -1045,37 +1028,27 @@ func main() { &cli.StringFlag{Name: "name", Aliases: []string{"n"}, Value: ""}, }, Action: func(p *cli.Context) error { - if err := r.blueprint.Remove(p); err != nil { + if err := r.remove(p); err != nil { return err } - if err := r.Record(r); err != nil { + if err := r.Settings.record(r); err != nil { return err } - fmt.Fprintln(style.Output, prefix(style.Green.Bold("Your project was successfully removed."))) + fmt.Fprintln(output, prefix(green.bold("Your project was successfully removed."))) return nil }, Before: before, }, - { - Name: "list", - Category: "Configuration", - Aliases: []string{"l"}, - Description: "Print projects list.", - Action: func(p *cli.Context) error { - return r.blueprint.List() - }, - Before: before, - }, { Name: "clean", Category: "Configuration", Aliases: []string{"c"}, Description: "Remove realize folder.", Action: func(p *cli.Context) error { - if err := r.Settings.Remove(settings.Directory); err != nil { + if err := r.Settings.del(Directory); err != nil { return err } - fmt.Fprintln(style.Output, prefix(style.Green.Bold("Realize folder successfully removed."))) + fmt.Fprintln(output, prefix(green.bold("Realize folder successfully removed."))) return nil }, Before: before, @@ -1083,7 +1056,7 @@ func main() { }, } if err := app.Run(os.Args); err != nil { - print(style.Red.Bold(err)) + print(red.bold(err)) os.Exit(1) } } @@ -1091,67 +1064,49 @@ func main() { // Prefix a given string func prefix(s string) string { if s != "" { - return fmt.Sprint(style.Yellow.Bold("[")+"REALIZE"+style.Yellow.Bold("]"), s) + return fmt.Sprint(yellow.bold("["), "REALIZE", yellow.bold("]"), s) } return "" } // Before is launched before each command func before(*cli.Context) error { + // custom log + log.SetFlags(0) + log.SetOutput(new(logWriter)) // Before of every exec of a cli method gopath := os.Getenv("GOPATH") if gopath == "" { return errors.New("$GOPATH isn't set properly") } r = realize{ - Sync: make(chan string), - Settings: settings.Settings{ - File: settings.File, - Server: settings.Server{ - Status: false, - Open: false, - Host: host, - Port: port, + sync: make(chan string), + Settings: Settings{ + file: File, + Legacy: Legacy{ + Interval: 100 * time.Millisecond, }, }, + Server: Server{ + parent: &r, + Status: false, + Open: false, + Host: host, + Port: port, + }, } - r.blueprint = watcher.Blueprint{ - Settings: &r.Settings, - Sync: r.Sync, - } - r.server = server{ - Blueprint: &r.blueprint, - Settings: &r.Settings, - Sync: r.Sync, - } - r.projects = &r.blueprint.Projects // read if exist - r.Read(&r) + r.Settings.read(&r) // increase the file limit - if r.FileLimit != 0 { - if err := r.Flimit(); err != nil { + if r.Settings.FileLimit != 0 { + if err := r.Settings.flimit(); err != nil { return err } } return nil } -// Check for polling option -func polling(c *cli.Context, s *settings.Legacy) { - if c.Bool("legacy") { - s.Interval = settings.Interval - } -} - -// Insert a project if there isn't already one -func insert(c *cli.Context, b *watcher.Blueprint) error { - if c.Bool("no-config") { - r.blueprint.Projects = []watcher.Project{} - } - if len(b.Projects) <= 0 { - if err := b.Add(c); err != nil { - return err - } - } - return nil +// Rewrite the layout of the log timestamp +func (w logWriter) Write(bytes []byte) (int, error) { + return fmt.Fprint(output, yellow.regular("["), time.Now().Format("15:04:05"), yellow.regular("]")+string(bytes)) } diff --git a/realize_test.go b/realize_test.go index 2e6d946..d85fbaa 100644 --- a/realize_test.go +++ b/realize_test.go @@ -1,56 +1,45 @@ package main -import ( - "flag" - "fmt" - "github.com/tockins/realize/settings" - "github.com/tockins/realize/style" - "github.com/tockins/realize/watcher" - "gopkg.in/urfave/cli.v2" - "testing" -) - -func TestPrefix(t *testing.T) { - input := settings.Rand(10) - value := fmt.Sprint(style.Yellow.Bold("[")+"REALIZE"+style.Yellow.Bold("]"), input) - result := prefix(input) - if result == "" { - t.Fatal("Expected a string") - } - if result != value { - t.Fatal("Expected", value, "Instead", result) - } -} - -func TestBefore(t *testing.T) { - context := cli.Context{} - if err := before(&context); err != nil { - t.Fatal(err) - } -} - -func TestPolling(t *testing.T) { - settings := settings.Legacy{} - set := flag.NewFlagSet("test", 0) - set.Bool("legacy", true, "") - params := cli.NewContext(nil, set, nil) - polling(params, &settings) - if settings.Interval == 0 { - t.Fatal("Expected interval", settings.Interval, "Instead", 0) - } -} - -func TestInsert(t *testing.T) { - b := watcher.Blueprint{} - b.Settings = &settings.Settings{} - set := flag.NewFlagSet("test", 0) - set.String("name", settings.Rand(5), "") - set.String("path", settings.Rand(5), "") - params := cli.NewContext(nil, set, nil) - if err := insert(params, &b); err != nil { - t.Fatal(err) - } - if len(b.Projects) == 0 { - t.Error("Expected one project") - } -} +//import ( +// "flag" +// "fmt" +// "github.com/tockins/realize/settings" +// "github.com/tockins/realize/style" +// "github.com/tockins/realize/watcher" +// "gopkg.in/urfave/cli.v2" +// "testing" +//) +// +//func TestPrefix(t *testing.T) { +// input := random(10) +// value := fmt.Sprint(yellow.bold("[")+"REALIZE"+yellow.bold("]"), input) +// result := prefix(input) +// if result == "" { +// t.Fatal("Expected a string") +// } +// if result != value { +// t.Fatal("Expected", value, "Instead", result) +// } +//} +// +//func TestBefore(t *testing.T) { +// context := cli.Context{} +// if err := before(&context); err != nil { +// t.Fatal(err) +// } +//} +// +//func TestInsert(t *testing.T) { +// b := Blueprint{} +// b.Settings = &Settings{} +// set := flag.NewFlagSet("test", 0) +// set.String("name", random(5), "") +// set.String("path", random(5), "") +// params := cli.NewContext(nil, set, nil) +// if err := insert(params, &b); err != nil { +// t.Fatal(err) +// } +// if len(b.Projects) == 0 { +// t.Error("Expected one project") +// } +//} diff --git a/server.go b/server.go index 411b14c..ad8f367 100644 --- a/server.go +++ b/server.go @@ -1,21 +1,18 @@ package main import ( + "bytes" "encoding/json" - "net/http" - "strconv" - + "fmt" "github.com/labstack/echo" "github.com/labstack/echo/middleware" - "github.com/tockins/realize/settings" - "github.com/tockins/realize/watcher" "golang.org/x/net/websocket" "gopkg.in/urfave/cli.v2" "io" - "runtime" - "fmt" + "net/http" "os/exec" - "bytes" + "runtime" + "strconv" ) // Dafault host and port @@ -25,50 +22,58 @@ const ( ) // Server settings -type server struct { - *settings.Settings `yaml:"-"` - *watcher.Blueprint `yaml:"-"` - Sync chan string `yaml:"-"` +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"` } -// Render return a web pages defined in bindata -func (s *server) render(c echo.Context, path string, mime int) error { - data, err := Asset(path) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound) - } - rs := c.Response() - // check content type by extensions - switch mime { - case 1: - rs.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) - break - case 2: - rs.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJavaScriptCharsetUTF8) - break - case 3: - rs.Header().Set(echo.HeaderContentType, "text/css") - break - case 4: - rs.Header().Set(echo.HeaderContentType, "image/svg+xml") - break - case 5: - rs.Header().Set(echo.HeaderContentType, "image/png") - break - } - rs.WriteHeader(http.StatusOK) - rs.Write(data) +// Websocket projects +func (s *Server) projects(c echo.Context) error { + websocket.Handler(func(ws *websocket.Conn) { + defer ws.Close() + msg, _ := json.Marshal(s.parent.Schema) + err := websocket.Message.Send(ws, string(msg)) + go func() { + for { + select { + case <-s.parent.sync: + msg, _ := json.Marshal(s.parent.Schema) + err = websocket.Message.Send(ws, string(msg)) + if err != nil { + break + } + } + } + }() + for { + // Read + text := "" + err := websocket.Message.Receive(ws, &text) + if err != nil { + break + } else { + err := json.Unmarshal([]byte(text), &s.parent.Schema) + if err != nil { + s.parent.Settings.record(s.parent.Settings) + break + } + } + } + }).ServeHTTP(c.Response(), c.Request()) return nil } // Start the web server -func (s *server) start(p *cli.Context) (err error) { +func (s *Server) start(p *cli.Context) (err error) { if p.Bool("server") { - s.Server.Status = p.Bool("server") - s.Server.Open = true + s.parent.Server.Status = p.Bool("server") + s.parent.Server.Open = true } - if s.Server.Status { + if s.parent.Server.Status { e := echo.New() e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ Level: 2, @@ -124,8 +129,8 @@ func (s *server) start(p *cli.Context) (err error) { //websocket e.GET("/ws", s.projects) - go e.Start(string(s.Settings.Server.Host) + ":" + strconv.Itoa(s.Settings.Server.Port)) - _, err = s.openURL("http://" + string(s.Settings.Server.Host) + ":" + strconv.Itoa(s.Settings.Server.Port)) + go e.Start(string(s.parent.Server.Host) + ":" + strconv.Itoa(s.parent.Server.Port)) + _, err = s.openURL("http://" + string(s.parent.Server.Host) + ":" + strconv.Itoa(s.parent.Server.Port)) if err != nil { return err } @@ -133,44 +138,8 @@ func (s *server) start(p *cli.Context) (err error) { return nil } -// Websocket projects -func (s *server) projects(c echo.Context) error { - websocket.Handler(func(ws *websocket.Conn) { - defer ws.Close() - msg, _ := json.Marshal(s.Blueprint.Projects) - err := websocket.Message.Send(ws, string(msg)) - go func() { - for { - select { - case <-s.Sync: - msg, _ := json.Marshal(s.Blueprint.Projects) - err = websocket.Message.Send(ws, string(msg)) - if err != nil { - break - } - } - } - }() - for { - // Read - text := "" - err := websocket.Message.Receive(ws, &text) - if err != nil { - break - } else { - err := json.Unmarshal([]byte(text), &s.Blueprint.Projects) - if err != nil { - s.Record(s.Settings) - break - } - } - } - }).ServeHTTP(c.Response(), c.Request()) - return nil -} - // OpenURL in a new tab of default browser -func (s *server) openURL(url string) (io.Writer, error) { +func (s *Server) openURL(url string) (io.Writer, error) { stderr := bytes.Buffer{} cmd := map[string]string{ "windows": "start", @@ -190,3 +159,33 @@ func (s *server) openURL(url string) (io.Writer, error) { } return nil, nil } + +// Render return a web pages defined in bindata +func (s *Server) render(c echo.Context, path string, mime int) error { + data, err := Asset(path) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound) + } + rs := c.Response() + // check content type by extensions + switch mime { + case 1: + rs.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) + break + case 2: + rs.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJavaScriptCharsetUTF8) + break + case 3: + rs.Header().Set(echo.HeaderContentType, "text/css") + break + case 4: + rs.Header().Set(echo.HeaderContentType, "image/svg+xml") + break + case 5: + rs.Header().Set(echo.HeaderContentType, "image/png") + break + } + rs.WriteHeader(http.StatusOK) + rs.Write(data) + return nil +} diff --git a/server_test.go b/server_test.go index f48cf87..f9d1f95 100644 --- a/server_test.go +++ b/server_test.go @@ -1,71 +1,72 @@ package main -import ( - "fmt" - "github.com/tockins/realize/settings" - "net/http" - "testing" -) - -func TestServer_Start(t *testing.T) { - s := settings.Settings{ - Server: settings.Server{ - Status: true, - Open: false, - Host: "localhost", - Port: 5000, - }, - } - server := Server{ - Settings: &s, - } - err := server.Start(nil) - if err != nil { - t.Fatal(err) - } - host := "http://localhost:5000/" - urls := []string{ - host, - host + "assets/js/all.min.js", - host + "assets/css/app.css", - host + "app/components/settings/index.html", - host + "app/components/project/index.html", - host + "app/components/project/index.html", - host + "app/components/index.html", - host + "assets/img/svg/ic_settings_black_24px.svg", - host + "assets/img/svg/ic_fullscreen_black_24px.svg", - host + "assets/img/svg/ic_add_black_24px.svg", - host + "assets/img/svg/ic_keyboard_backspace_black_24px.svg", - host + "assets/img/svg/ic_error_black_48px.svg", - host + "assets/img/svg/ic_remove_black_24px.svg", - host + "assets/img/svg/logo.svg", - host + "assets/img/favicon-32x32.png", - host + "assets/img/svg/ic_swap_vertical_circle_black_48px.svg", - } - for _, elm := range urls { - resp, err := http.Get(elm) - if err != nil || resp.StatusCode != 200 { - t.Fatal(err, resp.StatusCode, elm) - } - } -} - -func TestOpen(t *testing.T) { - config := settings.Settings{ - Server: settings.Server{ - Open: true, - }, - } - s := Server{ - Settings: &config, - } - url := "open_test" - out, err := s.openURL(url) - if err == nil { - t.Fatal("Unexpected, invalid url", url, err) - } - output := fmt.Sprint(out) - if output == "" { - t.Fatal("Unexpected, invalid url", url, output) - } -} +// +//import ( +// "fmt" +// "github.com/tockins/realize/settings" +// "net/http" +// "testing" +//) +// +//func TestServer_Start(t *testing.T) { +// s := settings.Settings{ +// Server: settings.Server{ +// Status: true, +// Open: false, +// Host: "localhost", +// Port: 5000, +// }, +// } +// server := Server{ +// Settings: &s, +// } +// err := server.Start(nil) +// if err != nil { +// t.Fatal(err) +// } +// host := "http://localhost:5000/" +// urls := []string{ +// host, +// host + "assets/js/all.min.js", +// host + "assets/css/app.css", +// host + "app/components/settings/index.html", +// host + "app/components/project/index.html", +// host + "app/components/project/index.html", +// host + "app/components/index.html", +// host + "assets/img/svg/ic_settings_black_24px.svg", +// host + "assets/img/svg/ic_fullscreen_black_24px.svg", +// host + "assets/img/svg/ic_add_black_24px.svg", +// host + "assets/img/svg/ic_keyboard_backspace_black_24px.svg", +// host + "assets/img/svg/ic_error_black_48px.svg", +// host + "assets/img/svg/ic_remove_black_24px.svg", +// host + "assets/img/svg/logo.svg", +// host + "assets/img/favicon-32x32.png", +// host + "assets/img/svg/ic_swap_vertical_circle_black_48px.svg", +// } +// for _, elm := range urls { +// resp, err := http.Get(elm) +// if err != nil || resp.StatusCode != 200 { +// t.Fatal(err, resp.StatusCode, elm) +// } +// } +//} +// +//func TestOpen(t *testing.T) { +// config := settings.Settings{ +// Server: settings.Server{ +// Open: true, +// }, +// } +// s := Server{ +// Settings: &config, +// } +// url := "open_test" +// out, err := s.openURL(url) +// if err == nil { +// t.Fatal("Unexpected, invalid url", url, err) +// } +// output := fmt.Sprint(out) +// if output == "" { +// t.Fatal("Unexpected, invalid url", url, output) +// } +//} diff --git a/settings.go b/settings.go new file mode 100644 index 0000000..67497a3 --- /dev/null +++ b/settings.go @@ -0,0 +1,198 @@ +package main + +import ( + "gopkg.in/yaml.v2" + "io/ioutil" + "log" + "math/rand" + "os" + "path/filepath" + "strings" + "syscall" + "time" +) + +// settings const +const ( + Permission = 0775 + Directory = ".realize" + File = "realize.yaml" + FileOut = "outputs.log" + FileErr = "errors.log" + FileLog = "logs.log" +) + +// random string preference +const ( + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + return string(b) +} + +// Wdir return the current working Directory +func (s Settings) wdir() string { + dir, err := os.Getwd() + s.validate(err) + return filepath.Base(dir) +} + +// Flimit defines the max number of watched files +func (s *Settings) flimit() error { + var rLimit syscall.Rlimit + rLimit.Max = uint64(s.FileLimit) + rLimit.Cur = uint64(s.FileLimit) + + if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil { + return err + } + return nil +} + +// Delete realize folder +func (s *Settings) del(d string) error { + _, err := os.Stat(d) + if !os.IsNotExist(err) { + return os.RemoveAll(d) + } + return err +} + +// Path cleaner +func (s Settings) path(path string) string { + return strings.Replace(filepath.Clean(path), "\\", "/", -1) +} + +// Validate checks a fatal error +func (s Settings) validate(err error) error { + if err != nil { + s.fatal(err, "") + } + return nil +} + +// Read from config file +func (s *Settings) read(out interface{}) error { + localConfigPath := s.file + // backward compatibility + path := filepath.Join(Directory, s.file) + if _, err := os.Stat(path); err == nil { + localConfigPath = path + } + content, err := s.stream(localConfigPath) + if err == nil { + err = yaml.Unmarshal(content, out) + return err + } + return err +} + +// Record create and unmarshal the yaml config file +func (s *Settings) record(out interface{}) error { + y, err := yaml.Marshal(out) + if err != nil { + return err + } + if _, err := os.Stat(Directory); os.IsNotExist(err) { + if err = os.Mkdir(Directory, Permission); err != nil { + return s.write(s.file, y) + } + } + return s.write(filepath.Join(Directory, s.file), y) +} + +// Stream return a byte stream of a given file +func (s Settings) stream(file string) ([]byte, error) { + _, err := os.Stat(file) + if err != nil { + return nil, err + } + content, err := ioutil.ReadFile(file) + s.validate(err) + return content, err +} + +// Fatal prints a fatal error with its additional messages +func (s Settings) fatal(err error, msg ...interface{}) { + if len(msg) > 0 && err != nil { + log.Fatalln(red.regular(msg...), err.Error()) + } else if err != nil { + log.Fatalln(err.Error()) + } +} + +// Write a file +func (s Settings) write(name string, data []byte) error { + err := ioutil.WriteFile(name, data, Permission) + return s.validate(err) +} + +// Name return the project name or the path of the working dir +func (s Settings) name(name string, path string) string { + if name == "" && path == "" { + return s.wdir() + } else if path != "/" { + return filepath.Base(path) + } + return name +} + +// Create a new file and return its pointer +func (s Settings) create(path string, name string) *os.File { + var file string + if _, err := os.Stat(Directory); err == nil { + file = filepath.Join(path, Directory, name) + } else { + file = filepath.Join(path, name) + } + out, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE|os.O_SYNC, Permission) + s.validate(err) + return out +} diff --git a/settings/flimit.go b/settings/flimit.go deleted file mode 100644 index f22489e..0000000 --- a/settings/flimit.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build !windows - -package settings - -import "syscall" - -// Flimit defines the max number of watched files -func (s *Settings) Flimit() error { - var rLimit syscall.Rlimit - rLimit.Max = uint64(s.FileLimit) - rLimit.Cur = uint64(s.FileLimit) - - if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil { - return err - } - return nil -} diff --git a/settings/flimit_test.go b/settings/flimit_test.go deleted file mode 100644 index 420738b..0000000 --- a/settings/flimit_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package settings - -import ( - "testing" -) - -func TestSettings_Flimit(t *testing.T) { - s := Settings{} - s.FileLimit = 100 - if err := s.Flimit(); err != nil { - t.Fatal("Unable to increase limit", err) - } -} diff --git a/settings/flimit_windows.go b/settings/flimit_windows.go deleted file mode 100644 index f74d23a..0000000 --- a/settings/flimit_windows.go +++ /dev/null @@ -1,7 +0,0 @@ -// build windows -package settings - -// Flimit defines the max number of watched files -func (s *Settings) Flimit() error { - return nil -} diff --git a/settings/io.go b/settings/io.go deleted file mode 100644 index 2124117..0000000 --- a/settings/io.go +++ /dev/null @@ -1,37 +0,0 @@ -package settings - -import ( - "io/ioutil" - "os" - "path/filepath" -) - -// Stream return a byte stream of a given file -func (s Settings) Stream(file string) ([]byte, error) { - _, err := os.Stat(file) - if err != nil { - return nil, err - } - content, err := ioutil.ReadFile(file) - s.Validate(err) - return content, err -} - -// Write a file -func (s Settings) Write(name string, data []byte) error { - err := ioutil.WriteFile(name, data, Permission) - return s.Validate(err) -} - -// Create a new file and return its pointer -func (s Settings) Create(path string, name string) *os.File { - var file string - if _, err := os.Stat(Directory); err == nil { - file = filepath.Join(path, Directory, name) - } else { - file = filepath.Join(path, name) - } - out, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE|os.O_SYNC, Permission) - s.Validate(err) - return out -} diff --git a/settings/io_test.go b/settings/io_test.go deleted file mode 100755 index 9224ba4..0000000 --- a/settings/io_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package settings - -import ( - "io/ioutil" - "os" - "testing" -) - -func TestSettings_Stream(t *testing.T) { - s := Settings{} - filename := Rand(4) - if _, err := s.Stream(filename); err == nil { - t.Fatal("Error expected, none found", filename, err) - } - - filename = "io.go" - if _, err := s.Stream(filename); err != nil { - t.Fatal("Error unexpected", filename, err) - } -} - -func TestSettings_Write(t *testing.T) { - s := Settings{} - data := "abcdefgh" - d, err := ioutil.TempFile("", "io_test") - if err != nil { - t.Fatal(err) - } - if err := s.Write(d.Name(), []byte(data)); err != nil { - t.Fatal(err) - } -} - -func TestSettings_Create(t *testing.T) { - s := Settings{} - p, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - f := s.Create(p, "io_test") - os.Remove(f.Name()) -} diff --git a/settings/settings.go b/settings/settings.go deleted file mode 100644 index 9f99536..0000000 --- a/settings/settings.go +++ /dev/null @@ -1,97 +0,0 @@ -package settings - -import ( - yaml "gopkg.in/yaml.v2" - "os" - "path/filepath" - "time" -) - -// settings const -const ( - Interval = 200 - Permission = 0775 - Directory = ".realize" - File = "realize.yaml" - FileOut = "outputs.log" - FileErr = "errors.log" - FileLog = "logs.log" -) - -// Settings defines a group of general settings and options -type Settings struct { - File string `yaml:"-" json:"-"` - Make bool `yaml:"-" json:"-"` - Files `yaml:"files,omitempty" json:"files,omitempty"` - Legacy `yaml:"legacy,omitempty" json:"legacy,omitempty"` - Server `yaml:"server,omitempty" json:"server,omitempty"` - FileLimit int64 `yaml:"flimit,omitempty" json:"flimit,omitempty"` -} - -// Legacy configuration -type Legacy struct { - Interval time.Duration `yaml:"interval" json:"interval"` -} - -// Server settings, used for the web panel -type Server struct { - Status bool `yaml:"status" json:"status"` - Open bool `yaml:"open" json:"open"` - Host string `yaml:"host" json:"host"` - Port int `yaml:"port" json:"port"` -} - -// Files defines the files generated by realize -type Files struct { - Outputs Resource `yaml:"outputs,omitempty" json:"outputs,omitempty"` - Logs Resource `yaml:"logs,omitempty" json:"log,omitempty"` - Errors Resource `yaml:"errors,omitempty" json:"error,omitempty"` -} - -// Resource status and file name -type Resource struct { - Status bool - Name string -} - -// Read from config file -func (s *Settings) Read(out interface{}) error { - localConfigPath := s.File - // backward compatibility - path := filepath.Join(Directory, s.File) - if _, err := os.Stat(path); err == nil { - localConfigPath = path - } - content, err := s.Stream(localConfigPath) - if err == nil { - err = yaml.Unmarshal(content, out) - return err - } - return err -} - -// Record create and unmarshal the yaml config file -func (s *Settings) Record(out interface{}) error { - if s.Make { - y, err := yaml.Marshal(out) - if err != nil { - return err - } - if _, err := os.Stat(Directory); os.IsNotExist(err) { - if err = os.Mkdir(Directory, Permission); err != nil { - return s.Write(s.File, y) - } - } - return s.Write(filepath.Join(Directory, s.File), y) - } - return nil -} - -// Remove realize folder -func (s *Settings) Remove(d string) error { - _, err := os.Stat(d) - if !os.IsNotExist(err) { - return os.RemoveAll(d) - } - return err -} diff --git a/settings/settings_test.go b/settings/settings_test.go deleted file mode 100644 index 5387755..0000000 --- a/settings/settings_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package settings - -import ( - "io/ioutil" - "path/filepath" - "testing" -) - -func TestSettings_Read(t *testing.T) { - s := Settings{} - var a interface{} - s.File = "settings_b" - if err := s.Read(a); err == nil { - t.Fatal("Error unexpected", err) - } - - s.File = "settings_test.yaml" - dir, err := ioutil.TempDir("", Directory) - if err != nil { - t.Fatal(err) - } - d, err := ioutil.TempFile(dir, "settings_test.yaml") - if err != nil { - t.Fatal(err) - } - s.File = d.Name() - if err := s.Read(a); err != nil { - t.Fatal("Error unexpected", err) - } -} - -func TestSettings_Remove(t *testing.T) { - s := Settings{} - if err := s.Remove("abcd"); err == nil { - t.Fatal("Error unexpected, dir dosn't exist", err) - } - - d, err := ioutil.TempDir("", "settings_test") - if err != nil { - t.Fatal(err) - } - if err := s.Remove(d); err != nil { - t.Fatal("Error unexpected, dir exist", err) - } -} - -func TestSettings_Record(t *testing.T) { - s := Settings{} - s.File = "settings_test.yaml" - var a interface{} - if err := s.Record(a); err != nil { - t.Fatal(err) - } - s.Remove(filepath.Join(Directory, s.File)) -} diff --git a/settings/utils.go b/settings/utils.go deleted file mode 100644 index 3aa9519..0000000 --- a/settings/utils.go +++ /dev/null @@ -1,76 +0,0 @@ -package settings - -import ( - "log" - "math/rand" - "os" - "path/filepath" - "strings" - - "github.com/tockins/realize/style" - "time" -) - -const ( - letterIdxBits = 6 // 6 bits to represent a letter index - letterIdxMask = 1< 0 && err != nil { - log.Fatalln(style.Red.Regular(msg...), err.Error()) - } else if err != nil { - log.Fatalln(err.Error()) - } -} - -// Name return the project name or the path of the working dir -func (s Settings) Name(name string, path string) string { - if name == "" && path == "" { - return s.Wdir() - } else if path != "/" { - return filepath.Base(path) - } - return name -} - -// Path cleaner -func (s Settings) Path(path string) string { - return strings.Replace(filepath.Clean(path), "\\", "/", -1) -} - -// Rand is used for generate a random string -func Rand(n int) string { - src := rand.NewSource(time.Now().UnixNano()) - b := make([]byte, n) - for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; { - if remain == 0 { - cache, remain = src.Int63(), letterIdxMax - } - if idx := int(cache & letterIdxMask); idx < len(letterBytes) { - b[i] = letterBytes[idx] - i-- - } - cache >>= letterIdxBits - remain-- - } - return string(b) -} diff --git a/settings/utils_test.go b/settings/utils_test.go deleted file mode 100644 index 612c502..0000000 --- a/settings/utils_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package settings - -import ( - "errors" - "os" - "path/filepath" - "strings" - "testing" -) - -func TestSettings_Wdir(t *testing.T) { - s := Settings{} - expected, err := os.Getwd() - if err != nil { - t.Error(err) - } - result := s.Wdir() - if result != filepath.Base(expected) { - t.Error("Expected", filepath.Base(expected), "instead", result) - } -} - -func TestSettings_Validate(t *testing.T) { - s := Settings{} - input := errors.New("") - input = nil - if err := s.Validate(input); err != nil { - t.Error("Expected", input, "instead", err) - } -} - -func TestSettings_Name(t *testing.T) { - s := Settings{} - name := Rand(8) - path := Rand(5) - dir, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - result := s.Name(name, path) - if result != dir && result != filepath.Base(path) { - t.Fatal("Expected", dir, "or", filepath.Base(path), "instead", result) - } - -} - -func TestSettings_Path(t *testing.T) { - s := Settings{} - path := Rand(5) - expected := strings.Replace(filepath.Clean(path), "\\", "/", -1) - result := s.Path(path) - if result != expected { - t.Fatal("Expected", expected, "instead", result) - } - -} diff --git a/settings_test.go b/settings_test.go new file mode 100644 index 0000000..d0ff560 --- /dev/null +++ b/settings_test.go @@ -0,0 +1,149 @@ +package main + +// +//import ( +// "errors" +// "io/ioutil" +// "os" +// "path/filepath" +// "strings" +// "testing" +//) +// +//func TestSettings_Flimit(t *testing.T) { +// s := Settings{} +// s.FileLimit = 100 +// if err := s.flimit(); err != nil { +// t.Fatal("Unable to increase limit", err) +// } +//} +// +//func TestSettings_Stream(t *testing.T) { +// s := Settings{} +// filename := random(4) +// if _, err := s.stream(filename); err == nil { +// t.Fatal("Error expected, none found", filename, err) +// } +// +// filename = "io.go" +// if _, err := s.stream(filename); err != nil { +// t.Fatal("Error unexpected", filename, err) +// } +//} +// +//func TestSettings_Write(t *testing.T) { +// s := Settings{} +// data := "abcdefgh" +// d, err := ioutil.TempFile("", "io_test") +// if err != nil { +// t.Fatal(err) +// } +// if err := s.write(d.Name(), []byte(data)); err != nil { +// t.Fatal(err) +// } +//} +// +//func TestSettings_Create(t *testing.T) { +// s := Settings{} +// p, err := os.Getwd() +// if err != nil { +// t.Fatal(err) +// } +// f := s.create(p, "io_test") +// os.Remove(f.Name()) +//} +// +//func TestSettings_Read(t *testing.T) { +// s := Settings{} +// var a interface{} +// s.File = "settings_b" +// if err := s.read(a); err == nil { +// t.Fatal("Error unexpected", err) +// } +// +// s.File = "settings_test.yaml" +// dir, err := ioutil.TempDir("", Directory) +// if err != nil { +// t.Fatal(err) +// } +// d, err := ioutil.TempFile(dir, "settings_test.yaml") +// if err != nil { +// t.Fatal(err) +// } +// s.File = d.Name() +// if err := s.read(a); err != nil { +// t.Fatal("Error unexpected", err) +// } +//} +// +//func TestSettings_Remove(t *testing.T) { +// s := Settings{} +// if err := s.delete("abcd"); err == nil { +// t.Fatal("Error unexpected, dir dosn't exist", err) +// } +// +// d, err := ioutil.TempDir("", "settings_test") +// if err != nil { +// t.Fatal(err) +// } +// if err := s.delete(d); err != nil { +// t.Fatal("Error unexpected, dir exist", err) +// } +//} +// +//func TestSettings_Record(t *testing.T) { +// s := Settings{} +// s.File = "settings_test.yaml" +// var a interface{} +// if err := s.record(a); err != nil { +// t.Fatal(err) +// } +// s.delete(filepath.Join(Directory, s.File)) +//} +// +//func TestSettings_Wdir(t *testing.T) { +// s := Settings{} +// expected, err := os.Getwd() +// if err != nil { +// t.Error(err) +// } +// result := s.wdir() +// if result != filepath.Base(expected) { +// t.Error("Expected", filepath.Base(expected), "instead", result) +// } +//} +// +//func TestSettings_Validate(t *testing.T) { +// s := Settings{} +// input := errors.New("") +// input = nil +// if err := s.validate(input); err != nil { +// t.Error("Expected", input, "instead", err) +// } +//} +// +//func TestSettings_Name(t *testing.T) { +// s := Settings{} +// name := random(8) +// path := random(5) +// dir, err := os.Getwd() +// if err != nil { +// t.Fatal(err) +// } +// result := s.name(name, path) +// if result != dir && result != filepath.Base(path) { +// t.Fatal("Expected", dir, "or", filepath.Base(path), "instead", result) +// } +// +//} +// +//func TestSettings_Path(t *testing.T) { +// s := Settings{} +// path := random(5) +// expected := strings.Replace(filepath.Clean(path), "\\", "/", -1) +// result := s.path(path) +// if result != expected { +// t.Fatal("Expected", expected, "instead", result) +// } +// +//} diff --git a/settings_windows.go b/settings_windows.go new file mode 100644 index 0000000..f177bac --- /dev/null +++ b/settings_windows.go @@ -0,0 +1,190 @@ +package main + +import ( + yaml "gopkg.in/yaml.v2" + "io/ioutil" + "os" + "path/filepath" + "strings" + "syscall" + "time" +) + +// settings const +const ( + Permission = 0775 + Directory = ".realize" + File = "realize.yaml" + FileOut = "outputs.log" + FileErr = "errors.log" + FileLog = "logs.log" +) + +// random string preference +const ( + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + return string(b) +} + +// Wdir return the current working Directory +func (s Settings) wdir() string { + dir, err := os.Getwd() + s.validate(err) + return filepath.Base(dir) +} + +// Flimit defines the max number of watched files +func (s *Settings) flimit() error { + return nil +} + +// Delete realize folder +func (s *Settings) delete(d string) error { + _, err := os.Stat(d) + if !os.IsNotExist(err) { + return os.RemoveAll(d) + } + return err +} + +// Path cleaner +func (s Settings) path(path string) string { + return strings.Replace(filepath.Clean(path), "\\", "/", -1) +} + +// Validate checks a fatal error +func (s Settings) validate(err error) error { + if err != nil { + s.fatal(err, "") + } + return nil +} + +// Read from config file +func (s *Settings) read(out interface{}) error { + localConfigPath := s.File + // backward compatibility + path := filepath.Join(Directory, s.File) + if _, err := os.Stat(path); err == nil { + localConfigPath = path + } + content, err := s.stream(localConfigPath) + if err == nil { + err = yaml.Unmarshal(content, out) + return err + } + return err +} + +// Record create and unmarshal the yaml config file +func (s *Settings) record(out interface{}) error { + y, err := yaml.Marshal(out) + if err != nil { + return err + } + if _, err := os.Stat(Directory); os.IsNotExist(err) { + if err = os.Mkdir(Directory, Permission); err != nil { + return s.write(s.File, y) + } + } + return s.write(filepath.Join(Directory, s.File), y) + return nil +} + +// Stream return a byte stream of a given file +func (s Settings) stream(file string) ([]byte, error) { + _, err := os.Stat(file) + if err != nil { + return nil, err + } + content, err := ioutil.ReadFile(file) + s.validate(err) + return content, err +} + +// Fatal prints a fatal error with its additional messages +func (s Settings) fatal(err error, msg ...interface{}) { + if len(msg) > 0 && err != nil { + log.Fatalln(red.regular(msg...), err.Error()) + } else if err != nil { + log.Fatalln(err.Error()) + } +} + +// Write a file +func (s Settings) write(name string, data []byte) error { + err := ioutil.WriteFile(name, data, Permission) + return s.validate(err) +} + +// Name return the project name or the path of the working dir +func (s Settings) name(name string, path string) string { + if name == "" && path == "" { + return s.wdir() + } else if path != "/" { + return filepath.Base(path) + } + return name +} + +// Create a new file and return its pointer +func (s Settings) create(path string, name string) *os.File { + var file string + if _, err := os.Stat(Directory); err == nil { + file = filepath.Join(path, Directory, name) + } else { + file = filepath.Join(path, name) + } + out, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE|os.O_SYNC, Permission) + s.validate(err) + return out +} diff --git a/style.go b/style.go new file mode 100644 index 0000000..ef8afc7 --- /dev/null +++ b/style.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/fatih/color" +) + +var ( + output = color.Output + red = colorBase(color.FgRed) + blue = colorBase(color.FgBlue) + green = colorBase(color.FgGreen) + yellow = colorBase(color.FgYellow) + magenta = colorBase(color.FgMagenta) +) + +type colorBase color.Attribute + +func (c colorBase) regular(a ...interface{}) string { + return color.New(color.Attribute(c)).Sprint(a...) +} + +func (c colorBase) bold(a ...interface{}) string { + return color.New(color.Attribute(c), color.Bold).Sprint(a...) +} diff --git a/style/style.go b/style/style.go deleted file mode 100644 index f1df6a9..0000000 --- a/style/style.go +++ /dev/null @@ -1,28 +0,0 @@ -package style - -import ( - "github.com/fatih/color" -) - -type colorBase color.Attribute - -func (s colorBase) Regular(a ...interface{}) string { - return color.New(color.Attribute(s)).Sprint(a...) -} - -func (s colorBase) Bold(a ...interface{}) string { - return color.New(color.Attribute(s), color.Bold).Sprint(a...) -} - -// allowed colors -var ( - Red = colorBase(color.FgRed) - Blue = colorBase(color.FgBlue) - Yellow = colorBase(color.FgYellow) - Magenta = colorBase(color.FgMagenta) - Green = colorBase(color.FgGreen) -) - -// Output defines the standard output of the print functions. By default, os.Stdout is used. -// When invoked on Windows machines, this automatically handles escape sequences. -var Output = color.Output diff --git a/style/style_test.go b/style/style_test.go deleted file mode 100644 index 7c15d26..0000000 --- a/style/style_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package style - -import ( - "bytes" - "fmt" - "testing" -) - -func TestColorBase_Regular(t *testing.T) { - c := new(colorBase) - strs := []string{"a", "b", "c"} - input := make([]interface{}, len(strs)) - for i, s := range strs { - input[i] = s - } - result := c.Regular(input) - expected := fmt.Sprint(input) - if !bytes.Equal([]byte(result), []byte(expected)) { - t.Error("Expected:", expected, "instead", result) - } -} - -func TestColorBase_Bold(t *testing.T) { - c := new(colorBase) - strs := []string{"a", "b", "c"} - input := make([]interface{}, len(strs)) - for i, s := range strs { - input[i] = s - } - result := c.Bold(input) - expected := fmt.Sprint(input) - if !bytes.Equal([]byte(result), []byte(expected)) { - t.Error("Expected:", expected, "instead", result) - } -} diff --git a/style_test.go b/style_test.go new file mode 100644 index 0000000..2c7c1cd --- /dev/null +++ b/style_test.go @@ -0,0 +1,35 @@ +package main + +// +//import ( +// "bytes" +// "fmt" +// "github.com/fatih/color" +// "testing" +//) +// +//func TestStyle_Regular(t *testing.T) { +// strs := []string{"a", "b", "c"} +// input := make([]interface{}, len(strs)) +// for i, s := range strs { +// input[i] = s +// } +// result := style.Regular(input) +// expected := fmt.Sprint(input) +// if !bytes.Equal([]byte(result), []byte(expected)) { +// t.Error("Expected:", expected, "instead", result) +// } +//} +// +//func TestStyle_Bold(t *testing.T) { +// strs := []string{"a", "b", "c"} +// input := make([]interface{}, len(strs)) +// for i, s := range strs { +// input[i] = s +// } +// result := style.Bold(input) +// expected := fmt.Sprint(input) +// if !bytes.Equal([]byte(result), []byte(expected)) { +// t.Error("Expected:", expected, "instead", result) +// } +//} diff --git a/watcher/utils.go b/utils.go similarity index 53% rename from watcher/utils.go rename to utils.go index 751e580..03f831a 100644 --- a/watcher/utils.go +++ b/utils.go @@ -1,14 +1,10 @@ -package watcher +package main import ( "errors" - "fmt" + "gopkg.in/urfave/cli.v2" "os" "path/filepath" - "time" - - "github.com/tockins/realize/style" - cli "gopkg.in/urfave/cli.v2" "strings" ) @@ -21,8 +17,8 @@ func getEnvPath(env string) string { return path[0] } -// Check if a string is inArray -func inArray(str string, list []string) bool { +// Array check if a string is in given array +func array(str string, list []string) bool { for _, v := range list { if v == str { return true @@ -31,8 +27,8 @@ func inArray(str string, list []string) bool { return false } -// Argsparam parse one by one the given argumentes -func argsParam(params *cli.Context) []string { +// Params parse one by one the given argumentes +func params(params *cli.Context) []string { argsN := params.NArg() if argsN > 0 { var args []string @@ -45,7 +41,7 @@ func argsParam(params *cli.Context) []string { } // Split each arguments in multiple fields -func arguments(args, fields []string) []string { +func split(args, fields []string) []string { for _, arg := range fields { arr := strings.Fields(arg) args = append(args, arr...) @@ -56,14 +52,25 @@ func arguments(args, fields []string) []string { // Duplicates check projects with same name or same combinations of main/path func duplicates(value Project, arr []Project) (Project, error) { for _, val := range arr { - if value.Path == val.Path && val.Name == value.Name { - return val, errors.New("There is already a project for '" + val.Path + "'. Check your config file!") + if value.Path == val.Path { + return val, errors.New("There is already a project with path '" + val.Path + "'. Check your config file!") + } + if value.Name == val.Name { + return val, errors.New("There is already a project with name '" + val.Name + "'. Check your config file!") } } return Project{}, nil } -// Rewrite the layout of the log timestamp -func (w logWriter) Write(bytes []byte) (int, error) { - return fmt.Fprint(style.Output, style.Yellow.Regular("[")+time.Now().Format("15:04:05")+style.Yellow.Regular("]")+string(bytes)) +func ext(path string) string { + var ext string + for i := len(path) - 1; i >= 0 && !os.IsPathSeparator(path[i]); i-- { + if path[i] == '.' { + ext = path[i:] + } + } + if ext != "" { + return ext[1:] + } + return "" } diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..ede7672 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,60 @@ +package main + +// +//import ( +// "flag" +// "gopkg.in/urfave/cli.v2" +// "os" +// "path/filepath" +// "testing" +//) +// +//func TestArgsParam(t *testing.T) { +// set := flag.NewFlagSet("test", 0) +// set.Bool("myflag", false, "doc") +// params := cli.NewContext(nil, set, nil) +// set.Parse([]string{"--myflag", "bat", "baz"}) +// result := argsParam(params) +// if len(result) != 2 { +// t.Fatal("Expected 2 instead", len(result)) +// } +//} +// +//func TestDuplicates(t *testing.T) { +// projects := []Project{ +// { +// Name: "a", +// }, { +// Name: "b", +// }, { +// Name: "c", +// }, +// } +// _, err := duplicates(projects[0], projects) +// if err == nil { +// t.Fatal("Error unexpected", err) +// } +// _, err = duplicates(Project{}, projects) +// if err != nil { +// t.Fatal("Error unexpected", err) +// } +// +//} +// +//func TestInArray(t *testing.T) { +// arr := []string{"a", "b", "c"} +// if !inArray(arr[0], arr) { +// t.Fatal("Unexpected", arr[0], "should be in", arr) +// } +// if inArray("d", arr) { +// t.Fatal("Unexpected", "d", "shouldn't be in", arr) +// } +//} +// +//func TestGetEnvPath(t *testing.T) { +// expected := filepath.SplitList(os.Getenv("GOPATH"))[0] +// result := getEnvPath("GOPATH") +// if expected != result { +// t.Fatal("Expected", expected, "instead", result) +// } +//} diff --git a/watcher.go b/watcher.go new file mode 100644 index 0000000..ba2acad --- /dev/null +++ b/watcher.go @@ -0,0 +1,432 @@ +package main + +import ( + "fmt" + "github.com/fsnotify/fsnotify" + "log" + "math/big" + "os" + "os/signal" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" +) + +var ( + msg string + out BufferOut + wg sync.WaitGroup +) + +// Watcher struct defines the livereload's logic +type Watch struct { + Paths []string `yaml:"paths" json:"paths"` + Exts []string `yaml:"extensions" json:"extensions"` + Ignore []string `yaml:"ignored_paths,omitempty" json:"ignored_paths,omitempty"` + Preview bool `yaml:"preview,omitempty" json:"preview,omitempty"` + Scripts []Command `yaml:"scripts,omitempty" json:"scripts,omitempty"` +} + +// Result channel with data stream and errors +type Result struct { + stream string + err error +} + +// Buffer define an array buffer for each log files +type Buffer struct { + StdOut []BufferOut `json:"stdOut"` + StdLog []BufferOut `json:"stdLog"` + StdErr []BufferOut `json:"stdErr"` +} + +// Project defines the informations of a single project +type Project struct { + parent *realize + watcher FileWatcher + init bool + files, folders int64 + base, path, 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"` + Cmds Cmds `yaml:"commands" json:"commands"` + Args []string `yaml:"args,omitempty" json:"args,omitempty"` + Watcher Watch `yaml:"watcher" json:"watcher"` + Buffer Buffer `yaml:"-" json:"buffer"` + ErrorOutputPattern string `yaml:"errorOutputPattern,omitempty" json:"errorOutputPattern,omitempty"` +} + +// Command options +type Command struct { + Type string `yaml:"type" json:"type"` + Command string `yaml:"command" json:"command"` + Path string `yaml:"path,omitempty" json:"path,omitempty"` + Global bool `yaml:"global,omitempty" json:"global,omitempty"` + Output bool `yaml:"output,omitempty" json:"output,omitempty"` +} + +// BufferOut is used for exchange information between "realize cli" and "web realize" +type BufferOut struct { + Time time.Time `json:"time"` + Text string `json:"text"` + Path string `json:"path"` + Type string `json:"type"` + Stream string `json:"stream"` + Errors []string `json:"errors"` +} + +// Watch the project by fsnotify +func (p *Project) watch() { + p.watcher, _ = Watcher() + stop, exit := make(chan bool), make(chan os.Signal, 2) + signal.Notify(exit, os.Interrupt, syscall.SIGTERM) + // before global commands + p.cmd(stop, "before", true) + // indexing files and dirs + for _, dir := range p.Watcher.Paths { + base := filepath.Join(p.base, dir) + if _, err := os.Stat(base); err == nil { + if err := filepath.Walk(base, p.walk); err == nil { + p.tool(stop, base) + } + } else { + p.err(err) + } + } + // indexing done, files and folders + msg = fmt.Sprintln(p.pname(p.Name, 1), ":", blue.bold("Watching"), magenta.bold(p.files), "file/s", magenta.bold(p.folders), "folder/s") + out = BufferOut{Time: time.Now(), Text: "Watching " + strconv.FormatInt(p.files, 10) + " files/s " + strconv.FormatInt(p.folders, 10) + " folder/s"} + p.stamp("log", out, msg, "") + // start + go p.routines(stop, p.watcher, "") + //is watching +L: + for { + select { + case event := <-p.watcher.Events(): + if time.Now().Truncate(time.Second).After(p.lastTime) || event.Name != p.lastFile { + switch event.Op { + case fsnotify.Chmod: + case fsnotify.Remove: + ext := ext(event.Name) + if !strings.Contains(ext, "_") && ext != "" { + close(stop) + stop = make(chan bool) + p.changed(event, stop) // stop + } + p.watcher.Remove(event.Name) + default: + file, err := os.Stat(event.Name) + if err != nil { + continue + } + if file.IsDir() { + if time.Now().Truncate(time.Second).After(p.lastTime) { + filepath.Walk(event.Name, p.walk) + } + } else if file.Size() > 0 { + 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) + } + p.lastTime = time.Now().Truncate(time.Second) + p.lastFile = event.Name + } + } + } + case err := <-p.watcher.Errors(): + p.err(err) + case <-exit: + p.cmd(nil, "after", true) + break L + } + } + wg.Done() + return +} + +// Error occurred +func (p *Project) err(err error) { + msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.regular(err.Error())) + out = BufferOut{Time: time.Now(), Text: err.Error()} + p.stamp("error", out, msg, "") +} + +// Cmd calls an wrapper for execute the commands after/before +func (p *Project) cmd(stop <-chan bool, flag string, global bool) { + done := make(chan bool) + // cmds are scheduled in sequence + go func() { + for _, cmd := range p.Watcher.Scripts { + if strings.ToLower(cmd.Type) == flag && cmd.Global == global { + err, logs := p.command(stop, cmd) + if err == "" && logs == "" { + continue + } + msg = fmt.Sprintln(p.pname(p.Name, 5), ":", green.bold("Command"), green.bold("\"")+cmd.Command+green.bold("\"")) + out = BufferOut{Time: time.Now(), Text: cmd.Command, Type: flag} + if err != "" { + p.stamp("error", out, msg, "") + + msg = fmt.Sprintln(red.regular(err)) + out = BufferOut{Time: time.Now(), Text: err, Type: flag} + p.stamp("error", out, "", msg) + + } else if logs != "" && cmd.Output { + msg = fmt.Sprintln(logs) + out = BufferOut{Time: time.Now(), Text: logs, Type: flag} + p.stamp("log", out, "", msg) + } else { + p.stamp("log", out, msg, "") + } + } + } + close(done) + }() + for { + select { + case <-stop: + return + case <-done: + return + } + } +} + +// 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() + channel := make(chan Result) + go func() { + log.Println(p.pname(p.Name, 1), ":", cmd.startTxt) + stream, err := p.goCompile(stop, cmd.Args) + if stream != "killed" { + channel <- Result{stream, err} + } + }() + select { + case r := <-channel: + if r.err != nil { + msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.bold(cmd.name), red.regular(r.err.Error())) + out = BufferOut{Time: time.Now(), Text: r.err.Error(), Type: cmd.name, Stream: r.stream} + p.stamp("error", out, msg, r.stream) + } else { + msg = fmt.Sprintln(p.pname(p.Name, 5), ":", green.regular(cmd.endTxt), "in", magenta.regular(big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3), " s")) + out = BufferOut{Time: time.Now(), Text: cmd.name + " in " + big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3) + " s"} + p.stamp("log", out, msg, r.stream) + } + return r.err + case <-stop: + return nil + } + } + return nil +} + +// Defines the colors scheme for the project name +func (p *Project) pname(name string, color int) string { + switch color { + case 1: + name = yellow.regular("[") + strings.ToUpper(name) + yellow.regular("]") + break + case 2: + name = yellow.regular("[") + red.bold(strings.ToUpper(name)) + yellow.regular("]") + break + case 3: + name = yellow.regular("[") + blue.bold(strings.ToUpper(name)) + yellow.regular("]") + break + case 4: + name = yellow.regular("[") + magenta.bold(strings.ToUpper(name)) + yellow.regular("]") + break + case 5: + name = yellow.regular("[") + green.bold(strings.ToUpper(name)) + yellow.regular("]") + break + } + return name +} + +// Tool logs the result of a go command +func (p *Project) tool(stop <-chan bool, path string) error { + if len(path) > 0 { + done := make(chan bool) + 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) + } + wg.Wait() + close(done) + }() + loop: + 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) + case <-done: + break loop + case <-stop: + break loop + } + } + } + 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 { + if strings.Contains(path, filepath.Join(p.base, v)) { + return nil + } + } + if !strings.Contains(path, "/.") && !strings.HasPrefix(path, ".") && (info.IsDir() || array(ext(path), p.Watcher.Exts)) { + result := p.watcher.Walk(path, p.init) + if result != "" { + if info.IsDir() { + p.folders++ + } else { + p.files++ + } + if p.Watcher.Preview { + log.Println(p.pname(p.Name, 1), ":", path) + } + } + } + return nil +} + +// Print on files, cli, ws +func (p *Project) stamp(t string, o BufferOut, msg string, stream string) { + switch t { + case "out": + p.Buffer.StdOut = append(p.Buffer.StdOut, o) + if p.Files.Outputs.Status { + f := p.create(p.base, p.Files.Outputs.Name) + t := time.Now() + s := []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Text, "\r\n"} + if _, err := f.WriteString(strings.Join(s, " ")); err != nil { + p.fatal(err, "") + } + } + case "log": + p.Buffer.StdLog = append(p.Buffer.StdLog, o) + if p.Files.Logs.Status { + f := p.create(p.base, p.Files.Logs.Name) + t := time.Now() + s := []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Text, "\r\n"} + if stream != "" { + s = []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Text, "\r\n", stream} + } + if _, err := f.WriteString(strings.Join(s, " ")); err != nil { + p.fatal(err, "") + } + } + case "error": + p.Buffer.StdErr = append(p.Buffer.StdErr, o) + if p.Files.Errors.Status { + f := p.create(p.base, p.Files.Errors.Name) + t := time.Now() + s := []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Type, o.Text, o.Path, "\r\n"} + if stream != "" { + s = []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Type, o.Text, o.Path, "\r\n", stream} + } + if _, err := f.WriteString(strings.Join(s, " ")); err != nil { + p.fatal(err, "") + } + } + } + if msg != "" { + log.Print(msg) + } + if stream != "" { + fmt.Fprint(output, stream) + } + go func() { + p.parent.sync <- "sync" + }() +} + +// Routines launches the toolchain run, build, install +func (p *Project) routines(stop <-chan bool, watcher FileWatcher, path string) { + var done bool + var install, build error + go func() { + for { + select { + case <-stop: + done = true + return + } + } + }() + if !done { + // before command + p.cmd(stop, "before", false) + } + if !done { + // Go supported tools + p.tool(stop, path) + // Prevent fake events on polling startup + p.init = true + } + if !done { + install = p.compile(stop, p.Cmds.Install) + } + if !done { + build = p.compile(stop, p.Cmds.Build) + } + 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 + } + } + } + if !done { + p.cmd(stop, "after", false) + } +} diff --git a/watcher/cmd.go b/watcher/cmd.go deleted file mode 100644 index 1edb3f2..0000000 --- a/watcher/cmd.go +++ /dev/null @@ -1,215 +0,0 @@ -package watcher - -import ( - "errors" - "fmt" - "github.com/tockins/realize/style" - cli "gopkg.in/urfave/cli.v2" - "os" - "path/filepath" - "strings" - "time" -) - -// Clean duplicate projects -func (h *Blueprint) Clean() { - arr := h.Projects - for key, val := range arr { - if _, err := duplicates(val, arr[key+1:]); err != nil { - h.Projects = append(arr[:key], arr[key+1:]...) - break - } - } -} - -// List of all the projects -func (h *Blueprint) List() error { - err := h.check() - if err == nil { - for _, val := range h.Projects { - fmt.Fprintln(style.Output, style.Blue.Bold("[")+strings.ToUpper(val.Name)+style.Blue.Bold("]")) - name := style.Magenta.Bold("[") + strings.ToUpper(val.Name) + style.Magenta.Bold("]") - - fmt.Fprintln(style.Output, name, style.Yellow.Regular("Base Path"), ":", style.Magenta.Regular(val.Path)) - fmt.Fprintln(style.Output, name, style.Yellow.Regular("Fmt"), ":", style.Magenta.Regular(val.Cmds.Fmt)) - fmt.Fprintln(style.Output, name, style.Yellow.Regular("Generate"), ":", style.Magenta.Regular(val.Cmds.Generate)) - fmt.Fprintln(style.Output, name, style.Yellow.Regular("Test"), ":", style.Magenta.Regular(val.Cmds.Test)) - fmt.Fprintln(style.Output, name, style.Yellow.Regular("Install"), ":", style.Magenta.Regular(val.Cmds.Install)) - fmt.Fprintln(style.Output, name, style.Yellow.Regular("Build"), ":", style.Magenta.Regular(val.Cmds.Build)) - fmt.Fprintln(style.Output, name, style.Yellow.Regular("Run"), ":", style.Magenta.Regular(val.Cmds.Run)) - if len(val.Args) > 0 { - fmt.Fprintln(style.Output, name, style.Yellow.Regular("Params"), ":", style.Magenta.Regular(val.Args)) - } - fmt.Fprintln(style.Output, name, style.Yellow.Regular("Watcher"), ":") - fmt.Fprintln(style.Output, name, "\t", style.Yellow.Regular("Preview"), ":", style.Magenta.Regular(val.Watcher.Preview)) - if len(val.Watcher.Exts) > 0 { - fmt.Fprintln(style.Output, name, "\t", style.Yellow.Regular("Extensions"), ":", style.Magenta.Regular(val.Watcher.Exts)) - } - if len(val.Watcher.Paths) > 0 { - fmt.Fprintln(style.Output, name, "\t", style.Yellow.Regular("Paths"), ":", style.Magenta.Regular(val.Watcher.Paths)) - } - if len(val.Watcher.Ignore) > 0 { - fmt.Fprintln(style.Output, name, "\t", style.Yellow.Regular("Ignored paths"), ":", style.Magenta.Regular(val.Watcher.Ignore)) - } - if len(val.Watcher.Scripts) > 0 { - fmt.Fprintln(style.Output, name, "\t", style.Yellow.Regular("Scripts"), ":") - for _, v := range val.Watcher.Scripts { - if v.Command != "" { - fmt.Fprintln(style.Output, name, "\t\t", style.Magenta.Regular("-"), style.Yellow.Regular("Command"), ":", style.Magenta.Regular(v.Command)) - if v.Path != "" { - fmt.Fprintln(style.Output, name, "\t\t", style.Yellow.Regular("Path"), ":", style.Magenta.Regular(v.Path)) - } - if v.Type != "" { - fmt.Fprintln(style.Output, name, "\t\t", style.Yellow.Regular("Type"), ":", style.Magenta.Regular(v.Type)) - } - } - } - } - } - return nil - } - return err -} - -// Check whether there is a project -func (h *Blueprint) check() error { - if len(h.Projects) > 0 { - h.Clean() - return nil - } - return errors.New("There are no projects.") -} - -// Add a new project -func (h *Blueprint) Add(p *cli.Context) error { - project := Project{ - Name: h.Name(p.String("name"), p.String("path")), - Path: h.Path(p.String("path")), - Cmds: Cmds{ - Vet: p.Bool("vet"), - Fmt: Cmd{ - Status: p.Bool("fmt"), - }, - Test: Cmd{ - Status: p.Bool("test"), - }, - Generate: Cmd{ - Status: p.Bool("generate"), - }, - Build: Cmd{ - Status: p.Bool("build"), - }, - Install: Cmd{ - Status: p.Bool("install"), - }, - Run: p.Bool("run"), - }, - Args: argsParam(p), - Watcher: Watcher{ - Paths: []string{"/"}, - Ignore: []string{"vendor"}, - Exts: []string{".go"}, - }, - } - if _, err := duplicates(project, h.Projects); err != nil { - return err - } - h.Projects = append(h.Projects, project) - return nil -} - -// Run launches the toolchain for each project -func (h *Blueprint) Run(p *cli.Context) error { - err := h.check() - if err == nil { - // loop projects - if p.String("name") != "" { - wg.Add(1) - } else { - wg.Add(len(h.Projects)) - } - for k, element := range h.Projects { - if p.String("name") != "" && h.Projects[k].Name != p.String("name") { - continue - } - if element.Cmds.Fmt.Status { - if len(element.Cmds.Fmt.Args) == 0{ - element.Cmds.Fmt.Args = []string{"-s", "-w", "-e"} - } - h.Projects[k].tools.Fmt = tool{ - status: element.Cmds.Fmt.Status, - cmd: "gofmt", - options: arguments([]string{}, element.Cmds.Fmt.Args), - name: "Go Fmt", - } - } - if element.Cmds.Generate.Status { - h.Projects[k].tools.Generate = tool{ - status: element.Cmds.Generate.Status, - cmd: "go", - options: arguments([]string{"generate"}, element.Cmds.Generate.Args), - name: "Go Generate", - } - } - if element.Cmds.Test.Status { - h.Projects[k].tools.Test = tool{ - status: element.Cmds.Test.Status, - cmd: "go", - options: arguments([]string{"test"}, element.Cmds.Test.Args), - name: "Go Test", - } - } - if element.Cmds.Vet { - h.Projects[k].tools.Vet = tool{ - status: element.Cmds.Vet, - cmd: "go", - options: []string{"vet"}, - name: "Go Vet", - } - } - h.Projects[k].parent = h - h.Projects[k].Settings = *h.Settings - h.Projects[k].path = h.Projects[k].Path - - // env variables - for key, item := range h.Projects[k].Environment { - if err := os.Setenv(key, item); err != nil { - h.Projects[k].Buffer.StdErr = append(h.Projects[k].Buffer.StdErr, BufferOut{Time: time.Now(), Text: err.Error(), Type: "Env error", Stream: ""}) - } - } - - // base path of the project - wd, err := os.Getwd() - if err != nil { - return err - } - if element.path == "." || element.path == "/" { - h.Projects[k].base = wd - h.Projects[k].path = element.Wdir() - } else if filepath.IsAbs(element.path) { - h.Projects[k].base = element.path - } else { - h.Projects[k].base = filepath.Join(wd, element.path) - } - if h.Legacy.Interval != 0 { - go h.Projects[k].watchByPolling() - } else { - go h.Projects[k].watchByNotify() - } - } - wg.Wait() - return nil - } - return err -} - -// Remove a project -func (h *Blueprint) Remove(p *cli.Context) error { - for key, val := range h.Projects { - if p.String("name") == val.Name { - h.Projects = append(h.Projects[:key], h.Projects[key+1:]...) - return nil - } - } - return errors.New("No project found.") -} diff --git a/watcher/cmd_test.go b/watcher/cmd_test.go deleted file mode 100644 index a0118d4..0000000 --- a/watcher/cmd_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package watcher - -import ( - "flag" - "github.com/tockins/realize/settings" - cli "gopkg.in/urfave/cli.v2" - "testing" - "time" -) - -func TestBlueprint_Run(t *testing.T) { - set := flag.NewFlagSet("test", 0) - params := cli.NewContext(nil, set, nil) - projects := Blueprint{} - projects.Settings = &settings.Settings{} - projects.Projects = []Project{ - { - Name: "test1", - Path: ".", - }, - { - Name: "test1", - Path: ".", - }, - { - Name: "test2", - Path: ".", - }, - } - go projects.Run(params) - time.Sleep(100 * time.Millisecond) -} - -func TestBlueprint_Add(t *testing.T) { - projects := Blueprint{} - projects.Settings = &settings.Settings{} - // add all flags, test with expected - set := flag.NewFlagSet("test", 0) - set.String("name", "default_name", "doc") - set.String("path", "default_path", "doc") - params := cli.NewContext(nil, set, nil) - set.Parse([]string{"--name", "name", "name"}) - set.Parse([]string{"--path", "path", "path"}) - projects.Add(params) -} diff --git a/watcher/main.go b/watcher/main.go deleted file mode 100644 index c6c10ff..0000000 --- a/watcher/main.go +++ /dev/null @@ -1,99 +0,0 @@ -package watcher - -import ( - "github.com/tockins/realize/settings" - "log" - "sync" - "time" -) - -var wg sync.WaitGroup - -// Watcher interface used by polling/fsnotify watching -type watcher interface { - Add(path string) error -} - -// Polling watcher -type pollWatcher struct { - paths map[string]bool -} - -// Log struct -type logWriter struct{} - -// Blueprint struct contains a projects list -type Blueprint struct { - *settings.Settings `yaml:"-"` - Projects []Project `yaml:"projects,omitempty" json:"projects,omitempty"` - Sync chan string `yaml:"-"` -} - -type tools struct { - Fmt, Test, Generate, Vet tool -} - -type tool struct { - status bool - cmd string - options []string - name string -} - -// Cmds go supported -type Cmds struct { - Vet bool `yaml:"vet,omitempty" json:"vet,omitempty"` - Fmt Cmd `yaml:"fmt,omitempty" json:"fmt,omitempty"` - Test Cmd `yaml:"test,omitempty" json:"test,omitempty"` - Generate Cmd `yaml:"generate,omitempty" json:"generate,omitempty"` - Install Cmd `yaml:"install" json:"install"` - Build Cmd `yaml:"build,omitempty" json:"build,omitempty"` - Run bool `yaml:"run,omitempty" json:"run,omitempty"` -} - -// Cmd buildmode options -type Cmd struct { - Status bool `yaml:"status,omitempty" json:"status,omitempty"` - Args []string `yaml:"args,omitempty" json:"args,omitempty"` -} - -// Watcher struct defines the livereload's logic -type Watcher struct { - Paths []string `yaml:"paths" json:"paths"` - Exts []string `yaml:"extensions" json:"extensions"` - Ignore []string `yaml:"ignored_paths,omitempty" json:"ignored_paths,omitempty"` - Preview bool `yaml:"preview,omitempty" json:"preview,omitempty"` - Scripts []Command `yaml:"scripts,omitempty" json:"scripts,omitempty"` -} - -// Command options -type Command struct { - Type string `yaml:"type" json:"type"` - Command string `yaml:"command" json:"command"` - Path string `yaml:"path,omitempty" json:"path,omitempty"` - Global bool `yaml:"global,omitempty" json:"global,omitempty"` - Output bool `yaml:"output,omitempty" json:"output,omitempty"` -} - -// Buffer define an array buffer for each log files -type Buffer struct { - StdOut []BufferOut `json:"stdOut"` - StdLog []BufferOut `json:"stdLog"` - StdErr []BufferOut `json:"stdErr"` -} - -// BufferOut is used for exchange information between "realize cli" and "web realize" -type BufferOut struct { - Time time.Time `json:"time"` - Text string `json:"text"` - Path string `json:"path"` - Type string `json:"type"` - Stream string `json:"stream"` - Errors []string `json:"errors"` -} - -// Initialize the application -func init() { - log.SetFlags(0) - log.SetOutput(new(logWriter)) -} diff --git a/watcher/utils_test.go b/watcher/utils_test.go deleted file mode 100644 index 2158caa..0000000 --- a/watcher/utils_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package watcher - -import ( - "flag" - "gopkg.in/urfave/cli.v2" - "os" - "path/filepath" - "testing" -) - -func TestArgsParam(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Bool("myflag", false, "doc") - params := cli.NewContext(nil, set, nil) - set.Parse([]string{"--myflag", "bat", "baz"}) - result := argsParam(params) - if len(result) != 2 { - t.Fatal("Expected 2 instead", len(result)) - } -} - -func TestDuplicates(t *testing.T) { - projects := []Project{ - { - Name: "a", - }, { - Name: "b", - }, { - Name: "c", - }, - } - _, err := duplicates(projects[0], projects) - if err == nil { - t.Fatal("Error unexpected", err) - } - _, err = duplicates(Project{}, projects) - if err != nil { - t.Fatal("Error unexpected", err) - } - -} - -func TestInArray(t *testing.T) { - arr := []string{"a", "b", "c"} - if !inArray(arr[0], arr) { - t.Fatal("Unexpected", arr[0], "should be in", arr) - } - if inArray("d", arr) { - t.Fatal("Unexpected", "d", "shouldn't be in", arr) - } -} - -func TestGetEnvPath(t *testing.T) { - expected := filepath.SplitList(os.Getenv("GOPATH"))[0] - result := getEnvPath("GOPATH") - if expected != result { - t.Fatal("Expected", expected, "instead", result) - } -} diff --git a/watcher/watcher.go b/watcher/watcher.go deleted file mode 100644 index 886a6f6..0000000 --- a/watcher/watcher.go +++ /dev/null @@ -1,428 +0,0 @@ -package watcher - -import ( - "errors" - "fmt" - "github.com/fsnotify/fsnotify" - "github.com/tockins/realize/settings" - "github.com/tockins/realize/style" - "log" - "math/big" - "os" - "os/signal" - "path/filepath" - "strconv" - "strings" - "sync" - "syscall" - "time" -) - -var msg string -var out BufferOut - -// Project defines the informations of a single project -type Project struct { - settings.Settings `yaml:"-" json:"-"` - parent *Blueprint - path string - tools tools - base string - paths []string - lastChangedOn time.Time - Name string `yaml:"name" json:"name"` - Path string `yaml:"path" json:"path"` - Environment map[string]string `yaml:"environment,omitempty" json:"environment,omitempty"` - Cmds Cmds `yaml:"commands" json:"commands"` - Args []string `yaml:"args,omitempty" json:"args,omitempty"` - Watcher Watcher `yaml:"watcher" json:"watcher"` - Buffer Buffer `yaml:"-" json:"buffer"` - ErrorOutputPattern string `yaml:"errorOutputPattern,omitempty" json:"errorOutputPattern,omitempty"` -} - -// Watch the project by fsnotify -func (p *Project) watchByNotify() { - wr := sync.WaitGroup{} - channel := make(chan bool, 1) - watcher := &fsnotify.Watcher{} - exit := make(chan os.Signal, 2) - signal.Notify(exit, os.Interrupt, syscall.SIGTERM) - watcher, err := fsnotify.NewWatcher() - p.Fatal(err) - defer func() { - p.cmd("after", true) - wg.Done() - }() - p.cmd("before", true) - go p.routines(&wr, channel, watcher, "") - p.lastChangedOn = time.Now().Truncate(time.Second) -L: - for { - select { - case event := <-watcher.Events: - if time.Now().Truncate(time.Second).After(p.lastChangedOn) { - file, err := os.Lstat(event.Name) - if (event.Op&fsnotify.Remove == fsnotify.Remove) || err == nil && file.Size() > 0 { - p.lastChangedOn = time.Now().Truncate(time.Second) - ext := filepath.Ext(event.Name) - if inArray(ext, p.Watcher.Exts) { - if p.Cmds.Run { - close(channel) - channel = make(chan bool) - } - // repeat the initial cycle - msg = fmt.Sprintln(p.pname(p.Name, 4), ":", style.Magenta.Bold(strings.ToUpper(ext[1:])+" changed"), style.Magenta.Bold(event.Name)) - out = BufferOut{Time: time.Now(), Text: strings.ToUpper(ext[1:]) + " changed " + event.Name} - p.stamp("log", out, msg, "") - // check if is deleted - if event.Op&fsnotify.Remove == fsnotify.Remove { - go p.routines(&wr, channel, watcher, "") - } else { - go p.routines(&wr, channel, watcher, event.Name) - } - p.lastChangedOn = time.Now().Truncate(time.Second) - } - } - } - case err := <-watcher.Errors: - msg = fmt.Sprintln(p.pname(p.Name, 2), ":", style.Red.Regular(err.Error())) - out = BufferOut{Time: time.Now(), Text: err.Error()} - p.stamp("error", out, msg, "") - case <-exit: - break L - } - } - return -} - -// Watch the project by polling -func (p *Project) watchByPolling() { - wr := sync.WaitGroup{} - watcher := new(pollWatcher) - channel := make(chan bool, 1) - exit := make(chan os.Signal, 2) - signal.Notify(exit, os.Interrupt, syscall.SIGTERM) - defer func() { - p.cmd("after", true) - wg.Done() - }() - p.cmd("before", true) - go p.routines(&wr, channel, watcher, "") - p.lastChangedOn = time.Now().Truncate(time.Second) - walk := func(changed string, info os.FileInfo, err error) error { - if err != nil { - return err - } else if !watcher.isWatching(changed) { - return nil - } else if !info.ModTime().Truncate(time.Second).After(p.lastChangedOn) { - return nil - } - if index := strings.Index(filepath.Ext(changed), "__"); index != -1 { - return nil - } - ext := filepath.Ext(changed) - if inArray(ext, p.Watcher.Exts) { - if p.Cmds.Run { - close(channel) - channel = make(chan bool) - } - p.lastChangedOn = time.Now().Truncate(time.Second) - // repeat the initial cycle - msg = fmt.Sprintln(p.pname(p.Name, 4), ":", style.Magenta.Bold(strings.ToUpper(ext[1:])+" changed"), style.Magenta.Bold(changed)) - out = BufferOut{Time: time.Now(), Text: strings.ToUpper(ext[1:]) + " changed " + changed} - p.stamp("log", out, msg, "") - go p.routines(&wr, channel, watcher, changed) - } - return nil - } - for { - for _, dir := range p.Watcher.Paths { - base := filepath.Join(p.base, dir) - if _, err := os.Stat(base); err == nil { - if err := filepath.Walk(base, walk); err != nil { - msg = fmt.Sprintln(p.pname(p.Name, 2), ":", style.Red.Regular(err.Error())) - out = BufferOut{Time: time.Now(), Text: err.Error()} - p.stamp("error", out, msg, "") - } - } else { - msg = fmt.Sprintln(p.pname(p.Name, 2), ":", base, "path doesn't exist") - out = BufferOut{Time: time.Now(), Text: base + " path doesn't exist"} - p.stamp("error", out, msg, "") - } - select { - case <-exit: - return - case <-time.After(p.parent.Legacy.Interval / time.Duration(len(p.Watcher.Paths))): - } - } - } -} - -// Build calls an implementation of the "go build" -func (p *Project) build() error { - if p.Cmds.Build.Status { - start := time.Now() - log.Println(p.pname(p.Name, 1), ":", "Building..") - stream, err := p.goBuild() - if err != nil { - msg = fmt.Sprintln(p.pname(p.Name, 2), ":", style.Red.Bold("Go Build"), style.Red.Regular(err.Error())) - out = BufferOut{Time: time.Now(), Text: err.Error(), Type: "Go Build", Stream: stream} - p.stamp("error", out, msg, stream) - } else { - msg = fmt.Sprintln(p.pname(p.Name, 5), ":", style.Green.Regular("Built")+" after", style.Magenta.Regular(big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3), " s")) - out = BufferOut{Time: time.Now(), Text: "Built after " + big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3) + " s"} - p.stamp("log", out, msg, stream) - } - return err - } - return nil -} - -// Install calls an implementation of "go install" -func (p *Project) install() error { - if p.Cmds.Install.Status { - start := time.Now() - log.Println(p.pname(p.Name, 1), ":", "Installing..") - stream, err := p.goInstall() - if err != nil { - msg = fmt.Sprintln(p.pname(p.Name, 2), ":", style.Red.Bold("Go Install"), style.Red.Regular(err.Error())) - out = BufferOut{Time: time.Now(), Text: err.Error(), Type: "Go Install", Stream: stream} - p.stamp("error", out, msg, stream) - } else { - msg = fmt.Sprintln(p.pname(p.Name, 5), ":", style.Green.Regular("Installed")+" after", style.Magenta.Regular(big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3), " s")) - out = BufferOut{Time: time.Now(), Text: "Installed after " + big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3) + " s"} - p.stamp("log", out, msg, stream) - } - return err - } - return nil -} - -// Ignore and validate a path -func (p *Project) ignore(str string) bool { - for _, v := range p.Watcher.Ignore { - if strings.Contains(str, filepath.Join(p.base, v)) { - return true - } - } - return false -} - -// Watch the files tree of a project -func (p *Project) walk(watcher watcher) error { - var files, folders int64 - walk := func(path string, info os.FileInfo, err error) error { - if !p.ignore(path) { - if ((info.IsDir() && len(filepath.Ext(path)) == 0 && !strings.HasPrefix(path, ".")) && !strings.Contains(path, "/.")) || (inArray(filepath.Ext(path), p.Watcher.Exts)) { - if p.Watcher.Preview { - log.Println(p.pname(p.Name, 1), ":", path) - } - if err = watcher.Add(path); err != nil { - return filepath.SkipDir - } - if inArray(filepath.Ext(path), p.Watcher.Exts) { - p.paths = append(p.paths, path) - files++ - } else { - folders++ - } - } - } - return nil - } - - for _, dir := range p.Watcher.Paths { - base := filepath.Join(p.base, dir) - if _, err := os.Stat(base); err == nil { - if err := filepath.Walk(base, walk); err != nil { - log.Println(style.Red.Bold(err.Error())) - p.tool(base, p.tools.Fmt) - p.tool(base, p.tools.Vet) - p.tool(base, p.tools.Test) - p.tool(base, p.tools.Generate) - } - } else { - return errors.New(base + " path doesn't exist") - } - } - msg = fmt.Sprintln(p.pname(p.Name, 1), ":", style.Blue.Bold("Watching"), style.Magenta.Bold(files), "file/s", style.Magenta.Bold(folders), "folder/s") - out = BufferOut{Time: time.Now(), Text: "Watching " + strconv.FormatInt(files, 10) + " files/s " + strconv.FormatInt(folders, 10) + " folder/s"} - p.stamp("log", out, msg, "") - return nil -} - -// Cmd calls an wrapper for execute the commands after/before -func (p *Project) cmd(flag string, global bool) { - for _, cmd := range p.Watcher.Scripts { - if strings.ToLower(cmd.Type) == flag && cmd.Global == global { - err, logs := p.command(cmd) - msg = fmt.Sprintln(p.pname(p.Name, 5), ":", style.Green.Bold("Command"), style.Green.Bold("\"")+cmd.Command+style.Green.Bold("\"")) - out = BufferOut{Time: time.Now(), Text: cmd.Command, Type: flag} - if err != "" { - p.stamp("error", out, msg, "") - } else { - p.stamp("log", out, msg, "") - } - if logs != "" && cmd.Output { - msg = fmt.Sprintln(logs) - out = BufferOut{Time: time.Now(), Text: logs, Type: flag} - p.stamp("log", out, "", msg) - } - if err != "" { - msg = fmt.Sprintln(style.Red.Regular(err)) - out = BufferOut{Time: time.Now(), Text: err, Type: flag} - p.stamp("error", out, "", msg) - } - } - } -} - -// Tool logs the result of a go command -func (p *Project) tool(path string, tool tool) error { - if tool.status { - if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "") { - if strings.HasSuffix(path, ".go") { - tool.options = append(tool.options, path) - path = p.base - } - if stream, err := p.goTool(path, tool.cmd, tool.options...); err != nil { - msg = fmt.Sprintln(p.pname(p.Name, 2), ":", style.Red.Bold(tool.name), style.Red.Regular("there are some errors in"), ":", style.Magenta.Bold(path)) - out = BufferOut{Time: time.Now(), Text: "there are some errors in", Path: path, Type: tool.name, Stream: stream} - p.stamp("error", out, msg, stream) - return err - } - } - } - return nil -} - -// Defines the colors scheme for the project name -func (p *Project) pname(name string, color int) string { - switch color { - case 1: - name = style.Yellow.Regular("[") + strings.ToUpper(name) + style.Yellow.Regular("]") - break - case 2: - name = style.Yellow.Regular("[") + style.Red.Bold(strings.ToUpper(name)) + style.Yellow.Regular("]") - break - case 3: - name = style.Yellow.Regular("[") + style.Blue.Bold(strings.ToUpper(name)) + style.Yellow.Regular("]") - break - case 4: - name = style.Yellow.Regular("[") + style.Magenta.Bold(strings.ToUpper(name)) + style.Yellow.Regular("]") - break - case 5: - name = style.Yellow.Regular("[") + style.Green.Bold(strings.ToUpper(name)) + style.Yellow.Regular("]") - break - } - return name -} - -// Install calls an implementation of "go run" -func (p *Project) run(channel chan bool, wr *sync.WaitGroup) { - if p.Cmds.Run { - start := time.Now() - runner := make(chan bool, 1) - log.Println(p.pname(p.Name, 1), ":", "Running..") - go p.goRun(channel, runner, wr) - for { - select { - case <-runner: - msg = fmt.Sprintln(p.pname(p.Name, 5), ":", style.Green.Regular("Started")+" after", style.Magenta.Regular(big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3), " s")) - out = BufferOut{Time: time.Now(), Text: "Started after " + big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3) + " s"} - p.stamp("log", out, msg, "") - return - } - } - } -} - -// Print on files, cli, ws -func (p *Project) stamp(t string, o BufferOut, msg string, stream string) { - switch t { - case "out": - p.Buffer.StdOut = append(p.Buffer.StdOut, o) - if p.Files.Outputs.Status { - f := p.Create(p.base, p.Files.Outputs.Name) - t := time.Now() - s := []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Text, "\r\n"} - if _, err := f.WriteString(strings.Join(s, " ")); err != nil { - p.Fatal(err, "") - } - } - case "log": - p.Buffer.StdLog = append(p.Buffer.StdLog, o) - if p.Files.Logs.Status { - f := p.Create(p.base, p.Files.Logs.Name) - t := time.Now() - s := []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Text, "\r\n"} - if stream != "" { - s = []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Text, "\r\n", stream} - } - if _, err := f.WriteString(strings.Join(s, " ")); err != nil { - p.Fatal(err, "") - } - } - case "error": - p.Buffer.StdErr = append(p.Buffer.StdErr, o) - if p.Files.Errors.Status { - f := p.Create(p.base, p.Files.Errors.Name) - t := time.Now() - s := []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Type, o.Text, o.Path, "\r\n"} - if stream != "" { - s = []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Type, o.Text, o.Path, "\r\n", stream} - } - if _, err := f.WriteString(strings.Join(s, " ")); err != nil { - p.Fatal(err, "") - } - } - } - if msg != "" { - log.Print(msg) - } - if stream != "" { - fmt.Fprint(style.Output, stream) - } - go func() { - p.parent.Sync <- "sync" - }() -} - -// Routines launches the toolchain run, build, install -func (p *Project) routines(wr *sync.WaitGroup, channel chan bool, watcher watcher, file string) { - p.cmd("before", false) - if len(file) > 0 { - path := filepath.Dir(file) - p.tool(file, p.tools.Fmt) - p.tool(path, p.tools.Vet) - p.tool(path, p.tools.Test) - p.tool(path, p.tools.Generate) - } else { - p.Fatal(p.walk(watcher)) - } - install := p.install() - build := p.build() - wr.Add(1) - if install == nil && build == nil { - go p.run(channel, wr) - } - wr.Wait() - if len(file) > 0 { - p.cmd("after", false) - } -} - -// Add a path to paths list -func (w *pollWatcher) Add(path string) error { - if w.paths == nil { - w.paths = map[string]bool{} - } - w.paths[path] = true - return nil -} - -// Check if is watching -func (w *pollWatcher) isWatching(path string) bool { - a, b := w.paths[path] - return a && b -} diff --git a/watcher_test.go b/watcher_test.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/watcher_test.go @@ -0,0 +1 @@ +package main