huge code refactoring, performance improved

This commit is contained in:
asoseil 2017-10-08 23:09:45 +02:00
parent a17591e7b0
commit ae86421ed5
38 changed files with 2076 additions and 1945 deletions

View File

@ -1,7 +1,6 @@
language: go
go:
- 1.7
- 1.8
- 1.9
- tip

30
Gopkg.lock generated
View File

@ -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"

View File

@ -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

220
cmd.go Normal file
View File

@ -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
}

141
cmd_test.go Normal file
View File

@ -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)
}

View File

@ -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 {
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
}
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)
}
}
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
}
}
}
}
}

1
exec_test.go Normal file
View File

@ -0,0 +1 @@
package main

View File

@ -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{}) {

1
notify_test.go Normal file
View File

@ -0,0 +1 @@
package main

File diff suppressed because it is too large Load Diff

View File

@ -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")
// }
//}

157
server.go
View File

@ -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)
// 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 {
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)
}
}
}()
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
}

View File

@ -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)
// }
//}

198
settings.go Normal file
View File

@ -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<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
// Settings defines a group of general settings and options
type Settings struct {
file string `yaml:"-" json:"-"`
Files `yaml:"files,omitempty" json:"files,omitempty"`
FileLimit int64 `yaml:"flimit,omitempty" json:"flimit,omitempty"`
Legacy Legacy `yaml:"legacy" json:"legacy"`
Recovery bool `yaml:"recovery,omitempty" json:"recovery,omitempty"`
}
// Legacy is used to force polling and set a custom interval
type Legacy struct {
Force bool `yaml:"force" json:"force"`
Interval time.Duration `yaml:"interval" json:"interval"`
}
// Files defines the files generated by realize
type Files struct {
Clean bool `yaml:"clean,omitempty" json:"clean,omitempty"`
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
}
// Rand is used for generate a random string
func random(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)
}
// 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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -1,7 +0,0 @@
// build windows
package settings
// Flimit defines the max number of watched files
func (s *Settings) Flimit() error {
return nil
}

View File

@ -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
}

View File

@ -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())
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
// Wdir return the current working Directory
func (s Settings) Wdir() string {
dir, err := os.Getwd()
s.Validate(err)
return filepath.Base(dir)
}
// Validate checks a fatal error
func (s Settings) Validate(err error) error {
if err != nil {
s.Fatal(err, "")
}
return nil
}
// 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(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)
}

View File

@ -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)
}
}

149
settings_test.go Normal file
View File

@ -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)
// }
//
//}

190
settings_windows.go Normal file
View File

@ -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<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
// Settings defines a group of general settings and options
type Settings struct {
File string `yaml:"-" json:"-"`
Files `yaml:"files,omitempty" json:"files,omitempty"`
Server `yaml:"server,omitempty" json:"server,omitempty"`
FileLimit int64 `yaml:"flimit,omitempty" json:"flimit,omitempty"`
}
// 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
}
// Rand is used for generate a random string
func random(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)
}
// 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
}

24
style.go Normal file
View File

@ -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...)
}

View File

@ -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

View File

@ -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)
}
}

35
style_test.go Normal file
View File

@ -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)
// }
//}

View File

@ -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 ""
}

60
utils_test.go Normal file
View File

@ -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)
// }
//}

432
watcher.go Normal file
View File

@ -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)
}
}

View File

@ -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.")
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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)
}
}

View File

@ -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
}

1
watcher_test.go Normal file
View File

@ -0,0 +1 @@
package main