initial commit
This commit is contained in:
commit
afb536e439
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
.haunt.yaml
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
.glide
|
||||
.idea
|
||||
.DS_Store
|
||||
.realize
|
||||
|
||||
realize/assets/*
|
||||
docker
|
||||
vendor
|
||||
bin
|
||||
glide.*
|
||||
Dockerfile
|
||||
docker-compose.yml
|
6
.woodpecker.yml
Normal file
6
.woodpecker.yml
Normal file
@ -0,0 +1,6 @@
|
||||
pipeline:
|
||||
build:
|
||||
image: golang
|
||||
commands:
|
||||
- go mod tidy
|
||||
- go build -o bin/realize
|
16
LICENSE
Normal file
16
LICENSE
Normal file
@ -0,0 +1,16 @@
|
||||
The GPLv3 License (GPLv3)
|
||||
|
||||
Copyright (c) 2023 abs3nt
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
34
Makefile
Normal file
34
Makefile
Normal file
@ -0,0 +1,34 @@
|
||||
pkgname := haunt
|
||||
build: ${pkgname}
|
||||
|
||||
${pkgname}: $(shell find . -name '*.go')
|
||||
mkdir -p bin
|
||||
go build -o bin/${pkgname} .
|
||||
|
||||
completions:
|
||||
mkdir -p completions
|
||||
./bin/${pkgname} completion zsh > completions/_${pkgname}
|
||||
./bin/${pkgname} completion bash > completions/${pkgname}
|
||||
./bin/${pkgname} completion fish > completions/${pkgname}.fish
|
||||
|
||||
run:
|
||||
go run main.go
|
||||
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
clean:
|
||||
rm -f bin
|
||||
rm -rf completions
|
||||
|
||||
uninstall:
|
||||
rm -f /usr/bin/${pkgname}
|
||||
rm -f /usr/share/zsh/site-functions/_${pkgname}
|
||||
rm -f /usr/share/bash-completion/completions/${pkgname}
|
||||
rm -f /usr/share/fish/vendor_completions.d/${pkgname}.fish
|
||||
|
||||
install:
|
||||
cp bin/${pkgname} /usr/bin
|
||||
bin/${pkgname} completion zsh > /usr/share/zsh/site-functions/_${pkgname}
|
||||
bin/${pkgname} completion bash > /usr/share/bash-completion/completions/${pkgname}
|
||||
bin/${pkgname} completion fish > /usr/share/fish/vendor_completions.d/${pkgname}.fish
|
141
README.md
Normal file
141
README.md
Normal file
@ -0,0 +1,141 @@
|
||||
## Quickstart
|
||||
```
|
||||
go install github.com/abs3ntdev/haunt@latest
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
git clone https://github.com/abs3ntdev/haunt
|
||||
cd haunt
|
||||
make build && sudo make install
|
||||
```
|
||||
|
||||
## Commands List
|
||||
|
||||
### Run Command
|
||||
From **project/projects** root execute:
|
||||
|
||||
|
||||
```
|
||||
haunt init
|
||||
```
|
||||
|
||||
then
|
||||
|
||||
```
|
||||
haunt start
|
||||
```
|
||||
|
||||
haunt init will add your root directory if it contains a main.go file and will add any directory inside of cmd as projects. If you wish to add additional projects run haunt add [name] or edit the config file manually. By default projects are set to run go install and go run [project]
|
||||
|
||||
***start*** command supports the following custom parameters:
|
||||
|
||||
--name="name" -> Run by name on existing configuration
|
||||
--path="haunt/server" -> Custom Path (if not specified takes the working directory name)
|
||||
--generate -> Enable go generate
|
||||
--fmt -> Enable go fmt
|
||||
--test -> Enable go test
|
||||
--vet -> Enable go vet
|
||||
--install -> Enable go install
|
||||
--build -> Enable go build
|
||||
--run -> Enable go run
|
||||
--server -> Enable the web server
|
||||
--open -> Open web ui in default browser
|
||||
--no-config -> Ignore an existing config / skip the creation of a new one
|
||||
|
||||
Some examples:
|
||||
|
||||
haunt start
|
||||
haunt start --path="mypath"
|
||||
haunt start --name="haunt" --build
|
||||
haunt start --path="haunt" --run --no-config
|
||||
haunt start --install --test --fmt --no-config
|
||||
haunt start --path="/Users/username/go/src/github.com/oxequa/haunt-examples/coin/"
|
||||
|
||||
### Init Command
|
||||
This command will generate a .haunt.yaml with sane default for your current project/projects.\
|
||||
If there is a main.go in the root directory it will be added along with any directories inside the relative path `cmd`
|
||||
|
||||
haunt init
|
||||
|
||||
### Add Command
|
||||
Add a project to an existing config file or create a new one.
|
||||
|
||||
haunt add [name]
|
||||
|
||||
### Remove Command
|
||||
Remove a project by its name
|
||||
|
||||
haunt remove [name]
|
||||
|
||||
|
||||
## Config sample
|
||||
|
||||
settings:
|
||||
legacy:
|
||||
force: true // force polling watcher instead fsnotifiy
|
||||
interval: 100ms // polling interval
|
||||
resources: // files names
|
||||
outputs: outputs.log
|
||||
logs: logs.log
|
||||
errors: errors.log
|
||||
server:
|
||||
status: false // server status
|
||||
open: false // open browser at start
|
||||
host: localhost // server host
|
||||
port: 5001 // server port
|
||||
schema:
|
||||
- name: coin
|
||||
path: cmd/coin // project path
|
||||
env: // env variables available at startup
|
||||
test: test
|
||||
myvar: value
|
||||
commands: // go commands supported
|
||||
vet:
|
||||
status: true
|
||||
fmt:
|
||||
status: true
|
||||
args:
|
||||
- -s
|
||||
- -w
|
||||
test:
|
||||
status: true
|
||||
method: gb test // support different build tools
|
||||
generate:
|
||||
status: true
|
||||
install:
|
||||
status: true
|
||||
build:
|
||||
status: false
|
||||
method: gb build // support differents build tool
|
||||
args: // additional params for the command
|
||||
- -race
|
||||
run:
|
||||
status: true
|
||||
args: // arguments to pass at the project
|
||||
- --myarg
|
||||
watcher:
|
||||
paths: // watched paths
|
||||
- /
|
||||
ignore_paths: // ignored paths
|
||||
- vendor
|
||||
extensions: // watched extensions
|
||||
- go
|
||||
- html
|
||||
scripts:
|
||||
- type: before
|
||||
command: echo before global
|
||||
global: true
|
||||
output: true
|
||||
- type: before
|
||||
command: echo before change
|
||||
output: true
|
||||
- type: after
|
||||
command: echo after change
|
||||
output: true
|
||||
- type: after
|
||||
command: echo after global
|
||||
global: true
|
||||
output: true
|
||||
errorOutputPattern: mypattern //custom error pattern
|
99
cmd/add.go
Normal file
99
cmd/add.go
Normal file
@ -0,0 +1,99 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/abs3ntdev/haunt/src/config"
|
||||
"github.com/abs3ntdev/haunt/src/haunt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var addConfig config.Flags
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Adds a project by name",
|
||||
Long: "Adds a project by name, if path is provided it will use 'cmd/name', all flags provided will be saved in the config file. By default go install and go run will be ran",
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1)),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) >= 1 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return getPotentialProjets(toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
RunE: add,
|
||||
}
|
||||
|
||||
func getPotentialProjets(in string) []string {
|
||||
r := haunt.NewHaunt()
|
||||
err := r.Settings.Read(&r)
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
out := []string{}
|
||||
cmdDir, err := os.ReadDir("cmd")
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
for _, dir := range cmdDir {
|
||||
exists := false
|
||||
for _, proj := range r.Projects {
|
||||
if dir.Name() == proj.Name {
|
||||
exists = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
if in == "" {
|
||||
out = append(out, dir.Name())
|
||||
} else {
|
||||
if strings.HasPrefix(dir.Name(), in) {
|
||||
out = append(out, dir.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(addCmd)
|
||||
addCmd.Flags().StringVarP(&addConfig.Path, "path", "p", "", "Project base path")
|
||||
addCmd.Flags().BoolVarP(&addConfig.Format, "fmt", "f", false, "Enable go fmt")
|
||||
addCmd.Flags().BoolVarP(&addConfig.Vet, "vet", "v", false, "Enable go vet")
|
||||
addCmd.Flags().BoolVarP(&addConfig.Test, "test", "t", false, "Enable go test")
|
||||
addCmd.Flags().BoolVarP(&addConfig.Generate, "generate", "g", false, "Enable go generate")
|
||||
addCmd.Flags().BoolVarP(&addConfig.Install, "install", "i", true, "Enable go install")
|
||||
addCmd.Flags().BoolVarP(&addConfig.Build, "build", "b", false, "Enable go build")
|
||||
addCmd.Flags().BoolVarP(&addConfig.Run, "run", "r", true, "Enable go run")
|
||||
}
|
||||
|
||||
// Add a project to an existing config or create a new one
|
||||
func add(cmd *cobra.Command, args []string) (err error) {
|
||||
addConfig.Name = args[0]
|
||||
r := haunt.NewHaunt()
|
||||
// read a config if exist
|
||||
err = r.Settings.Read(&r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if addConfig.Path == "" {
|
||||
addConfig.Path = "cmd/" + addConfig.Name
|
||||
}
|
||||
projects := len(r.Schema.Projects)
|
||||
// create and add a new project
|
||||
r.Schema.Add(r.Schema.New(addConfig))
|
||||
if len(r.Schema.Projects) > projects {
|
||||
// update config
|
||||
err = r.Settings.Write(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println(r.Prefix(haunt.Green.Bold("project successfully added")))
|
||||
} else {
|
||||
log.Println(r.Prefix(haunt.Green.Bold("project can't be added")))
|
||||
}
|
||||
return nil
|
||||
}
|
29
cmd/clean.go
Normal file
29
cmd/clean.go
Normal file
@ -0,0 +1,29 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/abs3ntdev/haunt/src/haunt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// cleanCmd represents the clean command
|
||||
var cleanCmd = &cobra.Command{
|
||||
Use: "clean",
|
||||
Short: "Deletes the haunt config file",
|
||||
RunE: clean,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(cleanCmd)
|
||||
}
|
||||
|
||||
// Clean remove haunt file
|
||||
func clean(cmd *cobra.Command, args []string) (err error) {
|
||||
r := haunt.NewHaunt()
|
||||
if err := r.Settings.Remove(haunt.RFile); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println(r.Prefix(haunt.Green.Bold("folder successfully removed")))
|
||||
return nil
|
||||
}
|
53
cmd/init.go
Normal file
53
cmd/init.go
Normal file
@ -0,0 +1,53 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/abs3ntdev/haunt/src/haunt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// initCmd represents the init command
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Generates a haunt config file using sane defaults",
|
||||
Long: "Generates a haunt config file using sane defaults, haunt will look for a main.go file and any directories inside the relative path 'cmd' and add them all as projects",
|
||||
RunE: defaultConfig,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(initCmd)
|
||||
}
|
||||
|
||||
func defaultConfig(cmd *cobra.Command, args []string) error {
|
||||
r := haunt.NewHaunt()
|
||||
write := true
|
||||
if _, err := os.Stat(haunt.RFile); err == nil {
|
||||
fmt.Print(r.Prefix("Config file exists. Overwire? " + haunt.Magenta.Bold("[y/n] ") + haunt.Green.Bold("(n) ")))
|
||||
var overwrite string
|
||||
fmt.Scanf("%s", &overwrite)
|
||||
write = false
|
||||
switch strings.ToLower(overwrite) {
|
||||
case "y", "ye", "yes":
|
||||
write = true
|
||||
}
|
||||
}
|
||||
if write {
|
||||
r.SetDefaults()
|
||||
err := r.Settings.Write(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println(r.Prefix(
|
||||
"Config file has successfully been saved at .haunt.yaml",
|
||||
))
|
||||
log.Println(r.Prefix(
|
||||
"Run haunt add --help to see how to add more projects",
|
||||
))
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
64
cmd/remove.go
Normal file
64
cmd/remove.go
Normal file
@ -0,0 +1,64 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/abs3ntdev/haunt/src/haunt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var removeCmd = &cobra.Command{
|
||||
Use: "remove [names]",
|
||||
Short: "Removes all projects by name from config file",
|
||||
Args: cobra.MatchAll(cobra.MinimumNArgs(1), cobra.OnlyValidArgs),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return getProjectNames(toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
RunE: remove,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(removeCmd)
|
||||
}
|
||||
|
||||
func getProjectNames(input string) []string {
|
||||
r := haunt.NewHaunt()
|
||||
// read a config if exist
|
||||
err := r.Settings.Read(&r)
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
names := []string{}
|
||||
for _, project := range r.Schema.Projects {
|
||||
if strings.HasPrefix(project.Name, input) {
|
||||
names = append(names, project.Name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Remove a project from an existing config
|
||||
func remove(cmd *cobra.Command, args []string) (err error) {
|
||||
r := haunt.NewHaunt()
|
||||
// read a config if exist
|
||||
err = r.Settings.Read(&r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, arg := range args {
|
||||
err := r.Schema.Remove(arg)
|
||||
if err != nil {
|
||||
log.Println(r.Prefix(haunt.Red.Bold(arg + " project not found")))
|
||||
continue
|
||||
}
|
||||
log.Println(r.Prefix(haunt.Green.Bold(arg + " successfully removed")))
|
||||
}
|
||||
// update config
|
||||
err = r.Settings.Write(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
31
cmd/root.go
Normal file
31
cmd/root.go
Normal file
@ -0,0 +1,31 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "haunt",
|
||||
Short: `Haunt: Go task automater and live-reloader.
|
||||
.-.
|
||||
(o o)
|
||||
| O \
|
||||
\ \
|
||||
'~~'
|
||||
`,
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
93
cmd/start.go
Normal file
93
cmd/start.go
Normal file
@ -0,0 +1,93 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/abs3ntdev/haunt/src/config"
|
||||
"github.com/abs3ntdev/haunt/src/haunt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var startConfig config.Flags
|
||||
|
||||
var startCmd = &cobra.Command{
|
||||
Use: "start",
|
||||
Short: "Start haunt on a given path, generates a config file if one does not already exist",
|
||||
RunE: start,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(startCmd)
|
||||
startCmd.Flags().StringVarP(&startConfig.Path, "path", "p", "", "Project base path")
|
||||
startCmd.Flags().StringVarP(&startConfig.Name, "name", "n", "", "Run a project by its name")
|
||||
startCmd.Flags().BoolVarP(&startConfig.Format, "fmt", "f", false, "Enable go fmt")
|
||||
startCmd.Flags().BoolVarP(&startConfig.Vet, "vet", "v", false, "Enable go vet")
|
||||
startCmd.Flags().BoolVarP(&startConfig.Test, "test", "t", false, "Enable go test")
|
||||
startCmd.Flags().BoolVarP(&startConfig.Generate, "generate", "g", false, "Enable go generate")
|
||||
startCmd.Flags().BoolVarP(&startConfig.Server, "server", "s", false, "Start server")
|
||||
startCmd.Flags().BoolVarP(&startConfig.Open, "open", "o", false, "Open into the default browser")
|
||||
startCmd.Flags().BoolVarP(&startConfig.Install, "install", "i", false, "Enable go install")
|
||||
startCmd.Flags().BoolVarP(&startConfig.Build, "build", "b", false, "Enable go build")
|
||||
startCmd.Flags().BoolVarP(&startConfig.Run, "run", "r", false, "Enable go run")
|
||||
startCmd.Flags().BoolVarP(&startConfig.Legacy, "legacy", "l", false, "Legacy watch by polling instead fsnotify")
|
||||
startCmd.Flags().BoolVarP(&startConfig.NoConfig, "no-config", "c", false, "Ignore existing config and doesn't create a new one")
|
||||
}
|
||||
|
||||
// Start haunt workflow
|
||||
func start(cmd *cobra.Command, args []string) (err error) {
|
||||
r := haunt.NewHaunt()
|
||||
// set legacy watcher
|
||||
if startConfig.Legacy {
|
||||
r.Settings.Legacy.Set(startConfig.Legacy, 1)
|
||||
}
|
||||
// set server
|
||||
if startConfig.Server {
|
||||
r.Server.Set(startConfig.Server, startConfig.Open, haunt.Port, haunt.Host)
|
||||
}
|
||||
|
||||
// check no-config and read
|
||||
if !startConfig.NoConfig {
|
||||
// read a config if exist
|
||||
err := r.Settings.Read(&r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if startConfig.Name != "" {
|
||||
// filter by name flag if exist
|
||||
r.Schema.Projects = r.Schema.Filter("Name", startConfig.Name)
|
||||
}
|
||||
// increase file limit
|
||||
if r.Settings.FileLimit != 0 {
|
||||
if err = r.Settings.Flimit(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// check project list length
|
||||
if len(r.Schema.Projects) == 0 {
|
||||
// create a new project based on given params
|
||||
project := r.Schema.New(startConfig)
|
||||
// Add to projects list
|
||||
r.Schema.Add(project)
|
||||
// save config
|
||||
if !startConfig.NoConfig {
|
||||
err = r.Settings.Write(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start web server
|
||||
if r.Server.Status {
|
||||
r.Server.Parent = r
|
||||
err = r.Server.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = r.Server.OpenURL()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// start workflow
|
||||
return r.Start()
|
||||
}
|
41
cmd/version.go
Normal file
41
cmd/version.go
Normal file
@ -0,0 +1,41 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/abs3ntdev/haunt/src/haunt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "A brief description of your command",
|
||||
Long: `A longer description that spans multiple lines and likely contains examples
|
||||
and usage of using your command. For example:
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
Run: version,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// versionCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// versionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
|
||||
// Version print current version
|
||||
func version(cmd *cobra.Command, args []string) {
|
||||
r := haunt.NewHaunt()
|
||||
log.Println(r.Prefix(haunt.Green.Bold(haunt.RVersion)))
|
||||
}
|
28
go.mod
Normal file
28
go.mod
Normal file
@ -0,0 +1,28 @@
|
||||
module github.com/abs3ntdev/haunt
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.14.1
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/labstack/echo v3.3.10+incompatible
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
golang.org/x/net v0.7.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.8.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
)
|
68
go.sum
Normal file
68
go.sum
Normal file
@ -0,0 +1,68 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
|
||||
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
7
main.go
Normal file
7
main.go
Normal file
@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "github.com/abs3ntdev/haunt/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
17
src/config/config.go
Normal file
17
src/config/config.go
Normal file
@ -0,0 +1,17 @@
|
||||
package config
|
||||
|
||||
type Flags struct {
|
||||
Path string
|
||||
Name string
|
||||
Format bool
|
||||
Vet bool
|
||||
Test bool
|
||||
Generate bool
|
||||
Server bool
|
||||
Open bool
|
||||
Install bool
|
||||
Build bool
|
||||
Run bool
|
||||
Legacy bool
|
||||
NoConfig bool
|
||||
}
|
538
src/haunt/bindata.go
Normal file
538
src/haunt/bindata.go
Normal file
File diff suppressed because one or more lines are too long
182
src/haunt/cli.go
Normal file
182
src/haunt/cli.go
Normal file
@ -0,0 +1,182 @@
|
||||
package haunt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
var (
|
||||
// RPrefix tool name
|
||||
RPrefix = "haunt"
|
||||
// RVersion current version
|
||||
RVersion = "3.0.0"
|
||||
// RExt file extension
|
||||
RExt = ".yaml"
|
||||
// RFile config file name
|
||||
RFile = "." + RPrefix + RExt
|
||||
// RExtWin windows extension
|
||||
RExtWin = ".exe"
|
||||
)
|
||||
|
||||
type (
|
||||
// LogWriter used for all log
|
||||
LogWriter struct{}
|
||||
|
||||
// Haunt main struct
|
||||
Haunt struct {
|
||||
Settings Settings `yaml:"settings" json:"settings"`
|
||||
Server Server `yaml:"server,omitempty" json:"server,omitempty"`
|
||||
Schema `yaml:",inline" json:",inline"`
|
||||
Sync chan string `yaml:"-" json:"-"`
|
||||
Err Func `yaml:"-" json:"-"`
|
||||
After Func `yaml:"-" json:"-"`
|
||||
Before Func `yaml:"-" json:"-"`
|
||||
Change Func `yaml:"-" json:"-"`
|
||||
Reload Func `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
// Context is used as argument for func
|
||||
Context struct {
|
||||
Path string
|
||||
Project *Project
|
||||
Stop <-chan bool
|
||||
Watcher FileWatcher
|
||||
Event fsnotify.Event
|
||||
}
|
||||
|
||||
// Func is used instead haunt func
|
||||
Func func(Context)
|
||||
)
|
||||
|
||||
func NewHaunt() *Haunt {
|
||||
return &Haunt{
|
||||
Sync: make(chan string),
|
||||
}
|
||||
}
|
||||
|
||||
// init check
|
||||
func init() {
|
||||
// custom log
|
||||
log.SetFlags(0)
|
||||
log.SetOutput(LogWriter{})
|
||||
if build.Default.GOPATH == "" {
|
||||
log.Fatal("$GOPATH isn't set properly")
|
||||
}
|
||||
path := filepath.SplitList(build.Default.GOPATH)
|
||||
if err := os.Setenv("GOBIN", filepath.Join(path[len(path)-1], "bin")); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Haunt) SetDefaults() {
|
||||
r.Server = Server{Parent: r, Status: true, Open: false, Port: Port}
|
||||
r.Settings.FileLimit = 0
|
||||
r.Settings.Legacy.Interval = 100 * time.Millisecond
|
||||
r.Settings.Legacy.Force = false
|
||||
r.Settings.Files.Errors = Resource{Name: FileErr, Status: false}
|
||||
r.Settings.Files.Errors = Resource{Name: FileOut, Status: false}
|
||||
r.Settings.Files.Errors = Resource{Name: FileLog, Status: false}
|
||||
if _, err := os.Stat("main.go"); err == nil {
|
||||
log.Println(r.Prefix(Green.Bold("Adding: " + filepath.Base(Wdir()))))
|
||||
r.Schema.Projects = append(r.Schema.Projects, Project{
|
||||
Name: filepath.Base(Wdir()),
|
||||
Path: Wdir(),
|
||||
Tools: Tools{
|
||||
Install: Tool{
|
||||
Status: true,
|
||||
},
|
||||
Run: Tool{
|
||||
Status: true,
|
||||
},
|
||||
},
|
||||
Watcher: Watch{
|
||||
Exts: []string{"go"},
|
||||
Paths: []string{"/"},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
log.Println(r.Prefix(Magenta.Bold("Skipping: " + filepath.Base(Wdir()) + " no main.go file in root")))
|
||||
}
|
||||
subDirs, err := os.ReadDir("cmd")
|
||||
if err != nil {
|
||||
log.Println(r.Prefix("cmd directory not found, skipping"))
|
||||
return
|
||||
}
|
||||
for _, dir := range subDirs {
|
||||
if dir.IsDir() {
|
||||
log.Println(r.Prefix(Green.Bold("Adding: " + dir.Name())))
|
||||
r.Schema.Projects = append(r.Schema.Projects, Project{
|
||||
Name: dir.Name(),
|
||||
Path: "cmd/" + dir.Name(),
|
||||
Tools: Tools{
|
||||
Install: Tool{
|
||||
Status: true,
|
||||
},
|
||||
Run: Tool{
|
||||
Status: true,
|
||||
},
|
||||
},
|
||||
Watcher: Watch{
|
||||
Exts: []string{"go"},
|
||||
Paths: []string{"/"},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
log.Println(r.Prefix(Magenta.Bold("Skipping: " + dir.Name() + " not a directory")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop haunt workflow
|
||||
func (r *Haunt) Stop() error {
|
||||
for k := range r.Schema.Projects {
|
||||
if r.Schema.Projects[k].exit != nil {
|
||||
close(r.Schema.Projects[k].exit)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start haunt workflow
|
||||
func (r *Haunt) Start() error {
|
||||
if len(r.Schema.Projects) > 0 {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(r.Schema.Projects))
|
||||
for k := range r.Schema.Projects {
|
||||
r.Schema.Projects[k].exit = make(chan os.Signal, 1)
|
||||
signal.Notify(r.Schema.Projects[k].exit, os.Interrupt)
|
||||
r.Schema.Projects[k].parent = r
|
||||
go r.Schema.Projects[k].Watch(&wg)
|
||||
}
|
||||
wg.Wait()
|
||||
} else {
|
||||
return errors.New("there are no projects")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prefix a given string with tool name
|
||||
func (r *Haunt) Prefix(input string) string {
|
||||
if len(input) > 0 {
|
||||
return fmt.Sprint(Yellow.Bold("["), strings.ToUpper(RPrefix), Yellow.Bold("]"), ": ", input)
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
// Rewrite the layout of the log timestamp
|
||||
func (w LogWriter) Write(bytes []byte) (int, error) {
|
||||
if len(bytes) > 0 {
|
||||
return fmt.Fprint(Output, Yellow.Regular("["), time.Now().Format("15:04:05"), Yellow.Regular("]"), string(bytes))
|
||||
}
|
||||
return 0, nil
|
||||
}
|
269
src/haunt/notify.go
Normal file
269
src/haunt/notify.go
Normal file
@ -0,0 +1,269 @@
|
||||
package haunt
|
||||
|
||||
// this code is imported from moby, unfortunately i can't import it directly as dependencies from its repo,
|
||||
// cause there was a problem between moby vendor and fsnotify
|
||||
// i have just added only the walk methods and some little changes to polling interval, originally set as static.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
// errPollerClosed is returned when the poller is closed
|
||||
errPollerClosed = errors.New("poller is closed")
|
||||
// errNoSuchWatch is returned when trying to remove a watch that doesn't exist
|
||||
errNoSuchWatch = errors.New("watch does not exist")
|
||||
)
|
||||
|
||||
type (
|
||||
// FileWatcher is an interface for implementing file notification watchers
|
||||
FileWatcher interface {
|
||||
Close() error
|
||||
Add(string) error
|
||||
Walk(string, bool) string
|
||||
Remove(string) error
|
||||
Errors() <-chan error
|
||||
Events() <-chan fsnotify.Event
|
||||
}
|
||||
// fsNotifyWatcher wraps the fsnotify package to satisfy the FileNotifier interface
|
||||
fsNotifyWatcher struct {
|
||||
*fsnotify.Watcher
|
||||
}
|
||||
// filePoller is used to poll files for changes, especially in cases where fsnotify
|
||||
// can't be run (e.g. when inotify handles are exhausted)
|
||||
// filePoller satisfies the FileWatcher interface
|
||||
filePoller struct {
|
||||
// watches is the list of files currently being polled, close the associated channel to stop the watch
|
||||
watches map[string]chan struct{}
|
||||
// events is the channel to listen to for watch events
|
||||
events chan fsnotify.Event
|
||||
// errors is the channel to listen to for watch errors
|
||||
errors chan error
|
||||
// mu locks the poller for modification
|
||||
mu sync.Mutex
|
||||
// closed is used to specify when the poller has already closed
|
||||
closed bool
|
||||
// polling interval
|
||||
interval time.Duration
|
||||
}
|
||||
)
|
||||
|
||||
// PollingWatcher returns a poll-based file watcher
|
||||
func PollingWatcher(interval time.Duration) FileWatcher {
|
||||
if interval == 0 {
|
||||
interval = time.Duration(1) * time.Second
|
||||
}
|
||||
return &filePoller{
|
||||
interval: interval,
|
||||
events: make(chan fsnotify.Event),
|
||||
errors: make(chan error),
|
||||
}
|
||||
}
|
||||
|
||||
// NewFileWatcher tries to use an fs-event watcher, and falls back to the poller if there is an error
|
||||
func NewFileWatcher(l Legacy) (FileWatcher, error) {
|
||||
if !l.Force {
|
||||
if w, err := EventWatcher(); err == nil {
|
||||
return w, nil
|
||||
}
|
||||
}
|
||||
return PollingWatcher(l.Interval), nil
|
||||
}
|
||||
|
||||
// EventWatcher returns an fs-event based file watcher
|
||||
func EventWatcher() (FileWatcher, error) {
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fsNotifyWatcher{Watcher: w}, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Walk fsnotify
|
||||
func (w *fsNotifyWatcher) Walk(path string, init bool) string {
|
||||
if err := w.Add(path); err != nil {
|
||||
return ""
|
||||
}
|
||||
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()
|
||||
if w.closed {
|
||||
w.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
w.closed = true
|
||||
for name := range w.watches {
|
||||
w.remove(name)
|
||||
delete(w.watches, name)
|
||||
}
|
||||
w.mu.Unlock()
|
||||
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
|
||||
// once added the file is polled for changes in a separate goroutine
|
||||
func (w *filePoller) Add(name string) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.closed {
|
||||
return errPollerClosed
|
||||
}
|
||||
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if w.watches == nil {
|
||||
w.watches = make(map[string]chan struct{})
|
||||
}
|
||||
if _, exists := w.watches[name]; exists {
|
||||
return fmt.Errorf("watch exists")
|
||||
}
|
||||
chClose := make(chan struct{})
|
||||
w.watches[name] = chClose
|
||||
go w.watch(f, fi, chClose)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove poller
|
||||
func (w *filePoller) remove(name string) error {
|
||||
if w.closed {
|
||||
return errPollerClosed
|
||||
}
|
||||
|
||||
chClose, exists := w.watches[name]
|
||||
if !exists {
|
||||
return errNoSuchWatch
|
||||
}
|
||||
close(chClose)
|
||||
delete(w.watches, name)
|
||||
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
|
||||
}
|
||||
|
||||
// Walk poller
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// sendEvent publishes the specified event to the events channel
|
||||
func (w *filePoller) sendEvent(e fsnotify.Event, chClose <-chan struct{}) error {
|
||||
select {
|
||||
case w.events <- 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{}) {
|
||||
defer f.Close()
|
||||
for {
|
||||
time.Sleep(w.interval)
|
||||
select {
|
||||
case <-chClose:
|
||||
logrus.Debugf("watch for %s closed", f.Name())
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
fi, err := os.Stat(f.Name())
|
||||
switch {
|
||||
case err != nil && lastFi != nil:
|
||||
// If it doesn't exist at this point, it must have been removed
|
||||
// no need to send the error here since this is a valid operation
|
||||
if os.IsNotExist(err) {
|
||||
if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Remove, Name: f.Name()}, chClose); err != nil {
|
||||
return
|
||||
}
|
||||
lastFi = nil
|
||||
}
|
||||
// at this point, send the error
|
||||
w.sendErr(err, chClose)
|
||||
return
|
||||
case lastFi == nil:
|
||||
if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Create, Name: f.Name()}, chClose); err != nil {
|
||||
return
|
||||
}
|
||||
lastFi = fi
|
||||
case fi.Mode() != lastFi.Mode():
|
||||
if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Chmod, Name: f.Name()}, chClose); err != nil {
|
||||
return
|
||||
}
|
||||
lastFi = fi
|
||||
case fi.ModTime() != lastFi.ModTime() || fi.Size() != lastFi.Size():
|
||||
if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Write, Name: f.Name()}, chClose); err != nil {
|
||||
return
|
||||
}
|
||||
lastFi = fi
|
||||
}
|
||||
}
|
||||
}
|
714
src/haunt/projects.go
Normal file
714
src/haunt/projects.go
Normal file
@ -0,0 +1,714 @@
|
||||
package haunt
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
var (
|
||||
msg string
|
||||
out BufferOut
|
||||
)
|
||||
|
||||
// Watch info
|
||||
type Watch struct {
|
||||
Exts []string `yaml:"extensions" json:"extensions"`
|
||||
Paths []string `yaml:"paths" json:"paths"`
|
||||
Scripts []Command `yaml:"scripts,omitempty" json:"scripts,omitempty"`
|
||||
Hidden bool `yaml:"hidden,omitempty" json:"hidden,omitempty"`
|
||||
Ignore []string `yaml:"ignored_paths,omitempty" json:"ignored_paths,omitempty"`
|
||||
}
|
||||
|
||||
type Ignore struct {
|
||||
Exts []string `yaml:"exts,omitempty" json:"exts,omitempty"`
|
||||
Paths []string `yaml:"paths,omitempty" json:"paths,omitempty"`
|
||||
}
|
||||
|
||||
// Command fields
|
||||
type Command struct {
|
||||
Cmd string `yaml:"command" json:"command"`
|
||||
Type string `yaml:"type" json:"type"`
|
||||
Path string `yaml:"path,omitempty" json:"path,omitempty"`
|
||||
Global bool `yaml:"global,omitempty" json:"global,omitempty"`
|
||||
Output bool `yaml:"output,omitempty" json:"output,omitempty"`
|
||||
}
|
||||
|
||||
// Project info
|
||||
type Project struct {
|
||||
parent *Haunt
|
||||
watcher FileWatcher
|
||||
stop chan bool
|
||||
exit chan os.Signal
|
||||
last last
|
||||
files int64
|
||||
folders int64
|
||||
init bool
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Path string `yaml:"path" json:"path"`
|
||||
Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`
|
||||
Args []string `yaml:"args,omitempty" json:"args,omitempty"`
|
||||
Tools Tools `yaml:"commands" json:"commands"`
|
||||
Watcher Watch `yaml:"watcher" json:"watcher"`
|
||||
Buffer Buffer `yaml:"-" json:"buffer"`
|
||||
ErrPattern string `yaml:"pattern,omitempty" json:"pattern,omitempty"`
|
||||
}
|
||||
|
||||
// Last is used to save info about last file changed
|
||||
type last struct {
|
||||
file string
|
||||
time time.Time
|
||||
}
|
||||
|
||||
// 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 "haunt cli" and "web haunt"
|
||||
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"`
|
||||
}
|
||||
|
||||
// After stop watcher
|
||||
func (p *Project) After() {
|
||||
if p.parent.After != nil {
|
||||
p.parent.After(Context{Project: p})
|
||||
return
|
||||
}
|
||||
p.cmd(nil, "after", true)
|
||||
}
|
||||
|
||||
// Before start watcher
|
||||
func (p *Project) Before() {
|
||||
if p.parent.Before != nil {
|
||||
p.parent.Before(Context{Project: p})
|
||||
return
|
||||
}
|
||||
|
||||
// setup go tools
|
||||
p.Tools.Setup()
|
||||
// global commands before
|
||||
p.cmd(p.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.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, "")
|
||||
}
|
||||
|
||||
// Err occurred
|
||||
func (p *Project) Err(err error) {
|
||||
if p.parent.Err != nil {
|
||||
p.parent.Err(Context{Project: p})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
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, "")
|
||||
}
|
||||
}
|
||||
|
||||
// Change event message
|
||||
func (p *Project) Change(event fsnotify.Event) {
|
||||
if p.parent.Change != nil {
|
||||
p.parent.Change(Context{Project: p, Event: event})
|
||||
return
|
||||
}
|
||||
// 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}
|
||||
p.stamp("log", out, msg, "")
|
||||
}
|
||||
|
||||
// Reload launches the toolchain run, build, install
|
||||
func (p *Project) Reload(path string, stop <-chan bool) {
|
||||
if p.parent.Reload != nil {
|
||||
p.parent.Reload(Context{Project: p, Watcher: p.watcher, Path: path, Stop: stop})
|
||||
return
|
||||
}
|
||||
var done bool
|
||||
var install, build Response
|
||||
go func() {
|
||||
for {
|
||||
<-stop
|
||||
done = true
|
||||
return
|
||||
}
|
||||
}()
|
||||
if done {
|
||||
return
|
||||
}
|
||||
// before command
|
||||
p.cmd(stop, "before", false)
|
||||
if done {
|
||||
return
|
||||
}
|
||||
// Go supported tools
|
||||
if len(path) > 0 {
|
||||
fi, err := os.Stat(path)
|
||||
if filepath.Ext(path) == "" {
|
||||
fi, err = os.Stat(path)
|
||||
}
|
||||
if err != nil {
|
||||
p.Err(err)
|
||||
}
|
||||
p.tools(stop, path, fi)
|
||||
}
|
||||
// Prevent fake events on polling startup
|
||||
p.init = true
|
||||
// prevent errors using haunt without config with only run flag
|
||||
if p.Tools.Run.Status && !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.print(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.print(start, p)
|
||||
}
|
||||
if done {
|
||||
return
|
||||
}
|
||||
if install.Err == nil && build.Err == nil && p.Tools.Run.Status {
|
||||
result := make(chan Response)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
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..")
|
||||
err := p.run(p.Path, result, stop)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintln(p.pname(p.Name, 2), ":", Red.Regular(err))
|
||||
out := BufferOut{Time: time.Now(), Text: err.Error(), Type: "Go Run"}
|
||||
p.stamp("error", out, msg, "")
|
||||
}
|
||||
}()
|
||||
}
|
||||
if done {
|
||||
return
|
||||
}
|
||||
p.cmd(stop, "after", false)
|
||||
}
|
||||
|
||||
// Watch a project
|
||||
func (p *Project) Watch(wg *sync.WaitGroup) {
|
||||
var err error
|
||||
// change channel
|
||||
p.stop = make(chan bool)
|
||||
// init a new watcher
|
||||
p.watcher, err = NewFileWatcher(p.parent.Settings.Legacy)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
close(p.stop)
|
||||
p.watcher.Close()
|
||||
}()
|
||||
// before start checks
|
||||
p.Before()
|
||||
// start watcher
|
||||
go p.Reload("", p.stop)
|
||||
L:
|
||||
for {
|
||||
select {
|
||||
case event := <-p.watcher.Events():
|
||||
if p.parent.Settings.Recovery.Events {
|
||||
log.Println("File:", event.Name, "LastFile:", p.last.file, "Time:", time.Now(), "LastTime:", p.last.time)
|
||||
}
|
||||
if time.Now().Truncate(time.Second).After(p.last.time) {
|
||||
// switch event type
|
||||
switch event.Op {
|
||||
case fsnotify.Chmod:
|
||||
case fsnotify.Remove:
|
||||
p.watcher.Remove(event.Name)
|
||||
if p.Validate(event.Name, false) && ext(event.Name) != "" {
|
||||
// stop and restart
|
||||
close(p.stop)
|
||||
p.stop = make(chan bool)
|
||||
p.Change(event)
|
||||
go p.Reload("", p.stop)
|
||||
}
|
||||
default:
|
||||
if p.Validate(event.Name, true) {
|
||||
fi, err := os.Stat(event.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if fi.IsDir() {
|
||||
filepath.Walk(event.Name, p.walk)
|
||||
} else {
|
||||
// stop and restart
|
||||
close(p.stop)
|
||||
p.stop = make(chan bool)
|
||||
p.Change(event)
|
||||
go p.Reload(event.Name, p.stop)
|
||||
p.last.time = time.Now().Truncate(time.Second)
|
||||
p.last.file = event.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case err := <-p.watcher.Errors():
|
||||
p.Err(err)
|
||||
case <-p.exit:
|
||||
p.After()
|
||||
break L
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
// Validate a file path
|
||||
func (p *Project) Validate(path string, fcheck bool) bool {
|
||||
if len(path) == 0 {
|
||||
return false
|
||||
}
|
||||
// check if skip hidden
|
||||
if p.Watcher.Hidden && isHidden(path) {
|
||||
return false
|
||||
}
|
||||
// check for a valid ext or path
|
||||
if e := ext(path); e != "" {
|
||||
if len(p.Watcher.Exts) == 0 {
|
||||
return false
|
||||
}
|
||||
// check ignored
|
||||
for _, v := range p.Watcher.Ignore {
|
||||
if v == e {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// supported extensions
|
||||
for index, v := range p.Watcher.Exts {
|
||||
if e == v {
|
||||
break
|
||||
}
|
||||
if index == len(p.Watcher.Exts)-1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.shouldIgnore(path) {
|
||||
return false
|
||||
}
|
||||
// file check
|
||||
if fcheck {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil || fi.Mode()&os.ModeSymlink != 0 || !fi.IsDir() && ext(path) == "" || fi.Size() <= 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 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("]")
|
||||
case 2:
|
||||
name = Yellow.Regular("[") + Red.Bold(strings.ToUpper(name)) + Yellow.Regular("]")
|
||||
case 3:
|
||||
name = Yellow.Regular("[") + Blue.Bold(strings.ToUpper(name)) + Yellow.Regular("]")
|
||||
case 4:
|
||||
name = Yellow.Regular("[") + Magenta.Bold(strings.ToUpper(name)) + Yellow.Regular("]")
|
||||
case 5:
|
||||
name = Yellow.Regular("[") + Green.Bold(strings.ToUpper(name)) + Yellow.Regular("]")
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// Tool logs the result of a go command
|
||||
func (p *Project) tools(stop <-chan bool, path string, fi os.FileInfo) {
|
||||
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)
|
||||
tool.parent = p
|
||||
if tool.Status && tool.isTool {
|
||||
if fi.IsDir() {
|
||||
if tool.dir {
|
||||
result <- tool.Exec(path, stop)
|
||||
}
|
||||
} else if !tool.dir {
|
||||
result <- tool.Exec(path, stop)
|
||||
}
|
||||
}
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-stop:
|
||||
return
|
||||
case r := <-result:
|
||||
if r.Err != nil {
|
||||
if fi.IsDir() {
|
||||
path, _ = filepath.Abs(fi.Name())
|
||||
}
|
||||
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("\""))
|
||||
if r.Err != nil {
|
||||
out = BufferOut{Time: time.Now(), Text: r.Err.Error(), Type: flag}
|
||||
p.stamp("error", out, msg, fmt.Sprint(Red.Regular(r.Err.Error())))
|
||||
} else {
|
||||
out = BufferOut{Time: time.Now(), Text: r.Out, Type: flag}
|
||||
p.stamp("log", out, msg, fmt.Sprint(r.Out))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch the files tree of a project
|
||||
func (p *Project) walk(path string, info os.FileInfo, err error) error {
|
||||
if p.shouldIgnore(path) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if p.Validate(path, true) {
|
||||
result := p.watcher.Walk(path, p.init)
|
||||
if result != "" {
|
||||
if p.parent.Settings.Recovery.Index {
|
||||
log.Println("Indexing", path)
|
||||
}
|
||||
p.tools(p.stop, path, info)
|
||||
if info.IsDir() {
|
||||
// tools dir
|
||||
p.folders++
|
||||
} else {
|
||||
// tools files
|
||||
p.files++
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Project) shouldIgnore(path string) bool {
|
||||
separator := string(os.PathSeparator)
|
||||
// supported paths
|
||||
for _, v := range p.Watcher.Ignore {
|
||||
s := append([]string{p.Path}, strings.Split(v, separator)...)
|
||||
abs, _ := filepath.Abs(filepath.Join(s...))
|
||||
if path == abs || strings.HasPrefix(path, abs+separator) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Print on files, cli, ws
|
||||
func (p *Project) stamp(t string, o BufferOut, msg string, stream string) {
|
||||
ctime := time.Now()
|
||||
content := []string{ctime.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.Fprintln(Output, stream)
|
||||
}
|
||||
go func() {
|
||||
p.parent.Sync <- "sync"
|
||||
}()
|
||||
}
|
||||
|
||||
func (p Project) buildEnvs() (envs []string) {
|
||||
for k, v := range p.Env {
|
||||
envs = append(envs, fmt.Sprintf("%s=%s", strings.Replace(k, "=", "", -1), v))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Run a project
|
||||
func (p *Project) run(path string, stream chan Response, stop <-chan bool) (err error) {
|
||||
var args []string
|
||||
var build *exec.Cmd
|
||||
var r Response
|
||||
defer func() {
|
||||
// https://github.com/golang/go/issues/5615
|
||||
// https://github.com/golang/go/issues/6720
|
||||
if build != nil {
|
||||
build.Process.Signal(os.Interrupt)
|
||||
build.Process.Wait()
|
||||
}
|
||||
}()
|
||||
|
||||
// custom error pattern
|
||||
isErrorText := func(string) bool {
|
||||
return false
|
||||
}
|
||||
errRegexp, err := regexp.Compile(p.ErrPattern)
|
||||
if err != nil {
|
||||
r.Err = err
|
||||
stream <- r
|
||||
} else {
|
||||
isErrorText = errRegexp.MatchString
|
||||
}
|
||||
|
||||
// add additional arguments
|
||||
for _, arg := range p.Args {
|
||||
a := strings.FieldsFunc(arg, func(i rune) bool {
|
||||
return i == '"' || i == '=' || i == '\''
|
||||
})
|
||||
args = append(args, a...)
|
||||
}
|
||||
dirPath := os.Getenv("GOBIN")
|
||||
if p.Tools.Run.Path != "" {
|
||||
dirPath, _ = filepath.Abs(p.Tools.Run.Path)
|
||||
}
|
||||
name := filepath.Base(path)
|
||||
if path == "." && p.Tools.Run.Path == "" {
|
||||
name = filepath.Base(Wdir())
|
||||
} else if p.Tools.Run.Path != "" {
|
||||
name = filepath.Base(dirPath)
|
||||
}
|
||||
path = filepath.Join(dirPath, name)
|
||||
if p.Tools.Run.Method != "" {
|
||||
path = p.Tools.Run.Method
|
||||
}
|
||||
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 {
|
||||
return errors.New("project not found")
|
||||
}
|
||||
}
|
||||
appendEnvs := p.buildEnvs()
|
||||
if len(appendEnvs) > 0 {
|
||||
build.Env = append(build.Env, appendEnvs...)
|
||||
}
|
||||
// scan project stream
|
||||
stdout, err := build.StdoutPipe()
|
||||
stderr, err := build.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if p.Tools.Run.Dir != "" {
|
||||
build.Dir = p.Tools.Run.Dir
|
||||
}
|
||||
if err := build.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
stream <- r
|
||||
r.Err = nil
|
||||
} else {
|
||||
r.Out = text
|
||||
stream <- 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print with time after
|
||||
func (r *Response) print(start time.Time, p *Project) {
|
||||
if r.Err != nil {
|
||||
msg = fmt.Sprintln(p.pname(p.Name, 2), ":", Red.Bold(r.Name), "\n", 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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() + stdout.String())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
87
src/haunt/schema.go
Normal file
87
src/haunt/schema.go
Normal file
@ -0,0 +1,87 @@
|
||||
package haunt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"github.com/abs3ntdev/haunt/src/config"
|
||||
)
|
||||
|
||||
// Schema projects list
|
||||
type Schema struct {
|
||||
Projects []Project `yaml:"schema" json:"schema"`
|
||||
}
|
||||
|
||||
// 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(flags config.Flags) Project {
|
||||
project := Project{
|
||||
Name: flags.Name,
|
||||
Path: flags.Path,
|
||||
Tools: Tools{
|
||||
Vet: Tool{
|
||||
Status: flags.Vet,
|
||||
},
|
||||
Fmt: Tool{
|
||||
Status: flags.Format,
|
||||
},
|
||||
Test: Tool{
|
||||
Status: flags.Test,
|
||||
},
|
||||
Generate: Tool{
|
||||
Status: flags.Generate,
|
||||
},
|
||||
Build: Tool{
|
||||
Status: flags.Build,
|
||||
},
|
||||
Install: Tool{
|
||||
Status: flags.Install,
|
||||
},
|
||||
Run: Tool{
|
||||
Status: flags.Run,
|
||||
},
|
||||
},
|
||||
Watcher: Watch{
|
||||
Paths: []string{"/"},
|
||||
Ignore: []string{".git", ".haunt", "vendor"},
|
||||
Exts: []string{"go"},
|
||||
},
|
||||
}
|
||||
return project
|
||||
}
|
||||
|
||||
// Filter project list by field
|
||||
func (s *Schema) Filter(field string, value interface{}) []Project {
|
||||
result := []Project{}
|
||||
for _, item := range s.Projects {
|
||||
v := reflect.ValueOf(item)
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if v.Type().Field(i).Name == field {
|
||||
if reflect.DeepEqual(v.Field(i).Interface(), value) {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
182
src/haunt/server.go
Normal file
182
src/haunt/server.go
Normal file
@ -0,0 +1,182 @@
|
||||
//go:generate go-bindata -pkg=haunt -o=bindata.go assets/...
|
||||
|
||||
package haunt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/middleware"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
// Dafault host and port
|
||||
const (
|
||||
Host = "localhost"
|
||||
Port = 5002
|
||||
)
|
||||
|
||||
// Server settings
|
||||
type Server struct {
|
||||
Parent *Haunt `yaml:"-" json:"-"`
|
||||
Status bool `yaml:"status" json:"status"`
|
||||
Open bool `yaml:"open" json:"open"`
|
||||
Port int `yaml:"port" json:"port"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
}
|
||||
|
||||
// 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 {
|
||||
<-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.Write(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)
|
||||
case 2:
|
||||
rs.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJavaScriptCharsetUTF8)
|
||||
case 3:
|
||||
rs.Header().Set(echo.HeaderContentType, "text/css")
|
||||
case 4:
|
||||
rs.Header().Set(echo.HeaderContentType, "image/svg+xml")
|
||||
case 5:
|
||||
rs.Header().Set(echo.HeaderContentType, "image/png")
|
||||
}
|
||||
rs.WriteHeader(http.StatusOK)
|
||||
rs.Write(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Set(status bool, open bool, port int, host string) {
|
||||
s.Open = open
|
||||
s.Port = port
|
||||
s.Host = host
|
||||
s.Status = status
|
||||
}
|
||||
|
||||
// Start the web server
|
||||
func (s *Server) Start() (err error) {
|
||||
if s.Status {
|
||||
e := echo.New()
|
||||
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
|
||||
Level: 2,
|
||||
}))
|
||||
e.Use(middleware.Recover())
|
||||
|
||||
// web panel
|
||||
e.GET("/", func(c echo.Context) error {
|
||||
return s.render(c, "assets/index.html", 1)
|
||||
})
|
||||
e.GET("/assets/js/all.min.js", func(c echo.Context) error {
|
||||
return s.render(c, "assets/assets/js/all.min.js", 2)
|
||||
})
|
||||
e.GET("/assets/css/app.css", func(c echo.Context) error {
|
||||
return s.render(c, "assets/assets/css/app.css", 3)
|
||||
})
|
||||
e.GET("/app/components/settings/index.html", func(c echo.Context) error {
|
||||
return s.render(c, "assets/app/components/settings/index.html", 1)
|
||||
})
|
||||
e.GET("/app/components/project/index.html", func(c echo.Context) error {
|
||||
return s.render(c, "assets/app/components/project/index.html", 1)
|
||||
})
|
||||
e.GET("/app/components/index.html", func(c echo.Context) error {
|
||||
return s.render(c, "assets/app/components/index.html", 1)
|
||||
})
|
||||
e.GET("/assets/img/logo.png", func(c echo.Context) error {
|
||||
return s.render(c, "assets/assets/img/logo.png", 5)
|
||||
})
|
||||
e.GET("/assets/img/svg/github-logo.svg", func(c echo.Context) error {
|
||||
return s.render(c, "assets/assets/img/svg/github-logo.svg", 4)
|
||||
})
|
||||
e.GET("/assets/img/svg/ic_arrow_back_black_48px.svg", func(c echo.Context) error {
|
||||
return s.render(c, "assets/assets/img/svg/ic_arrow_back_black_48px.svg", 4)
|
||||
})
|
||||
e.GET("/assets/img/svg/ic_clear_white_48px.svg", func(c echo.Context) error {
|
||||
return s.render(c, "assets/assets/img/svg/ic_clear_white_48px.svg", 4)
|
||||
})
|
||||
e.GET("/assets/img/svg/ic_menu_white_48px.svg", func(c echo.Context) error {
|
||||
return s.render(c, "assets/assets/img/svg/ic_menu_white_48px.svg", 4)
|
||||
})
|
||||
e.GET("/assets/img/svg/ic_settings_black_48px.svg", func(c echo.Context) error {
|
||||
return s.render(c, "assets/assets/img/svg/ic_settings_black_48px.svg", 4)
|
||||
})
|
||||
|
||||
// websocket
|
||||
e.GET("/ws", s.projects)
|
||||
e.HideBanner = true
|
||||
e.Debug = false
|
||||
go func() {
|
||||
log.Println(s.Parent.Prefix("Started on " + string(s.Host) + ":" + strconv.Itoa(s.Port)))
|
||||
e.Start(string(s.Host) + ":" + strconv.Itoa(s.Port))
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OpenURL in a new tab of default browser
|
||||
func (s *Server) OpenURL() error {
|
||||
url := "http://" + string(s.Parent.Server.Host) + ":" + strconv.Itoa(s.Parent.Server.Port)
|
||||
stderr := bytes.Buffer{}
|
||||
cmd := map[string]string{
|
||||
"windows": "start",
|
||||
"darwin": "open",
|
||||
"linux": "xdg-open",
|
||||
}
|
||||
if s.Open {
|
||||
open, err := cmd[runtime.GOOS]
|
||||
if !err {
|
||||
return fmt.Errorf("operating system %q is not supported", runtime.GOOS)
|
||||
}
|
||||
cmd := exec.Command(open, url)
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return errors.New(stderr.String())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
124
src/haunt/settings.go
Normal file
124
src/haunt/settings.go
Normal file
@ -0,0 +1,124 @@
|
||||
package haunt
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// settings const
|
||||
const (
|
||||
Permission = 0o775
|
||||
File = ".haunt.yaml"
|
||||
FileOut = ".r.outputs.log"
|
||||
FileErr = ".r.errors.log"
|
||||
FileLog = ".r.logs.log"
|
||||
)
|
||||
|
||||
// Settings defines a group of general settings and options
|
||||
type Settings struct {
|
||||
Files `yaml:"files,omitempty" json:"files,omitempty"`
|
||||
FileLimit int32 `yaml:"flimit,omitempty" json:"flimit,omitempty"`
|
||||
Legacy Legacy `yaml:"legacy" json:"legacy"`
|
||||
Recovery Recovery `yaml:"recovery,omitempty" json:"recovery,omitempty"`
|
||||
}
|
||||
|
||||
type Recovery struct {
|
||||
Index bool
|
||||
Events bool
|
||||
Tools bool
|
||||
}
|
||||
|
||||
// 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 haunt
|
||||
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
|
||||
Path string
|
||||
Name string
|
||||
}
|
||||
|
||||
// Set legacy watcher with an interval
|
||||
func (l *Legacy) Set(status bool, interval int) {
|
||||
l.Force = true
|
||||
l.Interval = time.Duration(interval) * time.Second
|
||||
}
|
||||
|
||||
// Remove haunt folder
|
||||
func (s *Settings) Remove(d string) error {
|
||||
_, err := os.Stat(d)
|
||||
if !os.IsNotExist(err) {
|
||||
return os.RemoveAll(d)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Read config file
|
||||
func (s *Settings) Read(out interface{}) error {
|
||||
// backward compatibility
|
||||
if _, err := os.Stat(RFile); err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := s.Stream(RFile)
|
||||
if err == nil {
|
||||
err = yaml.Unmarshal(content, out)
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Write config file
|
||||
func (s *Settings) Write(out interface{}) error {
|
||||
y, err := yaml.Marshal(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Fatal(ioutil.WriteFile(RFile, y, Permission))
|
||||
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.Fatal(err)
|
||||
return content, err
|
||||
}
|
||||
|
||||
// Fatal prints a fatal error with its additional messages
|
||||
func (s Settings) Fatal(err error, msg ...interface{}) {
|
||||
if err != nil {
|
||||
if len(msg) > 0 {
|
||||
log.Fatalln(Red.Regular(msg...), err.Error())
|
||||
} else {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new file and return its pointer
|
||||
func (s Settings) Create(path string, name string) *os.File {
|
||||
file := filepath.Join(path, name)
|
||||
out, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE|os.O_SYNC, Permission)
|
||||
s.Fatal(err)
|
||||
return out
|
||||
}
|
15
src/haunt/settings_unix.go
Normal file
15
src/haunt/settings_unix.go
Normal file
@ -0,0 +1,15 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package haunt
|
||||
|
||||
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)
|
||||
|
||||
return syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
}
|
9
src/haunt/settings_windows.go
Normal file
9
src/haunt/settings_windows.go
Normal file
@ -0,0 +1,9 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package haunt
|
||||
|
||||
// Flimit defines the max number of watched files
|
||||
func (s *Settings) Flimit() error {
|
||||
return nil
|
||||
}
|
33
src/haunt/style.go
Normal file
33
src/haunt/style.go
Normal file
@ -0,0 +1,33 @@
|
||||
package haunt
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
var (
|
||||
// Output writer
|
||||
Output = color.Output
|
||||
// Red color
|
||||
Red = colorBase(color.FgHiRed)
|
||||
// Blue color
|
||||
Blue = colorBase(color.FgHiBlue)
|
||||
// Green color
|
||||
Green = colorBase(color.FgHiGreen)
|
||||
// Yellow color
|
||||
Yellow = colorBase(color.FgHiYellow)
|
||||
// Magenta color
|
||||
Magenta = colorBase(color.FgHiMagenta)
|
||||
)
|
||||
|
||||
// ColorBase type
|
||||
type colorBase color.Attribute
|
||||
|
||||
// Regular font with a color
|
||||
func (c colorBase) Regular(a ...interface{}) string {
|
||||
return color.New(color.Attribute(c)).Sprint(a...)
|
||||
}
|
||||
|
||||
// Bold font with a color
|
||||
func (c colorBase) Bold(a ...interface{}) string {
|
||||
return color.New(color.Attribute(c), color.Bold).Sprint(a...)
|
||||
}
|
198
src/haunt/tools.go
Normal file
198
src/haunt/tools.go
Normal file
@ -0,0 +1,198 @@
|
||||
package haunt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Tool info
|
||||
type Tool struct {
|
||||
Args []string `yaml:"args,omitempty" json:"args,omitempty"`
|
||||
Method string `yaml:"method,omitempty" json:"method,omitempty"`
|
||||
Path string `yaml:"path,omitempty" json:"path,omitempty"`
|
||||
Dir string `yaml:"dir,omitempty" json:"dir,omitempty"` // wdir of the command
|
||||
Status bool `yaml:"status,omitempty" json:"status,omitempty"`
|
||||
Output bool `yaml:"output,omitempty" json:"output,omitempty"`
|
||||
dir bool
|
||||
isTool bool
|
||||
cmd []string
|
||||
name string
|
||||
parent *Project
|
||||
}
|
||||
|
||||
// Tools go
|
||||
type Tools struct {
|
||||
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 Tool `yaml:"run,omitempty" json:"run,omitempty"`
|
||||
}
|
||||
|
||||
// Setup go tools
|
||||
func (t *Tools) Setup() {
|
||||
gocmd := "go"
|
||||
|
||||
// go clean
|
||||
if t.Clean.Status {
|
||||
t.Clean.name = "Clean"
|
||||
t.Clean.isTool = true
|
||||
t.Clean.cmd = replace([]string{gocmd, "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{gocmd, "generate"}, t.Generate.Method)
|
||||
t.Generate.Args = split([]string{}, t.Generate.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{gocmd, "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{gocmd, "test"}, t.Test.Method)
|
||||
t.Test.Args = split([]string{}, t.Test.Args)
|
||||
}
|
||||
// go install
|
||||
t.Install.name = "Install"
|
||||
t.Install.cmd = replace([]string{gocmd, "install"}, t.Install.Method)
|
||||
fmt.Println(t.Install.cmd)
|
||||
t.Install.Args = split([]string{}, t.Install.Args)
|
||||
// go build
|
||||
if t.Build.Status {
|
||||
t.Build.name = "Build"
|
||||
t.Build.cmd = replace([]string{gocmd, "build"}, t.Build.Method)
|
||||
t.Build.Args = split([]string{}, t.Build.Args)
|
||||
}
|
||||
}
|
||||
|
||||
// Exec a go tool
|
||||
func (t *Tool) Exec(path string, stop <-chan bool) (response Response) {
|
||||
if t.dir {
|
||||
if filepath.Ext(path) != "" {
|
||||
path = filepath.Dir(path)
|
||||
}
|
||||
// check if there is at least one go file
|
||||
matched := false
|
||||
files, _ := ioutil.ReadDir(path)
|
||||
for _, f := range files {
|
||||
matched, _ = filepath.Match("*.go", f.Name())
|
||||
if matched {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return
|
||||
}
|
||||
} else if !strings.HasSuffix(path, ".go") {
|
||||
return
|
||||
}
|
||||
args := t.Args
|
||||
if strings.HasSuffix(path, ".go") {
|
||||
args = append(args, path)
|
||||
path = filepath.Dir(path)
|
||||
}
|
||||
if s := ext(path); s == "" || s == "go" {
|
||||
if t.parent.parent.Settings.Recovery.Tools {
|
||||
log.Println("Tool:", t.name, path, args)
|
||||
}
|
||||
var out, stderr bytes.Buffer
|
||||
done := make(chan error)
|
||||
args = append(t.cmd, args...)
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
if t.Dir != "" {
|
||||
cmd.Dir, _ = filepath.Abs(t.Dir)
|
||||
} else {
|
||||
cmd.Dir = path
|
||||
}
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &stderr
|
||||
// Start command
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
response.Name = t.name
|
||||
response.Err = err
|
||||
return
|
||||
}
|
||||
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() + err.Error())
|
||||
} else {
|
||||
if t.Output {
|
||||
response.Out = out.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Compile is used for build and install
|
||||
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:]...)
|
||||
if t.Dir != "" {
|
||||
cmd.Dir, _ = filepath.Abs(t.Dir)
|
||||
} else {
|
||||
cmd.Dir = path
|
||||
}
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &stderr
|
||||
// Start command
|
||||
cmd.Start()
|
||||
go func() { done <- cmd.Wait() }()
|
||||
// Wait a result
|
||||
response.Name = t.name
|
||||
select {
|
||||
case <-stop:
|
||||
// Stop running command
|
||||
cmd.Process.Kill()
|
||||
case err := <-done:
|
||||
// Command completed
|
||||
if err != nil {
|
||||
response.Err = errors.New(stderr.String() + err.Error())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
50
src/haunt/utils.go
Normal file
50
src/haunt/utils.go
Normal file
@ -0,0 +1,50 @@
|
||||
package haunt
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Split each arguments in multiple fields
|
||||
func split(args, fields []string) []string {
|
||||
for _, arg := range fields {
|
||||
arr := strings.Fields(arg)
|
||||
args = append(args, arr...)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// Get file extensions
|
||||
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 index := strings.LastIndex(ext, "."); index > 0 {
|
||||
ext = ext[index:]
|
||||
}
|
||||
}
|
||||
}
|
||||
if ext != "" {
|
||||
return ext[1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Replace if isn't empty and create a new array
|
||||
func replace(a []string, b string) []string {
|
||||
if len(b) > 0 {
|
||||
return strings.Fields(b)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Wdir return current working directory
|
||||
func Wdir() string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
return dir
|
||||
}
|
17
src/haunt/utils_unix.go
Normal file
17
src/haunt/utils_unix.go
Normal file
@ -0,0 +1,17 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package haunt
|
||||
|
||||
import "strings"
|
||||
|
||||
// isHidden check if a file or a path is hidden
|
||||
func isHidden(path string) bool {
|
||||
arr := strings.Split(path[len(Wdir()):], "/")
|
||||
for _, elm := range arr {
|
||||
if strings.HasPrefix(elm, ".") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
19
src/haunt/utils_windows.go
Normal file
19
src/haunt/utils_windows.go
Normal file
@ -0,0 +1,19 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package haunt
|
||||
|
||||
import "syscall"
|
||||
|
||||
// isHidden check if a file or a path is hidden
|
||||
func isHidden(path string) bool {
|
||||
p, e := syscall.UTF16PtrFromString(path)
|
||||
if e != nil {
|
||||
return false
|
||||
}
|
||||
attrs, e := syscall.GetFileAttributes(p)
|
||||
if e != nil {
|
||||
return false
|
||||
}
|
||||
return attrs&syscall.FILE_ATTRIBUTE_HIDDEN != 0
|
||||
}
|
Loading…
Reference in New Issue
Block a user