├── .gitignore ├── go.mod ├── go.sum ├── gow_cmd.go ├── gow_const_bsd.go ├── gow_const_linux.go ├── gow_flag.go ├── gow_main.go ├── gow_misc.go ├── gow_opt.go ├── gow_ps_bsd.go ├── gow_ps_darwin.go ├── gow_ps_linux.go ├── gow_ps_ps.go ├── gow_sig.go ├── gow_stdio.go ├── gow_term.go ├── gow_test.go ├── gow_watch_notify.go ├── makefile ├── readme.md ├── staticcheck.conf └── unlicense /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | /* 3 | !/.gitignore 4 | !/*.go 5 | !/readme.md 6 | !/makefile 7 | !/dockerfile 8 | !/staticcheck.conf 9 | !/unlicense -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mitranim/gow 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.0 6 | 7 | require ( 8 | github.com/mitranim/gg v0.1.19 9 | github.com/rjeczalik/notify v0.9.3 10 | golang.org/x/sys v0.31.0 11 | golang.org/x/term v0.30.0 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mitranim/gg v0.1.19 h1:4eTg5lOekYzz5mSBh8J3ruJH08AywMO4VEzJGZTk0gk= 2 | github.com/mitranim/gg v0.1.19/go.mod h1:x2V+nJJOpeMl/XEoHou9zlTvFxYAcGOCqOAKpVkF0Yc= 3 | github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= 4 | github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= 5 | golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 6 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 7 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 8 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 9 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 10 | -------------------------------------------------------------------------------- /gow_cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "sync/atomic" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/mitranim/gg" 11 | ) 12 | 13 | type Cmd struct { 14 | Mained 15 | Count atomic.Int64 16 | } 17 | 18 | func (self *Cmd) Deinit() { 19 | if self.Count.Load() > 0 { 20 | self.Broadcast(syscall.SIGTERM) 21 | } 22 | } 23 | 24 | /* 25 | Note: proc count may change immediately after the call. Decision making at the 26 | callsite must account for this. 27 | */ 28 | func (self *Cmd) IsRunning() bool { return self.Count.Load() == 0 } 29 | 30 | func (self *Cmd) Restart() { 31 | self.Deinit() 32 | 33 | main := self.Main() 34 | opt := main.Opt 35 | cmd := exec.Command(opt.Cmd, opt.Args...) 36 | 37 | if !main.Term.IsActive() { 38 | cmd.Stdin = os.Stdin 39 | } 40 | cmd.Stdout = os.Stdout 41 | cmd.Stderr = os.Stderr 42 | 43 | err := cmd.Start() 44 | if err != nil { 45 | log.Println(`unable to start subcommand:`, err) 46 | return 47 | } 48 | 49 | self.Count.Add(1) 50 | go self.ReportCmd(cmd, time.Now()) 51 | } 52 | 53 | func (self *Cmd) ReportCmd(cmd *exec.Cmd, start time.Time) { 54 | defer self.Count.Add(-1) 55 | opt := self.Main().Opt 56 | opt.LogCmdExit(cmd.Wait(), time.Since(start)) 57 | opt.TermSuf() 58 | } 59 | 60 | /* 61 | Sends the signal to all subprocesses (descendants included). 62 | 63 | Worth mentioning: across all the various Go versions tested (1.11 to 1.24), it 64 | seemed that the `go` commands such as `go run` or `go test` do not forward any 65 | interrupt or kill signals to its subprocess, and neither does `go test`. For 66 | us, this means that terminating the immediate child process is worth very 67 | little; we're concerned with terminating the grand-child processes, which may 68 | be spawned by the common cases `go run`, `go test`, or any replacement commands 69 | from `Opt.Cmd`. 70 | */ 71 | func (self *Cmd) Broadcast(sig syscall.Signal) { 72 | verb := self.Main().Opt.Verb 73 | pids, err := SubPids(os.Getpid(), verb) 74 | if err != nil { 75 | log.Println(err) 76 | return 77 | } 78 | if gg.IsEmpty(pids) { 79 | return 80 | } 81 | 82 | if !verb { 83 | for _, pid := range pids { 84 | gg.Nop1(syscall.Kill(pid, sig)) 85 | return 86 | } 87 | } 88 | 89 | var sent []int 90 | var unsent []int 91 | var errs []error 92 | 93 | for _, pid := range pids { 94 | err := syscall.Kill(pid, sig) 95 | if err != nil { 96 | unsent = append(unsent, pid) 97 | errs = append(errs, err) 98 | } else { 99 | sent = append(sent, pid) 100 | } 101 | } 102 | 103 | if gg.IsEmpty(errs) { 104 | log.Printf( 105 | `sent signal %q to %v subprocesses, pids: %v`, 106 | sig, len(pids), sent, 107 | ) 108 | } else { 109 | log.Printf( 110 | `tried to send signal %q to %v subprocesses, sent to pids: %v, not sent to pids: %v, errors: %q`, 111 | sig, len(pids), sent, unsent, errs, 112 | ) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /gow_const_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || netbsd || openbsd 2 | 3 | package main 4 | 5 | import "golang.org/x/sys/unix" 6 | 7 | const ioctlReadTermios = unix.TIOCGETA 8 | const ioctlWriteTermios = unix.TIOCSETA 9 | -------------------------------------------------------------------------------- /gow_const_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "golang.org/x/sys/unix" 4 | 5 | const ioctlReadTermios = unix.TCGETS 6 | const ioctlWriteTermios = unix.TCSETS 7 | -------------------------------------------------------------------------------- /gow_flag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "github.com/mitranim/gg" 8 | ) 9 | 10 | type FlagStrMultiline string 11 | 12 | func (self *FlagStrMultiline) Parse(src string) error { 13 | *self += FlagStrMultiline(withNewline(REP_SINGLE_MULTI.Replace(src))) 14 | return nil 15 | } 16 | 17 | func (self FlagStrMultiline) Dump(out io.Writer) { 18 | if len(self) > 0 && out != nil { 19 | gg.Nop2(out.Write(gg.ToBytes(self))) 20 | } 21 | } 22 | 23 | type FlagExtensions []string 24 | 25 | func (self *FlagExtensions) Parse(src string) (err error) { 26 | defer gg.Rec(&err) 27 | vals := commaSplit(src) 28 | gg.Each(vals, validateExtension) 29 | gg.Append(self, vals...) 30 | return 31 | } 32 | 33 | func (self FlagExtensions) Allow(path string) bool { 34 | return gg.IsEmpty(self) || gg.Has(self, cleanExtension(path)) 35 | } 36 | 37 | type FlagWatchDirs []string 38 | 39 | func (self *FlagWatchDirs) Parse(src string) error { 40 | gg.Append(self, commaSplit(src)...) 41 | return nil 42 | } 43 | 44 | type FlagIgnoreDirs []string 45 | 46 | func (self *FlagIgnoreDirs) Parse(src string) error { 47 | vals := FlagIgnoreDirs(commaSplit(src)) 48 | vals.Norm() 49 | gg.Append(self, vals...) 50 | return nil 51 | } 52 | 53 | func (self FlagIgnoreDirs) Norm() { gg.MapMut(self, toAbsDirPath) } 54 | 55 | func (self FlagIgnoreDirs) Allow(path string) bool { return !self.Ignore(path) } 56 | 57 | /* 58 | Assumes that the input is an absolute path. 59 | TODO: also ignore if the directory path is an exact match. 60 | */ 61 | func (self FlagIgnoreDirs) Ignore(path string) bool { 62 | return gg.Some(self, func(val string) bool { 63 | return strings.HasPrefix(path, val) 64 | }) 65 | } 66 | 67 | const ( 68 | EchoModeNone EchoMode = 0 69 | EchoModeGow EchoMode = 1 70 | EchoModePreserve EchoMode = 2 71 | ) 72 | 73 | var EchoModes = []EchoMode{ 74 | EchoModeNone, 75 | EchoModeGow, 76 | EchoModePreserve, 77 | } 78 | 79 | type EchoMode byte 80 | 81 | func (self EchoMode) String() string { 82 | switch self { 83 | case EchoModeNone: 84 | return `` 85 | case EchoModeGow: 86 | return `gow` 87 | case EchoModePreserve: 88 | return `preserve` 89 | default: 90 | panic(self.errInvalid()) 91 | } 92 | } 93 | 94 | func (self *EchoMode) Parse(src string) error { 95 | switch src { 96 | case ``: 97 | *self = EchoModeNone 98 | case `gow`: 99 | *self = EchoModeGow 100 | case `preserve`: 101 | *self = EchoModePreserve 102 | default: 103 | return gg.Errf(`unsupported echo mode %q; supported modes: %q`, src, gg.Map(EchoModes, EchoMode.String)) 104 | } 105 | return nil 106 | } 107 | 108 | func (self EchoMode) errInvalid() error { 109 | return gg.Errf(`invalid echo mode %v; valid modes: %v`, self, EchoModes) 110 | } 111 | -------------------------------------------------------------------------------- /gow_main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Go Watch: missing watch mode for the "go" command. Invoked exactly like the 3 | "go" command, but also watches Go files and reruns on changes. 4 | */ 5 | package main 6 | 7 | import ( 8 | l "log" 9 | "os" 10 | "syscall" 11 | 12 | "github.com/mitranim/gg" 13 | ) 14 | 15 | var ( 16 | log = l.New(os.Stderr, `[gow] `, 0) 17 | cwd = gg.Cwd() 18 | ) 19 | 20 | func main() { 21 | var main Main 22 | defer main.Exit() 23 | defer main.Deinit() 24 | main.Init() 25 | main.Run() 26 | } 27 | 28 | type Main struct { 29 | Opt Opt 30 | Cmd Cmd 31 | Stdio Stdio 32 | Watcher Watcher 33 | Term Term 34 | Sig Sig 35 | ChanRestart gg.Chan[struct{}] 36 | ChanKill gg.Chan[syscall.Signal] 37 | Pid int 38 | } 39 | 40 | func (self *Main) Init() { 41 | self.Opt.Init(os.Args[1:]) 42 | self.Term.Init(self) 43 | self.ChanRestart.Init() 44 | self.ChanKill.Init() 45 | self.Cmd.Init(self) 46 | self.Sig.Init(self) 47 | self.WatchInit() 48 | self.Stdio.Init(self) 49 | } 50 | 51 | /* 52 | We MUST call this before exiting because: 53 | 54 | - We modify global OS state: terminal, subprocs. 55 | - OS will NOT auto-cleanup after us. 56 | 57 | Otherwise: 58 | 59 | - Terminal is left in unusable state. 60 | - Subprocs become orphan daemons. 61 | 62 | We MUST call this manually before using `syscall.Kill` or `syscall.Exit` on the 63 | current process. Syscalls terminate the process bypassing Go `defer`. 64 | */ 65 | func (self *Main) Deinit() { 66 | self.Stdio.Deinit() 67 | self.Term.Deinit() 68 | self.WatchDeinit() 69 | self.Sig.Deinit() 70 | self.Cmd.Deinit() 71 | } 72 | 73 | func (self *Main) Run() { 74 | if self.Term.IsActive() { 75 | go self.Stdio.Run() 76 | } 77 | go self.Sig.Run() 78 | go self.WatchRun() 79 | self.CmdRun() 80 | } 81 | 82 | func (self *Main) WatchInit() { 83 | wat := new(WatchNotify) 84 | wat.Init(self) 85 | self.Watcher = wat 86 | } 87 | 88 | func (self *Main) WatchDeinit() { 89 | if self.Watcher != nil { 90 | self.Watcher.Deinit() 91 | self.Watcher = nil 92 | } 93 | } 94 | 95 | func (self *Main) WatchRun() { 96 | if self.Watcher != nil { 97 | self.Watcher.Run() 98 | } 99 | } 100 | 101 | func (self *Main) CmdRun() { 102 | if !self.Opt.Postpone { 103 | self.Cmd.Restart() 104 | } 105 | 106 | for { 107 | select { 108 | case <-self.ChanRestart: 109 | self.Opt.TermInter() 110 | self.Cmd.Restart() 111 | 112 | case sig := <-self.ChanKill: 113 | self.kill(sig) 114 | return 115 | } 116 | } 117 | } 118 | 119 | // Must be deferred. 120 | func (self *Main) Exit() { 121 | err := gg.AnyErrTraced(recover()) 122 | if err != nil { 123 | self.Opt.LogErr(err) 124 | os.Exit(1) 125 | } 126 | os.Exit(0) 127 | } 128 | 129 | func (self *Main) OnFsEvent(event FsEvent) { 130 | if !self.ShouldRestart(event) { 131 | return 132 | } 133 | if self.Opt.Verb { 134 | log.Println(`restarting on FS event:`, event) 135 | } 136 | self.Restart() 137 | } 138 | 139 | func (self *Main) ShouldRestart(event FsEvent) bool { 140 | return event != nil && 141 | !(self.Opt.Lazy && self.Cmd.IsRunning()) && 142 | self.Opt.AllowPath(event.Path()) 143 | } 144 | 145 | func (self *Main) Restart() { self.ChanRestart.SendZeroOpt() } 146 | 147 | func (self *Main) Kill(val syscall.Signal) { self.ChanKill.SendOpt(val) } 148 | 149 | // Must be called only on the main goroutine. 150 | func (self *Main) kill(sig syscall.Signal) { 151 | /** 152 | This should terminate any descendant processes, using their default behavior 153 | for the given signal. If any misbehaving processes do not terminate on a 154 | kill signal, this is out of our hands for now. We could use SIGKILL to 155 | ensure termination, but it's unclear if we should. 156 | */ 157 | self.Cmd.Broadcast(sig) 158 | 159 | /** 160 | This should restore previous terminal state and un-register our custom signal 161 | handling. 162 | */ 163 | self.Deinit() 164 | 165 | /** 166 | Re-send the signal after un-registering our signal handling. If our process is 167 | still running by the time the signal is received, the signal will be handled 168 | by the Go runtime, using the default behavior. Most of the time, this signal 169 | should not be received because after calling this method, we also return 170 | from the main function. 171 | */ 172 | gg.Nop1(syscall.Kill(os.Getpid(), sig)) 173 | } 174 | 175 | func (self *Main) GetEchoMode() EchoMode { 176 | if self.Term.IsActive() { 177 | return self.Opt.Echo 178 | } 179 | return EchoModeNone 180 | } 181 | -------------------------------------------------------------------------------- /gow_misc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | "strings" 9 | "syscall" 10 | 11 | "github.com/mitranim/gg" 12 | ) 13 | 14 | const ( 15 | // These names reflect standard naming and meaning. 16 | // Reference: https://en.wikipedia.org/wiki/Ascii. 17 | // See our re-interpretation below. 18 | ASCII_END_OF_TEXT = 3 // ^C 19 | ASCII_BACKSPACE = 8 // ^H 20 | ASCII_FILE_SEPARATOR = 28 // ^\ 21 | ASCII_DEVICE_CONTROL_2 = 18 // ^R 22 | ASCII_DEVICE_CONTROL_4 = 20 // ^T 23 | ASCII_UNIT_SEPARATOR = 31 // ^- or ^? 24 | ASCII_DELETE = 127 // ^H on MacOS 25 | 26 | // These names reflect our re-interpretation of standard codes. 27 | CODE_INTERRUPT = ASCII_END_OF_TEXT 28 | CODE_QUIT = ASCII_FILE_SEPARATOR 29 | CODE_RESTART = ASCII_DEVICE_CONTROL_2 30 | CODE_STOP = ASCII_DEVICE_CONTROL_4 31 | CODE_PRINT_COMMAND = ASCII_UNIT_SEPARATOR 32 | CODE_PRINT_HELP = ASCII_BACKSPACE 33 | CODE_PRINT_HELP_MACOS = ASCII_DELETE 34 | ) 35 | 36 | const HOTKEY_HELP = `Control codes / hotkeys: 37 | 38 | 3 ^C Kill subprocess with SIGINT. Repeat within 1s to kill gow. 39 | 18 ^R Kill subprocess with SIGTERM, restart. 40 | 20 ^T Kill subprocess with SIGTERM. Repeat within 1s to kill gow. 41 | 28 ^\ Kill subprocess with SIGQUIT. Repeat within 1s to kill gow. 42 | 31 ^- or ^? Print currently running command. 43 | 8 ^H Print hotkey help. 44 | 127 ^H (MacOS) Print hotkey help.` 45 | 46 | var ( 47 | FD_TERM = syscall.Stdin 48 | KILL_SIGS = []syscall.Signal{syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM} 49 | KILL_SIGS_OS = gg.Map(KILL_SIGS, toOsSignal[syscall.Signal]) 50 | KILL_SIG_SET = gg.SetOf(KILL_SIGS...) 51 | RE_WORD = regexp.MustCompile(`^\w+$`) 52 | PATH_SEP = string([]rune{os.PathSeparator}) 53 | 54 | REP_SINGLE_MULTI = strings.NewReplacer( 55 | `\r\n`, gg.Newline, 56 | `\r`, gg.Newline, 57 | `\n`, gg.Newline, 58 | ) 59 | ) 60 | 61 | /* 62 | Making `.main` private reduces the chance of accidental cyclic walking by 63 | reflection tools such as pretty printers. 64 | */ 65 | type Mained struct{ main *Main } 66 | 67 | func (self *Mained) Init(val *Main) { self.main = val } 68 | func (self *Mained) Main() *Main { return self.main } 69 | 70 | /* 71 | Implemented by `notify.EventInfo`. 72 | Path must be an absolute filesystem path. 73 | */ 74 | type FsEvent interface{ Path() string } 75 | 76 | // Implemented by `WatchNotify`. 77 | type Watcher interface { 78 | Init(*Main) 79 | Deinit() 80 | Run() 81 | } 82 | 83 | func commaSplit(val string) []string { 84 | if len(val) <= 0 { 85 | return nil 86 | } 87 | return strings.Split(val, `,`) 88 | } 89 | 90 | func cleanExtension(val string) string { 91 | ext := filepath.Ext(val) 92 | if len(ext) > 0 && ext[0] == '.' { 93 | return ext[1:] 94 | } 95 | return ext 96 | } 97 | 98 | func validateExtension(val string) { 99 | if !RE_WORD.MatchString(val) { 100 | panic(gg.Errf(`invalid extension %q`, val)) 101 | } 102 | } 103 | 104 | func toAbsPath(val string) string { 105 | if !filepath.IsAbs(val) { 106 | val = filepath.Join(cwd, val) 107 | } 108 | return filepath.Clean(val) 109 | } 110 | 111 | func toDirPath(val string) string { 112 | if val == `` || strings.HasSuffix(val, PATH_SEP) { 113 | return val 114 | } 115 | return val + PATH_SEP 116 | } 117 | 118 | func toAbsDirPath(val string) string { return toDirPath(toAbsPath(val)) } 119 | 120 | func toOsSignal[A os.Signal](src A) os.Signal { return src } 121 | 122 | func recLog() { 123 | val := recover() 124 | if val != nil { 125 | log.Println(val) 126 | } 127 | } 128 | 129 | func withNewline[A ~string](val A) A { 130 | if gg.HasNewlineSuffix(val) { 131 | return val 132 | } 133 | return val + A(gg.Newline) 134 | } 135 | 136 | func writeByte[A io.Writer](tar A, char byte) (int, error) { 137 | buf := [1]byte{char} 138 | return tar.Write(gg.NoEscUnsafe(&buf)[:]) 139 | } 140 | -------------------------------------------------------------------------------- /gow_opt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "time" 9 | 10 | "github.com/mitranim/gg" 11 | "golang.org/x/term" 12 | ) 13 | 14 | /* 15 | Determines how we spawn the subprocess and handle its stdin. 16 | If `false`, overrides raw mode. 17 | */ 18 | var IsTty = term.IsTerminal(int(os.Stdin.Fd())) 19 | 20 | func OptDefault() Opt { return gg.FlagParseTo[Opt](nil) } 21 | 22 | type Opt struct { 23 | Args []string `flag:""` 24 | Help bool `flag:"-h" desc:"Print help and exit."` 25 | Cmd string `flag:"-g" init:"go" desc:"Go tool to use."` 26 | Verb bool `flag:"-v" desc:"Verbose logging."` 27 | ClearHard bool `flag:"-c" desc:"Clear terminal on restart."` 28 | ClearSoft bool `flag:"-s" desc:"Soft-clear terminal, keeping scrollback."` 29 | Raw bool `flag:"-r" desc:"Enable hotkeys (via terminal raw mode)."` 30 | Pre FlagStrMultiline `flag:"-P" desc:"Prefix printed BEFORE each run; multi; supports \\n."` 31 | Suf FlagStrMultiline `flag:"-S" desc:"Suffix printed AFTER each run; multi; supports \\n."` 32 | Trace bool `flag:"-t" desc:"Print error trace on exit. Useful for debugging gow."` 33 | Echo EchoMode `flag:"-re" init:"gow" desc:"Stdin echoing in raw mode. Values: \"\" (none), \"gow\", \"preserve\"."` 34 | Lazy bool `flag:"-l" desc:"Lazy mode: restart only when subprocess is not running."` 35 | Postpone bool `flag:"-p" desc:"Postpone first run until FS event or manual ^R."` 36 | Extensions FlagExtensions `flag:"-e" init:"go,mod" desc:"Extensions to watch; multi."` 37 | WatchDirs FlagWatchDirs `flag:"-w" init:"." desc:"Directories to watch, relative to CWD; multi."` 38 | IgnoreDirs FlagIgnoreDirs `flag:"-i" desc:"Ignored directories, relative to CWD; multi."` 39 | } 40 | 41 | func (self *Opt) Init(src []string) { 42 | err := gg.FlagParseCatch(src, self) 43 | if err != nil { 44 | self.LogErr(err) 45 | gg.Write(log.Writer(), gg.Newline) 46 | self.PrintHelp() 47 | os.Exit(1) 48 | } 49 | 50 | if self.Help || gg.Head(self.Args) == `help` { 51 | self.PrintHelp() 52 | os.Exit(0) 53 | } 54 | 55 | if gg.IsEmpty(self.Args) { 56 | self.PrintHelp() 57 | os.Exit(1) 58 | } 59 | 60 | if self.Raw && !IsTty { 61 | self.Raw = false 62 | if self.Verb { 63 | log.Println(`not in an interactive terminal, disabling raw mode and hotkeys`) 64 | } 65 | } 66 | } 67 | 68 | func (self Opt) PrintHelp() { 69 | gg.FlagFmtDefault.Prefix = "\t" 70 | gg.FlagFmtDefault.Head = false 71 | 72 | gg.Nop2(fmt.Fprintf(log.Writer(), `"gow" is the missing watch mode for the "go" command. 73 | Runs an arbitrary "go" subcommand, watches files, and restarts on changes. 74 | 75 | Usage: 76 | 77 | gow 78 | 79 | Examples: 80 | 81 | gow -c -v test -v -count=1 . 82 | ↑ gow_flags ↑ cmd ↑ cmd_flags ↑ cmd_args 83 | 84 | gow run . a b c 85 | gow -c -v -e=go -e=mod -e=html run . 86 | gow -c -v test 87 | gow -c -v install 88 | gow -c -v -w=src -i=.git -i=tar vet 89 | 90 | Flags: 91 | 92 | %v 93 | "Multi" flags can be passed multiple times. 94 | Some also support comma-separated parsing. 95 | 96 | When using "gow" in an interactive terminal, enable hotkey support via "-r". 97 | The flag stands for "raw mode". Avoid this in non-interactive environments, 98 | or when running multiple "gow" concurrently in the same terminal. Examples: 99 | 100 | gow -v -r vet 101 | gow -v -r run . 102 | 103 | %v 104 | `, gg.FlagHelp[Opt](), HOTKEY_HELP)) 105 | } 106 | 107 | func (self Opt) LogErr(err error) { 108 | if err != nil { 109 | if self.Trace { 110 | log.Printf(`%+v`, err) 111 | } else { 112 | log.Println(err) 113 | } 114 | } 115 | } 116 | 117 | func (self Opt) LogCmdExit(err error, dur time.Duration) { 118 | if err == nil { 119 | if self.Verb { 120 | log.Printf(`subprocess done in %v`, dur) 121 | } 122 | return 123 | } 124 | 125 | if self.Verb || !self.ShouldSkipErr(err) { 126 | log.Printf(`subprocess error after %v: %v`, dur, err) 127 | } 128 | } 129 | 130 | /* 131 | `go run` reports exit code to stderr. `go test` reports test failures. 132 | In those cases, we suppress the "exit code" error to avoid redundancy. 133 | */ 134 | func (self Opt) ShouldSkipErr(err error) bool { 135 | head := gg.Head(self.Args) 136 | return (head == `run` || head == `test`) && errors.As(err, new(*exec.ExitError)) 137 | } 138 | 139 | func (self Opt) TermPre() { self.Pre.Dump(log.Writer()) } 140 | 141 | func (self Opt) TermSuf() { self.Suf.Dump(log.Writer()) } 142 | 143 | // TODO more descriptive name. 144 | func (self Opt) TermInter() { 145 | self.TermPre() 146 | self.TermClear() 147 | } 148 | 149 | func (self Opt) TermClear() { 150 | if self.ClearHard { 151 | gg.Write(os.Stdout, TermEscClearHard) 152 | } else if self.ClearSoft { 153 | gg.Write(os.Stdout, TermEscClearSoft) 154 | } 155 | } 156 | 157 | func (self Opt) AllowPath(path string) bool { 158 | return self.Extensions.Allow(path) && self.IgnoreDirs.Allow(path) 159 | } 160 | -------------------------------------------------------------------------------- /gow_ps_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build dragonfly || freebsd || netbsd || openbsd 2 | 3 | package main 4 | 5 | // On BSD, we simply fall back on `ps`. 6 | func SubPids(topPid int, _ bool) ([]int, error) { 7 | return SubPidsViaPs(topPid) 8 | } 9 | -------------------------------------------------------------------------------- /gow_ps_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/mitranim/gg" 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func SubPids(topPid int, verb bool) ([]int, error) { 13 | pid := os.Getpid() 14 | pids, err := SubPidsViaSyscall(pid) 15 | if err == nil { 16 | return pids, nil 17 | } 18 | if verb { 19 | log.Println(`unable to get pids via syscall, falling back on "ps":`, err) 20 | } 21 | return SubPidsViaPs(pid) 22 | } 23 | 24 | func SubPidsViaSyscall(topPid int) ([]int, error) { 25 | // Get all processes. 26 | infos, err := unix.SysctlKinfoProcSlice(`kern.proc.all`) 27 | if err != nil { 28 | return nil, gg.Wrap(err, `failed to get process list`) 29 | } 30 | 31 | // Index child pids by ppid. 32 | ppidToPids := make(map[int][]int, len(infos)) 33 | for _, info := range infos { 34 | pid := int(info.Proc.P_pid) 35 | ppid := int(info.Eproc.Ppid) 36 | ppidToPids[ppid] = append(ppidToPids[ppid], pid) 37 | } 38 | 39 | return procIndexToDescs(ppidToPids, topPid, 0), nil 40 | } 41 | -------------------------------------------------------------------------------- /gow_ps_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/mitranim/gg" 12 | ) 13 | 14 | func SubPids(topPid int, verb bool) ([]int, error) { 15 | pid := os.Getpid() 16 | pids, err := SubPidsViaProcDir(pid) 17 | if err == nil { 18 | return pids, nil 19 | } 20 | if verb { 21 | log.Println(`unable to get pids from "/proc", falling back on "ps":`, err) 22 | } 23 | return SubPidsViaPs(pid) 24 | } 25 | 26 | func SubPidsViaProcDir(topPid int) ([]int, error) { 27 | procEntries, err := os.ReadDir(`/proc`) 28 | if err != nil { 29 | return nil, gg.Wrap(err, `unable to read directory "/proc"`) 30 | } 31 | 32 | // Index of child pids by ppid. 33 | ppidToPids := map[int][]int{} 34 | 35 | for _, entry := range procEntries { 36 | if !entry.IsDir() { 37 | continue 38 | } 39 | 40 | pidStr := entry.Name() 41 | pid, err := strconv.Atoi(pidStr) 42 | if err != nil { 43 | // Non-numeric names don't describe processes, skip. 44 | continue 45 | } 46 | 47 | status, err := os.ReadFile(filepath.Join(`/proc`, pidStr, `status`)) 48 | if err != nil { 49 | // Process may have terminated, skip. 50 | continue 51 | } 52 | 53 | ppid := statusToPpid(gg.ToString(status)) 54 | if ppid != 0 { 55 | ppidToPids[ppid] = append(ppidToPids[ppid], pid) 56 | } 57 | } 58 | 59 | return procIndexToDescs(ppidToPids, topPid, 0), nil 60 | } 61 | 62 | func statusToPpid(src string) (_ int) { 63 | const prefix0 = `PPid:` 64 | const prefix1 = `Ppid:` 65 | 66 | ind := strings.Index(src, prefix0) 67 | if ind >= 0 { 68 | ind += len(prefix0) 69 | } else { 70 | ind = strings.Index(src, prefix1) 71 | if ind < 0 { 72 | return 73 | } 74 | ind += len(prefix1) 75 | } 76 | 77 | src = src[ind:] 78 | ind = strings.Index(src, "\n") 79 | if ind < 0 { 80 | return 81 | } 82 | src = src[:ind] 83 | src = strings.TrimSpace(src) 84 | 85 | out, _ := strconv.Atoi(src) 86 | return out 87 | } 88 | -------------------------------------------------------------------------------- /gow_ps_ps.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "regexp" 6 | "strconv" 7 | 8 | "github.com/mitranim/gg" 9 | ) 10 | 11 | func SubPidsViaPs(topPid int) ([]int, error) { 12 | cmd := exec.Command(`ps`, `-eo`, `pid=,ppid=`) 13 | var buf gg.Buf 14 | cmd.Stdout = &buf 15 | 16 | err := cmd.Run() 17 | if err != nil { 18 | return nil, gg.Wrap(err, `unexpected error: unable to invoke "ps" to get subprocess pids`) 19 | } 20 | 21 | var psPid int 22 | if cmd.Process != nil { 23 | psPid = cmd.Process.Pid 24 | } 25 | return PsOutToSubPids(buf.String(), topPid, psPid), nil 26 | } 27 | 28 | func PsOutToSubPids(src string, topPid, skipPid int) []int { 29 | return procIndexToDescs(procPairsToIndex(psOutToProcPairs(src)), topPid, skipPid) 30 | } 31 | 32 | /* 33 | Takes an index that maps ppids to child pids, and returns a slice of all 34 | descendant pids of the given ppid, skipping `skipPid`, sorted descending. 35 | */ 36 | func procIndexToDescs(src map[int][]int, topPid, skipPid int) []int { 37 | found := gg.Set[int]{} 38 | for _, val := range src[topPid] { 39 | if val != skipPid { 40 | addProcDescs(src, val, found) 41 | } 42 | } 43 | return sortPids(found) 44 | } 45 | 46 | func sortPids(src gg.Set[int]) []int { 47 | out := gg.MapKeys(src) 48 | gg.SortPrim(out) 49 | gg.Reverse(out) 50 | return out 51 | } 52 | 53 | func addProcDescs(src map[int][]int, ppid int, out gg.Set[int]) { 54 | if out.Has(ppid) { 55 | return 56 | } 57 | out.Add(ppid) 58 | for _, val := range src[ppid] { 59 | addProcDescs(src, val, out) 60 | } 61 | } 62 | 63 | func procPairsToIndex(src [][2]int) map[int][]int { 64 | out := map[int][]int{} 65 | for _, val := range src { 66 | out[val[1]] = append(out[val[1]], val[0]) 67 | } 68 | return out 69 | } 70 | 71 | func psOutToProcPairs(src string) [][2]int { 72 | return gg.MapCompact(gg.SplitLines(src), lineToProcPair) 73 | } 74 | 75 | func lineToProcPair(src string) (_ [2]int) { 76 | out := rePsLine.FindStringSubmatch(src) 77 | if len(out) == 3 { 78 | return [2]int{parsePid(out[1]), parsePid(out[2])} 79 | } 80 | return 81 | } 82 | 83 | var rePsLine = regexp.MustCompile(`^\s*(-?\d+)\s+(-?\d+)\s*$`) 84 | 85 | func parsePid(src string) int { 86 | return int(gg.Try1(strconv.ParseInt(src, 10, 32))) 87 | } 88 | -------------------------------------------------------------------------------- /gow_sig.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/mitranim/gg" 9 | ) 10 | 11 | /* 12 | Internal tool for handling OS signals. 13 | */ 14 | type Sig struct { 15 | Mained 16 | Chan gg.Chan[os.Signal] 17 | } 18 | 19 | /* 20 | This removes our custom handling of OS signals, falling back on the default 21 | behavior of the Go runtime. This doesn't bother to stop the `(*Sig).Run` 22 | goroutine. Terminating the entire `gow` process takes care of that. 23 | */ 24 | func (self *Sig) Deinit() { 25 | if self.Chan != nil { 26 | signal.Stop(self.Chan) 27 | } 28 | } 29 | 30 | /* 31 | We override Go's default signal handling to ensure cleanup before exit. 32 | Clean includes restoring the previous terminal state and broadcasting 33 | kill signals to any descendant processes. Without this override, some 34 | OS signals would kill us without allowing us to run cleanup. 35 | 36 | The set of signals registered here MUST match the set of signals explicitly 37 | handled by this program; see below. 38 | */ 39 | func (self *Sig) Init(main *Main) { 40 | self.Mained.Init(main) 41 | self.Chan.InitCap(1) 42 | signal.Notify(self.Chan, KILL_SIGS_OS...) 43 | } 44 | 45 | func (self *Sig) Run() { 46 | main := self.Main() 47 | 48 | for val := range self.Chan { 49 | // Should work on all Unix systems. At the time of writing, 50 | // we're not prepared to support other systems. 51 | sig := val.(syscall.Signal) 52 | 53 | if KILL_SIG_SET.Has(sig) { 54 | if main.Opt.Verb { 55 | log.Println(`received kill signal:`, sig) 56 | } 57 | main.Kill(sig) 58 | continue 59 | } 60 | 61 | if main.Opt.Verb { 62 | log.Println(`received unknown signal:`, sig) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /gow_stdio.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/mitranim/gg" 11 | ) 12 | 13 | // Used by the ^C hotkey. 14 | const DoubleInputDelay = time.Second 15 | 16 | /* 17 | Standard input/output adapter for terminal raw mode. Raw mode allows us to 18 | support our own control codes, but we're also responsible for interpreting 19 | common ASCII codes into OS signals, and optionally for echoing other characters 20 | to stdout. This adapter is unnecessary in non-raw mode where we simply pipe 21 | stdio to/from the child process. 22 | */ 23 | type Stdio struct { 24 | Mained 25 | LastChar byte 26 | LastInst time.Time 27 | } 28 | 29 | /* 30 | Doesn't require special cleanup before stopping `gow`. We run only one stdio 31 | loop, without ever replacing it. 32 | */ 33 | func (*Stdio) Deinit() {} 34 | 35 | /* 36 | TODO: ideally, this should be used only in terminal raw mode. See the comment in 37 | `Cmd.MakeCmd` that explains the issue. 38 | */ 39 | func (self *Stdio) Run() { 40 | self.LastInst = time.Now() 41 | 42 | for { 43 | var buf [1]byte 44 | size, err := os.Stdin.Read(buf[:]) 45 | if errors.Is(err, io.EOF) { 46 | return 47 | } 48 | if err != nil { 49 | log.Println(`error when reading stdin, shutting down stdio:`, err) 50 | return 51 | } 52 | gg.Try(err) 53 | if size <= 0 { 54 | return 55 | } 56 | self.OnByte(buf[0]) 57 | } 58 | } 59 | 60 | /* 61 | Interpret known ASCII codes as OS signals. 62 | Otherwise forward the input to the subprocess. 63 | */ 64 | func (self *Stdio) OnByte(char byte) { 65 | defer recLog() 66 | defer self.AfterByte(char) 67 | 68 | switch char { 69 | case CODE_INTERRUPT: 70 | self.OnCodeInterrupt() 71 | 72 | case CODE_QUIT: 73 | self.OnCodeQuit() 74 | 75 | case CODE_PRINT_COMMAND: 76 | self.OnCodePrintCommand() 77 | 78 | case CODE_RESTART: 79 | self.OnCodeRestart() 80 | 81 | case CODE_STOP: 82 | self.OnCodeStop() 83 | 84 | case CODE_PRINT_HELP, CODE_PRINT_HELP_MACOS: 85 | self.OnCodePrintHelp() 86 | 87 | default: 88 | self.OnByteAny(char) 89 | } 90 | } 91 | 92 | func (self *Stdio) AfterByte(char byte) { 93 | self.LastChar = char 94 | self.LastInst = time.Now() 95 | } 96 | 97 | func (self *Stdio) OnCodeInterrupt() { 98 | self.OnCodeSig(CODE_INTERRUPT, syscall.SIGINT, `^C`) 99 | } 100 | 101 | func (self *Stdio) OnCodeQuit() { 102 | self.OnCodeSig(CODE_QUIT, syscall.SIGQUIT, `^\`) 103 | } 104 | 105 | // TODO include all current subproces with their args. 106 | func (*Stdio) OnCodePrintCommand() { 107 | log.Printf(`current command: %q`, os.Args) 108 | } 109 | 110 | func (*Stdio) OnCodePrintHelp() { log.Println(HOTKEY_HELP) } 111 | 112 | func (self *Stdio) OnCodeRestart() { 113 | main := self.Main() 114 | if main.Opt.Verb { 115 | log.Println(`received ^R, restarting`) 116 | } 117 | main.Restart() 118 | } 119 | 120 | func (self *Stdio) OnCodeStop() { 121 | self.OnCodeSig(CODE_STOP, syscall.SIGTERM, `^T`) 122 | } 123 | 124 | func (self *Stdio) OnByteAny(char byte) { 125 | if self.Main().GetEchoMode() == EchoModeGow { 126 | gg.Nop2(writeByte(os.Stdout, char)) 127 | } 128 | } 129 | 130 | func (self *Stdio) OnCodeSig(code byte, sig syscall.Signal, desc string) { 131 | main := self.Main() 132 | 133 | if self.IsCodeRepeated(code) { 134 | log.Println(`received ` + desc + desc + `, shutting down`) 135 | main.Kill(sig) 136 | return 137 | } 138 | 139 | if main.Opt.Verb { 140 | log.Println(`broadcasting ` + desc + ` to subprocesses; repeat within ` + DoubleInputDelay.String() + ` to kill gow`) 141 | } 142 | main.Cmd.Broadcast(sig) 143 | } 144 | 145 | func (self *Stdio) IsCodeRepeated(char byte) bool { 146 | return self.LastChar == char && time.Since(self.LastInst) < DoubleInputDelay 147 | } 148 | -------------------------------------------------------------------------------- /gow_term.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mitranim/gg" 5 | "golang.org/x/sys/unix" 6 | ) 7 | 8 | // https://en.wikipedia.org/wiki/ANSI_escape_code 9 | const ( 10 | // Standard terminal escape sequence. Same as "\x1b" or "\033". 11 | TermEsc = string(rune(27)) 12 | 13 | // Control Sequence Introducer. Used for other codes. 14 | TermEscCsi = TermEsc + `[` 15 | 16 | // Update cursor position to first row, first column. 17 | TermEscCup = TermEscCsi + `1;1H` 18 | 19 | // Supposed to clear the screen without clearing the scrollback, aka soft 20 | // clear. Seems insufficient on its own, at least in some terminals. 21 | TermEscErase2 = TermEscCsi + `2J` 22 | 23 | // Supposed to clear the screen and the scrollback, aka hard clear. Seems 24 | // insufficient on its own, at least in some terminals. 25 | TermEscErase3 = TermEscCsi + `3J` 26 | 27 | // Supposed to reset the terminal to initial state, aka super hard clear. 28 | // Seems insufficient on its own, at least in some terminals. 29 | TermEscReset = TermEsc + `c` 30 | 31 | // Clear screen without clearing scrollback. 32 | TermEscClearSoft = TermEscCup + TermEscErase2 33 | 34 | // Clear screen AND scrollback. 35 | TermEscClearHard = TermEscCup + TermEscReset + TermEscErase3 36 | ) 37 | 38 | /* 39 | By default, any regular terminal uses what's known as "cooked mode", where the 40 | terminal buffers lines before sending them to the foreground process, and 41 | interprets ASCII control codes on stdin by sending the corresponding OS signals 42 | to the process. 43 | 44 | We switch the terminal into "raw mode", where it mostly forwards inputs to our 45 | process's stdin as-is, and interprets fewer special ASCII codes. This allows to 46 | support special key combinations such as ^R for restarting a subprocess. 47 | Unfortunately, this also makes us responsible for interpreting the rest of the 48 | ASCII control codes. It's possible that our support for those is incomplete. 49 | 50 | The terminal state is shared between all super- and sub-processes. Changes 51 | persist even after our process terminates. We endeavor to restore the previous 52 | state before exiting. 53 | 54 | References: 55 | 56 | https://en.wikibooks.org/wiki/Serial_Programming/termios 57 | 58 | man termios 59 | */ 60 | type Term struct{ State *unix.Termios } 61 | 62 | func (self Term) IsActive() bool { return self.State != nil } 63 | 64 | func (self *Term) Deinit() { 65 | state := self.State 66 | if state == nil { 67 | return 68 | } 69 | self.State = nil 70 | gg.Nop1(unix.IoctlSetTermios(FD_TERM, ioctlWriteTermios, state)) 71 | } 72 | 73 | /* 74 | Goal: 75 | 76 | - Get old terminal state. 77 | - Set new terminal state. 78 | - Remember old terminal state to restore it when exiting. 79 | 80 | Known issue: race condition between multiple concurrent `gow` processes in the 81 | same terminal tab. This is common when running `gow` recipes in a makefile. 82 | Our own `makefile` provides an example of how to avoid using multiple raw modes 83 | concurrently. 84 | */ 85 | func (self *Term) Init(main *Main) { 86 | self.Deinit() 87 | if !main.Opt.Raw { 88 | return 89 | } 90 | 91 | prev, err := unix.IoctlGetTermios(FD_TERM, ioctlReadTermios) 92 | if err != nil { 93 | log.Println(`unable to read terminal state:`, err) 94 | return 95 | } 96 | next := *prev 97 | 98 | /** 99 | In raw mode, we support multiple modes of echoing stdin to stdout. Each 100 | approach has different issues. 101 | 102 | Most terminals, in addition to echoing non-special characters, also have 103 | special support for various ASCII control codes, printing them in the 104 | so-called "caret notation". Codes that send signals are cosmetically printed 105 | as hotkeys such as `^C`, `^R`, and so on. The delete code (127) should cause 106 | the terminal to delete one character before the caret, moving the caret. At 107 | the time of writing, the built-in MacOS terminal doesn't properly handle the 108 | delete character when operating in raw mode, printing it in the caret 109 | notation `^?`, which is a jarring and useless change from non-raw mode. 110 | 111 | The workaround we use by default (mode `EchoModeGow`) is to suppress default 112 | echoing in raw mode, and echo by ourselves in the `Stdio` type. We don't 113 | print the caret notation at all. This works fine for most characters, but at 114 | least in some terminals, deletion via the delete character (see above) 115 | doesn't seem to work when we echo the character as-is. 116 | 117 | Other modes allow to suppress echoing completely or fall back on the buggy 118 | terminal default. 119 | */ 120 | switch main.Opt.Echo { 121 | case EchoModeNone: 122 | next.Lflag &^= unix.ECHO 123 | 124 | case EchoModeGow: 125 | // We suppress the default echoing here and replicate it ourselves in 126 | // `Stdio.OnByteAny`. 127 | next.Lflag &^= unix.ECHO 128 | 129 | case EchoModePreserve: 130 | // The point of this mode is to preserve the previous echo mode of the 131 | // terminal, whatever it was. 132 | 133 | default: 134 | panic(main.Opt.Echo.errInvalid()) 135 | } 136 | 137 | // Don't buffer lines. 138 | next.Lflag &^= unix.ICANON 139 | 140 | // No signals. 141 | next.Lflag &^= unix.ISIG 142 | 143 | // Seems unnecessary on my system. Might be needed elsewhere. 144 | // next.Cflag |= unix.CS8 145 | // next.Cc[unix.VMIN] = 1 146 | // next.Cc[unix.VTIME] = 0 147 | 148 | err = unix.IoctlSetTermios(FD_TERM, ioctlWriteTermios, &next) 149 | if err != nil { 150 | log.Println(`unable to switch terminal to raw mode:`, err) 151 | return 152 | } 153 | 154 | self.State = prev 155 | } 156 | -------------------------------------------------------------------------------- /gow_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/mitranim/gg" 12 | "github.com/mitranim/gg/gtest" 13 | ) 14 | 15 | var testIgnoredPath = filepath.Join(cwd, `ignore3/file.ext3`) 16 | 17 | var testIgnoredEvent = FsEvent(TestFsEvent(testIgnoredPath)) 18 | 19 | var testOpt = func() (tar Opt) { 20 | tar.Init([]string{ 21 | `-e=ext1`, 22 | `-e=ext2`, 23 | `-e=ext3`, 24 | `-i=./ignore1`, 25 | `-i=ignore2`, 26 | `-i=ignore3`, 27 | `some_command`, 28 | }) 29 | return 30 | }() 31 | 32 | var testMain = Main{Opt: testOpt} 33 | 34 | type TestFsEvent string 35 | 36 | func (self TestFsEvent) Path() string { return string(self) } 37 | 38 | func TestFlagExtensions(t *testing.T) { 39 | defer gtest.Catch(t) 40 | 41 | opt := OptDefault() 42 | gtest.Equal(opt.Extensions, FlagExtensions{`go`, `mod`}) 43 | 44 | { 45 | var tar FlagExtensions 46 | gtest.NoErr(tar.Parse(`one,two,three`)) 47 | gtest.Equal(tar, FlagExtensions{`one`, `two`, `three`}) 48 | } 49 | 50 | { 51 | var tar FlagExtensions 52 | gtest.NoErr(tar.Parse(`one`)) 53 | gtest.NoErr(tar.Parse(`two`)) 54 | gtest.NoErr(tar.Parse(`three`)) 55 | gtest.Equal(tar, FlagExtensions{`one`, `two`, `three`}) 56 | } 57 | } 58 | 59 | func testIgnore[Ignore interface { 60 | ~[]string 61 | Norm() 62 | Ignore(string) bool 63 | }](path string, ignore Ignore, exp bool) { 64 | // Even though we invoke this on a value type, this works because the method 65 | // mutates the underlying array, not the slice header itself. 66 | ignore.Norm() 67 | path = filepath.Join(cwd, path) 68 | msg := fmt.Sprintf(`ignore: %q; path: %q`, ignore, path) 69 | 70 | if exp { 71 | gtest.True(ignore.Ignore(path), msg) 72 | } else { 73 | gtest.False(ignore.Ignore(path), msg) 74 | } 75 | } 76 | 77 | func TestFlagIgnoreDirs_Ignore(t *testing.T) { 78 | defer gtest.Catch(t) 79 | 80 | type Ignore = FlagIgnoreDirs 81 | 82 | { 83 | testIgnore(`one.go`, Ignore{}, false) 84 | testIgnore(`one.go`, Ignore{`.`}, true) 85 | testIgnore(`one.go`, Ignore{`*`}, false) 86 | testIgnore(`one.go`, Ignore{`.`, `two`}, true) 87 | testIgnore(`one.go`, Ignore{`*`, `two`}, false) 88 | testIgnore(`one.go`, Ignore{`./*`}, false) 89 | testIgnore(`one.go`, Ignore{`two`}, false) 90 | testIgnore(`one.go`, Ignore{`one.go`}, false) 91 | } 92 | 93 | { 94 | testIgnore(`one/two.go`, Ignore{}, false) 95 | testIgnore(`one/two.go`, Ignore{`one/two.go`}, false) 96 | 97 | testIgnore(`one/two.go`, Ignore{`.`}, true) 98 | testIgnore(`one/two.go`, Ignore{`./.`}, true) 99 | testIgnore(`one/two.go`, Ignore{`././.`}, true) 100 | 101 | testIgnore(`one/two.go`, Ignore{`*`}, false) 102 | testIgnore(`one/two.go`, Ignore{`./*`}, false) 103 | testIgnore(`one/two.go`, Ignore{`*/*`}, false) 104 | testIgnore(`one/two.go`, Ignore{`./*/*`}, false) 105 | 106 | testIgnore(`one/two.go`, Ignore{`one`}, true) 107 | testIgnore(`one/two.go`, Ignore{`./one`}, true) 108 | 109 | testIgnore(`one/two.go`, Ignore{`one/.`}, true) 110 | testIgnore(`one/two.go`, Ignore{`./one/.`}, true) 111 | 112 | testIgnore(`one/two.go`, Ignore{`one/*`}, false) 113 | testIgnore(`one/two.go`, Ignore{`./one/*`}, false) 114 | 115 | testIgnore(`one/two.go`, Ignore{`one/two`}, false) 116 | testIgnore(`one/two.go`, Ignore{`./one/two`}, false) 117 | 118 | testIgnore(`one/two.go`, Ignore{`one/two/.`}, false) 119 | testIgnore(`one/two.go`, Ignore{`./one/two/.`}, false) 120 | 121 | testIgnore(`one/two.go`, Ignore{`one/two/*`}, false) 122 | testIgnore(`one/two.go`, Ignore{`./one/two/*`}, false) 123 | 124 | testIgnore(`one/two.go`, Ignore{`two`}, false) 125 | testIgnore(`one/two.go`, Ignore{`one`, `three`}, true) 126 | testIgnore(`one/two.go`, Ignore{`three`, `one`}, true) 127 | } 128 | 129 | { 130 | testIgnore(`.one/two.go`, Ignore{}, false) 131 | testIgnore(`.one/two.go`, Ignore{`.one`}, true) 132 | testIgnore(`.one/two.go`, Ignore{`./.one`}, true) 133 | testIgnore(`.one/two.go`, Ignore{`.one/.`}, true) 134 | testIgnore(`.one/two.go`, Ignore{`.one/*`}, false) 135 | testIgnore(`.one/two.go`, Ignore{`three`}, false) 136 | } 137 | 138 | { 139 | testIgnore(`.one/two/three.go`, Ignore{`.one`}, true) 140 | testIgnore(`.one/two/three.go`, Ignore{`.one/.`}, true) 141 | testIgnore(`.one/two/three.go`, Ignore{`.one/*`}, false) 142 | testIgnore(`.one/two/three.go`, Ignore{`.one/two`}, true) 143 | testIgnore(`.one/two/three.go`, Ignore{`.one/two/.`}, true) 144 | testIgnore(`.one/two/three.go`, Ignore{`.one/two/*`}, false) 145 | testIgnore(`.one/two/three.go`, Ignore{`.one/three`}, false) 146 | } 147 | } 148 | 149 | func BenchmarkOpt_AllowPath(b *testing.B) { 150 | gtest.False(testOpt.AllowPath(testIgnoredPath)) 151 | b.ResetTimer() 152 | 153 | for ind := 0; ind < b.N; ind++ { 154 | testOpt.AllowPath(testIgnoredPath) 155 | } 156 | } 157 | 158 | func BenchmarkMain_ShouldRestart(b *testing.B) { 159 | gtest.False(testMain.ShouldRestart(testIgnoredEvent)) 160 | b.ResetTimer() 161 | 162 | for ind := 0; ind < b.N; ind++ { 163 | testMain.ShouldRestart(testIgnoredEvent) 164 | } 165 | } 166 | 167 | func Test_PsOutToSubPids(t *testing.T) { 168 | defer gtest.Catch(t) 169 | 170 | const SRC = ` 171 | PID PPID 172 | 1 0 173 | 83 1 174 | 85 1 175 | 91 1 176 | 1634 1 177 | 1909 1 178 | 1951 3967 179 | 1982 3967 180 | PID PPID 181 | 2764 1 182 | 3967 1 183 | 3971 1 184 | 3975 3967 185 | 3976 3967 186 | 3977 3967 187 | 4000 3967 188 | 4008 3967 189 | 4009 3967 190 | 4060 1 191 | 4098 1 192 | 4801 4008 193 | 5125 1 194 | 5627 1 195 | 5682 4009 196 | 5683 1 197 | 10 20 30 40 (should be ignored) 198 | ` 199 | 200 | ppidToPids := procPairsToIndex(psOutToProcPairs(SRC)) 201 | 202 | gtest.Equal(ppidToPids, map[int][]int{ 203 | 0: {1}, 204 | 1: {83, 85, 91, 1634, 1909, 2764, 3967, 3971, 4060, 4098, 5125, 5627, 5683}, 205 | 3967: {1951, 1982, 3975, 3976, 3977, 4000, 4008, 4009}, 206 | 4008: {4801}, 207 | 4009: {5682}, 208 | }) 209 | 210 | const topPid = 3967 211 | const skipPid = 4008 212 | 213 | descs := procIndexToDescs(ppidToPids, topPid, skipPid) 214 | gtest.Equal(PsOutToSubPids(SRC, topPid, skipPid), descs) 215 | 216 | gtest.Equal( 217 | descs, 218 | []int{5682, 4009, 4000, 3977, 3976, 3975, 1982, 1951}, 219 | ) 220 | } 221 | 222 | func TestSubPids(t *testing.T) { 223 | defer gtest.Catch(t) 224 | 225 | /** 226 | Our process doesn't have any children, so we have to spawn one 227 | for testing purposes. 228 | */ 229 | 230 | ctx, cancel := context.WithCancel(context.Background()) 231 | t.Cleanup(cancel) 232 | 233 | cmd := exec.CommandContext(ctx, `sleep`, `1`) 234 | cmd.Start() 235 | 236 | pids := gg.Try1(SubPids(os.Getpid(), true)) 237 | gtest.Len(pids, 1) 238 | } 239 | -------------------------------------------------------------------------------- /gow_watch_notify.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/mitranim/gg" 7 | "github.com/rjeczalik/notify" 8 | ) 9 | 10 | // Implementation of `Watcher` that uses "github.com/rjeczalik/notify". 11 | type WatchNotify struct { 12 | Mained 13 | Done gg.Chan[struct{}] 14 | Events gg.Chan[notify.EventInfo] 15 | } 16 | 17 | func (self *WatchNotify) Init(main *Main) { 18 | self.Mained.Init(main) 19 | self.Done.Init() 20 | self.Events.InitCap(1) 21 | 22 | paths := main.Opt.WatchDirs 23 | verb := main.Opt.Verb && !gg.Equal(paths, OptDefault().WatchDirs) 24 | 25 | for _, path := range paths { 26 | // In "github.com/rjeczalik/notify", the "..." syntax is used to signify 27 | // recursive watching. 28 | path = filepath.Join(path, `...`) 29 | if verb { 30 | log.Printf(`watching %q`, path) 31 | } 32 | gg.Try(notify.Watch(path, self.Events, notify.All)) 33 | } 34 | } 35 | 36 | func (self *WatchNotify) Deinit() { 37 | self.Done.SendZeroOpt() 38 | if self.Events != nil { 39 | notify.Stop(self.Events) 40 | } 41 | } 42 | 43 | func (self WatchNotify) Run() { 44 | main := self.Main() 45 | 46 | for { 47 | select { 48 | case <-self.Done: 49 | return 50 | case event := <-self.Events: 51 | main.OnFsEvent(event) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # README 2 | # 3 | # This file is intended as an example for users of `gow`. It was adapted from a 4 | # larger Go project. At the time of writing, `gow` does not support its own 5 | # configuration files. For complex use cases, users are expected to use Make or 6 | # another similar tool. 7 | # 8 | # Despite being primarily an example, this file contains actual working rules 9 | # convenient for hacking on `gow`. 10 | 11 | # This variable `MAKEFLAGS` is special: it modifies Make's own behaviors. 12 | # 13 | # `--silent` causes Make to execute rules without additional verbose logging. 14 | # Without this, we would have to prefix each line in each rule with `@` to 15 | # suppress logging. 16 | # 17 | # `--always-make` makes all rules "abstract". It causes Make to execute each 18 | # rule without checking for an existing file matching the pattern represented 19 | # by the rule name. This is equivalent to marking every rule with `.PHONY`, but 20 | # keeps our makefile cleaner. In projects where some rule names are file names 21 | # or artifact path patterns, this should be removed, and abstract rules should 22 | # be explicitly marked with `.PHONY`. 23 | MAKEFLAGS := --silent --always-make 24 | 25 | # Shortcut for executing rules concurrently. See usage examples below. 26 | MAKE_CONC := $(MAKE) -j 128 CONC=true clear=$(or $(clear),false) 27 | 28 | VERB ?= $(if $(filter false,$(verb)),,-v) 29 | CLEAR ?= $(if $(filter false,$(clear)),,-c) 30 | GO_SRC ?= . 31 | GO_PKG ?= ./$(or $(pkg),$(GO_SRC)/...) 32 | GO_FLAGS ?= -tags=$(tags) -mod=mod 33 | GO_RUN_ARGS ?= $(GO_FLAGS) $(GO_SRC) $(run) 34 | GO_TEST_FAIL ?= $(if $(filter false,$(fail)),,-failfast) 35 | GO_TEST_SHORT ?= $(if $(filter true,$(short)), -short,) 36 | GO_TEST_FLAGS ?= -count=1 $(GO_FLAGS) $(VERB) $(GO_TEST_FAIL) $(GO_TEST_SHORT) 37 | GO_TEST_PATTERNS ?= -run="$(run)" 38 | GO_TEST_ARGS ?= $(GO_PKG) $(GO_TEST_FLAGS) $(GO_TEST_PATTERNS) 39 | IS_TTY ?= $(shell test -t 0 && printf " ") 40 | 41 | # Only one `gow` per terminal is allowed to use raw mode. 42 | # Otherwise they conflict with each other. 43 | GOW_HOTKEYS ?= -r=$(if $(filter true,$(CONC)),,$(if $(IS_TTY),true,false)) 44 | 45 | GOW_FLAGS ?= $(CLEAR) $(VERB) $(GOW_HOTKEYS) 46 | 47 | # Expects an existing stable version of `gow`. 48 | GOW ?= gow $(GOW_FLAGS) 49 | 50 | watch: 51 | $(MAKE_CONC) dev.test.w dev.vet.w 52 | 53 | # If everything works properly, then we should see a message about the FS event 54 | # (file change), and tests should rerun. And, if everything _really_ works 55 | # properly, modifying local files should trigger FS events in the container, 56 | # causing `gow` to restart the test. 57 | watch.linux: 58 | podman run --rm -it -v=$(PWD):/gow -w=/gow golang:alpine sleep 3 && echo 'package main' > touched.go & go run . -v test -v 59 | 60 | all: 61 | $(MAKE_CONC) test vet 62 | 63 | dev.test.w: 64 | go run $(GO_RUN_ARGS) $(GOW_FLAGS) test $(GO_TEST_FLAGS) 65 | 66 | test.w: 67 | $(GOW) test $(GO_TEST_ARGS) 68 | 69 | test: 70 | go test $(GO_TEST_ARGS) 71 | 72 | dev.vet.w: 73 | go run $(GO_RUN_ARGS) $(GOW_FLAGS) vet $(GO_FLAGS) 74 | 75 | vet.w: 76 | $(GOW) vet $(GO_FLAGS) 77 | 78 | vet: 79 | go vet $(GO_FLAGS) 80 | 81 | check: 82 | gopls check *.go 83 | 84 | run.w: 85 | $(GOW) run $(GO_RUN_ARGS) 86 | 87 | run: 88 | go run $(GO_RUN_ARGS) 89 | 90 | install: 91 | go install $(GO_FLAGS) $(GO_SRC) 92 | 93 | # This uses another watcher because if we're repeatedly reinstalling `gow`, 94 | # then it's because we're experimenting and it's probably broken. 95 | install.w: 96 | watchexec -n -r -d=1ms -- go install $(GO_FLAGS) $(GO_SRC) 97 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | **Go** **W**atch: missing watch mode for the `go` command. It's invoked exactly like `go`, but also watches Go files and reruns on changes. 4 | 5 | Currently requires Unix (MacOS, Linux, BSD). On Windows, runs under WSL. 6 | 7 | ## TOC 8 | 9 | * [Overview](#overview) 10 | * [Why](#why) 11 | * [Installation](#installation) 12 | * [Usage](#usage) 13 | * [Hotkeys](#hotkeys) 14 | * [Configuration](#configuration) 15 | * [Scripting](#scripting) 16 | * [Gotchas](#gotchas) 17 | * [Watching Templates](#watching-templates) 18 | * [Alternatives](#alternatives) 19 | * [License](#license) 20 | * [Misc](#misc) 21 | 22 | ## Why 23 | 24 | Why not other runners, general-purpose watchers, etc: 25 | 26 | * Go-specific, easy to remember. 27 | * Has hotkeys, such as `ctrl+r` to restart! (Opt-in via `-r`.) 28 | * Ignores non-Go files by default. 29 | * Better watcher: recursive, no delays, no polling; uses https://github.com/rjeczalik/notify. 30 | * Silent by default. (Opt-in logging via `-v`.) 31 | * No garbage files. 32 | * Properly clears the terminal on restart. (Opt-in via `-c`.) 33 | * Does not leak subprocesses. 34 | * Minimal dependencies. 35 | 36 | ## Installation 37 | 38 | Make sure you have Go installed, then run this: 39 | 40 | ```sh 41 | go install github.com/mitranim/gow@latest 42 | ``` 43 | 44 | This should download the source and compile the executable into `$GOPATH/bin/gow`. Make sure `$GOPATH/bin` is in your `$PATH` so the shell can discover the `gow` command. For example, my `~/.profile` contains this: 45 | 46 | ```sh 47 | export GOPATH="$HOME/go" 48 | export PATH="$GOPATH/bin:$PATH" 49 | ``` 50 | 51 | Alternatively, you can run the executable using the full path. At the time of writing, `~/go` is the default `$GOPATH` for Go installations. Some systems may have a different one. 52 | 53 | ```sh 54 | ~/go/bin/gow 55 | ``` 56 | 57 | On MacOS, if installation fails with dylib-related errors, you may need to run `xcode-select --install` or install Xcode. This is caused by `gow`'s dependencies, which depend on C. See [#15](https://github.com/mitranim/gow/issues/15). 58 | 59 | ## Usage 60 | 61 | The first argument to `gow`, after the flags, can be any Go subcommand: `build`, `install`, `tool`, you name it. 62 | 63 | ```sh 64 | # Start and restart on change 65 | gow run . 66 | 67 | # Pass args to the program 68 | gow run . arg0 arg1 ... 69 | 70 | # Run subdirectory 71 | gow run ./subdir 72 | 73 | # Vet and re-vet on change; verbose mode is recommended 74 | gow -v vet 75 | 76 | # Clear terminal on restart 77 | gow -c run . 78 | 79 | # Specify file extension to watch 80 | gow -e=go,mod,html run . 81 | 82 | # Enable hotkey support 83 | gow -v -r vet 84 | 85 | # Help 86 | gow -h 87 | ``` 88 | 89 | ## Hotkeys 90 | 91 | The flag `-r` enables hotkey support. Should be used in interactive terminals at the top level, but should be avoided in non-interactive environments (e.g. containers) and when running multiple `gow` concurrently (e.g. orchestrated via Make). 92 | 93 | This mode is only available in a TTY. In this mode, the subprocess is _not_ considered to be in a TTY, and cannot read stdin. Avoid it for programs which need to be interactive by reading user input from stdin. 94 | 95 | Supported control codes with commonly associated hotkeys. Exact keys may vary between terminal apps. For example, `^-` in MacOS Terminal vs `^?` in iTerm2. 96 | 97 | ``` 98 | 3 ^C Kill subprocess with SIGINT. 99 | 18 ^R Kill subprocess with SIGTERM, restart. 100 | 20 ^T Kill subprocess with SIGTERM. 101 | 28 ^\ Kill subprocess with SIGQUIT. 102 | 31 ^- or ^? Print currently running command. 103 | 8 ^H Print help. 104 | 127 ^H (MacOS) Print help. 105 | ``` 106 | 107 | In slightly more technical terms, `gow` switches the terminal into [raw mode](https://en.wikibooks.org/wiki/Serial_Programming/termios), reads from stdin, interprets some ASCII control codes, and forwards the other input to the subprocess as-is. In raw mode, pressing one of these hotkeys causes a terminal to write the corresponding byte to stdin, which is then interpreted by `gow`. 108 | 109 | See the example [`makefile`](makefile) for how to detect if we're about to run one or more `gow`, and enabling raw mode only when safe. 110 | 111 | ## Configuration 112 | 113 | At present, `gow` _does not_ support config files. All configuration is done through CLI flags. This is suitable for small, simple projects. Larger projects typically use a build tool such as Make, which is also sufficient for managing the configuration of `gow`. See the example [`makefile`](makefile). 114 | 115 | ## Scripting 116 | 117 | `gow` invokes an arbitrary executable; by default it invokes `go` which should be installed globally. For some advanced use cases, you may need a custom script. For example, if you want `gow` to run `go generate` before any other `go` operation, create a local shell script `go.sh`: 118 | 119 | ```sh 120 | touch go.sh 121 | chmod +x go.sh 122 | ``` 123 | 124 | ...with the following content: 125 | 126 | ```sh 127 | #!/bin/sh 128 | 129 | go generate && 130 | go $@ 131 | ``` 132 | 133 | To invoke it, use `-g` when running `gow`: 134 | 135 | ```sh 136 | gow -g=./go.sh -v -c run . 137 | ``` 138 | 139 | Alternatively, instead of creating script files, you can write recipes in a makefile; see [Configuration](#configuration) and the example [`makefile`](makefile). 140 | 141 | ## Gotchas 142 | 143 | Enabling hotkeys via `-r` involves switching the terminal into "raw mode"; see [1](https://en.wikibooks.org/wiki/Serial_Programming/termios). As a result, this is _only_ viable when: 144 | 145 | * You run `gow` in an interactive terminal. 146 | * There is only one instance of `gow` in this terminal tab. 147 | * There are no other processes in this tab that use raw mode; examples: 148 | * Editors such as `nano`/`vim`/`emacs`. 149 | * Another `gow` with `-r`. 150 | * The subprocess does not need to read from stdin. 151 | 152 | In Docker, or in any other non-interactive environment, `-r` may produce errors related to terminal state. Examples: 153 | 154 | ``` 155 | > unable to read terminal state 156 | > inappropriate ioctl for device 157 | > operation not supported by device 158 | ``` 159 | 160 | There should be only one `gow -r` per terminal tab. When running multiple `gow` processes in one terminal tab, most should be `gow -r=false`. `gow` processes do not coordinate. If several are attempting to modify the terminal state (from cooked mode to raw mode, then restore), due to a race condition, they may end up "restoring" the wrong state, leaving the terminal in the raw mode at the end. 161 | 162 | See [`makefile`](makefile), particularly the variable `GOW_HOTKEYS`, for how to detect concurrent execution of multiple tasks, and avoid enabling hotkeys / raw mode. 163 | 164 | When `gow` runs in raw mode, the subprocess's stdin is always empty, immediately closed (EOF), and is not a TTY. 165 | 166 | ## Watching Templates 167 | 168 | Many Go programs, such as servers, include template files, and want to recompile those templates on change. 169 | 170 | Easy but slow way: use `gow -e`. 171 | 172 | ```sh 173 | gow -e=go,mod,html run . 174 | ``` 175 | 176 | This restarts your entire app on change to any `.html` file in the current directory or sub-directories. Beware: if the app also generates files with the same extensions, this could cause an infinite restart loop. Ignore any output directories with `-i`: 177 | 178 | ```sh 179 | gow -e=go,mod,html -i=target run . 180 | ``` 181 | 182 | A smarter approach would be to watch the template files from _inside_ the app and recompile them without restarting the entire app. This is out of scope for `gow`. 183 | 184 | Finally, you can use a pure-Go rendering system such as [github.com/mitranim/gax](https://github.com/mitranim/gax). 185 | 186 | ## Alternatives 187 | 188 | For general purpose file watching, consider these excellent tools: 189 | * https://github.com/mattgreen/watchexec 190 | * https://github.com/emcrisostomo/fswatch 191 | 192 | ## TODO 193 | 194 | * Use GitHub Actions to automatically build executables. Research how to publish to various package managers. 195 | * Investigate switching from `golang.org/x/sys/unix` to `golang.org/x/term`, which is higher-level and supports Windows. This may allow `gow` to work on Windows. 196 | * Tried, seems to break stdio. 197 | * Consider having a flag that takes a string and writes that string to the subprocess stdin on each subprocess start. 198 | * Or better: read our own stdin on startup, buffer it, and pass it to the subproc every time. 199 | * Consider using `https://github.com/creack/pty` to allocate a pseudo-terminal for the subprocess when raw mode is enabled _and_ we're in a TTY, then forward any unhandled stdin to the subprocess. 200 | * Tried, didn't work. 201 | * Consider intercepting interrupt in non-raw mode, similar to raw mode. 202 | * Forgot why, probably unnecessary. 203 | 204 | ## License 205 | 206 | https://unlicense.org 207 | -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all", "-ST1003", "-ST1006", "-ST1020", "-ST1021", "-ST1022"] -------------------------------------------------------------------------------- /unlicense: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | --------------------------------------------------------------------------------