This commit is contained in:
asoseil 2017-11-20 00:52:31 +01:00
parent 14832a2c84
commit 3b44ec6291
22 changed files with 2207 additions and 2709 deletions

9
assets/index.html Normal file
View File

@ -0,0 +1,9 @@
<!DOCTYPE html><html ng-app="realize"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"><link href="http://fonts.googleapis.com/css?family=Open+Sans:400,700,600,300,800" rel="stylesheet"><link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"><link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,700,300" rel="stylesheet" type="text/css"><link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"><link rel="icon" href="/assets/img/fav.png"><!-- bower:css -->
<!-- endinject -->
<!-- inject:css -->
<link rel="stylesheet" href="/assets/css/app.css">
<!-- endinject --><title>Realize</title></head><body ui-view id="main" layout="column"><!-- bower:js -->
<!-- endinject -->
<!-- inject:js -->
<script src="/assets/js/all.min.js"></script>
<!-- endinject --></body></html>

View File

@ -398,7 +398,7 @@ func assetsIndexHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "assets/index.html", size: 936, mode: os.FileMode(420), modTime: time.Unix(1505914049, 0)}
info := bindataFileInfo{name: "assets/index.html", size: 936, mode: os.FileMode(420), modTime: time.Unix(1510563869, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}

1144
cli.go Normal file

File diff suppressed because it is too large Load Diff

157
cmd.go
View File

@ -1,157 +0,0 @@
package main
import (
"errors"
"gopkg.in/urfave/cli.v2"
"os"
"path/filepath"
)
// Tool options customizable, should be moved in Cmd
type tool struct {
name, err, out string
cmd, options []string
dir, status bool
}
// Cmds list of go commands
type Cmds struct {
Fix Cmd `yaml:"fix,omitempty" json:"fix,omitempty"`
Clean Cmd `yaml:"clean,omitempty" json:"clean,omitempty"`
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,omitempty" json:"install,omitempty"`
Build Cmd `yaml:"build,omitempty" json:"build,omitempty"`
Run bool `yaml:"run,omitempty" json:"run,omitempty"`
}
// Cmd single command fields and options
type Cmd struct {
Method string `yaml:"method,omitempty" json:"method,omitempty"`
Args []string `yaml:"args,omitempty" json:"args,omitempty"`
Status bool `yaml:"status,omitempty" json:"status,omitempty"`
tool bool
method []string
name, startTxt, endTxt string
}
// Clean duplicate projects
func (r *realize) clean() error {
if len(r.Schema) > 0 {
arr := r.Schema
for key, val := range arr {
if _, err := duplicates(val, arr[key+1:]); err != nil {
// path validation
r.Schema = append(arr[:key], arr[key+1:]...)
break
}
}
return nil
}
return errors.New("there are no projects")
}
// Add a new project
func (r *realize) add(p *cli.Context) (err error) {
// project init
name := filepath.Base(p.String("path"))
if name == "." {
name = filepath.Base(wdir())
}
project := Project{
Name: name,
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{".git", ".realize", "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 {
var match bool
// check projects and remove duplicates
if err := r.clean(); err != nil {
return err
}
// set gobin
if err := os.Setenv("GOBIN", filepath.Join(os.Getenv("GOPATH"), "bin")); err != nil {
return err
}
// loop projects
if p.String("name") != "" {
wg.Add(1)
} else {
wg.Add(len(r.Schema))
}
for k, elm := range r.Schema {
// command start using name flag
if p.String("name") != "" && elm.Name != p.String("name") {
continue
}
match = true
r.Schema[k].config(r)
go r.Schema[k].watch()
}
if !match {
return errors.New("there is no project with the given name")
}
wg.Wait()
return nil
}
// 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 current project if there isn't already one
func (r *realize) insert(c *cli.Context) error {
if c.Bool("no-config") {
r.Schema = []Project{}
}
if len(r.Schema) <= 0 {
if err := r.add(c); err != nil {
return err
}
}
return nil
}

View File

@ -1,156 +0,0 @@
package main
import (
"flag"
"gopkg.in/urfave/cli.v2"
"log"
"os"
"path/filepath"
"reflect"
"testing"
"time"
)
type loggerT struct{}
func (loggerT) Write(bytes []byte) (int, error) {
return 0, nil
}
func TestMain(m *testing.M) {
log.SetFlags(0)
log.SetOutput(loggerT{})
os.Exit(m.Run())
}
func TestRealize_Clean(t *testing.T) {
r := realize{}
r.Schema = append(r.Schema, Project{Name: "test0"})
r.Schema = append(r.Schema, Project{Name: "test0"})
r.clean()
if len(r.Schema) > 1 {
t.Error("Expected only one project")
}
r.Schema = append(r.Schema, Project{Path: "test1"})
r.Schema = append(r.Schema, Project{Path: "test1"})
r.clean()
if len(r.Schema) != 2 {
t.Error("Expected two projects")
}
}
func TestRealize_Add(t *testing.T) {
r := realize{}
// 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", wdir(), "")
c := cli.NewContext(nil, set, nil)
set.Parse([]string{"--fmt", "--install", "--run", "--build", "--generate", "--test", "--vet"})
r.add(c)
expected := Project{
Name: filepath.Base(wdir()),
Path: wdir(),
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{".git", ".realize", "vendor"},
Exts: []string{"go"},
},
}
if !reflect.DeepEqual(r.Schema[0], expected) {
t.Error("Expected equal struct")
}
}
func TestRealize_Run(t *testing.T) {
set := flag.NewFlagSet("test", 0)
params := cli.NewContext(nil, set, nil)
m := make(map[string]string)
m["test"] = "test"
r := realize{}
r.Schema = []Project{
{
Name: "test0",
Path: ".",
},
{
Name: "test1",
Path: "/test",
},
{
Name: "test2",
Path: "/test",
},
}
go r.run(params)
time.Sleep(1 * time.Second)
}
func TestRealize_Remove(t *testing.T) {
r := realize{}
set := flag.NewFlagSet("name", 0)
set.String("name", "", "")
c := cli.NewContext(nil, set, nil)
set.Parse([]string{"--name=test0"})
err := r.remove(c)
if err == nil {
t.Error("Expected an error, there are no projects")
}
// Append a new project
r.Schema = append(r.Schema, Project{Name: "test0"})
err = r.remove(c)
if err != nil {
t.Error("Error unexpected, the project should be remove", err)
}
}
func TestRealize_Insert(t *testing.T) {
r := realize{}
// add all flags, test with expected
set := flag.NewFlagSet("test", 0)
set.Bool("no-config", false, "")
c := cli.NewContext(nil, set, nil)
set.Parse([]string{"--no-config"})
r.insert(c)
if len(r.Schema) != 1 {
t.Error("Expected one project instead", len(r.Schema))
}
r.Schema = []Project{}
r.Schema = append(r.Schema, Project{})
r.Schema = append(r.Schema, Project{})
c = cli.NewContext(nil, set, nil)
r.insert(c)
if len(r.Schema) != 1 {
t.Error("Expected one project instead", len(r.Schema))
}
}

55
commands.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"bytes"
"github.com/go-siris/siris/core/errors"
"os/exec"
"path/filepath"
"strings"
)
// Command options
type Command struct {
Type string `yaml:"type" json:"type"`
Cmd 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"`
}
// Exec an additional command from a defined path if specified
func (c *Command) Exec(base string, stop <-chan bool) (response Response) {
var stdout bytes.Buffer
var stderr bytes.Buffer
done := make(chan error)
args := strings.Split(strings.Replace(strings.Replace(c.Cmd, "'", "", -1), "\"", "", -1), " ")
ex := exec.Command(args[0], args[1:]...)
ex.Dir = base
// make cmd path
if c.Path != "" {
if strings.Contains(c.Path, base) {
ex.Dir = c.Path
} else {
ex.Dir = filepath.Join(base, c.Path)
}
}
ex.Stdout = &stdout
ex.Stderr = &stderr
// Start command
ex.Start()
go func() { done <- ex.Wait() }()
// Wait a result
select {
case <-stop:
// Stop running command
ex.Process.Kill()
case err := <-done:
// Command completed
response.Name = c.Cmd
response.Out = stdout.String()
if err != nil {
response.Err = errors.New(stderr.String())
}
}
return
}

228
exec.go
View File

@ -1,228 +0,0 @@
package main
import (
"bufio"
"bytes"
"fmt"
"github.com/pkg/errors"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
)
// GoCompile is used for compile a project
func (p *Project) goCompile(stop <-chan bool, method []string, args []string) (string, error) {
var out bytes.Buffer
var stderr bytes.Buffer
done := make(chan error)
args = append(method, args...)
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = p.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()
return msgStop, nil
case err := <-done:
// Command completed
if err != nil {
return stderr.String(), err
}
return "", nil
}
return "", nil
}
// GoRun is an implementation of the bin execution
func (p *Project) goRun(stop <-chan bool, runner chan bool) {
var build *exec.Cmd
var args []string
// custom error pattern
isErrorText := func(string) bool {
return false
}
errRegexp, err := regexp.Compile(p.ErrorOutputPattern)
if err != nil {
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 {
isErrorText = func(t string) bool {
return errRegexp.MatchString(t)
}
}
// add additional arguments
for _, arg := range p.Args {
a := strings.FieldsFunc(arg, func(i rune) bool {
return i == '"' || i == '=' || i == '\''
})
args = append(args, a...)
}
gobin := os.Getenv("GOBIN")
dirPath := filepath.Base(p.Path)
if p.Path == "." {
dirPath = filepath.Base(wdir())
}
path := filepath.Join(gobin, dirPath)
if _, err := os.Stat(path); err == nil {
build = exec.Command(path, args...)
} else if _, err := os.Stat(path + extWindows); err == nil {
build = exec.Command(path+extWindows, args...)
} else {
if _, err = os.Stat(path); err == nil {
build = exec.Command(path, args...)
} else if _, err = os.Stat(path + extWindows); err == nil {
build = exec.Command(path+extWindows, args...)
} else {
p.err(errors.New("Build not found"))
return
}
}
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", ":")
}
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, "")
}()
// scan project stream
stdout, err := build.StdoutPipe()
stderr, err := build.StderrPipe()
if err != nil {
log.Println(red.bold(err.Error()))
return
}
if err := build.Start(); err != nil {
log.Println(red.bold(err.Error()))
return
}
close(runner)
execOutput, execError := bufio.NewScanner(stdout), bufio.NewScanner(stderr)
stopOutput, stopError := make(chan bool, 1), make(chan bool, 1)
scanner := func(stop chan bool, output *bufio.Scanner, isError bool) {
for output.Scan() {
text := output.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, "")
} else {
out := BufferOut{Time: time.Now(), Text: text, Type: "Go Run"}
p.stamp("out", out, msg, "")
}
}
close(stop)
}
go scanner(stopOutput, execOutput, false)
go scanner(stopError, execError, true)
for {
select {
case <-stop:
return
case <-stopOutput:
return
case <-stopError:
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), " ")
ex := exec.Command(args[0], args[1:]...)
ex.Dir = p.Path
// make cmd path
if cmd.Path != "" {
if strings.Contains(cmd.Path, p.Path) {
ex.Dir = cmd.Path
} else {
ex.Dir = filepath.Join(p.Path, cmd.Path)
}
}
ex.Stdout = &stdout
ex.Stderr = &stderr
// Start command
ex.Start()
go func() { done <- ex.Wait() }()
// Wait a result
select {
case <-stop:
// Stop running command
ex.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 tool.dir && filepath.Ext(path) != "" {
path = filepath.Dir(path)
}
if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "") {
if strings.HasSuffix(path, ".go") {
tool.options = append(tool.options, path)
path = p.Path
}
if s := ext(path); s == "" || s == "go" {
var out, stderr bytes.Buffer
done := make(chan error)
tool.cmd = append(tool.cmd, tool.options...)
cmd := exec.Command(tool.cmd[0], tool.cmd[1:]...)
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()
return
case err := <-done:
// Command completed
if err != nil {
tool.err = stderr.String() + out.String()
// send command result
result <- tool
} else {
tool.out = out.String()
}
return
}
}
}
}
}

