9fans.net/go@v0.0.5/acme/Watch/main.go (about) 1 // Copyright 2012 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Watch runs a command each time any file in the current directory is written. 6 // 7 // Usage: 8 // 9 // Watch cmd [args...] 10 // 11 // Watch opens a new acme window named for the current directory 12 // with a suffix of /+watch. The window shows the execution of the given 13 // command. Each time any file in that directory is Put from within acme, 14 // Watch reexecutes the command and updates the window. 15 // 16 // The command and arguments are joined by spaces and passed to rc(1) 17 // to be interpreted as a shell command line. 18 // 19 // The command is printed at the top of the window, preceded by a "% " prompt. 20 // Changing that line changes the command run each time the window is updated. 21 // Adding other lines beginning with "% " will cause those commands to be run 22 // as well. 23 // 24 // Executing Quit sends a SIGQUIT on systems that support that signal. 25 // (Go programs receiving that signal will dump goroutine stacks and exit.) 26 // 27 // Executing Kill stops any commands being executed. On Unix it sends the commands 28 // a SIGINT, followed 100ms later by a SIGTERM, followed 100ms later by a SIGKILL. 29 // On other systems it sends os.Interrupt followed 100ms later by os.Kill 30 package main // import "9fans.net/go/acme/Watch" 31 32 import ( 33 "bytes" 34 "flag" 35 "fmt" 36 "log" 37 "os" 38 "os/exec" 39 "path" 40 "path/filepath" 41 "regexp" 42 "strings" 43 "sync" 44 "time" 45 "unicode/utf8" 46 47 "9fans.net/go/acme" 48 ) 49 50 var args []string 51 var win *acme.Win 52 var needrun = make(chan bool, 1) 53 var recursive = flag.Bool("r", false, "watch all subdirectories recursively") 54 55 func usage() { 56 fmt.Fprintf(os.Stderr, "usage: Watch [-r] cmd args...\n") 57 os.Exit(2) 58 } 59 60 func main() { 61 log.SetFlags(0) 62 log.SetPrefix("Watch: ") 63 flag.Usage = usage 64 flag.Parse() 65 args = flag.Args() 66 67 var err error 68 win, err = acme.New() 69 if err != nil { 70 log.Fatal(err) 71 } 72 pwd, _ := os.Getwd() 73 pwdSlash := strings.TrimSuffix(pwd, "/") + "/" 74 win.Name(pwdSlash + "+watch") 75 win.Ctl("clean") 76 win.Ctl("dumpdir " + pwd) 77 cmd := "dump Watch" 78 if *recursive { 79 cmd += " -r" 80 } 81 win.Ctl(cmd) 82 win.Fprintf("tag", "Get Kill Quit ") 83 win.Fprintf("body", "%% %s\n", strings.Join(args, " ")) 84 85 needrun <- true 86 go events() 87 go runner(pwd) 88 89 r, err := acme.Log() 90 if err != nil { 91 log.Fatal(err) 92 } 93 for { 94 ev, err := r.Read() 95 if err != nil { 96 log.Fatal(err) 97 } 98 if ev.Op == "put" && (path.Dir(ev.Name) == pwd || *recursive && strings.HasPrefix(ev.Name, pwdSlash)) { 99 select { 100 case needrun <- true: 101 default: 102 } 103 // slow down any runaway loops 104 time.Sleep(100 * time.Millisecond) 105 } 106 } 107 } 108 109 func events() { 110 for e := range win.EventChan() { 111 switch e.C2 { 112 case 'x', 'X': // execute 113 if string(e.Text) == "Get" { 114 select { 115 case needrun <- true: 116 default: 117 } 118 continue 119 } 120 if string(e.Text) == "Kill" { 121 run.Lock() 122 cmd := run.cmd 123 run.kill = true 124 run.Unlock() 125 if cmd != nil { 126 kill(cmd) 127 } 128 continue 129 } 130 if string(e.Text) == "Quit" { 131 run.Lock() 132 cmd := run.cmd 133 run.Unlock() 134 if cmd != nil { 135 quit(cmd) 136 } 137 continue 138 } 139 if string(e.Text) == "Del" { 140 win.Ctl("delete") 141 } 142 } 143 win.WriteEvent(e) 144 } 145 os.Exit(0) 146 } 147 148 var run struct { 149 sync.Mutex 150 id int 151 cmd *exec.Cmd 152 kill bool 153 } 154 155 func runner(dir string) { 156 for range needrun { 157 run.Lock() 158 run.id++ 159 id := run.id 160 lastcmd := run.cmd 161 run.cmd = nil 162 run.kill = false 163 run.Unlock() 164 if lastcmd != nil { 165 kill(lastcmd) 166 } 167 lastcmd = nil 168 169 runSetup(id) 170 go runBackground(id, dir) 171 } 172 } 173 174 var cmdRE = regexp.MustCompile(`(?m)^%[ \t]+[^ \t\n].*\n`) 175 176 func runSetup(id int) { 177 // Remove old output, but leave commands. 178 // Running synchronously in runner, so no need to watch run.id. 179 data, _ := win.ReadAll("body") 180 matches := cmdRE.FindAllIndex(data, -1) 181 if len(matches) == 0 { 182 // reset window 183 win.Addr(",") 184 win.Write("data", nil) 185 win.Write("body", []byte(fmt.Sprintf("%% %s\n", strings.Join(args, " ")))) 186 } else { 187 end, endByte := utf8.RuneCount(data), len(data) 188 for i := len(matches) - 1; i >= 0; i-- { 189 m := matches[i] 190 mEnd := end - utf8.RuneCount(data[m[1]:endByte]) 191 mStart := mEnd - utf8.RuneCount(data[m[0]:m[1]]) 192 if mStart != utf8.RuneCount(data[:m[0]]) || mEnd != utf8.RuneCount(data[:m[1]]) { 193 log.Fatal("bad runes") 194 } 195 if mEnd < end { 196 win.Addr("#%d,#%d", mEnd, end) 197 win.Write("data", nil) 198 } 199 end, endByte = mStart, m[0] 200 } 201 if end > 0 { 202 win.Addr(",#%d", end) 203 win.Write("data", nil) 204 } 205 } 206 win.Addr("#0") 207 } 208 209 func runBackground(id int, dir string) { 210 buf := make([]byte, 4096) 211 run.Lock() 212 for { 213 if id != run.id || run.kill { 214 run.Unlock() 215 return 216 } 217 q0, _, err := win.ReadAddr() 218 if err != nil { 219 log.Fatal(err) 220 } 221 err = win.Addr("#%d/^%%[ \t][^ \t\\n].*\\n/", q0) 222 if err != nil { 223 run.Unlock() 224 log.Print("no command") 225 return 226 } 227 m0, _, err := win.ReadAddr() 228 if m0 < q0 { 229 // wrapped around 230 win.Addr("#%d", q0) 231 win.Write("data", []byte("% \n")) 232 win.Ctl("clean") 233 win.Addr("#%d", m0) 234 win.Ctl("dot=addr") 235 win.Ctl("show") 236 run.Unlock() 237 return 238 } 239 data, err := win.ReadAll("xdata") 240 if err != nil { 241 log.Fatal(err) 242 } 243 run.Unlock() 244 245 line := data[1:] // chop % 246 247 // Find the plan9port rc. 248 // There may be a different rc in the PATH, 249 // but there probably won't be a different 9. 250 // Don't just invoke 9, because it will change 251 // the PATH. 252 var rc string 253 if dir := os.Getenv("PLAN9"); dir != "" { 254 rc = filepath.Join(dir, "bin/rc") 255 } else if nine, err := exec.LookPath("9"); err == nil { 256 rc = filepath.Join(filepath.Dir(nine), "rc") 257 } else { 258 rc = "/usr/local/plan9/bin/rc" 259 } 260 261 cmd := exec.Command(rc, "-c", string(line)) 262 cmd.Dir = dir 263 r, w, err := os.Pipe() 264 if err != nil { 265 log.Fatal(err) 266 } 267 cmd.Stdout = w 268 cmd.Stderr = w 269 isolate(cmd) 270 err = cmd.Start() 271 w.Close() 272 run.Lock() 273 if run.id != id || run.kill { 274 r.Close() 275 run.Unlock() 276 kill(cmd) 277 return 278 } 279 if err != nil { 280 r.Close() 281 win.Fprintf("data", "(exec: %s)\n", err) 282 continue 283 } 284 run.cmd = cmd 285 run.Unlock() 286 287 bol := true 288 for { 289 n, err := r.Read(buf) 290 if err != nil { 291 break 292 } 293 run.Lock() 294 if id == run.id && n > 0 { 295 // Insert leading space in front of % at start of line 296 // to avoid introducing new commands. 297 p := buf[:n] 298 if bol && p[0] == '%' { 299 win.Write("data", []byte(" ")) 300 } 301 for { 302 // invariant: len(p) > 0. 303 // invariant: not at beginning of line in acme window output 304 // (either not at beginning of line in actual output, or leading space printed). 305 i := bytes.Index(p, []byte("\n%")) 306 if i < 0 { 307 break 308 } 309 win.Write("data", p[:i+1]) 310 win.Write("data", []byte(" ")) 311 p = p[i+1:] 312 } 313 win.Write("data", p) 314 bol = p[len(p)-1] == '\n' 315 } 316 run.Unlock() 317 } 318 err = cmd.Wait() 319 run.Lock() 320 if id == run.id { 321 // If output was missing final newline, print trailing backslash and add newline. 322 if !bol { 323 win.Fprintf("data", "\\\n") 324 } 325 if err != nil { 326 win.Fprintf("data", "(%v)\n", err) 327 } 328 } 329 // Continue loop with lock held 330 } 331 }