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  }