View File

@ -1,29 +0,0 @@
package main
import (
"testing"
"time"
)
func TestProject_GoCompile(t *testing.T) {
p := Project{}
stop := make(chan bool)
response := make(chan string)
result, err := p.goCompile(stop, []string{"echo"}, []string{"test"})
if err != nil {
t.Error("Unexpected", err)
}
go func() {
result, _ = p.goCompile(stop, []string{"sleep"}, []string{"20s"})
response <- result
}()
close(stop)
select {
case v := <-response:
if v != msgStop {
t.Error("Unexpected result", response)
}
case <-time.After(2 * time.Second):
t.Error("Channel doesn't works")
}
}

529
projects.go Normal file
View File

@ -0,0 +1,529 @@
package main
import (
"bufio"
"errors"
"fmt"
"github.com/fsnotify/fsnotify"
"log"
"math/big"
"os"
"os/exec"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"time"
)
var (
msg string
out BufferOut
)
// Watch info
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"`
Scripts []Command `yaml:"scripts,omitempty" json:"scripts,omitempty"`
}
// Project info
type Project struct {
parent *Realize
watcher FileWatcher
init bool
files int64
folders int64
name string
lastFile string
paths []string
lastTime time.Time
Name string `yaml:"name" json:"name"`
Path string `yaml:"path" json:"path"`
Environment map[string]string `yaml:"environment,omitempty" json:"environment,omitempty"`
Tools Tools `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"`
}
// Response exec
type Response struct {
Name string
Out 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"`
}
// 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"`
}
// Setup a project
func (p *Project) Setup() {
// get base path
p.name = filepath.Base(p.Path)
// set env const
for key, item := range p.Environment {
if err := os.Setenv(key, item); err != nil {
p.Buffer.StdErr = append(p.Buffer.StdErr, BufferOut{Time: time.Now(), Text: err.Error(), Type: "Env error", Stream: ""})
}
}
p.Tools.Setup()
}
// Watch a project
func (p *Project) Watch(exit chan os.Signal) {
var err error
stop := make(chan bool)
// init a new watcher
p.watcher, err = Watcher(p.parent.Settings.Legacy.Force, p.parent.Settings.Legacy.Interval)
if err != nil {
log.Fatal(err)
}
// global commands before
p.cmd(stop, "before", true)
// indexing files and dirs
for _, dir := range p.Watcher.Paths {
base, _ := filepath.Abs(p.Path)
base = filepath.Join(base, dir)
if _, err := os.Stat(base); err == nil {
if err := filepath.Walk(base, p.walk); err == nil {
p.tools(stop, base)
}
} else {
p.err(err)
}
}
// start message
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 watcher
go p.Reload(p.watcher, "", stop)
L:
for {
select {
case event := <-p.watcher.Events():
if time.Now().Truncate(time.Second).After(p.lastTime) || event.Name != p.lastFile {
// event time
eventTime := time.Now()
// file extension
ext := ext(event.Name)
if ext == "" {
ext = "DIR"
}
// change message
msg = fmt.Sprintln(p.pname(p.Name, 4), ":", magenta.bold(strings.ToUpper(ext)), "changed", magenta.bold(event.Name))
out = BufferOut{Time: time.Now(), Text: ext + " changed " + event.Name}
// switch event type
switch event.Op {
case fsnotify.Chmod:
case fsnotify.Remove:
p.watcher.Remove(event.Name)
if !strings.Contains(ext, "_") && !strings.Contains(ext, ".") && array(ext, p.Watcher.Exts) {
close(stop)
stop = make(chan bool)
p.stamp("log", out, msg, "")
go p.Reload(p.watcher, "", stop)
}
default:
file, err := os.Stat(event.Name)
if err != nil {
continue
}
if file.IsDir() {
filepath.Walk(event.Name, p.walk)
} else if file.Size() > 0 {
if !strings.Contains(ext, "_") && !strings.Contains(ext, ".") && array(ext, p.Watcher.Exts) {
// change watched
// check if a file is still writing #119
if event.Op != fsnotify.Write || (eventTime.Truncate(time.Millisecond).After(file.ModTime().Truncate(time.Millisecond)) || event.Name != p.lastFile) {
close(stop)
stop = make(chan bool)
// stop and start again
p.stamp("log", out, msg, "")
go p.Reload(p.watcher, event.Name, 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
}
}
}
// Reload launches the toolchain run, build, install
func (p *Project) Reload(watcher FileWatcher, path string, stop <-chan bool) {
var done bool
var install, build Response
go func() {
for {
select {
case <-stop:
done = true
return
}
}
}()
if done {
return
}
// before command
p.cmd(stop, "before", false)
if done {
return
}
// Go supported tools
p.tools(stop, path)
// Prevent fake events on polling startup
p.init = true
// prevent errors using realize without config with only run flag
if p.Tools.Run && !p.Tools.Install.Status && !p.Tools.Build.Status {
p.Tools.Install.Status = true
}
if done {
return
}
if p.Tools.Install.Status {
msg = fmt.Sprintln(p.pname(p.Name, 1), ":", green.regular(p.Tools.Install.name), "started")
out = BufferOut{Time: time.Now(), Text: p.Tools.Install.name + " started"}
p.stamp("log", out, msg, "")
start := time.Now()
install = p.Tools.Install.Compile(p.Path, stop)
install.printAfter(start, p)
}
if done {
return
}
if p.Tools.Build.Status {
msg = fmt.Sprintln(p.pname(p.Name, 1), ":", green.regular(p.Tools.Build.name), "started")
out = BufferOut{Time: time.Now(), Text: p.Tools.Build.name + " started"}
p.stamp("log", out, msg, "")
start := time.Now()
build = p.Tools.Build.Compile(p.Path, stop)
build.printAfter(start, p)
}
if done {
return
}
if install.Err == nil && build.Err == nil && p.Tools.Run {
var start time.Time
result := make(chan Response)
go func() {
select {
case r := <-result:
if r.Err != nil {
msg := fmt.Sprintln(p.pname(p.Name, 2), ":", red.regular(r.Err))
out := BufferOut{Time: time.Now(), Text: r.Err.Error(), Type: "Go Run"}
p.stamp("error", out, msg, "")
}
if r.Out != "" {
msg := fmt.Sprintln(p.pname(p.Name, 3), ":", blue.regular(r.Out))
out := BufferOut{Time: time.Now(), Text: r.Out, Type: "Go Run"}
p.stamp("out", out, msg, "")
}
}
}()
go func() {
log.Println(p.pname(p.Name, 1), ":", "Running..")
start = time.Now()
p.Run(p.Path, stop)
}()
}
if done {
return
}
p.cmd(stop, "after", false)
}
// Run a project
func (p *Project) Run(path string, stop <-chan bool) (response chan Response) {
var args []string
var build *exec.Cmd
var r Response
defer func() {
if err := build.Process.Kill(); err != nil {
r.Err = err
}
}()
// custom error pattern
isErrorText := func(string) bool {
return false
}
errRegexp, err := regexp.Compile(p.ErrorOutputPattern)
if err != nil {
r.Err = err
response <- r
r.Err = nil
} else {
isErrorText = func(t string) bool {
return errRegexp.MatchString(t)
}
}
// add additional arguments
for _, arg := range p.Args {
a := strings.FieldsFunc(arg, func(i rune) bool {
return i == '"' || i == '=' || i == '\''
})
args = append(args, a...)
}
gobin := os.Getenv("GOBIN")
dirPath := filepath.Base(path)
if path == "." {
dirPath = filepath.Base(wdir())
}
path = filepath.Join(gobin, dirPath)
if _, err := os.Stat(path); err == nil {
build = exec.Command(path, args...)
} else if _, err := os.Stat(path + RExtWin); err == nil {
build = exec.Command(path+RExtWin, args...)
} else {
if _, err = os.Stat(path); err == nil {
build = exec.Command(path, args...)
} else if _, err = os.Stat(path + RExtWin); err == nil {
build = exec.Command(path+RExtWin, args...)
} else {
r.Err = errors.New("project not found")
return
}
}
// scan project stream
stdout, err := build.StdoutPipe()
stderr, err := build.StderrPipe()
if err != nil {
r.Err = err
return
}
if err := build.Start(); err != nil {
r.Err = err
return
}
execOutput, execError := bufio.NewScanner(stdout), bufio.NewScanner(stderr)
stopOutput, stopError := make(chan bool, 1), make(chan bool, 1)
scanner := func(stop chan bool, output *bufio.Scanner, isError bool) {
for output.Scan() {
text := output.Text()
if isError && !isErrorText(text) {
r.Err = errors.New(text)
response <- r
r.Err = nil
} else {
r.Out = text
response <- r
r.Out = ""
}
}
close(stop)
}
go scanner(stopOutput, execOutput, false)
go scanner(stopError, execError, true)
for {
select {
case <-stop:
return
case <-stopOutput:
return
case <-stopError:
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, "")
}
// 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) tools(stop <-chan bool, path string) {
if len(path) > 0 {
done := make(chan bool)
result := make(chan Response)
v := reflect.ValueOf(p.Tools)
go func() {
for i := 0; i < v.NumField()-1; i++ {
tool := v.Field(i).Interface().(Tool)
if tool.Status && tool.isTool {
result <- tool.Exec(path, stop)
}
}
close(done)
}()
for {
select {
case <-done:
return
case <-stop:
return
case r := <-result:
if r.Err != nil {
msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.bold(r.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: r.Name, Stream: r.Err.Error()}
p.stamp("error", buff, msg, r.Err.Error())
} else if r.Out != "" {
msg = fmt.Sprintln(p.pname(p.Name, 3), ":", red.bold(r.Name), red.regular("outputs"), ":", blue.bold(path))
buff := BufferOut{Time: time.Now(), Text: "outputs", Path: path, Type: r.Name, Stream: r.Out}
p.stamp("out", buff, msg, r.Out)
}
}
}
}
}
// Cmd after/before
func (p *Project) cmd(stop <-chan bool, flag string, global bool) {
done := make(chan bool)
result := make(chan Response)
// commands sequence
go func() {
for _, cmd := range p.Watcher.Scripts {
if strings.ToLower(cmd.Type) == flag && cmd.Global == global {
result <- cmd.Exec(p.Path, stop)
}
}
close(done)
}()
for {
select {
case <-stop:
return
case <-done:
return
case r := <-result:
msg = fmt.Sprintln(p.pname(p.Name, 5), ":", green.bold("Command"), green.bold("\"")+r.Name+green.bold("\""))
out = BufferOut{Time: time.Now(), Text: r.Name, Type: flag}
if r.Err != nil {
p.stamp("error", out, msg, "")
out = BufferOut{Time: time.Now(), Text: r.Err.Error(), Type: flag}
p.stamp("error", out, "", fmt.Sprintln(red.regular(r.Err.Error())))
}
if r.Out != "" {
out = BufferOut{Time: time.Now(), Text: r.Out, Type: flag}
p.stamp("log", out, "", fmt.Sprintln(r.Out))
} else {
p.stamp("log", out, msg, "")
}
}
}
}
// 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 {
s := append([]string{p.Path}, strings.Split(v, string(os.PathSeparator))...)
if strings.Contains(path, filepath.Join(s...)) {
return nil
}
}
if !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++
}
}
}
return nil
}
// Print on files, cli, ws
func (p *Project) stamp(t string, o BufferOut, msg string, stream string) {
time := time.Now()
content := []string{time.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Text, "\r\n", stream}
switch t {
case "out":
p.Buffer.StdOut = append(p.Buffer.StdOut, o)
if p.parent.Settings.Files.Outputs.Status {
f := p.parent.Settings.Create(p.Path, p.parent.Settings.Files.Outputs.Name)
if _, err := f.WriteString(strings.Join(content, " ")); err != nil {
p.parent.Settings.Fatal(err, "")
}
}
case "log":
p.Buffer.StdLog = append(p.Buffer.StdLog, o)
if p.parent.Settings.Files.Logs.Status {
f := p.parent.Settings.Create(p.Path, p.parent.Settings.Files.Logs.Name)
if _, err := f.WriteString(strings.Join(content, " ")); err != nil {
p.parent.Settings.Fatal(err, "")
}
}
case "error":
p.Buffer.StdErr = append(p.Buffer.StdErr, o)
if p.parent.Settings.Files.Errors.Status {
f := p.parent.Settings.Create(p.Path, p.parent.Settings.Files.Errors.Name)
if _, err := f.WriteString(strings.Join(content, " ")); err != nil {
p.parent.Settings.Fatal(err, "")
}
}
}
if msg != "" {
log.Print(msg)
}
if stream != "" {
fmt.Fprint(output, stream)
}
}
func (r *Response) printAfter(start time.Time, p *Project) {
if r.Err != nil {
msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.bold(r.Name), red.regular(r.Err.Error()))
out = BufferOut{Time: time.Now(), Text: r.Err.Error(), Type: r.Name, Stream: r.Out}
p.stamp("error", out, msg, r.Out)
} else {
msg = fmt.Sprintln(p.pname(p.Name, 5), ":", green.bold(r.Name), "completed in", magenta.regular(big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3), " s"))
out = BufferOut{Time: time.Now(), Text: r.Name + " in " + big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3) + " s"}
p.stamp("log", out, msg, r.Out)
}
}

