├── .github
└── workflows
│ ├── ci.yaml
│ └── release.yaml
├── Makefile
├── README.md
├── auto
└── auto.go
├── cmd
└── goemon
│ ├── main.go
│ ├── public
│ ├── c.yml
│ ├── go.yml
│ ├── md.yml
│ ├── pom.yml
│ ├── ruby.yml
│ ├── rust.yml
│ └── web.yml
│ └── statik
│ └── statik.go
├── command.go
├── data
└── goemon.png
├── go.mod
├── go.sum
├── goemon.go
├── goemon_test.go
├── livereload.go
├── proc_posix.go
└── proc_windows.go
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | name: Test
8 | runs-on: ${{ matrix.os }}
9 | strategy:
10 | matrix:
11 | os: [ubuntu-latest, macos-latest, windows-latest]
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@master
15 | - name: Setup Go
16 | uses: actions/setup-go@v2
17 | with:
18 | go-version: 1.x
19 | - name: Test
20 | run: make test
21 | - name: Lint
22 | run: make lint
23 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | release:
10 | name: Release
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@master
15 | - name: Setup Go
16 | uses: actions/setup-go@v2
17 | with:
18 | go-version: 1.x
19 | - name: Cross build
20 | run: make cross
21 | - name: Create Release
22 | id: create_release
23 | uses: actions/create-release@master
24 | env:
25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 | with:
27 | tag_name: ${{ github.ref }}
28 | release_name: Release ${{ github.ref }}
29 | - name: Upload
30 | run: make upload
31 | env:
32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BIN := goemon
2 | VERSION := $$(make -s show-version)
3 | VERSION_PATH := cmd/$(BIN)
4 | CURRENT_REVISION := $(shell git rev-parse --short HEAD)
5 | BUILD_LDFLAGS := "-s -w -X main.revision=$(CURRENT_REVISION)"
6 | GOBIN ?= $(shell go env GOPATH)/bin
7 | export GO111MODULE=on
8 |
9 | .PHONY: all
10 | all: clean build
11 |
12 | .PHONY: build
13 | build:
14 | go build -ldflags=$(BUILD_LDFLAGS) -o $(BIN) ./cmd/$(BIN)
15 |
16 | .PHONY: install
17 | install:
18 | go install -ldflags=$(BUILD_LDFLAGS) ./...
19 |
20 | .PHONY: show-version
21 | show-version: $(GOBIN)/gobump
22 | @gobump show -r $(VERSION_PATH)
23 |
24 | $(GOBIN)/gobump:
25 | @cd && go get github.com/x-motemen/gobump/cmd/gobump
26 |
27 | .PHONY: cross
28 | cross: $(GOBIN)/goxz
29 | goxz -n $(BIN) -pv=v$(VERSION) -build-ldflags=$(BUILD_LDFLAGS) ./cmd/$(BIN)
30 |
31 | $(GOBIN)/goxz:
32 | cd && go get github.com/Songmu/goxz/cmd/goxz
33 |
34 | .PHONY: test
35 | test: build
36 | go test -v ./...
37 |
38 | .PHONY: lint
39 | lint: $(GOBIN)/golint
40 | go vet ./...
41 | golint -set_exit_status ./...
42 |
43 | $(GOBIN)/golint:
44 | cd && go get golang.org/x/lint/golint
45 |
46 | .PHONY: clean
47 | clean:
48 | rm -rf $(BIN) goxz
49 | go clean
50 |
51 | .PHONY: bump
52 | bump: $(GOBIN)/gobump
53 | ifneq ($(shell git status --porcelain),)
54 | $(error git workspace is dirty)
55 | endif
56 | ifneq ($(shell git rev-parse --abbrev-ref HEAD),master)
57 | $(error current branch is not master)
58 | endif
59 | @gobump up -w "$(VERSION_PATH)"
60 | git commit -am "bump up version to $(VERSION)"
61 | git tag "v$(VERSION)"
62 | git push origin master
63 | git push origin "refs/tags/v$(VERSION)"
64 |
65 | .PHONY: upload
66 | upload: $(GOBIN)/ghr
67 | ghr "v$(VERSION)" goxz
68 |
69 | $(GOBIN)/ghr:
70 | cd && go get github.com/tcnksm/ghr
71 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # goemon
2 |
3 | 
4 |
5 | **Go** **E**xtensible **Mon**itoring
6 |
7 | Speed up your development.
8 | If you updated js files, the page should be reloaded. If you updated go files, the app should be recompiled, and should be restarted. And also, the page should be reloaded
9 |
10 | ## Expected directory structure
11 |
12 | ```
13 | +---assets
14 | | +- index.html
15 | | +- app.css
16 | | +- app.js
17 | +- main.go
18 | ```
19 |
20 | ## Usage
21 |
22 | For example:
23 |
24 | ### Run web server
25 | ```
26 | $ goemon -g > goemon.yml
27 | $ goemon go run main.go
28 | ```
29 |
30 | ### Writing markdown
31 | ```
32 | $ goemon -g md > goemon.yml
33 | $ goemon --
34 | ```
35 |
36 | ### Writing C code
37 | ```
38 | $ goemon -g c > goemon.yml
39 | $ goemon --
40 | ```
41 |
42 | ## Default configuration
43 |
44 | ```yaml
45 | # Generated by goemon -g
46 | livereload: :35730
47 | tasks:
48 | - match: './assets/*.js'
49 | commands:
50 | - minifyjs -m -i ${GOEMON_TARGET_FILE} > ${GOEMON_TARGET_DIR}/${GOEMON_TARGET_NAME}.min.js
51 | - :livereload /
52 | - match: './assets/*.css'
53 | commands:
54 | - :livereload /
55 | - match: './assets/*.html'
56 | commands:
57 | - :livereload /
58 | - match: '*.go'
59 | commands:
60 | - go build
61 | - :restart
62 | - :livereload /
63 | ```
64 |
65 | * `match` is wildcard. You can use `./foo/bar/**/*.js` like a shell.
66 | * `commands` is list of commands to run. `:XXX` is internal command.
67 |
68 | | Internal Command | Behavior |
69 | |-------------------|---------------------------------|
70 | | :livereload /path | reload `path` |
71 | | :minify | minify js/css(work in progress) |
72 | | :restart | restart app |
73 | | :sleep 3000 | sleep 3000ms |
74 | | :fizzbuzz 100 | do fizzbuzz(1 to 100) |
75 | | :event :Foo | fire event :Foo |
76 |
77 | `:event :Foo` fire event defined `- match: :Foo`.
78 |
79 | Currently, `:minify` is work in progress. So you should run `minifyjs` command to do it.
80 | For example, configuration in above works as below.
81 |
82 | | Pattern | Behavior |
83 | |------------------|---------------------------------|
84 | | ./assets/\*.css | reload page |
85 | | ./assets/\*.js | minify js/css, reload page |
86 | | ./assets/\*.html | reload page |
87 | | ./assets/\*.go | build, restart app, reload page |
88 |
89 | ## LiveReload
90 |
91 | You can use livereload feature.
92 |
93 | ```html
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | Your App
102 |
103 |
104 |
105 |
106 |
107 | ```
108 |
109 | ## Use goemon as library
110 |
111 | ```
112 | cat > goemon.go
113 | package main
114 |
115 | import (
116 | _ "github.com/mattn/goemon/auto"
117 | )
118 | ^D
119 | ```
120 |
121 | Then `go build`. You don't need to use `goemon` command.
122 |
123 |
124 | ## Installation
125 |
126 | ```
127 | $ go get github.com/mattn/goemon/cmd/goemon
128 | ```
129 | If you want to minify js, install minifyjs like below.
130 |
131 | ```
132 | $ npm install -g minifyjs
133 | ```
134 |
135 | ## License
136 |
137 | MIT
138 |
139 | ## Author
140 |
141 | Yasuhiro Matsumoto (a.k.a mattn)
142 |
--------------------------------------------------------------------------------
/auto/auto.go:
--------------------------------------------------------------------------------
1 | package auto
2 |
3 | import (
4 | "github.com/mattn/goemon"
5 | )
6 |
7 | func init() {
8 | goemon.Run()
9 | }
10 |
--------------------------------------------------------------------------------
/cmd/goemon/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | //go:generate go get github.com/rakyll/statik
4 | //go:generate statik
5 |
6 | import (
7 | "fmt"
8 | "io/ioutil"
9 | "log"
10 | "net/http"
11 | "os"
12 | "runtime"
13 | "sort"
14 |
15 | "github.com/mattn/goemon"
16 | _ "github.com/mattn/goemon/cmd/goemon/statik"
17 | "github.com/rakyll/statik/fs"
18 | )
19 |
20 | const (
21 | name = "goemon"
22 | version = "0.0.3"
23 | revision = "HEAD"
24 | )
25 |
26 | func usage() {
27 | fmt.Printf("Usage of %s [options] [command] [args...]\n", os.Args[0])
28 | fmt.Println(" goemon -g [NAME] : generate default configuration")
29 | fmt.Println(" goemon -c [FILE] ... : set configuration file")
30 | fmt.Println("")
31 | fmt.Println("* Examples:")
32 | fmt.Println(" Generate default configuration:")
33 | fmt.Println(" goemon -g > goemon.yml")
34 | fmt.Println("")
35 | fmt.Println(" Generate C configuration:")
36 | fmt.Println(" goemon -g c > goemon.yml")
37 | fmt.Println("")
38 | fmt.Println(" List default configurations:")
39 | fmt.Println(" goemon -g ?")
40 | fmt.Println("")
41 | fmt.Println(" Start standalone server:")
42 | fmt.Println(" goemon --")
43 | fmt.Println(" Start web server:")
44 | fmt.Println(" goemon -a :5000")
45 | os.Exit(1)
46 | }
47 |
48 | var hfs http.FileSystem
49 |
50 | func init() {
51 | var err error
52 | hfs, err = fs.New()
53 | if err != nil {
54 | log.Fatal(err)
55 | }
56 | }
57 |
58 | func asset(name string) ([]byte, error) {
59 | f, err := hfs.Open("/" + name)
60 | if err != nil {
61 | return nil, err
62 | }
63 | defer f.Close()
64 | return ioutil.ReadAll(f)
65 | }
66 |
67 | func names() []string {
68 | dir, err := hfs.Open("/")
69 | if err != nil {
70 | log.Fatal(err)
71 | }
72 | defer dir.Close()
73 | fss, err := dir.Readdir(-1)
74 | if err != nil {
75 | log.Fatal(err)
76 | }
77 | var files []string
78 | for _, fsi := range fss {
79 | files = append(files, fsi.Name())
80 | }
81 | return files
82 | }
83 |
84 | func main() {
85 | file := ""
86 | args := []string{}
87 | addr := ""
88 |
89 | switch len(os.Args) {
90 | case 1:
91 | usage()
92 | default:
93 | switch os.Args[1] {
94 | case "-h":
95 | usage()
96 | case "-g":
97 | if len(os.Args) == 2 {
98 | b, _ := asset("web.yml")
99 | fmt.Print(string(string(b)))
100 | } else if os.Args[2] == "?" {
101 | keys := names()
102 | sort.Strings(keys)
103 | for _, k := range keys {
104 | fmt.Println(k[:len(k)-4])
105 | }
106 | } else if t, err := asset(os.Args[2] + ".yml"); err == nil {
107 | fmt.Print(string(t))
108 | } else {
109 | usage()
110 | }
111 | return
112 | case "-a":
113 | if len(os.Args) == 2 {
114 | usage()
115 | return
116 | }
117 | addr = os.Args[2]
118 | args = os.Args[3:]
119 | case "-c":
120 | if len(os.Args) == 2 {
121 | usage()
122 | return
123 | }
124 | file = os.Args[2]
125 | args = os.Args[3:]
126 | case "--":
127 | args = os.Args[2:]
128 | case "-v":
129 | fmt.Printf("%s %s (rev: %s/%s)\n", name, version, revision, runtime.Version())
130 | os.Exit(1)
131 | default:
132 | args = os.Args[1:]
133 | }
134 | }
135 |
136 | g := goemon.NewWithArgs(args)
137 | if file != "" {
138 | g.File = file
139 | }
140 | g.Run()
141 | if len(args) == 0 {
142 | if addr != "" {
143 | http.Handle("/", http.FileServer(http.Dir(".")))
144 | http.ListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
145 | g.Logger.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL)
146 | http.DefaultServeMux.ServeHTTP(w, r)
147 | }))
148 | } else {
149 | select {}
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/cmd/goemon/public/c.yml:
--------------------------------------------------------------------------------
1 | # Generated by goemon -g
2 | tasks:
3 | - match: '**/*.c|**/*.cpp|**/*.cxx|Makefile'
4 | commands:
5 | - make
6 |
--------------------------------------------------------------------------------
/cmd/goemon/public/go.yml:
--------------------------------------------------------------------------------
1 | # Generated by goemon -g
2 | tasks:
3 | - match: '**/*.go|**/*.c|**/*.cpp|**/*.cxx'
4 | commands:
5 | - go build
6 |
--------------------------------------------------------------------------------
/cmd/goemon/public/md.yml:
--------------------------------------------------------------------------------
1 | # Generated by goemon -g
2 | tasks:
3 | - match: '*.md'
4 | commands:
5 | - pandoc -f markdown -t html -o ${GOEMON_TARGET_DIR}/${GOEMON_TARGET_NAME}.html ${GOEMON_TARGET_FILE}
6 |
--------------------------------------------------------------------------------
/cmd/goemon/public/pom.yml:
--------------------------------------------------------------------------------
1 | # Generated by goemon -g
2 | tasks:
3 | - match: '**/*.java'
4 | commands:
5 | - mvn clean
6 | - mvn compile
7 | - :restart
8 |
--------------------------------------------------------------------------------
/cmd/goemon/public/ruby.yml:
--------------------------------------------------------------------------------
1 | # Generated by goemon -g
2 | livereload: :35730
3 | tasks:
4 | - match: './assets/*.js'
5 | commands:
6 | - minifyjs -m -i ${GOEMON_TARGET_FILE} > ${GOEMON_TARGET_DIR}/${GOEMON_TARGET_NAME}.min.js
7 | - :livereload /
8 | - match: './assets/*.css'
9 | commands:
10 | - :livereload /
11 | - match: './assets/*.html'
12 | commands:
13 | - :livereload /
14 | - match: '**/*.rb'
15 | commands:
16 | - go build
17 | - :restart
18 | - :livereload /
19 |
20 |
--------------------------------------------------------------------------------
/cmd/goemon/public/rust.yml:
--------------------------------------------------------------------------------
1 | # Generated by goemon -g
2 | tasks:
3 | - match: '**/*.rs'
4 | commands:
5 | - cargo build
6 |
--------------------------------------------------------------------------------
/cmd/goemon/public/web.yml:
--------------------------------------------------------------------------------
1 | # Generated by goemon -g
2 | livereload: :35730
3 | tasks:
4 | - match: './assets/*.js'
5 | commands:
6 | - minifyjs -m -i ${GOEMON_TARGET_FILE} > ${GOEMON_TARGET_DIR}/${GOEMON_TARGET_NAME}.min.js
7 | - :livereload /
8 | - match: './assets/*.css'
9 | commands:
10 | - :livereload /
11 | - match: './assets/*.html'
12 | commands:
13 | - :livereload /
14 | - match: '**/*.go'
15 | commands:
16 | - go build
17 | - :restart
18 | - :livereload /
19 |
--------------------------------------------------------------------------------
/cmd/goemon/statik/statik.go:
--------------------------------------------------------------------------------
1 | // Code generated by statik. DO NOT EDIT.
2 |
3 | package statik
4 |
5 | import (
6 | "github.com/rakyll/statik/fs"
7 | )
8 |
9 | func init() {
10 | data := "PK\x03\x04\x14\x00\x08\x00\x08\x00\xe095P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00 \x00c.ymlUT\x05\x00\x01\xf5\xa4&^RVpO\xcdK-J,IMQH\xaaTH\xcfO\xcd\xcd\xcfS\xd0M\xe7*I,\xce.\xb6\xe2\xd2U\xc8M,I\xce\xb0RP\xd7\xd2\xd2\xd7\xd2K\xae\x81P\x05\x05PFEE\x8dobvjZfN\xaa:\x97\x82Br~nnb^J\xb1\x15\x97\x82\x02Hkv*\x17 \x00\x00\xff\xffPK\x07\x08\xb6\x04\xd1\xf8\\\x00\x00\x00b\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\xe695P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00go.ymlUT\x05\x00\x01\x00\xa5&^,\xc6A\n\x82!\x10\x06\xd0\xfd\x9c\xe2\x83\x16?\x08\xd6\xde\x0bt\x8eQ\x87)j\x1cI\x03\x83\x0e\x1f\x84\xab\xf7N\xb8J\x93\x17O\xa9\xc8\x1f\xa8\x8byCT\x9a<\x1e#Q\x84\xf1,\xb7\x84#\x84K8\xab\x7f\xff\x96M\xef;k\x1d\x04\x147\xe3VG\" B\x1d\xf9}\x7fV\xfa\x05\x00\x00\xff\xffPK\x07\x08\xea/\xe2\xa0[\x00\x00\x00e\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\xad5(P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00md.ymlUT\x05\x00\x01\x87z\x15^\\\xcb\xc1\n\x82@\x10\x87\xf1\xfb>\xc5\x1f\n\x84`\xec\xeeMh\x13!\x15\xc4\xbbL\xee\xa6`\xb3\x13\xba\x10\x11\xbe{\xd0\xd1\xeb\xf7\xf1;\xa0\xf0\xc1/\x1c\xbd\xc3\xfd\x83Q\xbdh\x00\x8d&\xf2:\xaf\x99!\x08\xc7a\xca\x90\x9cRq\x89\x01\x06\x15\xe1\xe0\xd6\xcc\x00\x84\x17\x07\xa7\x03\xe8\x01\xe1ev\xfa\x0e\xa0\x88)\xca\x13\xa48~\x8b\xc6VM\xddwy[\xd8\xae\xbf\x94\xedv\xde\xc7:\xaf\xec\x96\xfe\xc9~]\xcb\x9b\xdd\xcc/\x00\x00\xff\xffPK\x07\x08\xc1\x9c\xc3\x06\x84\x00\x00\x00\xa4\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\xe995P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00 \x00pom.ymlUT\x05\x00\x01\x06\xa5&^D\xc7M\n\x021\x0c\x05\xe0}N\xf1\xc0\xc5@\xa1\xba\xef\x05<\xc7\xb3\x13\xc6\x9fI*M\x18\xf0\xf6\x82\x1b\x97\xdf Wu\x9dL]q\xfb`\x1bj\xc3Q7I\xc6+\x9aT\x18\xb3\xdf\x1b\x96R.\xe5\xfc\xe4\xc1E\x80>\xcc\xe8k4\x01*\xecp\xf4]\xe9\x7f\x0d{?v\xfd\xb9M\x8d\xe4L\xf9\x06\x00\x00\xff\xffPK\x07\x08\x87\x19\n\xba`\x00\x00\x00l\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\xf195P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00 \x00ruby.ymlUT\x05\x00\x01\x16\xa5&^\x8c\xceAK\xc30\x18\xc6\xf1{>\xc5\x03\n\x83B\x12a\x88\x90\x830\xb0\x96\x81\xdb`\xec>\xd26v\xa9y\x13\xc8\x1b\x85!\xfd\xee\x82\x1e\x84\xad\x87]\x1f\xf8=\xfc\xef\xd0\xb8\xe8\xb2-\xaeG{\xc6\x90\x1c\xa5\x089\x88\xe0\xbf\\v!\xd9\xde\xc0,\x1f\x9f\x96\x0f\xa2X\xfe`#$\xc8\x96\xeed\xb0P\xda2\xbb\xc2\xbaR#/\x04\xd0%\"\x1b{6\x02\x90 \x1f\xfd\xfbydH\x82\xf4\xb8\xffnv\xf5f\xb7=\x1eV\xfb\xa6>\x1c_\xd7o\xf5\x84\xe7\xab\xfde\xbd\x9f\xf4\xe5\xb8]m\xeaI\x91\x8fj\xe4\xdfs\xf3\x1f\x08=\xdf\xd4\xf1L\xd4\x0d\xeeT(\xdc\x0e\xabJW*\xb7\xd7`Hh?}\xe8\xfftv\\l.3W\xe2'\x00\x00\xff\xffPK\x07\x08\x1a\x00\xb7O\xc5\x00\x00\x00\x84\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\xe295P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00 \x00rust.ymlUT\x05\x00\x01\xf8\xa4&^\x04\xc0\xc1 \xc5 \x0c\x06\xe0{\xa6\xf8\xe1\x1d\x04\xc1\xd7\xbb\x0bt\x8e\xa8\xc1\x966\x06\x8c=t\xfb~?\xec2d\xf2\x92\x86\xf2\xa2\x9b\xa8\x0d\xa4N\x8b\xfd\xf2L \xca\xab\x1e\x19!\xc6-\xfe\xa7\x07\x02\xaa\xa9\xf2h\x9e H\xa8<\xbb\xa1<\xe7\xdd\xe8\x0b\x00\x00\xff\xffPK\x07\x08\x1fH\x05\xd6Q\x00\x00\x00O\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\xf595P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00 \x00web.ymlUT\x05\x00\x01\x1e\xa5&^\x8c\xceAK\xc30\x18\xc6\xf1{>\xc5\x03\n\x83B\x12a\x88\x90\x830\xb0\x96\x81\xdb`\xec>\xb2\xf6\xb5K\xed\x9b@\xdf(\x0c\xe9w\x17\xf4 l=\xf4\xfa\xc0\xef\xe1\x7f\x87\x8a\"\x0d>S\x83\xd3\x05m\"N\x11\xbaU}\xf8\xa2\x81\xfa\xe4\x1b\x07\xb7||Z>\xa8\xec\xe5C\x9c\xd2`\x9f\xeb\xb3\xc3\xc2X/BYla:Y(\xa0N\xcc>6\xe2\x14\xa0\xc1!\x86\xf7K'\xd0\x0c\x1dp\xff]\xed\xca\xcdn{<\xac\xf6Uy8\xbe\xae\xdf\xca\x11\xcf7\xfb\xcbz?\xda\xebq\xbb\xda\x94\xa3\xe1\x10M'\xbf\xe7\xee?\x10v\xba\xa9\x96\x89\xa8\x19\xee\x9c\xb9\x9f\x0f\x8b\xc2\x16\xa6M\xb7\xa0M8}\x86\xbe\xf9\xd3\x03I\xf6C\x9e\xb8\xfa \x00\x00\xff\xffPK\x07\x08\xfbgK\xc0\xc5\x00\x00\x00\x83\x01\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xe095P\xb6\x04\xd1\xf8\\\x00\x00\x00b\x00\x00\x00\x05\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x81\x00\x00\x00\x00c.ymlUT\x05\x00\x01\xf5\xa4&^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xe695P\xea/\xe2\xa0[\x00\x00\x00e\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x81\x98\x00\x00\x00go.ymlUT\x05\x00\x01\x00\xa5&^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xad5(P\xc1\x9c\xc3\x06\x84\x00\x00\x00\xa4\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x810\x01\x00\x00md.ymlUT\x05\x00\x01\x87z\x15^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xe995P\x87\x19\n\xba`\x00\x00\x00l\x00\x00\x00\x07\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x81\xf1\x01\x00\x00pom.ymlUT\x05\x00\x01\x06\xa5&^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xf195P\x1a\x00\xb7O\xc5\x00\x00\x00\x84\x01\x00\x00\x08\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x81\x8f\x02\x00\x00ruby.ymlUT\x05\x00\x01\x16\xa5&^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xe295P\x1fH\x05\xd6Q\x00\x00\x00O\x00\x00\x00\x08\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x81\x93\x03\x00\x00rust.ymlUT\x05\x00\x01\xf8\xa4&^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xf595P\xfbgK\xc0\xc5\x00\x00\x00\x83\x01\x00\x00\x07\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x81#\x04\x00\x00web.ymlUT\x05\x00\x01\x1e\xa5&^PK\x05\x06\x00\x00\x00\x00\x07\x00\x07\x00\xb0\x01\x00\x00&\x05\x00\x00\x00\x00"
11 | fs.Register(data)
12 | }
13 |
--------------------------------------------------------------------------------
/command.go:
--------------------------------------------------------------------------------
1 | package goemon
2 |
3 | import (
4 | "io/ioutil"
5 | "net"
6 | "net/http"
7 | "os"
8 | "os/exec"
9 | "path/filepath"
10 | "runtime"
11 | "strconv"
12 | "strings"
13 | "time"
14 |
15 | "github.com/fsnotify/fsnotify"
16 | "github.com/omeid/jsmin"
17 | "github.com/omeid/livereload"
18 | "github.com/tdewolff/minify"
19 | "github.com/tdewolff/minify/css"
20 | )
21 |
22 | func (g *Goemon) internalCommand(command, file string) bool {
23 | ss := commandRe.FindStringSubmatch(command)
24 | switch ss[1] {
25 | case ":livereload":
26 | for _, s := range ss[2:] {
27 | g.Logger.Println("reloading", s)
28 | g.lrs.Reload(s, true)
29 | }
30 | return true
31 | case ":sleep":
32 | for _, s := range ss[2:] {
33 | si, err := strconv.ParseInt(s, 10, 64)
34 | if err != nil {
35 | g.Logger.Println("failed to parse argument for :sleep command:", err)
36 | return false
37 | }
38 | g.Logger.Println("sleeping", s+"ms")
39 | time.Sleep(time.Duration(si) * time.Microsecond)
40 | }
41 | return true
42 | case ":fizzbuzz":
43 | for _, s := range ss[2:] {
44 | si, err := strconv.ParseInt(s, 10, 64)
45 | if err != nil {
46 | g.Logger.Println("failed to parse argument for :fizzbuzz command:", err)
47 | return false
48 | }
49 | for i := int64(1); i <= si; i++ {
50 | switch {
51 | case i%15 == 0:
52 | g.Logger.Println("FizzBuzz")
53 | case i%3 == 0:
54 | g.Logger.Println("Fizz")
55 | case i%5 == 0:
56 | g.Logger.Println("Buzz")
57 | default:
58 | g.Logger.Println(i)
59 | }
60 | }
61 | }
62 | return true
63 | case ":minify":
64 | return g.minify(file)
65 | case ":restart!":
66 | return g.terminate(os.Kill) == nil
67 | case ":restart":
68 | return g.terminate(os.Interrupt) == nil
69 | case ":event":
70 | for _, s := range ss[2:] {
71 | g.Logger.Println("fire", s)
72 | g.task(fsnotify.Event{Name: s, Op: fsnotify.Write})
73 | }
74 | }
75 | return false
76 | }
77 |
78 | func (g *Goemon) externalCommand(command, file string) bool {
79 | var cmd *exec.Cmd
80 | command = os.Expand(command, func(s string) string {
81 | switch s {
82 | case "GOEMON_TARGET_FILE":
83 | return file
84 | case "GOEMON_TARGET_BASE":
85 | return filepath.Base(file)
86 | case "GOEMON_TARGET_DIR":
87 | return filepath.ToSlash(filepath.Dir(file))
88 | case "GOEMON_TARGET_EXT":
89 | return filepath.Ext(file)
90 | case "GOEMON_TARGET_NAME":
91 | fn := filepath.Base(file)
92 | ext := filepath.Ext(file)
93 | return fn[:len(fn)-len(ext)]
94 | }
95 | return os.Getenv(s)
96 | })
97 | if runtime.GOOS == "windows" {
98 | cmd = exec.Command("cmd", "/c", command)
99 | } else {
100 | cmd = exec.Command("sh", "-c", command)
101 | }
102 | g.Logger.Println("executing", command)
103 | cmd.Stdin = os.Stdin
104 | cmd.Stdout = os.Stdout
105 | cmd.Stderr = os.Stderr
106 | err := cmd.Run()
107 | if err != nil {
108 | g.Logger.Println(err)
109 | return false
110 | }
111 | return true
112 | }
113 |
114 | func (g *Goemon) minify(name string) bool {
115 | if strings.HasSuffix(filepath.Base(name), ".min.") {
116 | return true // ignore
117 | }
118 | ext := filepath.Ext(name)
119 | if ext == "" {
120 | return true // ignore
121 | }
122 | in, err := os.Open(name)
123 | if err != nil {
124 | g.Logger.Println(err)
125 | return false
126 | }
127 | defer in.Close()
128 |
129 | switch ext {
130 | case ".js":
131 |
132 | buf, err := jsmin.Minify(in)
133 | if err != nil {
134 | g.Logger.Println(err)
135 | return false
136 | }
137 | err = ioutil.WriteFile(name[:len(name)-len(ext)]+".min.js", buf.Bytes(), 0644)
138 | if err != nil {
139 | g.Logger.Println(err)
140 | return false
141 | }
142 | return true
143 | case ".css":
144 | out, err := os.Create(name[:len(name)-len(ext)] + ".min.css")
145 | if err != nil {
146 | g.Logger.Println(err)
147 | return false
148 | }
149 | m := minify.New()
150 | m.AddFunc("text/css", css.Minify)
151 | if err := m.Minify("text/css", out, in); err != nil {
152 | g.Logger.Println(err)
153 | return false
154 | }
155 | return true
156 | }
157 | return false
158 | }
159 |
160 | func (g *Goemon) livereload() error {
161 | g.lrs = livereload.New("goemon")
162 | defer g.lrs.Close()
163 | addr := g.conf.LiveReload
164 | if addr == "" {
165 | addr = os.Getenv("GOEMON_LIVERELOAD_ADDR")
166 | }
167 | if addr == "" {
168 | addr = ":35730"
169 | }
170 | var err error
171 | g.lrc, err = net.Listen("tcp", addr)
172 | if err != nil {
173 | return err
174 | }
175 | defer g.lrc.Close()
176 | mux := http.NewServeMux()
177 | mux.HandleFunc("/livereload.js", func(w http.ResponseWriter, r *http.Request) {
178 | w.Header().Set("Content-Type", "application/javascript")
179 | _, err := w.Write([]byte(liveReloadScript))
180 | if err != nil {
181 | g.lrc.Close()
182 | }
183 | })
184 | mux.Handle("/livereload", g.lrs)
185 | return http.Serve(g.lrc, mux)
186 | }
187 |
--------------------------------------------------------------------------------
/data/goemon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattn/goemon/20d0f2b0e34a9aee1502dcf03bd9fdea48216d78/data/goemon.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mattn/goemon
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/fsnotify/fsnotify v1.4.7
7 | github.com/gorilla/websocket v1.4.1 // indirect
8 | github.com/omeid/jsmin v0.0.0-20150224091327-9678cd8e78f2
9 | github.com/omeid/livereload v0.0.0-20180903043807-18d58b752b26
10 | github.com/rakyll/statik v0.1.7
11 | github.com/tdewolff/minify v2.3.6+incompatible
12 | github.com/tdewolff/parse v2.3.4+incompatible // indirect
13 | github.com/tdewolff/test v1.0.6 // indirect
14 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect
15 | gopkg.in/yaml.v2 v2.2.8
16 | )
17 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
2 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
3 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
4 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
5 | github.com/omeid/jsmin v0.0.0-20150224091327-9678cd8e78f2 h1:W6npz/Y8HkE9mWjok4pCdKRhbx5RqL30u3HojKi5Bvk=
6 | github.com/omeid/jsmin v0.0.0-20150224091327-9678cd8e78f2/go.mod h1:BmdVUPRTTyyXAt5kB3Q9y21b9eS1RrZyjOi84NFHHMw=
7 | github.com/omeid/livereload v0.0.0-20180903043807-18d58b752b26 h1:UgrpxortNaAijXWp2dVezOb/2lVNtGlaI/y6SAosQds=
8 | github.com/omeid/livereload v0.0.0-20180903043807-18d58b752b26/go.mod h1:zwZKGxj+J+XPXOcKyE1ByX0oRLb+iWZwy4wO7W9LHTM=
9 | github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
10 | github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
11 | github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo=
12 | github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs=
13 | github.com/tdewolff/parse v2.3.4+incompatible h1:x05/cnGwIMf4ceLuDMBOdQ1qGniMoxpP46ghf0Qzh38=
14 | github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ=
15 | github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
16 | github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
17 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
18 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
21 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
22 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
23 |
--------------------------------------------------------------------------------
/goemon.go:
--------------------------------------------------------------------------------
1 | package goemon
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "net"
9 | "os"
10 | "os/exec"
11 | "os/signal"
12 | "path/filepath"
13 | "regexp"
14 | "runtime"
15 | "strings"
16 | "sync"
17 | "sync/atomic"
18 | "time"
19 |
20 | "github.com/fsnotify/fsnotify"
21 | "github.com/omeid/livereload"
22 | "gopkg.in/yaml.v2"
23 | )
24 |
25 | const logFlag = log.Ldate | log.Ltime | log.Lshortfile
26 |
27 | var commandRe = regexp.MustCompile(`^\s*(:[a-z]+!?)(?:\s+(\S+))*$`)
28 |
29 | // Goemon is structure of this application
30 | type Goemon struct {
31 | tasks uint64
32 |
33 | File string
34 | Logger *log.Logger
35 | Args []string
36 | lrc net.Listener
37 | lrs *livereload.Server
38 | fsw *fsnotify.Watcher
39 | cmd *exec.Cmd
40 | conf conf
41 | }
42 |
43 | type task struct {
44 | Match string `yaml:"match"`
45 | Ignore string `yaml:"ignore"`
46 | Commands []string `yaml:"commands"`
47 | Ops []string `yaml:"ops"`
48 | mre *regexp.Regexp
49 | ire *regexp.Regexp
50 | mops uint32
51 | hit bool
52 | mutex sync.Mutex
53 | }
54 |
55 | type conf struct {
56 | Command string
57 | LiveReload string `yaml:"livereload"`
58 | Tasks []*task `yaml:"tasks"`
59 | }
60 |
61 | // New create new instance of goemon
62 | func New() *Goemon {
63 | return &Goemon{
64 | File: "goemon.yml",
65 | Logger: log.New(os.Stderr, "GOEMON ", logFlag),
66 | }
67 | }
68 |
69 | // NewWithArgs create new instance of goemon with specified arguments by args
70 | func NewWithArgs(args []string) *Goemon {
71 | g := New()
72 | g.Args = args
73 | return g
74 | }
75 |
76 | // Run start goemon server
77 | func Run() *Goemon {
78 | return New().Run()
79 | }
80 |
81 | func compilePattern(pattern string) (*regexp.Regexp, error) {
82 | if pattern[0] == '%' {
83 | return regexp.Compile(pattern[1:])
84 | }
85 |
86 | var buf bytes.Buffer
87 |
88 | for n, pat := range strings.Split(pattern, "|") {
89 | if n == 0 {
90 | buf.WriteString("^")
91 | } else {
92 | buf.WriteString("$|")
93 | }
94 | if fs, err := filepath.Abs(pat); err == nil {
95 | pat = filepath.ToSlash(fs)
96 | }
97 | rs := []rune(pat)
98 | for i := 0; i < len(rs); i++ {
99 | if rs[i] == '/' {
100 | if runtime.GOOS == "windows" {
101 | buf.WriteString(`[/\\]`)
102 | } else {
103 | buf.WriteRune(rs[i])
104 | }
105 | } else if rs[i] == '*' {
106 | if i < len(rs)-1 && rs[i+1] == '*' {
107 | i++
108 | if i < len(rs)-1 && rs[i+1] == '/' {
109 | i++
110 | buf.WriteString(`.*`)
111 | } else {
112 | return nil, fmt.Errorf("invalid wildcard: %s", pattern)
113 | }
114 | } else {
115 | buf.WriteString(`[^/]+`)
116 | }
117 | } else if rs[i] == '?' {
118 | buf.WriteString(`\S`)
119 | } else {
120 | buf.WriteString(fmt.Sprintf(`[\x%x]`, rs[i]))
121 | }
122 | }
123 | buf.WriteString("$")
124 | }
125 |
126 | return regexp.Compile(buf.String())
127 | }
128 |
129 | func (g *Goemon) restart() error {
130 | if len(g.Args) == 0 {
131 | return nil
132 | }
133 | g.terminate(nil)
134 | return g.spawn()
135 | }
136 |
137 | func (t *task) match(file string) bool {
138 | return (t.mre != nil && t.mre.MatchString(file)) && (t.ire == nil || !t.ire.MatchString(file))
139 | }
140 |
141 | func (t *task) matchOp(op fsnotify.Op) bool {
142 | if t.mops == 0 {
143 | return true
144 | }
145 | return uint32(op)&t.mops == uint32(op)
146 | }
147 |
148 | func (g *Goemon) task(event fsnotify.Event) {
149 | file := filepath.ToSlash(event.Name)
150 | for _, t := range g.conf.Tasks {
151 | if strings.HasPrefix(event.Name, ":") {
152 | if t.Match != file {
153 | continue
154 | }
155 | } else {
156 | if !t.match(file) {
157 | continue
158 | }
159 | }
160 | if !t.matchOp(event.Op) {
161 | continue
162 | }
163 | t.mutex.Lock()
164 | if t.hit {
165 | t.mutex.Unlock()
166 | continue
167 | }
168 | t.hit = true
169 | t.mutex.Unlock()
170 | g.Logger.Println(event)
171 | go func(name string, t *task) {
172 | atomic.AddUint64(&g.tasks, 1)
173 |
174 | loopCommand:
175 | for _, command := range t.Commands {
176 | switch {
177 | case commandRe.MatchString(command):
178 | if !g.internalCommand(command, file) {
179 | break loopCommand
180 | }
181 | default:
182 | if !g.externalCommand(command, file) {
183 | break loopCommand
184 | }
185 | }
186 | }
187 | t.mutex.Lock()
188 | t.hit = false
189 | t.mutex.Unlock()
190 | atomic.AddUint64(&g.tasks, ^uint64(0))
191 | }(event.Name, t)
192 | }
193 | }
194 |
195 | func (g *Goemon) watch() error {
196 | var err error
197 | g.fsw, err = fsnotify.NewWatcher()
198 | if err != nil {
199 | return err
200 | }
201 | g.fsw.Add(g.File)
202 |
203 | root, err := filepath.Abs(".")
204 | if err != nil {
205 | g.Logger.Println(err)
206 | }
207 |
208 | dup := map[string]bool{}
209 | g.fsw.Add(root)
210 | dup[root] = true
211 |
212 | err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
213 | if info == nil {
214 | return err
215 | }
216 | if info.IsDir() {
217 | return nil
218 | }
219 | dir := filepath.Dir(path)
220 | if _, ok := dup[dir]; !ok {
221 | for _, t := range g.conf.Tasks {
222 | if t.match(path) {
223 | g.fsw.Add(dir)
224 | dup[dir] = true
225 | break
226 | }
227 | }
228 | }
229 | return nil
230 | })
231 | if err != nil {
232 | g.Logger.Println(err)
233 | }
234 |
235 | g.Logger.Println("goemon loaded", g.File)
236 |
237 | for {
238 | select {
239 | case event := <-g.fsw.Events:
240 | if event.Name == g.File {
241 | return nil
242 | }
243 | g.task(event)
244 | case err := <-g.fsw.Errors:
245 | if err != nil {
246 | g.Logger.Println("error:", err)
247 | }
248 | }
249 | }
250 | }
251 |
252 | func (g *Goemon) load() error {
253 | g.conf.Tasks = []*task{}
254 | fn, err := filepath.Abs(g.File)
255 | if err != nil {
256 | return err
257 | }
258 | g.File = fn
259 | var b []byte
260 | for i := 0; i < 3; i++ {
261 | b, err = ioutil.ReadFile(fn)
262 | if err == nil {
263 | break
264 | }
265 | time.Sleep(100 * time.Millisecond)
266 | }
267 | if err != nil {
268 | return err
269 | }
270 | err = yaml.Unmarshal(b, &g.conf)
271 | if err != nil {
272 | return err
273 | }
274 | if len(g.Args) == 0 && g.conf.Command != "" {
275 | if runtime.GOOS == "windows" {
276 | g.Args = []string{"cmd", "/c", g.conf.Command}
277 | } else {
278 | g.Args = []string{"sh", "-c", g.conf.Command}
279 | }
280 | }
281 | for _, t := range g.conf.Tasks {
282 | if t.Match == "" {
283 | continue
284 | }
285 | t.mre, err = compilePattern(t.Match)
286 | if err != nil {
287 | g.Logger.Println(err)
288 | continue
289 | }
290 | if t.Ignore != "" {
291 | t.ire, err = compilePattern(t.Ignore)
292 | if err != nil {
293 | g.Logger.Println(err)
294 | }
295 | } else {
296 | t.ire = nil
297 | }
298 | for _, op := range t.Ops {
299 | switch strings.ToUpper(op) {
300 | case fsnotify.Create.String():
301 | t.mops = t.mops | uint32(fsnotify.Create)
302 | case fsnotify.Write.String():
303 | t.mops = t.mops | uint32(fsnotify.Write)
304 | case fsnotify.Remove.String():
305 | t.mops = t.mops | uint32(fsnotify.Remove)
306 | case fsnotify.Rename.String():
307 | t.mops = t.mops | uint32(fsnotify.Rename)
308 | case fsnotify.Chmod.String():
309 | t.mops = t.mops | uint32(fsnotify.Chmod)
310 | default:
311 | g.Logger.Printf("unknow operation %v", op)
312 | }
313 | }
314 | }
315 | return nil
316 | }
317 |
318 | // Run start tasks
319 | func (g *Goemon) Run() *Goemon {
320 | err := g.load()
321 | if err != nil {
322 | g.Logger.Println(err)
323 | }
324 |
325 | go func() {
326 | g.Logger.Println("loading", g.File)
327 | for {
328 | err := g.watch()
329 | if err != nil {
330 | g.Logger.Println(err)
331 | time.Sleep(time.Second)
332 | }
333 | g.Logger.Println("reloading", g.File)
334 | err = g.load()
335 | if err != nil {
336 | g.Logger.Println(err)
337 | time.Sleep(time.Second)
338 | }
339 | }
340 | }()
341 |
342 | go func() {
343 | g.Logger.Println("starting livereload")
344 | for {
345 | err := g.livereload()
346 | if err != nil {
347 | g.Logger.Println(err)
348 | time.Sleep(time.Second)
349 | }
350 | g.Logger.Println("restarting livereload")
351 | }
352 | }()
353 |
354 | if len(g.Args) > 0 {
355 | g.Logger.Println("starting command", g.Args)
356 | sig := make(chan os.Signal, 1)
357 | signal.Notify(sig, os.Interrupt)
358 | errChan := make(chan error, 1)
359 | for {
360 | if atomic.LoadUint64(&g.tasks) > 0 {
361 | time.Sleep(time.Second)
362 | continue
363 | }
364 | go func() {
365 | err := g.restart()
366 | errChan <- err
367 | }()
368 | select {
369 | case err := <-errChan:
370 | if err != nil {
371 | g.Logger.Println(err)
372 | time.Sleep(time.Second)
373 | }
374 | g.Logger.Println("restarting command")
375 | case <-sig:
376 | g.terminate(nil)
377 | os.Exit(0)
378 | }
379 | }
380 | }
381 | return g
382 | }
383 |
384 | // Terminate stop goemon server
385 | func (g *Goemon) Terminate() {
386 | if g.lrc != nil {
387 | g.lrc.Close()
388 | }
389 | if g.fsw != nil {
390 | g.fsw.Close()
391 | }
392 | if g.cmd.Process != nil {
393 | g.terminate(nil)
394 | }
395 | g.Logger.Println("goemon terminated")
396 | }
397 |
--------------------------------------------------------------------------------
/goemon_test.go:
--------------------------------------------------------------------------------
1 | package goemon
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "path/filepath"
7 | "runtime"
8 | "testing"
9 |
10 | "github.com/fsnotify/fsnotify"
11 | )
12 |
13 | func TestCompilePattern(t *testing.T) {
14 |
15 | tests := []struct {
16 | re string
17 | path string
18 | }{
19 | {`/path/**/*.txt`, `/path/to/file.txt`},
20 | }
21 | for _, test := range tests {
22 | if runtime.GOOS == "windows" {
23 | if p, err := filepath.Abs(test.path); err == nil {
24 | test.path = p
25 | }
26 | }
27 | re, err := compilePattern(test.re)
28 | if err != nil {
29 | t.Fatal(err)
30 | }
31 | if !re.MatchString(test.path) {
32 | t.Fatalf("%v should match as %v: %v", test.re, test.path, re.String())
33 | }
34 | }
35 | }
36 |
37 | func TestJsmin(t *testing.T) {
38 | dir, err := ioutil.TempDir(os.TempDir(), "goemon")
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 | defer os.RemoveAll(dir)
43 |
44 | g := New()
45 | f := filepath.Join(dir, "foo.js")
46 | if g.minify(f) {
47 | t.Fatal("Should not be succeeded")
48 | }
49 | _, err = os.Stat(filepath.Join(dir, "foo.min.js"))
50 | if err == nil {
51 | t.Fatalf("Should be fail for non-exists file: %v", err)
52 | }
53 | if !os.IsNotExist(err) {
54 | t.Fatalf("Should be fail for non-exists file: %v", err)
55 | }
56 | err = ioutil.WriteFile(f, []byte(``), 0644)
57 | if err != nil {
58 | t.Fatal(err)
59 | }
60 | if !g.minify(f) {
61 | t.Fatal("Should be succeeded")
62 | }
63 | _, err = os.Stat(filepath.Join(dir, "foo.min.js"))
64 | if err != nil {
65 | t.Fatal(t)
66 | }
67 | }
68 |
69 | func TestSpawn(t *testing.T) {
70 | dir, err := ioutil.TempDir(os.TempDir(), "goemon")
71 | if err != nil {
72 | t.Fatal(err)
73 | }
74 | defer os.RemoveAll(dir)
75 |
76 | g := New()
77 | g.Args = []string{"go", "version"}
78 |
79 | err = g.terminate(os.Interrupt)
80 | if err != nil {
81 | t.Fatal("Should be succeeded", err)
82 | }
83 | err = g.spawn()
84 | if err != nil {
85 | t.Fatal("Should be succeeded", err)
86 | }
87 | }
88 |
89 | func TestLoad(t *testing.T) {
90 | dir, err := ioutil.TempDir(os.TempDir(), "goemon")
91 | if err != nil {
92 | t.Fatal(err)
93 | }
94 | defer os.RemoveAll(dir)
95 |
96 | tmp, err := ioutil.TempFile(dir, "goemon")
97 | if err != nil {
98 | t.Fatal(err)
99 | }
100 |
101 | ioutil.WriteFile(tmp.Name(), []byte(``), 0644)
102 |
103 | g := New()
104 | g.File = tmp.Name()
105 | err = g.load()
106 | if err != nil {
107 | t.Fatal("Should be succeeded", err)
108 | }
109 |
110 | ioutil.WriteFile(tmp.Name(), []byte(`asdfasdf`), 0644)
111 |
112 | err = g.load()
113 | if err == nil {
114 | t.Fatal("Should not be succeeded")
115 | }
116 |
117 | ioutil.WriteFile(tmp.Name(), []byte(`
118 | tasks:
119 | - match: './assets/*.js'
120 | commands:
121 | `), 0644)
122 |
123 | err = g.load()
124 | if err != nil {
125 | t.Fatal("Should be succeeded")
126 | }
127 |
128 | if len(g.conf.Tasks) != 1 {
129 | t.Fatal("Should have a task at least")
130 | }
131 |
132 | ioutil.WriteFile(tmp.Name(), []byte(`
133 | tasks:
134 | - match: './assets/*.js'
135 | commands:
136 | ops:
137 | `), 0644)
138 |
139 | err = g.load()
140 | if err != nil {
141 | t.Fatal("Should be succeeded")
142 | }
143 |
144 | if len(g.conf.Tasks) != 1 {
145 | t.Fatal("Should have a task at least")
146 | }
147 | }
148 |
149 | func TestMatch(t *testing.T) {
150 | dir, err := ioutil.TempDir(os.TempDir(), "goemon")
151 | if err != nil {
152 | t.Fatal(err)
153 | }
154 | defer os.RemoveAll(dir)
155 |
156 | tmp, err := ioutil.TempFile(dir, "goemon")
157 | if err != nil {
158 | t.Fatal(err)
159 | }
160 |
161 | ioutil.WriteFile(tmp.Name(), []byte(`
162 | tasks:
163 | - match: './assets/*.js'
164 | commands:
165 | `), 0644)
166 |
167 | g := New()
168 | g.File = tmp.Name()
169 | err = g.load()
170 | if err != nil {
171 | t.Fatal("Should be succeeded", err)
172 | }
173 |
174 | tests := []struct {
175 | file string
176 | result bool
177 | }{
178 | {"foo", false},
179 | {"assets", false},
180 | {"assets/js", false},
181 | {"assets/.js", false},
182 | {"assets/a.js", true},
183 | }
184 |
185 | for _, test := range tests {
186 | file, _ := filepath.Abs(test.file)
187 | file = filepath.ToSlash(file)
188 | if g.conf.Tasks[0].match(file) {
189 | if !test.result {
190 | t.Fatal("Should not match:", test.file)
191 | }
192 | } else {
193 | if test.result {
194 | t.Fatal("Should be match:", test.file)
195 | }
196 | }
197 | }
198 |
199 | ioutil.WriteFile(tmp.Name(), []byte(`
200 | tasks:
201 | - match: './assets/*/*.js'
202 | commands:
203 | `), 0644)
204 |
205 | err = g.load()
206 | if err != nil {
207 | t.Fatal("Should be succeeded", err)
208 | }
209 |
210 | tests = []struct {
211 | file string
212 | result bool
213 | }{
214 | {"assets/a.js", false},
215 | {"assets/a/.js", false},
216 | {"assets/a/foooo.js", true},
217 | {"assets/a/foo/bar.js", false},
218 | }
219 |
220 | for _, test := range tests {
221 | file, _ := filepath.Abs(test.file)
222 | file = filepath.ToSlash(file)
223 | if g.conf.Tasks[0].match(file) {
224 | if !test.result {
225 | t.Fatal("Should not match:", test.file)
226 | }
227 | } else {
228 | if test.result {
229 | t.Fatal("Should be match:", test.file)
230 | }
231 | }
232 | }
233 |
234 | ioutil.WriteFile(tmp.Name(), []byte(`
235 | tasks:
236 | - match: './assets/*/**/*.js'
237 | commands:
238 | `), 0644)
239 |
240 | err = g.load()
241 | if err != nil {
242 | t.Fatal("Should be succeeded", err)
243 | }
244 |
245 | tests = []struct {
246 | file string
247 | result bool
248 | }{
249 | {"assets/a/foooo.js", true},
250 | {"assets/a/foo/bar.js", true},
251 | {"assets/a/foo/baz/bar.js", true},
252 | }
253 |
254 | for _, test := range tests {
255 | file, _ := filepath.Abs(test.file)
256 | file = filepath.ToSlash(file)
257 | if g.conf.Tasks[0].match(file) {
258 | if !test.result {
259 | t.Fatal("Should not match:", test.file)
260 | }
261 | } else {
262 | if test.result {
263 | t.Fatal("Should be match:", test.file)
264 | }
265 | }
266 | }
267 |
268 | ioutil.WriteFile(tmp.Name(), []byte(`
269 | tasks:
270 | - match: './assets/**/foo.js'
271 | commands:
272 | `), 0644)
273 |
274 | err = g.load()
275 | if err != nil {
276 | t.Fatal("Should be succeeded", err)
277 | }
278 |
279 | tests = []struct {
280 | file string
281 | result bool
282 | }{
283 | {"foooo.js", false},
284 | {"foo.js", false},
285 | {"assets/foo.js", true},
286 | {"assets/foo/bar.js", false},
287 | {"assets/foo/foo.js", true},
288 | {"assets/foo/barz/bar.js", false},
289 | {"assets/a/foo/baz/foo.js", true},
290 | }
291 |
292 | for _, test := range tests {
293 | file, _ := filepath.Abs(test.file)
294 | file = filepath.ToSlash(file)
295 | if g.conf.Tasks[0].match(file) {
296 | if !test.result {
297 | t.Fatal("Should not match:", test.file)
298 | }
299 | } else {
300 | if test.result {
301 | t.Fatal("Should be match:", test.file)
302 | }
303 | }
304 | }
305 | }
306 |
307 | func TestMatchOp(t *testing.T) {
308 | dir, err := ioutil.TempDir(os.TempDir(), "goemon")
309 | if err != nil {
310 | t.Fatal(err)
311 | }
312 | defer os.RemoveAll(dir)
313 |
314 | tmp, err := ioutil.TempFile(dir, "goemon")
315 | if err != nil {
316 | t.Fatal(err)
317 | }
318 |
319 | ioutil.WriteFile(tmp.Name(), []byte(`
320 | tasks:
321 | - match: './assets/*.js'
322 | commands:
323 | `), 0644)
324 |
325 | g := New()
326 | g.File = tmp.Name()
327 | err = g.load()
328 | if err != nil {
329 | t.Fatal("Should be succeeded", err)
330 | }
331 |
332 | tests := []struct {
333 | file string
334 | op fsnotify.Op
335 | result bool
336 | }{
337 | {"assets/a.js", fsnotify.Create, true},
338 | {"foo", fsnotify.Write, false},
339 | {"assets/a.js", fsnotify.Write, true},
340 | {"foo", fsnotify.Remove, false},
341 | {"assets/a.js", fsnotify.Remove, true},
342 | {"foo", fsnotify.Rename, false},
343 | {"assets/a.js", fsnotify.Rename, true},
344 | {"foo", fsnotify.Chmod, false},
345 | {"assets/a.js", fsnotify.Chmod, true},
346 | }
347 |
348 | for _, test := range tests {
349 | file, _ := filepath.Abs(test.file)
350 | file = filepath.ToSlash(file)
351 | if g.conf.Tasks[0].match(file) && g.conf.Tasks[0].matchOp(test.op) {
352 | if !test.result {
353 | t.Fatal("Should not match:", test.file, test.op)
354 | }
355 | } else {
356 | if test.result {
357 | t.Fatal("Should not match:", test.file, test.op)
358 | }
359 | }
360 | }
361 |
362 | ioutil.WriteFile(tmp.Name(), []byte(`
363 | tasks:
364 | - match: './assets/*.js'
365 | commands:
366 | ops:
367 | - CREATE
368 | `), 0644)
369 |
370 | err = g.load()
371 | if err != nil {
372 | t.Fatal("Should be succeeded", err)
373 | }
374 |
375 | tests = []struct {
376 | file string
377 | op fsnotify.Op
378 | result bool
379 | }{
380 | {"foo", fsnotify.Create, false},
381 | {"assets/a.js", fsnotify.Create, true},
382 | {"assets/a.js", fsnotify.Write, false},
383 | {"assets/a.js", fsnotify.Remove, false},
384 | {"assets/a.js", fsnotify.Rename, false},
385 | {"assets/a.js", fsnotify.Chmod, false},
386 | }
387 |
388 | for _, test := range tests {
389 | file, _ := filepath.Abs(test.file)
390 | file = filepath.ToSlash(file)
391 | if g.conf.Tasks[0].match(file) && g.conf.Tasks[0].matchOp(test.op) {
392 | if !test.result {
393 | t.Fatal("Should not match:", test.file, test.op)
394 | }
395 | } else {
396 | if test.result {
397 | t.Fatal("Should not match:", test.file, test.op)
398 | }
399 | }
400 | }
401 |
402 | ioutil.WriteFile(tmp.Name(), []byte(`
403 | tasks:
404 | - match: './assets/*.js'
405 | commands:
406 | ops:
407 | - CREATE
408 | - chmod
409 | `), 0644)
410 |
411 | err = g.load()
412 | if err != nil {
413 | t.Fatal("Should be succeeded", err)
414 | }
415 |
416 | tests = []struct {
417 | file string
418 | op fsnotify.Op
419 | result bool
420 | }{
421 | {"foo", fsnotify.Create, false},
422 | {"assets/a.js", fsnotify.Create, true},
423 | {"assets/a.js", fsnotify.Write, false},
424 | {"assets/a.js", fsnotify.Remove, false},
425 | {"assets/a.js", fsnotify.Rename, false},
426 | {"assets/a.js", fsnotify.Chmod, true},
427 | }
428 |
429 | for _, test := range tests {
430 | file, _ := filepath.Abs(test.file)
431 | file = filepath.ToSlash(file)
432 | if g.conf.Tasks[0].match(file) && g.conf.Tasks[0].matchOp(test.op) {
433 | if !test.result {
434 | t.Fatal("Should not match:", test.file, test.op)
435 | }
436 | } else {
437 | if test.result {
438 | t.Fatal("Should be match:", test.file, test.op)
439 | }
440 | }
441 | }
442 |
443 | ioutil.WriteFile(tmp.Name(), []byte(`
444 | tasks:
445 | - match: './assets/*.js'
446 | commands:
447 | ops:
448 | - CREATE
449 | - chmod
450 | - wriTe
451 | - remove
452 | - rename
453 | `), 0644)
454 |
455 | err = g.load()
456 | if err != nil {
457 | t.Fatal("Should be succeeded", err)
458 | }
459 |
460 | tests = []struct {
461 | file string
462 | op fsnotify.Op
463 | result bool
464 | }{
465 | {"foo", fsnotify.Create, false},
466 | {"assets/a.js", fsnotify.Create, true},
467 | {"assets/a.js", fsnotify.Write, true},
468 | {"assets/a.js", fsnotify.Remove, true},
469 | {"assets/a.js", fsnotify.Rename, true},
470 | {"assets/a.js", fsnotify.Chmod, true},
471 | }
472 |
473 | for _, test := range tests {
474 | file, _ := filepath.Abs(test.file)
475 | file = filepath.ToSlash(file)
476 | if g.conf.Tasks[0].match(file) && g.conf.Tasks[0].matchOp(test.op) {
477 | if !test.result {
478 | t.Fatal("Should not match:", test.file, test.op)
479 | }
480 | } else {
481 | if test.result {
482 | t.Fatal("Should be match:", test.file, test.op)
483 | }
484 | }
485 | }
486 | }
487 |
--------------------------------------------------------------------------------
/livereload.go:
--------------------------------------------------------------------------------
1 | package goemon
2 |
3 | const liveReloadScript = `
4 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o tag");
325 | return;
326 | }
327 | }
328 | this.reloader = new Reloader(this.window, this.console, Timer);
329 | this.connector = new Connector(this.options, this.WebSocket, Timer, {
330 | connecting: (function(_this) {
331 | return function() {};
332 | })(this),
333 | socketConnected: (function(_this) {
334 | return function() {};
335 | })(this),
336 | connected: (function(_this) {
337 | return function(protocol) {
338 | var _base;
339 | if (typeof (_base = _this.listeners).connect === "function") {
340 | _base.connect();
341 | }
342 | _this.log("LiveReload is connected to " + _this.options.host + ":" + _this.options.port + " (protocol v" + protocol + ").");
343 | return _this.analyze();
344 | };
345 | })(this),
346 | error: (function(_this) {
347 | return function(e) {
348 | if (e instanceof ProtocolError) {
349 | if (typeof console !== "undefined" && console !== null) {
350 | return console.log("" + e.message + ".");
351 | }
352 | } else {
353 | if (typeof console !== "undefined" && console !== null) {
354 | return console.log("LiveReload internal error: " + e.message);
355 | }
356 | }
357 | };
358 | })(this),
359 | disconnected: (function(_this) {
360 | return function(reason, nextDelay) {
361 | var _base;
362 | if (typeof (_base = _this.listeners).disconnect === "function") {
363 | _base.disconnect();
364 | }
365 | switch (reason) {
366 | case 'cannot-connect':
367 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + ", will retry in " + nextDelay + " sec.");
368 | case 'broken':
369 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + ", reconnecting in " + nextDelay + " sec.");
370 | case 'handshake-timeout':
371 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake timeout), will retry in " + nextDelay + " sec.");
372 | case 'handshake-failed':
373 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake failed), will retry in " + nextDelay + " sec.");
374 | case 'manual':
375 | break;
376 | case 'error':
377 | break;
378 | default:
379 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + " (" + reason + "), reconnecting in " + nextDelay + " sec.");
380 | }
381 | };
382 | })(this),
383 | message: (function(_this) {
384 | return function(message) {
385 | switch (message.command) {
386 | case 'reload':
387 | return _this.performReload(message);
388 | case 'alert':
389 | return _this.performAlert(message);
390 | }
391 | };
392 | })(this)
393 | });
394 | this.initialized = true;
395 | }
396 |
397 | LiveReload.prototype.on = function(eventName, handler) {
398 | return this.listeners[eventName] = handler;
399 | };
400 |
401 | LiveReload.prototype.log = function(message) {
402 | return this.console.log("" + message);
403 | };
404 |
405 | LiveReload.prototype.performReload = function(message) {
406 | var _ref, _ref1;
407 | this.log("LiveReload received reload request: " + (JSON.stringify(message, null, 2)));
408 | return this.reloader.reload(message.path, {
409 | liveCSS: (_ref = message.liveCSS) != null ? _ref : true,
410 | liveImg: (_ref1 = message.liveImg) != null ? _ref1 : true,
411 | originalPath: message.originalPath || '',
412 | overrideURL: message.overrideURL || '',
413 | serverURL: "http://" + this.options.host + ":" + this.options.port
414 | });
415 | };
416 |
417 | LiveReload.prototype.performAlert = function(message) {
418 | return alert(message.message);
419 | };
420 |
421 | LiveReload.prototype.shutDown = function() {
422 | var _base;
423 | if (!this.initialized) {
424 | return;
425 | }
426 | this.connector.disconnect();
427 | this.log("LiveReload disconnected.");
428 | return typeof (_base = this.listeners).shutdown === "function" ? _base.shutdown() : void 0;
429 | };
430 |
431 | LiveReload.prototype.hasPlugin = function(identifier) {
432 | return !!this.pluginIdentifiers[identifier];
433 | };
434 |
435 | LiveReload.prototype.addPlugin = function(pluginClass) {
436 | var plugin;
437 | if (!this.initialized) {
438 | return;
439 | }
440 | if (this.hasPlugin(pluginClass.identifier)) {
441 | return;
442 | }
443 | this.pluginIdentifiers[pluginClass.identifier] = true;
444 | plugin = new pluginClass(this.window, {
445 | _livereload: this,
446 | _reloader: this.reloader,
447 | _connector: this.connector,
448 | console: this.console,
449 | Timer: Timer,
450 | generateCacheBustUrl: (function(_this) {
451 | return function(url) {
452 | return _this.reloader.generateCacheBustUrl(url);
453 | };
454 | })(this)
455 | });
456 | this.plugins.push(plugin);
457 | this.reloader.addPlugin(plugin);
458 | };
459 |
460 | LiveReload.prototype.analyze = function() {
461 | var plugin, pluginData, pluginsData, _i, _len, _ref;
462 | if (!this.initialized) {
463 | return;
464 | }
465 | if (!(this.connector.protocol >= 7)) {
466 | return;
467 | }
468 | pluginsData = {};
469 | _ref = this.plugins;
470 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
471 | plugin = _ref[_i];
472 | pluginsData[plugin.constructor.identifier] = pluginData = (typeof plugin.analyze === "function" ? plugin.analyze() : void 0) || {};
473 | pluginData.version = plugin.constructor.version;
474 | }
475 | this.connector.sendCommand({
476 | command: 'info',
477 | plugins: pluginsData,
478 | url: this.window.location.href
479 | });
480 | };
481 |
482 | return LiveReload;
483 |
484 | })();
485 |
486 | }).call(this);
487 |
488 | },{"./connector":1,"./options":5,"./reloader":7,"./timer":9}],5:[function(require,module,exports){
489 | (function() {
490 | var Options;
491 |
492 | exports.Options = Options = (function() {
493 | function Options() {
494 | this.https = false;
495 | this.host = null;
496 | this.port = 35729;
497 | this.snipver = null;
498 | this.ext = null;
499 | this.extver = null;
500 | this.mindelay = 1000;
501 | this.maxdelay = 60000;
502 | this.handshake_timeout = 5000;
503 | }
504 |
505 | Options.prototype.set = function(name, value) {
506 | if (typeof value === 'undefined') {
507 | return;
508 | }
509 | if (!isNaN(+value)) {
510 | value = +value;
511 | }
512 | return this[name] = value;
513 | };
514 |
515 | return Options;
516 |
517 | })();
518 |
519 | Options.extract = function(document) {
520 | var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len1, _ref, _ref1;
521 | _ref = document.getElementsByTagName('script');
522 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
523 | element = _ref[_i];
524 | if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) {
525 | options = new Options();
526 | options.https = src.indexOf("https") === 0;
527 | if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) {
528 | options.host = mm[1];
529 | if (mm[2]) {
530 | options.port = parseInt(mm[2], 10);
531 | }
532 | }
533 | if (m[2]) {
534 | _ref1 = m[2].split('&');
535 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
536 | pair = _ref1[_j];
537 | if ((keyAndValue = pair.split('=')).length > 1) {
538 | options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('='));
539 | }
540 | }
541 | }
542 | return options;
543 | }
544 | }
545 | return null;
546 | };
547 |
548 | }).call(this);
549 |
550 | },{}],6:[function(require,module,exports){
551 | (function() {
552 | var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError,
553 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
554 |
555 | exports.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6';
556 |
557 | exports.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7';
558 |
559 | exports.ProtocolError = ProtocolError = (function() {
560 | function ProtocolError(reason, data) {
561 | this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\".";
562 | }
563 |
564 | return ProtocolError;
565 |
566 | })();
567 |
568 | exports.Parser = Parser = (function() {
569 | function Parser(handlers) {
570 | this.handlers = handlers;
571 | this.reset();
572 | }
573 |
574 | Parser.prototype.reset = function() {
575 | return this.protocol = null;
576 | };
577 |
578 | Parser.prototype.process = function(data) {
579 | var command, e, message, options, _ref;
580 | try {
581 | if (this.protocol == null) {
582 | if (data.match(/^!!ver:([\d.]+)$/)) {
583 | this.protocol = 6;
584 | } else if (message = this._parseMessage(data, ['hello'])) {
585 | if (!message.protocols.length) {
586 | throw new ProtocolError("no protocols specified in handshake message");
587 | } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) {
588 | this.protocol = 7;
589 | } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) {
590 | this.protocol = 6;
591 | } else {
592 | throw new ProtocolError("no supported protocols found");
593 | }
594 | }
595 | return this.handlers.connected(this.protocol);
596 | } else if (this.protocol === 6) {
597 | message = JSON.parse(data);
598 | if (!message.length) {
599 | throw new ProtocolError("protocol 6 messages must be arrays");
600 | }
601 | command = message[0], options = message[1];
602 | if (command !== 'refresh') {
603 | throw new ProtocolError("unknown protocol 6 command");
604 | }
605 | return this.handlers.message({
606 | command: 'reload',
607 | path: options.path,
608 | liveCSS: (_ref = options.apply_css_live) != null ? _ref : true
609 | });
610 | } else {
611 | message = this._parseMessage(data, ['reload', 'alert']);
612 | return this.handlers.message(message);
613 | }
614 | } catch (_error) {
615 | e = _error;
616 | if (e instanceof ProtocolError) {
617 | return this.handlers.error(e);
618 | } else {
619 | throw e;
620 | }
621 | }
622 | };
623 |
624 | Parser.prototype._parseMessage = function(data, validCommands) {
625 | var e, message, _ref;
626 | try {
627 | message = JSON.parse(data);
628 | } catch (_error) {
629 | e = _error;
630 | throw new ProtocolError('unparsable JSON', data);
631 | }
632 | if (!message.command) {
633 | throw new ProtocolError('missing "command" key', data);
634 | }
635 | if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) {
636 | throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data);
637 | }
638 | return message;
639 | };
640 |
641 | return Parser;
642 |
643 | })();
644 |
645 | }).call(this);
646 |
647 | },{}],7:[function(require,module,exports){
648 | (function() {
649 | var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl;
650 |
651 | splitUrl = function(url) {
652 | var hash, index, params;
653 | if ((index = url.indexOf('#')) >= 0) {
654 | hash = url.slice(index);
655 | url = url.slice(0, index);
656 | } else {
657 | hash = '';
658 | }
659 | if ((index = url.indexOf('?')) >= 0) {
660 | params = url.slice(index);
661 | url = url.slice(0, index);
662 | } else {
663 | params = '';
664 | }
665 | return {
666 | url: url,
667 | params: params,
668 | hash: hash
669 | };
670 | };
671 |
672 | pathFromUrl = function(url) {
673 | var path;
674 | url = splitUrl(url).url;
675 | if (url.indexOf('file://') === 0) {
676 | path = url.replace(/^file:\/\/(localhost)?/, '');
677 | } else {
678 | path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/');
679 | }
680 | return decodeURIComponent(path);
681 | };
682 |
683 | pickBestMatch = function(path, objects, pathFunc) {
684 | var bestMatch, object, score, _i, _len;
685 | bestMatch = {
686 | score: 0
687 | };
688 | for (_i = 0, _len = objects.length; _i < _len; _i++) {
689 | object = objects[_i];
690 | score = numberOfMatchingSegments(path, pathFunc(object));
691 | if (score > bestMatch.score) {
692 | bestMatch = {
693 | object: object,
694 | score: score
695 | };
696 | }
697 | }
698 | if (bestMatch.score > 0) {
699 | return bestMatch;
700 | } else {
701 | return null;
702 | }
703 | };
704 |
705 | numberOfMatchingSegments = function(path1, path2) {
706 | var comps1, comps2, eqCount, len;
707 | path1 = path1.replace(/^\/+/, '').toLowerCase();
708 | path2 = path2.replace(/^\/+/, '').toLowerCase();
709 | if (path1 === path2) {
710 | return 10000;
711 | }
712 | comps1 = path1.split('/').reverse();
713 | comps2 = path2.split('/').reverse();
714 | len = Math.min(comps1.length, comps2.length);
715 | eqCount = 0;
716 | while (eqCount < len && comps1[eqCount] === comps2[eqCount]) {
717 | ++eqCount;
718 | }
719 | return eqCount;
720 | };
721 |
722 | pathsMatch = function(path1, path2) {
723 | return numberOfMatchingSegments(path1, path2) > 0;
724 | };
725 |
726 | IMAGE_STYLES = [
727 | {
728 | selector: 'background',
729 | styleNames: ['backgroundImage']
730 | }, {
731 | selector: 'border',
732 | styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage']
733 | }
734 | ];
735 |
736 | exports.Reloader = Reloader = (function() {
737 | function Reloader(window, console, Timer) {
738 | this.window = window;
739 | this.console = console;
740 | this.Timer = Timer;
741 | this.document = this.window.document;
742 | this.importCacheWaitPeriod = 200;
743 | this.plugins = [];
744 | }
745 |
746 | Reloader.prototype.addPlugin = function(plugin) {
747 | return this.plugins.push(plugin);
748 | };
749 |
750 | Reloader.prototype.analyze = function(callback) {
751 | return results;
752 | };
753 |
754 | Reloader.prototype.reload = function(path, options) {
755 | var plugin, _base, _i, _len, _ref;
756 | this.options = options;
757 | if ((_base = this.options).stylesheetReloadTimeout == null) {
758 | _base.stylesheetReloadTimeout = 15000;
759 | }
760 | _ref = this.plugins;
761 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
762 | plugin = _ref[_i];
763 | if (plugin.reload && plugin.reload(path, options)) {
764 | return;
765 | }
766 | }
767 | if (options.liveCSS) {
768 | if (path.match(/\.css$/i)) {
769 | if (this.reloadStylesheet(path)) {
770 | return;
771 | }
772 | }
773 | }
774 | if (options.liveImg) {
775 | if (path.match(/\.(jpe?g|png|gif)$/i)) {
776 | this.reloadImages(path);
777 | return;
778 | }
779 | }
780 | return this.reloadPage();
781 | };
782 |
783 | Reloader.prototype.reloadPage = function() {
784 | return this.window.document.location.reload();
785 | };
786 |
787 | Reloader.prototype.reloadImages = function(path) {
788 | var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _results;
789 | expando = this.generateUniqueString();
790 | _ref = this.document.images;
791 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
792 | img = _ref[_i];
793 | if (pathsMatch(path, pathFromUrl(img.src))) {
794 | img.src = this.generateCacheBustUrl(img.src, expando);
795 | }
796 | }
797 | if (this.document.querySelectorAll) {
798 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) {
799 | _ref1 = IMAGE_STYLES[_j], selector = _ref1.selector, styleNames = _ref1.styleNames;
800 | _ref2 = this.document.querySelectorAll("[style*=" + selector + "]");
801 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
802 | img = _ref2[_k];
803 | this.reloadStyleImages(img.style, styleNames, path, expando);
804 | }
805 | }
806 | }
807 | if (this.document.styleSheets) {
808 | _ref3 = this.document.styleSheets;
809 | _results = [];
810 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) {
811 | styleSheet = _ref3[_l];
812 | _results.push(this.reloadStylesheetImages(styleSheet, path, expando));
813 | }
814 | return _results;
815 | }
816 | };
817 |
818 | Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) {
819 | var e, rule, rules, styleNames, _i, _j, _len, _len1;
820 | try {
821 | rules = styleSheet != null ? styleSheet.cssRules : void 0;
822 | } catch (_error) {
823 | e = _error;
824 | }
825 | if (!rules) {
826 | return;
827 | }
828 | for (_i = 0, _len = rules.length; _i < _len; _i++) {
829 | rule = rules[_i];
830 | switch (rule.type) {
831 | case CSSRule.IMPORT_RULE:
832 | this.reloadStylesheetImages(rule.styleSheet, path, expando);
833 | break;
834 | case CSSRule.STYLE_RULE:
835 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) {
836 | styleNames = IMAGE_STYLES[_j].styleNames;
837 | this.reloadStyleImages(rule.style, styleNames, path, expando);
838 | }
839 | break;
840 | case CSSRule.MEDIA_RULE:
841 | this.reloadStylesheetImages(rule, path, expando);
842 | }
843 | }
844 | };
845 |
846 | Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) {
847 | var newValue, styleName, value, _i, _len;
848 | for (_i = 0, _len = styleNames.length; _i < _len; _i++) {
849 | styleName = styleNames[_i];
850 | value = style[styleName];
851 | if (typeof value === 'string') {
852 | newValue = value.replace(/\burl\s*\(([^)]*)\)/, (function(_this) {
853 | return function(match, src) {
854 | if (pathsMatch(path, pathFromUrl(src))) {
855 | return "url(" + (_this.generateCacheBustUrl(src, expando)) + ")";
856 | } else {
857 | return match;
858 | }
859 | };
860 | })(this));
861 | if (newValue !== value) {
862 | style[styleName] = newValue;
863 | }
864 | }
865 | }
866 | };
867 |
868 | Reloader.prototype.reloadStylesheet = function(path) {
869 | var imported, link, links, match, style, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1;
870 | links = (function() {
871 | var _i, _len, _ref, _results;
872 | _ref = this.document.getElementsByTagName('link');
873 | _results = [];
874 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
875 | link = _ref[_i];
876 | if (link.rel.match(/^stylesheet$/i) && !link.__LiveReload_pendingRemoval) {
877 | _results.push(link);
878 | }
879 | }
880 | return _results;
881 | }).call(this);
882 | imported = [];
883 | _ref = this.document.getElementsByTagName('style');
884 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
885 | style = _ref[_i];
886 | if (style.sheet) {
887 | this.collectImportedStylesheets(style, style.sheet, imported);
888 | }
889 | }
890 | for (_j = 0, _len1 = links.length; _j < _len1; _j++) {
891 | link = links[_j];
892 | this.collectImportedStylesheets(link, link.sheet, imported);
893 | }
894 | if (this.window.StyleFix && this.document.querySelectorAll) {
895 | _ref1 = this.document.querySelectorAll('style[data-href]');
896 | for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
897 | style = _ref1[_k];
898 | links.push(style);
899 | }
900 | }
901 | this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets");
902 | match = pickBestMatch(path, links.concat(imported), (function(_this) {
903 | return function(l) {
904 | return pathFromUrl(_this.linkHref(l));
905 | };
906 | })(this));
907 | if (match) {
908 | if (match.object.rule) {
909 | this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href);
910 | this.reattachImportedRule(match.object);
911 | } else {
912 | this.console.log("LiveReload is reloading stylesheet: " + (this.linkHref(match.object)));
913 | this.reattachStylesheetLink(match.object);
914 | }
915 | } else {
916 | this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one");
917 | for (_l = 0, _len3 = links.length; _l < _len3; _l++) {
918 | link = links[_l];
919 | this.reattachStylesheetLink(link);
920 | }
921 | }
922 | return true;
923 | };
924 |
925 | Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) {
926 | var e, index, rule, rules, _i, _len;
927 | try {
928 | rules = styleSheet != null ? styleSheet.cssRules : void 0;
929 | } catch (_error) {
930 | e = _error;
931 | }
932 | if (rules && rules.length) {
933 | for (index = _i = 0, _len = rules.length; _i < _len; index = ++_i) {
934 | rule = rules[index];
935 | switch (rule.type) {
936 | case CSSRule.CHARSET_RULE:
937 | continue;
938 | case CSSRule.IMPORT_RULE:
939 | result.push({
940 | link: link,
941 | rule: rule,
942 | index: index,
943 | href: rule.href
944 | });
945 | this.collectImportedStylesheets(link, rule.styleSheet, result);
946 | break;
947 | default:
948 | break;
949 | }
950 | }
951 | }
952 | };
953 |
954 | Reloader.prototype.waitUntilCssLoads = function(clone, func) {
955 | var callbackExecuted, executeCallback, poll;
956 | callbackExecuted = false;
957 | executeCallback = (function(_this) {
958 | return function() {
959 | if (callbackExecuted) {
960 | return;
961 | }
962 | callbackExecuted = true;
963 | return func();
964 | };
965 | })(this);
966 | clone.onload = (function(_this) {
967 | return function() {
968 | _this.console.log("LiveReload: the new stylesheet has finished loading");
969 | _this.knownToSupportCssOnLoad = true;
970 | return executeCallback();
971 | };
972 | })(this);
973 | if (!this.knownToSupportCssOnLoad) {
974 | (poll = (function(_this) {
975 | return function() {
976 | if (clone.sheet) {
977 | _this.console.log("LiveReload is polling until the new CSS finishes loading...");
978 | return executeCallback();
979 | } else {
980 | return _this.Timer.start(50, poll);
981 | }
982 | };
983 | })(this))();
984 | }
985 | return this.Timer.start(this.options.stylesheetReloadTimeout, executeCallback);
986 | };
987 |
988 | Reloader.prototype.linkHref = function(link) {
989 | return link.href || link.getAttribute('data-href');
990 | };
991 |
992 | Reloader.prototype.reattachStylesheetLink = function(link) {
993 | var clone, parent;
994 | if (link.__LiveReload_pendingRemoval) {
995 | return;
996 | }
997 | link.__LiveReload_pendingRemoval = true;
998 | if (link.tagName === 'STYLE') {
999 | clone = this.document.createElement('link');
1000 | clone.rel = 'stylesheet';
1001 | clone.media = link.media;
1002 | clone.disabled = link.disabled;
1003 | } else {
1004 | clone = link.cloneNode(false);
1005 | }
1006 | clone.href = this.generateCacheBustUrl(this.linkHref(link));
1007 | parent = link.parentNode;
1008 | if (parent.lastChild === link) {
1009 | parent.appendChild(clone);
1010 | } else {
1011 | parent.insertBefore(clone, link.nextSibling);
1012 | }
1013 | return this.waitUntilCssLoads(clone, (function(_this) {
1014 | return function() {
1015 | var additionalWaitingTime;
1016 | if (/AppleWebKit/.test(navigator.userAgent)) {
1017 | additionalWaitingTime = 5;
1018 | } else {
1019 | additionalWaitingTime = 200;
1020 | }
1021 | return _this.Timer.start(additionalWaitingTime, function() {
1022 | var _ref;
1023 | if (!link.parentNode) {
1024 | return;
1025 | }
1026 | link.parentNode.removeChild(link);
1027 | clone.onreadystatechange = null;
1028 | return (_ref = _this.window.StyleFix) != null ? _ref.link(clone) : void 0;
1029 | });
1030 | };
1031 | })(this));
1032 | };
1033 |
1034 | Reloader.prototype.reattachImportedRule = function(_arg) {
1035 | var href, index, link, media, newRule, parent, rule, tempLink;
1036 | rule = _arg.rule, index = _arg.index, link = _arg.link;
1037 | parent = rule.parentStyleSheet;
1038 | href = this.generateCacheBustUrl(rule.href);
1039 | media = rule.media.length ? [].join.call(rule.media, ', ') : '';
1040 | newRule = "@import url(\"" + href + "\") " + media + ";";
1041 | rule.__LiveReload_newHref = href;
1042 | tempLink = this.document.createElement("link");
1043 | tempLink.rel = 'stylesheet';
1044 | tempLink.href = href;
1045 | tempLink.__LiveReload_pendingRemoval = true;
1046 | if (link.parentNode) {
1047 | link.parentNode.insertBefore(tempLink, link);
1048 | }
1049 | return this.Timer.start(this.importCacheWaitPeriod, (function(_this) {
1050 | return function() {
1051 | if (tempLink.parentNode) {
1052 | tempLink.parentNode.removeChild(tempLink);
1053 | }
1054 | if (rule.__LiveReload_newHref !== href) {
1055 | return;
1056 | }
1057 | parent.insertRule(newRule, index);
1058 | parent.deleteRule(index + 1);
1059 | rule = parent.cssRules[index];
1060 | rule.__LiveReload_newHref = href;
1061 | return _this.Timer.start(_this.importCacheWaitPeriod, function() {
1062 | if (rule.__LiveReload_newHref !== href) {
1063 | return;
1064 | }
1065 | parent.insertRule(newRule, index);
1066 | return parent.deleteRule(index + 1);
1067 | });
1068 | };
1069 | })(this));
1070 | };
1071 |
1072 | Reloader.prototype.generateUniqueString = function() {
1073 | return 'livereload=' + Date.now();
1074 | };
1075 |
1076 | Reloader.prototype.generateCacheBustUrl = function(url, expando) {
1077 | var hash, oldParams, originalUrl, params, _ref;
1078 | if (expando == null) {
1079 | expando = this.generateUniqueString();
1080 | }
1081 | _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params;
1082 | if (this.options.overrideURL) {
1083 | if (url.indexOf(this.options.serverURL) < 0) {
1084 | originalUrl = url;
1085 | url = this.options.serverURL + this.options.overrideURL + "?url=" + encodeURIComponent(url);
1086 | this.console.log("LiveReload is overriding source URL " + originalUrl + " with " + url);
1087 | }
1088 | }
1089 | params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) {
1090 | return "" + sep + expando;
1091 | });
1092 | if (params === oldParams) {
1093 | if (oldParams.length === 0) {
1094 | params = "?" + expando;
1095 | } else {
1096 | params = "" + oldParams + "&" + expando;
1097 | }
1098 | }
1099 | return url + params + hash;
1100 | };
1101 |
1102 | return Reloader;
1103 |
1104 | })();
1105 |
1106 | }).call(this);
1107 |
1108 | },{}],8:[function(require,module,exports){
1109 | (function() {
1110 | var CustomEvents, LiveReload, k;
1111 |
1112 | CustomEvents = require('./customevents');
1113 |
1114 | LiveReload = window.LiveReload = new (require('./livereload').LiveReload)(window);
1115 |
1116 | for (k in window) {
1117 | if (k.match(/^LiveReloadPlugin/)) {
1118 | LiveReload.addPlugin(window[k]);
1119 | }
1120 | }
1121 |
1122 | LiveReload.addPlugin(require('./less'));
1123 |
1124 | LiveReload.on('shutdown', function() {
1125 | return delete window.LiveReload;
1126 | });
1127 |
1128 | LiveReload.on('connect', function() {
1129 | return CustomEvents.fire(document, 'LiveReloadConnect');
1130 | });
1131 |
1132 | LiveReload.on('disconnect', function() {
1133 | return CustomEvents.fire(document, 'LiveReloadDisconnect');
1134 | });
1135 |
1136 | CustomEvents.bind(document, 'LiveReloadShutDown', function() {
1137 | return LiveReload.shutDown();
1138 | });
1139 |
1140 | }).call(this);
1141 |
1142 | },{"./customevents":2,"./less":3,"./livereload":4}],9:[function(require,module,exports){
1143 | (function() {
1144 | var Timer;
1145 |
1146 | exports.Timer = Timer = (function() {
1147 | function Timer(func) {
1148 | this.func = func;
1149 | this.running = false;
1150 | this.id = null;
1151 | this._handler = (function(_this) {
1152 | return function() {
1153 | _this.running = false;
1154 | _this.id = null;
1155 | return _this.func();
1156 | };
1157 | })(this);
1158 | }
1159 |
1160 | Timer.prototype.start = function(timeout) {
1161 | if (this.running) {
1162 | clearTimeout(this.id);
1163 | }
1164 | this.id = setTimeout(this._handler, timeout);
1165 | return this.running = true;
1166 | };
1167 |
1168 | Timer.prototype.stop = function() {
1169 | if (this.running) {
1170 | clearTimeout(this.id);
1171 | this.running = false;
1172 | return this.id = null;
1173 | }
1174 | };
1175 |
1176 | return Timer;
1177 |
1178 | })();
1179 |
1180 | Timer.start = function(timeout, func) {
1181 | return setTimeout(func, timeout);
1182 | };
1183 |
1184 | }).call(this);
1185 |
1186 | },{}]},{},[8]);
1187 | `
1188 |
--------------------------------------------------------------------------------
/proc_posix.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package goemon
4 |
5 | import (
6 | "os"
7 | "os/exec"
8 | "time"
9 | )
10 |
11 | func (g *Goemon) spawn() error {
12 | g.cmd = exec.Command(g.Args[0], g.Args[1:]...)
13 | g.cmd.Stdout = os.Stdout
14 | g.cmd.Stderr = os.Stderr
15 | return g.cmd.Run()
16 | }
17 |
18 | func (g *Goemon) terminate(sig os.Signal) error {
19 | if g.cmd != nil && g.cmd.Process != nil {
20 | if sig == os.Kill {
21 | return g.cmd.Process.Kill()
22 | }
23 | if err := g.cmd.Process.Signal(sig); err != nil {
24 | g.Logger.Println(err)
25 | return g.cmd.Process.Kill()
26 | }
27 |
28 | deadline := time.Now().Add(5 * time.Second)
29 | for time.Now().Before(deadline) {
30 | if g.cmd.ProcessState != nil && g.cmd.ProcessState.Exited() {
31 | return nil
32 | }
33 | time.Sleep(100)
34 | }
35 | return g.cmd.Process.Kill()
36 | }
37 | return nil
38 | }
39 |
--------------------------------------------------------------------------------
/proc_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package goemon
4 |
5 | import (
6 | "fmt"
7 | "os"
8 | "os/exec"
9 | "syscall"
10 | "time"
11 | )
12 |
13 | var (
14 | libkernel32 = syscall.MustLoadDLL("kernel32")
15 | procSetConsoleCtrlHandler = libkernel32.MustFindProc("SetConsoleCtrlHandler")
16 | procGenerateConsoleCtrlEvent = libkernel32.MustFindProc("GenerateConsoleCtrlEvent")
17 | )
18 |
19 | func (g *Goemon) spawn() error {
20 | g.cmd = exec.Command(g.Args[0], g.Args[1:]...)
21 | g.cmd.Stdout = os.Stdout
22 | g.cmd.Stderr = os.Stderr
23 | g.cmd.SysProcAttr = &syscall.SysProcAttr{
24 | CreationFlags: syscall.CREATE_UNICODE_ENVIRONMENT | 0x00000200,
25 | }
26 | return g.cmd.Run()
27 | }
28 |
29 | func kill(p *os.Process) error {
30 | return exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprint(p.Pid)).Run()
31 | }
32 |
33 | func (g *Goemon) terminate(sig os.Signal) error {
34 | if g.cmd != nil && g.cmd.Process != nil {
35 | if err := interrupt(g.cmd.Process, sig); err != nil {
36 | g.Logger.Println(err)
37 | return kill(g.cmd.Process)
38 | }
39 |
40 | deadline := time.Now().Add(5 * time.Second)
41 | for time.Now().Before(deadline) {
42 | if g.cmd.ProcessState != nil && g.cmd.ProcessState.Exited() {
43 | return nil
44 | }
45 | time.Sleep(100)
46 | }
47 | return kill(g.cmd.Process)
48 | }
49 | return nil
50 | }
51 |
52 | func interrupt(p *os.Process, sig os.Signal) error {
53 | if sig == os.Kill {
54 | return p.Kill()
55 | }
56 | procSetConsoleCtrlHandler.Call(0, 1)
57 | defer procSetConsoleCtrlHandler.Call(0, 0)
58 | r1, _, err := procGenerateConsoleCtrlEvent.Call(syscall.CTRL_C_EVENT, uintptr(p.Pid))
59 | if r1 == 0 {
60 | return err
61 | }
62 | return nil
63 | }
64 |
--------------------------------------------------------------------------------