├── .gitignore ├── .tower.yml ├── .travis.yml ├── README.md ├── app.go ├── main.go ├── main_test.go ├── page.go ├── page.html ├── proxy.go ├── site └── page.graffle ├── test ├── configs │ └── tower.yml ├── files │ ├── error.go_ │ └── server2.go_ └── server1.go ├── tmp └── .gitkeep ├── tower.sh ├── tower.yml ├── utils.go └── watcher.go /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | -------------------------------------------------------------------------------- /.tower.yml: -------------------------------------------------------------------------------- 1 | 2 | # file name of the "go run" command 3 | main: test/server1.go 4 | 5 | port: 5000 6 | 7 | # file types to watch for changes in. use "|" to separate multiple types, for example, go|html 8 | watch: go 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | install: 3 | - go get github.com/shaoshing/gotest 4 | - go get github.com/howeyc/fsnotify 5 | - go get github.com/kylelemons/go-gypsy/yaml 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tower 2 | 3 | Tower makes your Go web development much more dynamic by monitoring file's changes in your project and then re-run your 4 | app to apply those changes – yeah, no more stopping and running manually! It will also show compiler error, panic and 5 | runtime error through a clean page (see the demo below). 6 | 7 | [![Build Status](https://travis-ci.org/shaoshing/tower.png?branch=master)](https://travis-ci.org/shaoshing/tower) 8 | 9 | ## Demo 10 | 11 | Watch at [Youtube](http://youtu.be/QRg7yWn1jzI) 12 | 13 | ## Install 14 | ```bash 15 | go get github.com/shaoshing/tower 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```bash 21 | cd your/project 22 | tower # now visit localhost:8000 23 | ``` 24 | 25 | Tower will, by default, assume your web app's main file is _main.go_ and the port is _5000_. These can be changed by: 26 | 27 | ```bash 28 | tower -m app.go -p 3000 29 | ``` 30 | 31 | Or put them in a config file: 32 | 33 | ```bash 34 | tower init 35 | vim .tower.yml 36 | tower 37 | ``` 38 | 39 | ## Troubleshooting 40 | 41 | #### 'Too many open files' 42 | 43 | Run the following command to increase the number of files that a process can open: 44 | 45 | ```bash 46 | ulimit -S -n 2048 # OSX 47 | ``` 48 | 49 | ## How it works? 50 | 51 | ``` 52 | browser: http://localhost:8000 53 | \/ 54 | tower (listening 8000) 55 | \/ (reverse proxy) 56 | your web app (listening 5000) 57 | ``` 58 | 59 | Any request comes from localhost:8000 will be handled by Tower and then be redirected to your app. The redirection is 60 | done by using _[httputil.ReverseProxy](http://golang.org/pkg/net/http/httputil/#ReverseProxy)_. Before redirecting the request, Tower will compile and run your app in 61 | another process if your app hasn't been run or file has been changed; Tower is using 62 | _[howeyc/fsnotify](https://github.com/howeyc/fsnotify)_ to monitor file changes. 63 | 64 | ## License 65 | 66 | Tower is released under the [MIT License](http://www.opensource.org/licenses/MIT). 67 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "os/signal" 10 | "path" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | const ( 18 | HttpPanicMessage = "http: panic serving" 19 | ) 20 | 21 | var ( 22 | AppBin = "/tmp/tower-app-" + strconv.FormatInt(time.Now().Unix(), 10) 23 | ) 24 | 25 | type App struct { 26 | Cmd *exec.Cmd 27 | MainFile string 28 | Port string 29 | Name string 30 | Root string 31 | KeyPress bool 32 | LastError string 33 | 34 | start *sync.Once 35 | startErr error 36 | restart *sync.Once 37 | restartErr error 38 | } 39 | 40 | type StderrCapturer struct { 41 | app *App 42 | } 43 | 44 | func (this StderrCapturer) Write(p []byte) (n int, err error) { 45 | httpError := strings.Contains(string(p), HttpPanicMessage) 46 | 47 | if httpError { 48 | this.app.LastError = string(p) 49 | os.Stdout.Write([]byte("----------- Application Error -----------\n")) 50 | n, err = os.Stdout.Write(p) 51 | os.Stdout.Write([]byte("-----------------------------------------\n")) 52 | } else { 53 | n, err = os.Stdout.Write(p) 54 | } 55 | return 56 | } 57 | 58 | func NewApp(mainFile, port string) (app App) { 59 | app.MainFile = mainFile 60 | app.Port = port 61 | wd, _ := os.Getwd() 62 | app.Name = path.Base(wd) 63 | app.Root = path.Dir(mainFile) 64 | app.start = &sync.Once{} 65 | app.restart = &sync.Once{} 66 | return 67 | } 68 | 69 | func (this *App) Start(build bool) error { 70 | this.start.Do(func() { 71 | if build { 72 | this.startErr = this.build() 73 | if this.startErr != nil { 74 | fmt.Println("== Fail to build " + this.Name) 75 | this.start = &sync.Once{} 76 | return 77 | } 78 | } 79 | 80 | this.startErr = this.run() 81 | if this.startErr != nil { 82 | this.startErr = errors.New("Fail to run " + this.Name) 83 | this.start = &sync.Once{} 84 | return 85 | } 86 | 87 | this.RestartOnReturn() 88 | this.start = &sync.Once{} 89 | }) 90 | 91 | return this.startErr 92 | } 93 | 94 | func (this *App) Restart() error { 95 | this.restart.Do(func() { 96 | this.Stop() 97 | this.restartErr = this.Start(true) 98 | this.restart = &sync.Once{} // Assign new Once to allow calling Start again. 99 | }) 100 | 101 | return this.restartErr 102 | } 103 | 104 | func (this *App) Stop() { 105 | if this.IsRunning() { 106 | os.Remove(AppBin) 107 | fmt.Println("== Stopping " + this.Name) 108 | this.Cmd.Process.Kill() 109 | this.Cmd = nil 110 | } 111 | } 112 | 113 | func (this *App) run() (err error) { 114 | _, err = os.Stat(AppBin) 115 | if err != nil { 116 | return 117 | } 118 | 119 | fmt.Println("== Running " + this.Name) 120 | this.Cmd = exec.Command(AppBin) 121 | this.Cmd.Stdout = os.Stdout 122 | this.Cmd.Stderr = StderrCapturer{this} 123 | go func() { 124 | this.Cmd.Run() 125 | }() 126 | 127 | err = dialAddress("127.0.0.1:"+this.Port, 60) 128 | return 129 | } 130 | 131 | func (this *App) build() (err error) { 132 | fmt.Println("== Building " + this.Name) 133 | out, _ := exec.Command("go", "build", "-o", AppBin, this.MainFile).CombinedOutput() 134 | if len(out) > 0 { 135 | msg := strings.Replace(string(out), "# command-line-arguments\n", "", 1) 136 | fmt.Printf("----------- Build Error -----------\n%s-----------------------------------\n", msg) 137 | return errors.New(msg) 138 | } 139 | return nil 140 | } 141 | 142 | func (this *App) IsRunning() bool { 143 | return this.Cmd != nil && this.Cmd.ProcessState == nil 144 | } 145 | 146 | func (this *App) IsQuit() bool { 147 | return this.Cmd != nil && this.Cmd.ProcessState != nil 148 | } 149 | 150 | func (this *App) RestartOnReturn() { 151 | if this.KeyPress { 152 | return 153 | } 154 | this.KeyPress = true 155 | 156 | // Listen to keypress of "return" and restart the app automatically 157 | go func() { 158 | in := bufio.NewReader(os.Stdin) 159 | for { 160 | input, _ := in.ReadString('\n') 161 | if input == "\n" { 162 | this.Restart() 163 | } 164 | } 165 | }() 166 | 167 | // Listen to "^C" signal and stop the app properly 168 | go func() { 169 | sig := make(chan os.Signal, 1) 170 | signal.Notify(sig, os.Interrupt) 171 | <-sig // wait for the "^C" signal 172 | fmt.Println("") 173 | this.Stop() 174 | os.Exit(0) 175 | }() 176 | } 177 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/kylelemons/go-gypsy/yaml" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "runtime" 11 | ) 12 | 13 | const ConfigName = ".tower.yml" 14 | 15 | func main() { 16 | appMainFile := flag.String("m", "main.go", "path to your app's main file.") 17 | appPort := flag.String("p", "5000", "port of your app.") 18 | verbose := flag.Bool("v", false, "show more stuff.") 19 | 20 | flag.Parse() 21 | 22 | args := flag.Args() 23 | if len(args) == 1 && args[0] == "init" { 24 | generateExampleConfig() 25 | return 26 | } 27 | 28 | startTower(*appMainFile, *appPort, *verbose) 29 | } 30 | 31 | func generateExampleConfig() { 32 | _, file, _, _ := runtime.Caller(0) 33 | exampleConfig := path.Dir(file) + "/tower.yml" 34 | exec.Command("cp", exampleConfig, ConfigName).Run() 35 | fmt.Println("== Generated config file " + ConfigName) 36 | } 37 | 38 | var ( 39 | app App 40 | ) 41 | 42 | func startTower(appMainFile, appPort string, verbose bool) { 43 | watchedFiles := "" 44 | 45 | config, err := yaml.ReadFile(ConfigName) 46 | if err == nil { 47 | if verbose { 48 | fmt.Println("== Load config from " + ConfigName) 49 | } 50 | appMainFile, _ = config.Get("main") 51 | appPort, _ = config.Get("port") 52 | watchedFiles, _ = config.Get("watch") 53 | } 54 | 55 | err = dialAddress("127.0.0.1:"+appPort, 1) 56 | if err == nil { 57 | fmt.Println("Error: port (" + appPort + ") already in used.") 58 | os.Exit(1) 59 | } 60 | 61 | if verbose { 62 | fmt.Println("== Application Info") 63 | fmt.Printf(" build app with: %s\n", appMainFile) 64 | fmt.Printf(" redirect requests from localhost:%s to localhost:%s\n\n", ProxyPort, appPort) 65 | } 66 | 67 | app = NewApp(appMainFile, appPort) 68 | watcher := NewWatcher(app.Root, watchedFiles) 69 | proxy := NewProxy(&app, &watcher) 70 | 71 | go func() { 72 | mustSuccess(watcher.Watch()) 73 | }() 74 | mustSuccess(proxy.Listen()) 75 | } 76 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/shaoshing/gotest" 6 | "io/ioutil" 7 | "net/http" 8 | "os/exec" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestCmd(t *testing.T) { 14 | assert.Test = t 15 | 16 | go startTower("", "", true) 17 | err := dialAddress("127.0.0.1:8000", 60) 18 | if err != nil { 19 | panic(err) 20 | } 21 | defer func() { 22 | app.Stop() 23 | fmt.Println("\n\n\n\n\n") 24 | }() 25 | 26 | assert.Equal("server 1", get("http://127.0.0.1:8000/")) 27 | assert.Equal("server 1", get("http://127.0.0.1:8000/?k=v1&k=v2&k1=v3")) // Test logging parameters 28 | assert.Equal("server 1", get("http://127.0.0.1:5000/")) 29 | 30 | app.Stop() 31 | concurrency := 10 32 | compileChan := make(chan bool) 33 | for i := 0; i < concurrency; i++ { 34 | go func() { 35 | get("http://127.0.0.1:8000/") 36 | compileChan <- true 37 | }() 38 | } 39 | 40 | for i := 0; i < concurrency; i++ { 41 | select { 42 | case <-compileChan: 43 | case <-time.After(10 * time.Second): 44 | assert.TrueM(false, "Timeout on concurrency testing.") 45 | } 46 | } 47 | 48 | // test app exits unexpectedly 49 | assert.Contain("App quit unexpetedly", get("http://127.0.0.1:8000/exit")) // should restart the application 50 | 51 | // test error page 52 | highlightCode := `    ` 53 | assert.Contain("panic: Panic !!", get("http://127.0.0.1:8000/panic")) // should be able to detect panic 54 | assert.Contain(highlightCode+`panic(errors.New`, get("http://127.0.0.1:8000/panic")) // should show code snippet 55 | assert.Contain(`36`, get("http://127.0.0.1:8000/panic")) // should show line number 56 | assert.Contain("runtime error: index out of range", get("http://127.0.0.1:8000/error")) // should be able to detect runtime error 57 | assert.Contain(highlightCode+`paths[0]`, get("http://127.0.0.1:8000/error")) // should show code snippet 58 | assert.Contain(`17`, get("http://127.0.0.1:8000/error")) // should show line number 59 | 60 | defer exec.Command("git", "checkout", "test").Run() 61 | 62 | exec.Command("cp", "test/files/server2.go_", "test/server1.go").Run() 63 | time.Sleep(100 * time.Millisecond) 64 | assert.Equal("server 2", get("http://127.0.0.1:8000/")) 65 | 66 | exec.Command("cp", "test/files/error.go_", "test/server1.go").Run() 67 | assert.Match("Build Error", get("http://127.0.0.1:8000/")) 68 | } 69 | 70 | func get(url string) string { 71 | resp, err := http.Get(url) 72 | if err != nil { 73 | panic(err) 74 | } 75 | b_body, _ := ioutil.ReadAll(resp.Body) 76 | return string(b_body) 77 | } 78 | -------------------------------------------------------------------------------- /page.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html" 5 | "html/template" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "path" 10 | "regexp" 11 | "runtime" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | var errorTemplate *template.Template 18 | 19 | func init() { 20 | _, filename, _, _ := runtime.Caller(1) 21 | pkgPath := path.Dir(filename) 22 | templatePath := pkgPath + "/page.html" 23 | 24 | var err error 25 | errorTemplate, err = template.ParseFiles(templatePath) 26 | if err != nil { 27 | panic(err) 28 | } 29 | } 30 | 31 | func RenderError(w http.ResponseWriter, app *App, message string) { 32 | info := ErrorInfo{Title: "Error", Message: template.HTML(message)} 33 | info.Prepare() 34 | 35 | renderPage(w, info) 36 | } 37 | 38 | func RenderBuildError(w http.ResponseWriter, app *App, message string) { 39 | info := ErrorInfo{Title: "Build Error", Message: template.HTML(message)} 40 | info.Prepare() 41 | 42 | renderPage(w, info) 43 | } 44 | 45 | const SnippetLineNumbers = 13 46 | 47 | func RenderAppError(w http.ResponseWriter, app *App, errMessage string) { 48 | info := ErrorInfo{Title: "Application Error"} 49 | message, trace, appIndex := extractAppErrorInfo(errMessage) 50 | 51 | // from: 2013/02/12 18:24:15 http: panic serving 127.0.0.1:54114: Validation Error 52 | // to: Validation Error 53 | message[0] = string(regexp.MustCompile(`.+\d+\.\d+.\d+.\d+\:\d+\: `).ReplaceAll([]byte(message[0]), []byte(""))) 54 | if !strings.Contains(message[0], "runtime error") { 55 | message[0] = "panic: " + message[0] 56 | } 57 | 58 | info.Message = template.HTML(strings.Join(message, "\n")) 59 | info.Trace = trace 60 | info.ShowTrace = true 61 | 62 | // from: test/server1.go:16 (0x211e) 63 | // to: [test/server1.go, 16] 64 | appFileInfo := strings.Split(strings.Split(trace[appIndex].File, " ")[0], ":") 65 | info.SnippetPath = appFileInfo[0] 66 | info.ShowSnippet = true 67 | curLineNum, _ := strconv.ParseInt(appFileInfo[1], 10, 16) 68 | info.Snippet = extractAppSnippet(appFileInfo[0], int(curLineNum)) 69 | 70 | info.Prepare() 71 | renderPage(w, info) 72 | } 73 | 74 | func renderPage(w http.ResponseWriter, info ErrorInfo) { 75 | err := errorTemplate.Execute(w, info) 76 | if err != nil { 77 | panic(err) 78 | } 79 | } 80 | 81 | // Example input 82 | // 2013/02/12 18:24:15 http: panic serving 127.0.0.1:54114: Panic !! 83 | // /usr/local/Cellar/go/1.0.3/src/pkg/net/http/server.go:589 (0x31ed9) 84 | // _func_004: buf.Write(debug.Stack()) 85 | // /usr/local/Cellar/go/1.0.3/src/pkg/runtime/proc.c:1443 (0x10b83) 86 | // panic: reflect·call(d->fn, d->args, d->siz); 87 | // /Users/user/tower/test/server1.go:16 (0x211e) 88 | // Panic: panic(errors.New("Panic !!")) 89 | 90 | // Example output 91 | // message: 92 | // [2013/02/12 18:24:15 http: panic serving 127.0.0.1:54114: Panic !!] 93 | // trace: 94 | // [ 95 | // [test/server1.go:16 (0x211e), Panic: panic(errors.New("Panic !!"))] 96 | // ] 97 | func extractAppErrorInfo(errMessage string) (message []string, trace []Trace, appIndex int) { 98 | // from: /Users/user/tower/test/server1.go:16 (0x211e) 99 | // Panic: panic(errors.New("Panic !!")) 100 | // to: //Users/user/tower/test/server1.go:16 (0x211e)Panic: panic(errors.New("Panic !!")) 101 | errMessage = strings.Replace(strings.Replace(errMessage, "\n", "", -1), "/", "//", -1) 102 | 103 | wd, _ := os.Getwd() 104 | wd = wd + "/" 105 | for i, line := range strings.Split(errMessage, "/") { 106 | lines := strings.Split(line, "") 107 | if i == 0 { 108 | message = lines 109 | continue 110 | } 111 | 112 | t := Trace{Func: lines[1]} 113 | if strings.Index(lines[0], wd) != -1 { 114 | if appIndex == 0 { 115 | appIndex = i - 1 116 | } 117 | t.AppFile = true 118 | } 119 | t.File = strings.Replace(lines[0], wd, "", 1) 120 | // from: /Users/user/tower/test/server1.go:16 (0x211e) 121 | // to: /Users/user/tower/test/server1.go:16 122 | t.File = string(regexp.MustCompile(`\(.+\)$`).ReplaceAll([]byte(t.File), []byte(""))) 123 | trace = append(trace, t) 124 | } 125 | return 126 | } 127 | 128 | func extractAppSnippet(appFile string, curLineNum int) (snippet []Snippet) { 129 | content, err := ioutil.ReadFile(appFile) 130 | if err != nil { 131 | panic(err) 132 | } 133 | lines := strings.Split(string(content), "\n") 134 | for lineNum := curLineNum - SnippetLineNumbers/2; lineNum <= curLineNum+SnippetLineNumbers/2; lineNum++ { 135 | if len(lines) >= lineNum { 136 | c := html.EscapeString(lines[lineNum-1]) 137 | c = strings.Replace(c, "\t", "    ", -1) 138 | c = strings.Replace(c, " ", " ", -1) 139 | snippet = append(snippet, Snippet{lineNum, template.HTML(c), lineNum == curLineNum}) 140 | } 141 | } 142 | return 143 | } 144 | 145 | type ErrorInfo struct { 146 | Title string 147 | Time string 148 | Message template.HTML 149 | 150 | Trace []Trace 151 | ShowTrace bool 152 | 153 | SnippetPath string 154 | Snippet []Snippet 155 | ShowSnippet bool 156 | } 157 | 158 | type Snippet struct { 159 | Number int 160 | Code template.HTML 161 | Current bool 162 | } 163 | 164 | type Trace struct { 165 | File string 166 | Func string 167 | AppFile bool 168 | } 169 | 170 | func (this *ErrorInfo) Prepare() { 171 | this.TrimMessage() 172 | this.Time = time.Now().Format("15:04:05") 173 | } 174 | 175 | func (this *ErrorInfo) TrimMessage() { 176 | html := strings.Join(strings.Split(string(this.Message), "\n"), "
") 177 | this.Message = template.HTML(html) 178 | } 179 | -------------------------------------------------------------------------------- /page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 74 | 75 | 76 |
77 |

