├── LICENSE ├── README.md ├── communication └── communication.go ├── main.go └── watchclient └── main.go /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Watchserver 3 | =========== 4 | 5 | A simple tcp server that notifies all clients whenever a file is modified. 6 | Includes a simple client which blinks the lights on a keyboard when a it 7 | recieves any notification from the server. 8 | 9 | ## Installation 10 | 11 | go get github.com/lelandbatey/watchserver/... 12 | # Allow watchclient to run as root so it can blink the lights 13 | sudo chown root $(which watchclient) && sudo chmod u+s $(which watchclient) 14 | watchserver /tmp/ 15 | watchclient 16 | echo "what" >> /tmp/example 17 | 18 | -------------------------------------------------------------------------------- /communication/communication.go: -------------------------------------------------------------------------------- 1 | package communication 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "reflect" 7 | "time" 8 | ) 9 | 10 | type Connection struct { 11 | alive bool 12 | conn net.Conn 13 | Errs chan error 14 | Notification chan []byte 15 | } 16 | 17 | func (c *Connection) Alive() bool { 18 | return c.alive 19 | } 20 | 21 | func New(addr string) (*Connection, error) { 22 | con, err := net.Dial("tcp", addr) 23 | if err != nil { 24 | return nil, err 25 | } 26 | rv := Connection{ 27 | alive: true, 28 | conn: con, 29 | Notification: make(chan []byte, 10), 30 | Errs: make(chan error), 31 | } 32 | go watchConnection(&rv) 33 | return &rv, nil 34 | } 35 | 36 | func watchConnection(con *Connection) { 37 | buf := make([]byte, 1) 38 | for { 39 | con.conn.SetDeadline(time.Now().Add(15 * time.Second)) 40 | _, err := con.conn.Read(buf) 41 | if err != nil { 42 | log.Printf("There was an error: %v", err) 43 | con.Errs <- err 44 | err = con.conn.Close() 45 | if err != nil { 46 | log.Printf("There was an error while closing the connection: %v", err) 47 | con.Errs <- err 48 | } 49 | con.alive = false 50 | return 51 | } 52 | if reflect.DeepEqual(buf, []byte{'\x00'}) { 53 | con.Notification <- buf 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/rjeczalik/notify" 11 | flag "github.com/spf13/pflag" 12 | ) 13 | 14 | func isDir(path string) bool { 15 | f, err := os.Open(path) 16 | if err != nil { 17 | return false 18 | } 19 | fi, err := f.Stat() 20 | if err != nil { 21 | return false 22 | } 23 | return fi.IsDir() 24 | } 25 | 26 | // Creates a TCP server which watches a particular file system path provided by 27 | // the user via command line argument. That server accepts any connection and 28 | // whenever it receives any file system event for the directory it's watching, 29 | // it writes a single null byte across all connections. 30 | func main() { 31 | 32 | var host = flag.String("host", "0.0.0.0", "Host to attempt to connect to") 33 | var port = flag.String("port", "6754", "Port to attempt to make connection on") 34 | flag.Parse() 35 | 36 | // Boilerplate for keeping track of client connections 37 | cons := []net.Conn{} 38 | _addcon := func(c net.Conn) { 39 | cons = append(cons, c) 40 | } 41 | _rmcon := func(c net.Conn) { 42 | idx := -1 43 | // Find index of item in collection 44 | for i, v := range cons { 45 | if c == v { 46 | idx = i 47 | } 48 | } 49 | // If item not found, ignore 50 | if idx == -1 { 51 | return 52 | } 53 | // Remove item from collection 54 | cons = append(cons[:idx], cons[idx+1:]...) 55 | } 56 | rm := make(chan net.Conn) 57 | add := make(chan net.Conn) 58 | go func() { 59 | for { 60 | select { 61 | case c := <-add: 62 | _addcon(c) 63 | case c := <-rm: 64 | _rmcon(c) 65 | } 66 | } 67 | }() 68 | 69 | addr := *host + ":" + *port 70 | server, err := net.Listen("tcp", addr) 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | // Listen for new connections 76 | go func() { 77 | for { 78 | c, err := server.Accept() 79 | if c == nil || err != nil { 80 | log.Printf("Failed to accept connection %v for reason %q\n", c, err) 81 | continue 82 | } 83 | log.Printf("Recieved a new connection: %v\n", c.RemoteAddr()) 84 | // Create a goroutine which sends heartbeat informantion. If 85 | // heartbeat fails, close the connection and stop trying to send 86 | // heartbeat so the connection will be cleaned up by the fs notify 87 | // loop. 88 | go func() { 89 | for { 90 | bw, err := c.Write([]byte{'\x01'}) 91 | if err != nil || bw != 1 { 92 | c.Close() 93 | return 94 | } 95 | time.Sleep(10 * time.Second) 96 | } 97 | }() 98 | add <- c 99 | } 100 | }() 101 | 102 | // Set up the filesystem monitoring 103 | event_chan := make(chan notify.EventInfo, 1) 104 | path, err := filepath.Abs(os.Args[1]) 105 | if err != nil { 106 | log.Fatal("Could not find absolute path for %q: %v", os.Args[1], err) 107 | } 108 | if isDir(path) { 109 | path = path + "/..." 110 | } 111 | if err = notify.Watch(path, event_chan, notify.All); err != nil { 112 | log.Fatalf("Could not register notify on location %q: %v", os.Args[1], err) 113 | } 114 | 115 | done := make(chan bool) 116 | // Listen for fs events forever 117 | go func() { 118 | for { 119 | select { 120 | case event := <-event_chan: 121 | badcons := []net.Conn{} 122 | // If we got an event, write a null byte across each connection 123 | for _, c := range cons { 124 | bw, err := c.Write([]byte{'\x00'}) 125 | // If we couldn't do either of those, then the connection 126 | // is bad and we should remove that connection from our 127 | // connection list 128 | if err != nil || bw != 1 { 129 | badcons = append(badcons, c) 130 | log.Println("Marking connection as bad:", c.LocalAddr()) 131 | } 132 | } 133 | // Remove the bad connections 134 | for _, bc := range badcons { 135 | bc.Close() 136 | rm <- bc 137 | } 138 | log.Println("event:", event) 139 | } 140 | } 141 | }() 142 | 143 | <-done 144 | } 145 | -------------------------------------------------------------------------------- /watchclient/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "time" 7 | 8 | "github.com/lelandbatey/blink" 9 | "github.com/lelandbatey/watchserver/communication" 10 | flag "github.com/spf13/pflag" 11 | ) 12 | 13 | func blinker(c chan bool) func() { 14 | return func() { 15 | for { 16 | <-c 17 | blink.Do(200 * time.Millisecond) 18 | time.Sleep(100 * time.Millisecond) 19 | } 20 | } 21 | } 22 | 23 | func readConn(addr string, c net.Conn, blinkchan chan bool) error { 24 | buf := make([]byte, 1) 25 | for { 26 | n, err := c.Read(buf) 27 | if err != nil { 28 | return err 29 | } 30 | log.Printf("Read '%v' bytes from connection\n", n) 31 | blinkchan <- true 32 | } 33 | } 34 | 35 | func main() { 36 | var host = flag.String("host", "127.0.0.1", "Host to attempt to connect to") 37 | var port = flag.String("port", "6754", "Port to attempt to make connection on") 38 | flag.Parse() 39 | 40 | blinkchan := make(chan bool, 10) 41 | go blinker(blinkchan)() 42 | addr := *host + ":" + *port 43 | for { 44 | log.Printf("Attempting to connect to address %q...", addr) 45 | con, err := communication.New(addr) 46 | if err != nil { 47 | log.Printf("Failed to connect to address: %q", addr) 48 | time.Sleep(10 * time.Second) 49 | continue 50 | } 51 | log.Printf("Successfully connected to address: %q", addr) 52 | for { 53 | if !con.Alive() { 54 | log.Printf("Connection died, restarting connection") 55 | break 56 | } 57 | select { 58 | case <-con.Notification: 59 | blinkchan <- true 60 | case err = <-con.Errs: 61 | log.Printf("Error '%v'", err) 62 | } 63 | } 64 | } 65 | } 66 | --------------------------------------------------------------------------------