1215
realize.go

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +0,0 @@
package main
import (
"fmt"
"gopkg.in/urfave/cli.v2"
"reflect"
"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 TestNew(t *testing.T) {
r := new()
if reflect.TypeOf(r).String() != "main.realize" {
t.Error("Expected a realize struct")
}
}

76
schema.go Normal file
View File

@ -0,0 +1,76 @@
package main
import (
"errors"
"gopkg.in/urfave/cli.v2"
"path/filepath"
"reflect"
)
type Schema struct {
Projects []Project `yaml:"projects" json:"projects"`
}
// Add a project if unique
func (s *Schema) Add(p Project) {
for _, val := range s.Projects {
if reflect.DeepEqual(val, p) {
return
}
}
s.Projects = append(s.Projects, p)
}
// Remove a project
func (s *Schema) Remove(name string) error {
for key, val := range s.Projects {
if name == val.Name {
s.Projects = append(s.Projects[:key], s.Projects[key+1:]...)
return nil
}
}
return errors.New("project not found")
}
// New create a project using cli fields
func (s *Schema) New(c *cli.Context) Project {
name := filepath.Base(c.String("path"))
if name == "." {
name = filepath.Base(wdir())
}
project := Project{
Name: name,
Path: c.String("path"),
Tools: Tools{
Vet: Tool{
Status: c.Bool("vet"),
},
Fmt: Tool{
Status: c.Bool("fmt"),
},
Test: Tool{
Status: c.Bool("test"),
},
Generate: Tool{
Status: c.Bool("generate"),
},
Build: Tool{
Status: c.Bool("build"),
},
Install: Tool{
Status: c.Bool("install"),
},
Run: c.Bool("run"),
},
Args: params(c),
Watcher: Watch{
Paths: []string{"/"},
Ignore: []string{".git", ".realize", "vendor"},
Exts: []string{"go"},
},
}
return project
}
// Filter project list by field
func (s *Schema) Filter(field string, value interface{}) {}