{{.Title}} -- {{.Time}}

78 |
79 | 80 |
81 |
82 | {{.Message}} 83 |
84 | 85 | 86 | {{if .ShowSnippet}} 87 |

{{.SnippetPath}}

88 |
89 |
90 | {{range .Snippet}} 91 | {{if .Current}} 92 | {{.Number}} 93 | {{else}} 94 | {{.Number}} 95 | {{end}} 96 |
97 | {{end}} 98 |
99 | 100 |
101 | {{range .Snippet}} 102 | {{if .Current}} 103 | {{.Code}} 104 | {{else}} 105 | {{.Code}} 106 | {{end}} 107 |
108 | {{end}} 109 |
110 |
111 |
112 | {{end}} 113 | 114 | 115 | {{if .ShowTrace}} 116 |

Trace

117 |
118 |
    119 | {{range .Trace}} 120 |
  • 121 | {{if .AppFile}} 122 | {{.File}} 123 | {{end}} 124 | {{if not .AppFile}} 125 | {{.File}} 126 | {{end}} 127 |
    128 | {{.Func}} 129 |
  • 130 | {{end}} 131 |
132 |
133 | {{end}} 134 |
135 | 136 | 137 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/http/httputil" 9 | "net/url" 10 | "regexp" 11 | "strings" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | const ProxyPort = ":8000" 17 | 18 | type Proxy struct { 19 | App *App 20 | ReserveProxy *httputil.ReverseProxy 21 | Watcher *Watcher 22 | FirstRequest *sync.Once 23 | } 24 | 25 | func NewProxy(app *App, watcher *Watcher) (proxy Proxy) { 26 | proxy.App = app 27 | proxy.Watcher = watcher 28 | return 29 | } 30 | 31 | func (this *Proxy) Listen() (err error) { 32 | fmt.Println("== Listening to http://localhost" + ProxyPort) 33 | url, _ := url.ParseRequestURI("http://localhost:" + this.App.Port) 34 | this.ReserveProxy = httputil.NewSingleHostReverseProxy(url) 35 | this.FirstRequest = &sync.Once{} 36 | 37 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 38 | this.ServeRequest(w, r) 39 | }) 40 | return http.ListenAndServe(ProxyPort, nil) 41 | } 42 | 43 | func (this *Proxy) ServeRequest(w http.ResponseWriter, r *http.Request) { 44 | mw := ResponseWriterWrapper{ResponseWriter: w} 45 | this.logStartRequest(r) 46 | defer this.logEndRequest(&mw, r, time.Now()) 47 | 48 | if !this.App.IsRunning() || this.Watcher.Changed { 49 | this.Watcher.Reset() 50 | err := this.App.Restart() 51 | if err != nil { 52 | RenderBuildError(&mw, this.App, err.Error()) 53 | return 54 | } 55 | 56 | this.FirstRequest.Do(func() { 57 | this.ReserveProxy.ServeHTTP(&mw, r) 58 | this.FirstRequest = &sync.Once{} 59 | }) 60 | } 61 | 62 | this.App.LastError = "" 63 | 64 | if !mw.Processed { 65 | this.ReserveProxy.ServeHTTP(&mw, r) 66 | } 67 | 68 | if len(this.App.LastError) != 0 { 69 | RenderAppError(&mw, this.App, this.App.LastError) 70 | } 71 | 72 | if this.App.IsQuit() { 73 | fmt.Println("== App quit unexpetedly") 74 | this.App.Start(false) 75 | RenderError(&mw, this.App, "App quit unexpetedly.") 76 | } 77 | } 78 | 79 | var staticExp = regexp.MustCompile(`\.(png|jpg|jpeg|gif|svg|ico|swf|js|css|html|woff)`) 80 | 81 | func (this *Proxy) isStaticRequest(uri string) bool { 82 | return staticExp.Match([]byte(uri)) 83 | } 84 | 85 | func (this *Proxy) logStartRequest(r *http.Request) { 86 | if !this.isStaticRequest(r.RequestURI) { 87 | fmt.Printf("\n\n\nStarted %s \"%s\" at %s\n", r.Method, r.RequestURI, time.Now().Format("2006-01-02 15:04:05 +700")) 88 | params := this.formatRequestParams(r) 89 | if len(params) > 0 { 90 | fmt.Printf(" Parameters: %s\n", params) 91 | } 92 | } 93 | } 94 | 95 | type MyReadCloser struct { 96 | bytes.Buffer 97 | } 98 | 99 | func (this *MyReadCloser) Close() error { 100 | return nil 101 | } 102 | 103 | func (this *Proxy) formatRequestParams(r *http.Request) string { 104 | // Keep an copy of request Body, and restore it after parsed form. 105 | var b1, b2 MyReadCloser 106 | io.Copy(&b1, r.Body) 107 | io.Copy(&b2, &b1) 108 | r.Body = &b1 109 | r.ParseForm() 110 | r.Body = &b2 111 | 112 | if r.Form == nil { 113 | return "" 114 | } 115 | 116 | var params []string 117 | for key, vals := range r.Form { 118 | var strVals []string 119 | for _, val := range vals { 120 | strVals = append(strVals, `"`+val+`"`) 121 | } 122 | params = append(params, `"`+key+`":[`+strings.Join(strVals, ", ")+`]`) 123 | } 124 | return strings.Join(params, ", ") 125 | } 126 | 127 | func (this *Proxy) logEndRequest(mw *ResponseWriterWrapper, r *http.Request, startTime time.Time) { 128 | if !this.isStaticRequest(r.RequestURI) { 129 | fmt.Printf("Completed %d in %dms\n", mw.Status, time.Since(startTime)/time.Millisecond) 130 | } 131 | } 132 | 133 | // A response Wrapper to capture request's status code. 134 | type ResponseWriterWrapper struct { 135 | Status int 136 | Processed bool 137 | http.ResponseWriter 138 | } 139 | 140 | func (this *ResponseWriterWrapper) WriteHeader(status int) { 141 | this.Status = status 142 | this.Processed = true 143 | this.ResponseWriter.WriteHeader(status) 144 | } 145 | -------------------------------------------------------------------------------- /site/page.graffle: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ActiveLayerIndex 6 | 0 7 | ApplicationVersion 8 | 9 | com.omnigroup.OmniGrafflePro 10 | 139.16.0.171715 11 | 12 | AutoAdjust 13 | 14 | BackgroundGraphic 15 | 16 | Bounds 17 | {{0, 0}, {576, 733}} 18 | Class 19 | SolidGraphic 20 | ID 21 | 2 22 | Style 23 | 24 | shadow 25 | 26 | Draws 27 | NO 28 | 29 | stroke 30 | 31 | Draws 32 | NO 33 | 34 | 35 | 36 | BaseZoom 37 | 0 38 | CanvasOrigin 39 | {0, 0} 40 | ColumnAlign 41 | 1 42 | ColumnSpacing 43 | 36 44 | CreationDate 45 | 2013-02-16 03:18:55 +0000 46 | Creator 47 | Shaoshing 48 | DisplayScale 49 | 1.000 cm = 1.000 cm 50 | GraphDocumentVersion 51 | 8 52 | GraphicsList 53 | 54 | 55 | Bounds 56 | {{0, -7.1999999999999984}, {573, 54}} 57 | Class 58 | ShapedGraphic 59 | ID 60 | 3 61 | Shape 62 | Rectangle 63 | Style 64 | 65 | fill 66 | 67 | Color 68 | 69 | b 70 | 0.94902 71 | g 72 | 0.898039 73 | r 74 | 0.847059 75 | 76 | 77 | shadow 78 | 79 | Draws 80 | NO 81 | 82 | stroke 83 | 84 | Draws 85 | NO 86 | 87 | 88 | Text 89 | 90 | Align 91 | 0 92 | Text 93 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 94 | \cocoascreenfonts1{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;} 95 | {\colortbl;\red255\green255\blue255;} 96 | \deftab720 97 | \pard\pardeftab720\sa400 98 | 99 | \f0\b\fs48 \cf0 Application Error:} 100 | 101 | 102 | 103 | Bounds 104 | {{30.449987792968592, 77.525001525878906}, {203, 14}} 105 | Class 106 | ShapedGraphic 107 | FitText 108 | Vertical 109 | Flow 110 | Resize 111 | ID 112 | 12 113 | Shape 114 | Rectangle 115 | Style 116 | 117 | fill 118 | 119 | Draws 120 | NO 121 | 122 | shadow 123 | 124 | Draws 125 | NO 126 | 127 | stroke 128 | 129 | Draws 130 | NO 131 | 132 | 133 | Text 134 | 135 | Align 136 | 0 137 | Pad 138 | 0 139 | Text 140 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 141 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 142 | {\colortbl;\red255\green255\blue255;} 143 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural 144 | 145 | \f0\fs24 \cf0 runtime error: index out of range} 146 | VerticalPad 147 | 0 148 | 149 | 150 | 151 | Bounds 152 | {{23.249987792968767, 445.60009765624932}, {500, 126}} 153 | Class 154 | ShapedGraphic 155 | ID 156 | 43 157 | Shape 158 | Rectangle 159 | Style 160 | 161 | fill 162 | 163 | Draws 164 | NO 165 | 166 | shadow 167 | 168 | Draws 169 | NO 170 | 171 | stroke 172 | 173 | Draws 174 | NO 175 | 176 | 177 | Text 178 | 179 | Align 180 | 0 181 | Pad 182 | 0 183 | Text 184 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 185 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 186 | {\colortbl;\red255\green255\blue255;} 187 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural 188 | 189 | \f0\b\fs24 \cf0 /usr/local/Cellar/go/1.0.3/src/pkg/net/http/server.go:589 190 | \b0 \ 191 | _func_004: buf.Write(debug.Stack())\ 192 | \ 193 | /usr/local/Cellar/go/1.0.3/src/pkg/runtime/proc.c:1443\ 194 | panic: reflect\'b7call(d->fn, d->args, d->siz);\ 195 | /usr/local/Cellar/go/1.0.3/src/pkg/runtime/runtime.c:128\ 196 | panicstring: runtime\'b7panic(err);\ 197 | /usr/local/Cellar/go/1.0.3/src/pkg/runtime/runtime.c:85\ 198 | panicindex: runtime\'b7panicstring("index out of range");\ 199 | /Users/shaoshing/Projects/golang/src/github.com/shaoshing/tower/test/server1.go:21\ 200 | Error: paths[0] = "index out of range"\ 201 | /usr/local/Cellar/go/1.0.3/src/pkg/net/http/server.go:703\ 202 | HandlerFunc.ServeHTTP: f(w, r)\ 203 | /usr/local/Cellar/go/1.0.3/src/pkg/net/http/server.go:941\ 204 | (*ServeMux).ServeHTTP: mux.handler(r).ServeHTTP(w, r)\ 205 | /usr/local/Cellar/go/1.0.3/src/pkg/net/http/server.go:669\ 206 | (*conn).serve: handler.ServeHTTP(w, w.req)\ 207 | /usr/local/Cellar/go/1.0.3/src/pkg/runtime/proc.c:271\ 208 | goexit: runtime\'b7goexit(void)} 209 | VerticalPad 210 | 0 211 | 212 | Wrap 213 | NO 214 | 215 | 216 | Bounds 217 | {{23.249987792968746, 338.80000000000001}, {203, 20}} 218 | Class 219 | ShapedGraphic 220 | FitText 221 | Vertical 222 | Flow 223 | Resize 224 | FontInfo 225 | 226 | Font 227 | Helvetica 228 | Size 229 | 16 230 | 231 | ID 232 | 14 233 | Shape 234 | Rectangle 235 | Style 236 | 237 | fill 238 | 239 | Draws 240 | NO 241 | 242 | shadow 243 | 244 | Draws 245 | NO 246 | 247 | stroke 248 | 249 | Draws 250 | NO 251 | 252 | 253 | Text 254 | 255 | Align 256 | 0 257 | Pad 258 | 0 259 | Text 260 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 261 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 262 | {\colortbl;\red255\green255\blue255;} 263 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural 264 | 265 | \f0\fs34 \cf0 Trace} 266 | VerticalPad 267 | 0 268 | 269 | 270 | 271 | Bounds 272 | {{30.449987792968592, 129.80000000000001}, {203, 20}} 273 | Class 274 | ShapedGraphic 275 | FitText 276 | Vertical 277 | Flow 278 | Resize 279 | FontInfo 280 | 281 | Font 282 | Helvetica 283 | Size 284 | 16 285 | 286 | ID 287 | 13 288 | Shape 289 | Rectangle 290 | Style 291 | 292 | fill 293 | 294 | Draws 295 | NO 296 | 297 | shadow 298 | 299 | Draws 300 | NO 301 | 302 | stroke 303 | 304 | Draws 305 | NO 306 | 307 | 308 | Text 309 | 310 | Align 311 | 0 312 | Pad 313 | 0 314 | Text 315 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 316 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 317 | {\colortbl;\red255\green255\blue255;} 318 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural 319 | 320 | \f0\fs34 \cf0 test/server1.go} 321 | VerticalPad 322 | 0 323 | 324 | 325 | 326 | Bounds 327 | {{55.84998779296879, 168.39998168945323}, {289, 126}} 328 | Class 329 | ShapedGraphic 330 | ID 331 | 10 332 | Shape 333 | Rectangle 334 | Style 335 | 336 | fill 337 | 338 | Draws 339 | NO 340 | 341 | shadow 342 | 343 | Draws 344 | NO 345 | 346 | stroke 347 | 348 | Draws 349 | NO 350 | 351 | 352 | Text 353 | 354 | Align 355 | 0 356 | Pad 357 | 0 358 | Text 359 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 360 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 361 | {\colortbl;\red255\green255\blue255;} 362 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural 363 | 364 | \f0\fs24 \cf0 panic(errors.New("Panic !!"))\ 365 | \}\ 366 | \ 367 | func Error(w http.ResponseWriter, req *http.Request) \{\ 368 | var paths []string\ 369 | 370 | \b paths[0] = "index out of range"\ 371 | 372 | \b0 \}\ 373 | \ 374 | func main() \{} 375 | VerticalPad 376 | 0 377 | 378 | Wrap 379 | NO 380 | 381 | 382 | Bounds 383 | {{16.599981689452875, 363.20001220703125}, {539.79998779296875, 282.19998168945312}} 384 | Class 385 | ShapedGraphic 386 | ID 387 | 28 388 | Shape 389 | Rectangle 390 | Style 391 | 392 | fill 393 | 394 | Color 395 | 396 | b 397 | 0.950502 398 | g 399 | 0.901433 400 | r 401 | 0.850639 402 | 403 | Draws 404 | NO 405 | 406 | shadow 407 | 408 | Draws 409 | NO 410 | 411 | stroke 412 | 413 | Color 414 | 415 | b 416 | 0.945619 417 | g 418 | 0.901698 419 | r 420 | 0.846022 421 | 422 | CornerRadius 423 | 7 424 | 425 | 426 | Text 427 | 428 | Align 429 | 0 430 | 431 | 432 | 433 | Bounds 434 | {{16.600006103515511, 155.39999694824269}, {539.79998779296875, 152}} 435 | Class 436 | ShapedGraphic 437 | ID 438 | 22 439 | Shape 440 | Rectangle 441 | Style 442 | 443 | fill 444 | 445 | Color 446 | 447 | b 448 | 0.950502 449 | g 450 | 0.901433 451 | r 452 | 0.850639 453 | 454 | Draws 455 | NO 456 | 457 | shadow 458 | 459 | Draws 460 | NO 461 | 462 | stroke 463 | 464 | Color 465 | 466 | b 467 | 0.945619 468 | g 469 | 0.901698 470 | r 471 | 0.846022 472 | 473 | CornerRadius 474 | 7 475 | 476 | 477 | Text 478 | 479 | Align 480 | 0 481 | 482 | 483 | 484 | Bounds 485 | {{23.249987792968739, 168.39999694824235}, {25.150012969970703, 126}} 486 | Class 487 | ShapedGraphic 488 | FitText 489 | Vertical 490 | Flow 491 | Resize 492 | FontInfo 493 | 494 | Color 495 | 496 | b 497 | 0.572549 498 | g 499 | 0.572549 500 | r 501 | 0.572549 502 | 503 | 504 | ID 505 | 11 506 | Shape 507 | Rectangle 508 | Style 509 | 510 | fill 511 | 512 | Color 513 | 514 | b 515 | 0.843252 516 | g 517 | 0.843252 518 | r 519 | 0.843252 520 | 521 | Draws 522 | NO 523 | 524 | shadow 525 | 526 | Draws 527 | NO 528 | 529 | stroke 530 | 531 | Draws 532 | NO 533 | 534 | 535 | Text 536 | 537 | Align 538 | 2 539 | Pad 540 | 4 541 | Text 542 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 543 | \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 544 | {\colortbl;\red255\green255\blue255;\red147\green147\blue147;} 545 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr 546 | 547 | \f0\fs24 \cf2 16\ 548 | 17\ 549 | 18\ 550 | 19\ 551 | 20\ 552 | 553 | \b 21 554 | \b0 \ 555 | 22\ 556 | 23\ 557 | 24} 558 | VerticalPad 559 | 0 560 | 561 | 562 | 563 | GridInfo 564 | 565 | GuidesLocked 566 | NO 567 | GuidesVisible 568 | NO 569 | HPages 570 | 1 571 | ImageCounter 572 | 2 573 | KeepToScale 574 | 575 | Layers 576 | 577 | 578 | Lock 579 | NO 580 | Name 581 | Layer 1 582 | Print 583 | YES 584 | View 585 | YES 586 | 587 | 588 | LayoutInfo 589 | 590 | Animate 591 | NO 592 | circoMinDist 593 | 18 594 | circoSeparation 595 | 0.0 596 | layoutEngine 597 | dot 598 | neatoSeparation 599 | 0.0 600 | twopiSeparation 601 | 0.0 602 | 603 | LinksVisible 604 | NO 605 | MagnetsVisible 606 | NO 607 | MasterSheets 608 | 609 | ModificationDate 610 | 2013-02-16 06:01:39 +0000 611 | Modifier 612 | Shaoshing 613 | NotesVisible 614 | NO 615 | Orientation 616 | 2 617 | OriginVisible 618 | NO 619 | PageBreaks 620 | YES 621 | PrintInfo 622 | 623 | NSBottomMargin 624 | 625 | float 626 | 41 627 | 628 | NSHorizonalPagination 629 | 630 | coded 631 | BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG 632 | 633 | NSLeftMargin 634 | 635 | float 636 | 18 637 | 638 | NSPaperSize 639 | 640 | size 641 | {612, 792} 642 | 643 | NSPrintReverseOrientation 644 | 645 | int 646 | 0 647 | 648 | NSRightMargin 649 | 650 | float 651 | 18 652 | 653 | NSTopMargin 654 | 655 | float 656 | 18 657 | 658 | 659 | PrintOnePage 660 | 661 | ReadOnly 662 | NO 663 | RowAlign 664 | 1 665 | RowSpacing 666 | 36 667 | SheetTitle 668 | Canvas 1 669 | SmartAlignmentGuidesActive 670 | YES 671 | SmartDistanceGuidesActive 672 | YES 673 | UniqueID 674 | 1 675 | UseEntirePage 676 | 677 | VPages 678 | 1 679 | WindowInfo 680 | 681 | CurrentSheet 682 | 0 683 | ExpandedCanvases 684 | 685 | 686 | name 687 | Canvas 1 688 | 689 | 690 | Frame 691 | {{41, 8}, {942, 766}} 692 | ListView 693 | 694 | OutlineWidth 695 | 142 696 | RightSidebar 697 | 698 | ShowRuler 699 | 700 | Sidebar 701 | 702 | SidebarWidth 703 | 120 704 | VisibleRegion 705 | {{-26, 148}, {628, 501.60000000000002}} 706 | Zoom 707 | 1.25 708 | ZoomValues 709 | 710 | 711 | Canvas 1 712 | 1.25 713 | 1.2699999809265137 714 | 715 | 716 | 717 | 718 | 719 | -------------------------------------------------------------------------------- /test/configs/tower.yml: -------------------------------------------------------------------------------- 1 | main: test/server1.go 2 | port: 5000 3 | -------------------------------------------------------------------------------- /test/files/error.go_: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | oops 5 | } 6 | -------------------------------------------------------------------------------- /test/files/server2.go_: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func HelloServer(w http.ResponseWriter, req *http.Request) { 10 | io.WriteString(w, "server 2") 11 | } 12 | 13 | func main() { 14 | http.HandleFunc("/", HelloServer) 15 | err := http.ListenAndServe(":5000", nil) 16 | if err != nil { 17 | log.Fatal("ListenAndServe: ", err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/server1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | func HelloServer(w http.ResponseWriter, req *http.Request) { 12 | io.WriteString(w, "server 1") 13 | } 14 | 15 | func Error(w http.ResponseWriter, req *http.Request) { 16 | var paths []string 17 | paths[0] = "index out of range" 18 | } 19 | 20 | func main() { 21 | http.HandleFunc("/panic", Panic) 22 | http.HandleFunc("/error", Error) 23 | http.HandleFunc("/exit", Exit) 24 | http.HandleFunc("/", HelloServer) 25 | err := http.ListenAndServe(":5000", nil) 26 | if err != nil { 27 | log.Fatal("ListenAndServe: ", err) 28 | } 29 | } 30 | 31 | func Exit(w http.ResponseWriter, req *http.Request) { 32 | os.Exit(0) 33 | } 34 | 35 | func Panic(w http.ResponseWriter, req *http.Request) { 36 | panic(errors.New("Panic !!")) 37 | } 38 | -------------------------------------------------------------------------------- /tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaoshing/tower/03ffdb2f8fe6c73909873f228a02f009bf1f0b8d/tmp/.gitkeep -------------------------------------------------------------------------------- /tower.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | go build -o tmp/tower . 4 | tmp/tower -c test/configs/tower.yml 5 | -------------------------------------------------------------------------------- /tower.yml: -------------------------------------------------------------------------------- 1 | 2 | # file name of the "go run" command 3 | main: main.go 4 | 5 | port: 5000 6 | 7 | # file types to watch for changes in. use "|" to separate multiple types, for example, go|html 8 | watch: go 9 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "time" 8 | ) 9 | 10 | func dialAddress(address string, timeOut int) (err error) { 11 | for { 12 | select { 13 | case <-time.After(1 * time.Second): 14 | _, err = net.Dial("tcp", address) 15 | if err == nil { 16 | return 17 | } 18 | case <-time.After(5 * time.Second): 19 | fmt.Println("== Waiting for " + address) 20 | case <-time.After(time.Duration(timeOut) * time.Second): 21 | return errors.New("Time out") 22 | } 23 | } 24 | return 25 | } 26 | 27 | func mustSuccess(err error) { 28 | if err != nil { 29 | panic(err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /watcher.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/howeyc/fsnotify" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | ) 10 | 11 | const DefaultWatchedFiles = "go" 12 | 13 | type Watcher struct { 14 | WatchedDir string 15 | Changed bool 16 | Watcher *fsnotify.Watcher 17 | FilePattern string 18 | } 19 | 20 | func NewWatcher(dir, filePattern string) (w Watcher) { 21 | w.WatchedDir = dir 22 | w.FilePattern = DefaultWatchedFiles 23 | if len(filePattern) != 0 { 24 | w.FilePattern = filePattern 25 | } 26 | 27 | watcher, err := fsnotify.NewWatcher() 28 | if err != nil { 29 | panic(err) 30 | } 31 | w.Watcher = watcher 32 | 33 | return 34 | } 35 | 36 | func (this *Watcher) Watch() (err error) { 37 | for _, dir := range this.dirsToWatch() { 38 | err = this.Watcher.Watch(dir) 39 | if err != nil { 40 | return 41 | } 42 | } 43 | 44 | expectedFileReg := regexp.MustCompile(`\.(` + this.FilePattern + `)$`) 45 | for { 46 | file := <-this.Watcher.Event 47 | if expectedFileReg.Match([]byte(file.Name)) { 48 | fmt.Println("== Change detected:", file.Name) 49 | this.Changed = true 50 | } 51 | } 52 | return nil 53 | } 54 | 55 | func (this *Watcher) dirsToWatch() (dirs []string) { 56 | ignoredPathReg := regexp.MustCompile(`(public)|(\/\.\w+)|(^\.)|(\.\w+$)`) 57 | matchedDirs := make(map[string]bool) 58 | matchedDirs["./"] = true 59 | filepath.Walk(this.WatchedDir, func(filePath string, info os.FileInfo, e error) (err error) { 60 | if !info.IsDir() || ignoredPathReg.Match([]byte(filePath)) || matchedDirs[filePath] { 61 | return 62 | } 63 | 64 | matchedDirs[filePath] = true 65 | return 66 | }) 67 | 68 | for dir, _ := range matchedDirs { 69 | dirs = append(dirs, dir) 70 | } 71 | return 72 | } 73 | 74 | func (this *Watcher) Reset() { 75 | this.Changed = false 76 | } 77 | --------------------------------------------------------------------------------