├── .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 | ![](https://raw.githubusercontent.com/mattn/goemon/master/data/goemon.png) 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 | --------------------------------------------------------------------------------