143
server.go
View File

@ -2,12 +2,9 @@ package main
import (
"bytes"
"encoding/json"
"fmt"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"golang.org/x/net/websocket"
"gopkg.in/urfave/cli.v2"
"io"
"net/http"
"os/exec"
@ -17,13 +14,13 @@ import (
// Dafault host and port
const (
host = "localhost"
port = 5001
Host = "localhost"
Port = 5001
)
// Server settings
type Server struct {
parent *realize
parent *Realize
Status bool `yaml:"status" json:"status"`
Open bool `yaml:"open" json:"open"`
Port int `yaml:"port" json:"port"`
@ -32,50 +29,72 @@ type Server struct {
// Websocket projects
func (s *Server) projects(c echo.Context) (err error) {
websocket.Handler(func(ws *websocket.Conn) {
msg, _ := json.Marshal(s.parent)
err = websocket.Message.Send(ws, string(msg))
go func() {
for {
select {
case <-s.parent.sync:
msg, _ := json.Marshal(s.parent)
err = websocket.Message.Send(ws, string(msg))
//websocket.Handler(func(ws *websocket.Conn) {
// msg, _ := json.Marshal(s.parent)
// err = websocket.Message.Send(ws, string(msg))
// go func() {
// for {
// select {
// case <-s.parent.sync:
// msg, _ := json.Marshal(s.parent)
// err = websocket.Message.Send(ws, string(msg))
// if err != nil {
// break
// }
// }
// }
// }()
// for {
// // Read
// text := ""
// err = websocket.Message.Receive(ws, &text)
// if err != nil {
// break
// } else {
// err := json.Unmarshal([]byte(text), &s.parent)
// if err == nil {
// s.parent.Settings.record(s.parent)
// break
// }
// }
// }
// ws.Close()
//}).ServeHTTP(c.Response(), c.Request())
return 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
}
}
}
}()
for {
// Read
text := ""
err = websocket.Message.Receive(ws, &text)
if err != nil {
break
} else {
err := json.Unmarshal([]byte(text), &s.parent)
if err == nil {
s.parent.Settings.record(s.parent)
break
}
}
}
ws.Close()
}).ServeHTTP(c.Response(), c.Request())
rs.WriteHeader(http.StatusOK)
rs.Write(data)
return nil
}
// Start the web server
func (s *Server) start(p *cli.Context) (err error) {
if p.Bool("server") {
s.parent.Server.Status = true
}
if p.Bool("open") {
s.parent.Server.Open = true
}
if s.parent.Server.Status {
func (s *Server) Start() (err error) {
e := echo.New()
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Level: 2,
@ -134,16 +153,12 @@ func (s *Server) start(p *cli.Context) (err error) {
e.HideBanner = true
e.Debug = false
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
}
}
return nil
}
// OpenURL in a new tab of default browser
func (s *Server) openURL(url string) (io.Writer, error) {
func (s *Server) OpenURL() (io.Writer, error) {
url := "http://" + string(s.parent.Server.Host) + ":" + strconv.Itoa(s.parent.Server.Port)
stderr := bytes.Buffer{}
cmd := map[string]string{
"windows": "start",
@ -163,33 +178,3 @@ 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,60 +0,0 @@
package main
//import (
// "fmt"
// "net/http"
// "testing"
//)
//
//func TestServer_Start(t *testing.T) {
// s := Server{
// Status: true,
// Open: false,
// Host: "localhost",
// Port: 5000,
// }
// err := s.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 TestServer_Open(t *testing.T) {
// c := Server{
// Open: true,
// }
// url := "open_test"
// out, err := c.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)
// }
//}

View File

@ -12,12 +12,11 @@ import (
// settings const
const (
permission = 0775
directory = ".realize"
file = "realize.yaml"
fileOut = "outputs.log"
fileErr = "errors.log"
fileLog = "logs.log"
Permission = 0775
File = "realize.yaml"
FileOut = "outputs.log"
FileErr = "errors.log"
FileLog = "logs.log"
)
// random string preference
@ -30,7 +29,6 @@ const (
// Settings defines a group of general settings and options
type Settings struct {
file string
Files `yaml:"files,omitempty" json:"files,omitempty"`
Legacy Legacy `yaml:"legacy" json:"legacy"`
FileLimit int32 `yaml:"flimit,omitempty" json:"flimit,omitempty"`
@ -75,8 +73,8 @@ func random(n int) string {
return string(b)
}
// Delete realize folder
func (s *Settings) del(d string) error {
// Remove realize folder
func (s *Settings) Remove(d string) error {
_, err := os.Stat(d)
if !os.IsNotExist(err) {
return os.RemoveAll(d)
@ -84,23 +82,17 @@ func (s *Settings) del(d string) error {
return err
}
// 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
// Read config file
func (s *Settings) Read(out interface{}) error {
localConfigPath := RFile
// backward compatibility
path := filepath.Join(directory, s.file)
path := filepath.Join(RDir, RFile)
if _, err := os.Stat(path); err == nil {
localConfigPath = path
} else {
return nil
}
content, err := s.stream(localConfigPath)
content, err := s.Stream(localConfigPath)
if err == nil {
err = yaml.Unmarshal(content, out)
return err
@ -108,55 +100,52 @@ func (s *Settings) read(out interface{}) error {
return err
}
// Record create and unmarshal the yaml config file
func (s *Settings) record(out interface{}) error {
// Write config file
func (s *Settings) Write(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)
if _, err := os.Stat(RDir); os.IsNotExist(err) {
if err = os.Mkdir(RDir, Permission); err != nil {
s.Fatal(ioutil.WriteFile(RFile, y, Permission))
}
}
return s.write(filepath.Join(directory, s.file), y)
s.Fatal(ioutil.WriteFile(filepath.Join(RDir, RFile), y, Permission))
return nil
}
// Stream return a byte stream of a given file
func (s Settings) stream(file string) ([]byte, error) {
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)
s.Fatal(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 {
func (s Settings) Fatal(err error, msg ...interface{}) {
if err != nil {
if len(msg) > 0 {
log.Fatalln(red.regular(msg...), err.Error())
} else if err != nil {
} else {
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)
}
// Create a new file and return its pointer
func (s Settings) create(path string, name string) *os.File {
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)
if _, err := os.Stat(RDir); err == nil {
file = filepath.Join(path, RDir, 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)
out, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE|os.O_SYNC, Permission)
s.Fatal(err)
return out
}

View File

@ -1,114 +0,0 @@
package main
import (
"errors"
"io/ioutil"
"os"
"path/filepath"
"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 = "settings.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_Del(t *testing.T) {
s := Settings{}
if err := s.del("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.del(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.del(filepath.Join(directory, s.file))
}
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_Fatal(t *testing.T) {
s := Settings{}
s.fatal(nil, "test")
}

View File

@ -5,7 +5,7 @@ package main
import "syscall"
// Flimit defines the max number of watched files
func (s *Settings) flimit() error {
func (s *Settings) Flimit() error {
var rLimit syscall.Rlimit
rLimit.Max = uint64(s.FileLimit)
rLimit.Cur = uint64(s.FileLimit)

View File

@ -3,6 +3,6 @@
package main
// Flimit defines the max number of watched files
func (s *Settings) flimit() error {
func (s *Settings) Flimit() error {
return nil
}

160
tools.go Normal file
View File

@ -0,0 +1,160 @@
package main
import (
"bytes"
"errors"
"os/exec"
"path/filepath"
"strings"
)
type Tool struct {
Args []string `yaml:"args,omitempty" json:"args,omitempty"`
Method string `yaml:"method,omitempty" json:"method,omitempty"`
Status bool `yaml:"status,omitempty" json:"status,omitempty"`
dir bool
isTool bool
method []string
cmd []string
name string
}
type Tools struct {
Fix Tool `yaml:"fix,omitempty" json:"fix,omitempty"`
Clean Tool `yaml:"clean,omitempty" json:"clean,omitempty"`
Vet Tool `yaml:"vet,omitempty" json:"vet,omitempty"`
Fmt Tool `yaml:"fmt,omitempty" json:"fmt,omitempty"`
Test Tool `yaml:"test,omitempty" json:"test,omitempty"`
Generate Tool `yaml:"generate,omitempty" json:"generate,omitempty"`
Install Tool `yaml:"install,omitempty" json:"install,omitempty"`
Build Tool `yaml:"build,omitempty" json:"build,omitempty"`
Run bool `yaml:"run,omitempty" json:"run,omitempty"`
}
func (t *Tools) Setup() {
// go clean
if t.Clean.Status {
t.Clean.name = "Clean"
t.Clean.isTool = true
t.Clean.cmd = replace([]string{"go clean"}, t.Clean.Method)
t.Clean.Args = split([]string{}, t.Clean.Args)
}
// go generate
if t.Generate.Status {
t.Generate.dir = true
t.Generate.isTool = true
t.Generate.name = "Generate"
t.Generate.cmd = replace([]string{"go", "generate"}, t.Generate.Method)
t.Generate.Args = split([]string{}, t.Generate.Args)
}
// go fix
if t.Fix.Status {
t.Fix.name = "Fix"
t.Fix.isTool = true
t.Fix.cmd = replace([]string{"go fix"}, t.Fix.Method)
t.Fix.Args = split([]string{}, t.Fix.Args)
}
// go fmt
if t.Fmt.Status {
if len(t.Fmt.Args) == 0 {
t.Fmt.Args = []string{"-s", "-w", "-e", "./"}
}
t.Fmt.name = "Fmt"
t.Fmt.isTool = true
t.Fmt.cmd = replace([]string{"gofmt"}, t.Fmt.Method)
t.Fmt.Args = split([]string{}, t.Fmt.Args)
}
// go vet
if t.Vet.Status {
t.Vet.dir = true
t.Vet.name = "Vet"
t.Vet.isTool = true
t.Vet.cmd = replace([]string{"go", "vet"}, t.Vet.Method)
t.Vet.Args = split([]string{}, t.Vet.Args)
}
// go test
if t.Test.Status {
t.Test.dir = true
t.Test.isTool = true
t.Test.name = "Test"
t.Test.cmd = replace([]string{"go", "test"}, t.Test.Method)
t.Test.Args = split([]string{}, t.Test.Args)
}
// go install
t.Install.name = "Install"
t.Install.cmd = replace([]string{"go", "install"}, t.Install.Method)
t.Install.Args = split([]string{}, t.Install.Args)
// go build
if t.Build.Status {
t.Build.name = "Build"
t.Build.cmd = replace([]string{"go", "build"}, t.Build.Method)
t.Build.Args = split([]string{}, t.Build.Args)
}
}
func (t *Tool) Exec(path string, stop <-chan bool) (response Response) {
if t.dir && filepath.Ext(path) != "" {
path = filepath.Dir(path)
}
if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "") {
args := []string{}
if strings.HasSuffix(path, ".go") {
args = append(t.Args, path)
path = filepath.Dir(path)
}
if s := ext(path); s == "" || s == "go" {
var out, stderr bytes.Buffer
done := make(chan error)
args = append(t.cmd, t.Args...)
cmd := exec.Command(args[0], args[1:]...)
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()
case err := <-done:
// Command completed
response.Name = t.name
if err != nil {
response.Err = errors.New(stderr.String() + out.String())
} else {
response.Out = out.String()
}
}
}
}
return
}
func (t *Tool) Compile(path string, stop <-chan bool) (response Response) {
var out bytes.Buffer
var stderr bytes.Buffer
done := make(chan error)
args := append(t.cmd, t.Args...)
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = filepath.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()
case err := <-done:
// Command completed
response.Name = t.name
if err != nil {
response.Err = errors.New(stderr.String() + err.Error())
}
}
return
}

View File

@ -76,7 +76,7 @@ func replace(a []string, b string) []string {
func wdir() string {
dir, err := os.Getwd()
if err != nil {
log.Fatal(prefix(err.Error()))
log.Fatal(err.Error())
}
return dir
}

View File

@ -1,532 +0,0 @@
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
)
const (
msgStop = "killed"
extWindows = ".exe"
)
// Watch struct defines options for livereload
type Watch struct {
Preview bool `yaml:"preview,omitempty" json:"preview,omitempty"`
Paths []string `yaml:"paths" json:"paths"`
Exts []string `yaml:"extensions" json:"extensions"`
Ignore []string `yaml:"ignored_paths,omitempty" json:"ignored_paths,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
Settings `yaml:"-" json:"-"`
files, folders int64
name, lastFile string
tools []tool
paths []string
lastTime 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 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
func (p *Project) watch() {
p.watcher, _ = Watcher(r.Settings.Legacy.Force, r.Settings.Legacy.Interval)
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.Abs(p.Path)
base = filepath.Join(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 {
// event time
eventTime := time.Now()
// file extension
ext := ext(event.Name)
if ext == "" {
ext = "DIR"
}
// change message
msg = fmt.Sprintln(p.pname(p.Name, 4), ":", magenta.bold(strings.ToUpper(ext)), "changed", magenta.bold(event.Name))
out = BufferOut{Time: time.Now(), Text: ext + " changed " + event.Name}
// switch event type
switch event.Op {
case fsnotify.Chmod:
case fsnotify.Remove:
p.watcher.Remove(event.Name)
if !strings.Contains(ext, "_") && !strings.Contains(ext, ".") && array(ext, p.Watcher.Exts) {
close(stop)
stop = make(chan bool)
p.stamp("log", out, msg, "")
go p.routines(stop, p.watcher, "")
}
default:
file, err := os.Stat(event.Name)
if err != nil {
continue
}
if file.IsDir() {
filepath.Walk(event.Name, p.walk)
} else if file.Size() > 0 {
// used only for test and debug
if p.parent.Settings.Recovery {
log.Println(event)
}
if !strings.Contains(ext, "_") && !strings.Contains(ext, ".") && array(ext, p.Watcher.Exts) {
// change watched
// check if a file is still writing #119
if event.Op != fsnotify.Write || (eventTime.Truncate(time.Millisecond).After(file.ModTime().Truncate(time.Millisecond)) || event.Name != p.lastFile) {
close(stop)
stop = make(chan bool)
// stop and start again
p.stamp("log", out, msg, "")
go p.routines(stop, p.watcher, event.Name)
}
}
p.lastTime = time.Now().Truncate(time.Second)
p.lastFile = event.Name
}
}
}
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, "")
}
// Config project init
func (p *Project) config(r *realize) {
// get basepath name
p.name = filepath.Base(p.Path)
// env variables
for key, item := range p.Environment {
if err := os.Setenv(key, item); err != nil {
p.Buffer.StdErr = append(p.Buffer.StdErr, BufferOut{Time: time.Now(), Text: err.Error(), Type: "Env error", Stream: ""})
}
}
// init commands
if len(p.Cmds.Fmt.Args) == 0 {
p.Cmds.Fmt.Args = []string{"-s", "-w", "-e", "./"}
}
p.tools = append(p.tools, tool{
status: p.Cmds.Clean.Status,
cmd: replace([]string{"go clean"}, p.Cmds.Clean.Method),
options: split([]string{}, p.Cmds.Clean.Args),
name: "Clean",
})
p.tools = append(p.tools, tool{
status: p.Cmds.Generate.Status,
cmd: replace([]string{"go", "generate"}, p.Cmds.Generate.Method),
options: split([]string{}, p.Cmds.Generate.Args),
name: "Generate",
dir: true,
})
p.tools = append(p.tools, tool{
status: p.Cmds.Fix.Status,
cmd: replace([]string{"go fix"}, p.Cmds.Fix.Method),
options: split([]string{}, p.Cmds.Fix.Args),
name: "Fix",
})
p.tools = append(p.tools, tool{
status: p.Cmds.Fmt.Status,
cmd: replace([]string{"gofmt"}, p.Cmds.Fmt.Method),
options: split([]string{}, p.Cmds.Fmt.Args),
name: "Fmt",
})
p.tools = append(p.tools, tool{
status: p.Cmds.Vet.Status,
cmd: replace([]string{"go", "vet"}, p.Cmds.Vet.Method),
options: split([]string{}, p.Cmds.Vet.Args),
name: "Vet",
dir: true,
})
p.tools = append(p.tools, tool{
status: p.Cmds.Test.Status,
cmd: replace([]string{"go", "test"}, p.Cmds.Test.Method),
options: split([]string{}, p.Cmds.Test.Args),
name: "Test",
dir: true,
})
p.Cmds.Install = Cmd{
Status: p.Cmds.Install.Status,
Args: append([]string{}, p.Cmds.Install.Args...),
method: replace([]string{"go", "install"}, p.Cmds.Install.Method),
name: "Install",
startTxt: "Installing...",
endTxt: "Installed",
}
p.Cmds.Build = Cmd{
Status: p.Cmds.Build.Status,
Args: append([]string{}, p.Cmds.Build.Args...),
method: replace([]string{"go", "build"}, p.Cmds.Build.Method),
name: "Build",
startTxt: "Building...",
endTxt: "Built",
}
p.parent = r
}
// Cmd calls the method that execute commands after/before and display the results
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 {
var start time.Time
channel := make(chan Result)
go func() {
log.Println(p.pname(p.Name, 1), ":", cmd.startTxt)
start = time.Now()
stream, err := p.goCompile(stop, cmd.method, cmd.Args)
if stream != msgStop {
channel <- Result{stream, err}
}
}()
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
for _, element := range p.tools {
// no need a sequence, these commands can be asynchronous
if element.status {
wg.Add(1)
p.goTool(&wg, stop, result, path, element)
}
}
wg.Wait()
close(done)
}()
loop:
for {
select {
case tool := <-result:
if tool.err != "" {
msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.bold(tool.name), red.regular("there are some errors in"), ":", magenta.bold(path))
buff := BufferOut{Time: time.Now(), Text: "there are some errors in", Path: path, Type: tool.name, Stream: tool.err}
p.stamp("error", buff, msg, tool.err)
} else if tool.out != "" {
msg = fmt.Sprintln(p.pname(p.Name, 3), ":", red.bold(tool.name), red.regular("outputs"), ":", blue.bold(path))
buff := BufferOut{Time: time.Now(), Text: "outputs", Path: path, Type: tool.name, Stream: tool.out}
p.stamp("out", buff, msg, tool.out)
}
case <-done:
break loop
case <-stop:
break loop
}
}
}
return nil
}
// 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 {
s := append([]string{p.Path}, strings.Split(v, string(os.PathSeparator))...)
if strings.Contains(path, filepath.Join(s...)) {
return nil
}
}
if !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.Path, 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.Path, 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.Path, 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 {
return
}
// before command
p.cmd(stop, "before", false)
if done {
return
}
// Go supported tools
p.tool(stop, path)
// Prevent fake events on polling startup
p.init = true
// prevent errors using realize without config with only run flag
if p.Cmds.Run && !p.Cmds.Install.Status && !p.Cmds.Build.Status {
p.Cmds.Install.Status = true
}
if done {
return
}
install = p.compile(stop, p.Cmds.Install)
if done {
return
}
build = p.compile(stop, p.Cmds.Build)
if done {
return
}
if install == nil && build == nil && p.Cmds.Run {
var start time.Time
runner := make(chan bool, 1)
go func() {
log.Println(p.pname(p.Name, 1), ":", "Running..")
start = time.Now()
p.goRun(stop, runner)
}()
select {
case <-runner:
msg = fmt.Sprintln(p.pname(p.Name, 5), ":", green.regular("Started"), "in", magenta.regular(big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3), " s"))
out = BufferOut{Time: time.Now(), Text: "Started in " + big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3) + " s"}
p.stamp("log", out, msg, "")
case <-stop:
return
}
}
if done {
return
}
p.cmd(stop, "after", false)
}

View File

@ -1,72 +0,0 @@
package main
import (
"testing"
"os"
)
type fileWatcherMock struct {
FileWatcher
}
func (f *fileWatcherMock) Walk(path string, _ bool) string {
return path
}
type fileInfoMock struct {
os.FileInfo
FileIsDir bool
}
func (m *fileInfoMock) IsDir() bool { return m.FileIsDir }
func TestWalk(t *testing.T) {
p := Project{
Name: "Test Project",
Watcher: Watch{
Paths: []string{"/"},
Ignore: []string{"vendor"},
Exts: []string{"go"},
},
Path: "/go/project",
watcher: &fileWatcherMock{},
init: true,
}
files := []struct {
Path string
IsDir bool
}{
// valid files
{"/go/project", true},
{"/go/project/main.go", false},
{"/go/project/main_test.go", false},
// invalid relative path
{"./relative/path", true},
{"./relative/path/file.go", false},
// invalid extension
{"/go/project/settings.yaml", false},
// invalid vendor files
{"/go/project/vendor/foo", true},
{"/go/project/vendor/foo/main.go", false},
}
for _, file := range files {
fileInfo := fileInfoMock{
FileIsDir: file.IsDir,
}
err := p.walk(file.Path, &fileInfo, nil)
if err != nil {
t.Errorf("Error not expected: %s", err)
}
}
if p.files != 2 {
t.Errorf("Exepeted %d files, but was %d", 2, p.files)
}
if p.folders != 1 {
t.Errorf("Exepeted %d folders, but was %d", 2, p.folders)
}
}