github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/cmd/autogazelle/server_unix.go (about) 1 //go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris || windows 2 // +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris windows 3 4 /* Copyright 2018 The Bazel Authors. All rights reserved. 5 6 Licensed under the Apache License, Version 2.0 (the "License"); 7 you may not use this file except in compliance with the License. 8 You may obtain a copy of the License at 9 10 http://www.apache.org/licenses/LICENSE-2.0 11 12 Unless required by applicable law or agreed to in writing, software 13 distributed under the License is distributed on an "AS IS" BASIS, 14 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 See the License for the specific language governing permissions and 16 limitations under the License. 17 */ 18 19 package main 20 21 import ( 22 "io" 23 "log" 24 "net" 25 "os" 26 "os/exec" 27 "path" 28 "path/filepath" 29 "strings" 30 "sync" 31 "time" 32 33 "github.com/fsnotify/fsnotify" 34 ) 35 36 // startServer starts a new server process. This is called by the client. 37 func startServer() error { 38 exe, err := os.Executable() 39 if err != nil { 40 return err 41 } 42 args := []string{"-server"} 43 args = append(args, os.Args[1:]...) 44 cmd := exec.Command(exe, args...) 45 log.Printf("starting server: %s", strings.Join(cmd.Args, " ")) 46 if err := cmd.Start(); err != nil { 47 return err 48 } 49 if err := cmd.Process.Release(); err != nil { 50 return err 51 } 52 return nil 53 } 54 55 // runServer performs the main work of the server. Once started, the server 56 // will: 57 // 58 // * Copy BUILD.in and BUILD.bazel.in files to BUILD and BUILD.bazel. 59 // * Watch for file system writes in the whole repository. 60 // * Listen for clients on a UNIX-domain socket. 61 // 62 // When the server accepts a connection, it runs Gazelle. On the first run, 63 // it runs Gazelle on the entire repository. On subsequent runs, it runs 64 // Gazelle only in directories that have changed. 65 // 66 // The server stops after being idle for a while. It can also be stopped 67 // with SIGINT or SIGTERM. 68 func runServer() error { 69 // Begin logging to the log file. 70 logFile, err := os.OpenFile(*logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o666) 71 if err != nil { 72 return err 73 } 74 defer logFile.Close() 75 log.SetOutput(logFile) 76 77 // Start listening on the socket before other initialization work. The client 78 // will dial immediately after starting the server, and we don't want 79 // the client to time out. 80 os.Remove(*socketPath) 81 ln, err := net.Listen("unix", *socketPath) 82 if err != nil { 83 return err 84 } 85 uln := ln.(*net.UnixListener) 86 uln.SetUnlinkOnClose(true) 87 defer ln.Close() 88 if err := uln.SetDeadline(time.Now().Add(*serverTimeout)); err != nil { 89 return err 90 } 91 log.Printf("started server with pid %d", os.Getpid()) 92 93 // Copy BUILD.in files to BUILD. 94 restoreBuildFilesInRepo() 95 96 // Listen for file writes within the repository. 97 cancelWatch, err := watchDir(".", recordWrite) 98 isWatching := err == nil 99 if err != nil { 100 log.Print(err) 101 } 102 if isWatching { 103 defer cancelWatch() 104 } 105 106 // Wait for clients to connect. Each time the client connects, we run 107 // gazelle, either in the whole repository or in changed directories. 108 mode := fullMode 109 for { 110 c, err := ln.Accept() 111 if err != nil { 112 if operr, ok := err.(*net.OpError); ok { 113 if operr.Timeout() { 114 return nil 115 } 116 if operr.Temporary() { 117 log.Printf("temporary watch error: %v", err) 118 continue 119 } 120 } 121 return err 122 } 123 124 log.SetOutput(io.MultiWriter(c, logFile)) 125 dirs := getAndClearWrittenDirs() 126 for _, dir := range dirs { 127 restoreBuildFilesInDir(dir) 128 } 129 if err := runGazelle(mode, dirs); err != nil { 130 log.Print(err) 131 } 132 log.SetOutput(logFile) 133 c.Close() 134 if isWatching { 135 mode = fastMode 136 } 137 } 138 } 139 140 // watchDir listens for file system changes in root and its 141 // subdirectories. The record function is called with directories whose 142 // contents have changed. New directories are watched recursively. 143 // The returned cancel function may be called to stop watching. 144 func watchDir(root string, record func(string)) (cancel func(), err error) { 145 w, err := fsnotify.NewWatcher() 146 if err != nil { 147 return nil, err 148 } 149 150 dirs, errs := listDirs(root) 151 for _, err := range errs { 152 log.Print(err) 153 } 154 gitDir := filepath.Join(root, ".git") 155 for _, dir := range dirs { 156 if dir == gitDir { 157 continue 158 } 159 if err := w.Add(dir); err != nil { 160 log.Print(err) 161 } 162 } 163 164 done := make(chan struct{}) 165 go func() { 166 for { 167 select { 168 case ev := <-w.Events: 169 if shouldIgnore(ev.Name) { 170 continue 171 } 172 if ev.Op == fsnotify.Create { 173 if st, err := os.Lstat(ev.Name); err != nil { 174 log.Print(err) 175 } else if st.IsDir() { 176 dirs, errs := listDirs(ev.Name) 177 for _, err := range errs { 178 log.Print(err) 179 } 180 for _, dir := range dirs { 181 if err := w.Add(dir); err != nil { 182 log.Print(err) 183 } 184 recordWrite(dir) 185 } 186 } 187 } else { 188 recordWrite(filepath.Dir(ev.Name)) 189 } 190 case err := <-w.Errors: 191 log.Print(err) 192 case <-done: 193 if err := w.Close(); err != nil { 194 log.Print(err) 195 } 196 return 197 } 198 } 199 }() 200 return func() { close(done) }, nil 201 } 202 203 // listDirs returns a slice containing all the subdirectories under dir, 204 // including dir itself. 205 func listDirs(dir string) ([]string, []error) { 206 var dirs []string 207 var errs []error 208 err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 209 if err != nil { 210 errs = append(errs, err) 211 return nil 212 } 213 if info.IsDir() { 214 dirs = append(dirs, path) 215 } 216 return nil 217 }) 218 if err != nil { 219 errs = append(errs, err) 220 } 221 return dirs, errs 222 } 223 224 // shouldIgnore returns whether a write to the given file should be ignored 225 // because they were caused by gazelle or autogazelle or something unrelated 226 // to the build. 227 func shouldIgnore(p string) bool { 228 p = strings.TrimPrefix(filepath.ToSlash(p), "./") 229 base := path.Base(p) 230 return strings.HasPrefix(p, "tools/") || base == ".git" || base == "BUILD" || base == "BUILD.bazel" 231 } 232 233 var ( 234 dirSetMutex sync.Mutex 235 dirSet = map[string]bool{} 236 ) 237 238 // recordWrite records that a directory has been modified and that its build 239 // file should be updated the next time gazelle runs. 240 func recordWrite(path string) { 241 dirSetMutex.Lock() 242 defer dirSetMutex.Unlock() 243 dirSet[path] = true 244 } 245 246 // getAndClearWrittenDirs retrieves a list of directories that have been 247 // modified since the last time getAndClearWrittenDirs was called. 248 func getAndClearWrittenDirs() []string { 249 dirSetMutex.Lock() 250 defer dirSetMutex.Unlock() 251 dirs := make([]string, 0, len(dirSet)) 252 for d := range dirSet { 253 dirs = append(dirs, d) 254 } 255 dirSet = make(map[string]bool) 256 return dirs 257 }