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  }