diff --git a/index/index.go b/index/index.go index f84d933b..69a4b328 100644 --- a/index/index.go +++ b/index/index.go @@ -13,6 +13,7 @@ import ( "github.com/hound-search/hound/codesearch/index" "github.com/hound-search/hound/codesearch/regexp" + "github.com/hound-search/hound/vcs" ) const ( @@ -115,13 +116,24 @@ func (n *Index) Close() error { return n.idx.Close() } +// Destroy the index and if possible, destroy the index directory func (n *Index) Destroy() error { n.lck.Lock() defer n.lck.Unlock() if err := n.idx.Close(); err != nil { return err } - return n.Ref.Remove() + err := n.Ref.Remove() + if err != nil { + parent := filepath.Dir(n.GetDir()) + if writeable, _ := vcs.IsWriteable(parent); writeable { + // if it seems like we can write to the parent directory, but we can't remove the + // vcs directory, continue to return the error + return err + } + return nil + } + return nil } func (n *Index) GetDir() string { diff --git a/searcher/searcher.go b/searcher/searcher.go index 791ce810..d515c60e 100644 --- a/searcher/searcher.go +++ b/searcher/searcher.go @@ -249,6 +249,40 @@ func buildAndOpenIndex( return index.Open(idxDir) } +// finds an already built index for a new revision. Used for a read only +// replica instance to update the index associated with a repository when +// a new repository revision is pulled +func findNewExistingIndex( + s *Searcher, + dbpath string, + name string, + url string, + rev string, + newRev string, +) (string, bool) { + refs, err := findExistingRefs(dbpath) + if err != nil { + log.Printf("failed to update existing indices (%s): %s", name, err) + return rev, false + } + ref := refs.find(url, newRev) + if ref == nil { + log.Printf("not able to find existing index (%s)", name) + return rev, false + } + idx, err := index.Open(ref.Dir()) + if err != nil { + log.Printf("failed to open existing index (%s): %s", name, err) + return rev, false + } + if err := s.swapIndexes(idx); err != nil { + log.Printf("failed index swap (%s): %s", name, err) + return rev, false + } + + return newRev, true +} + // Simply prints out statistics about the heap. When hound rebuilds a new // index it will expand the heap with a decent amount of garbage. This is // helpful to ensure the heap growth looks sane. @@ -264,7 +298,7 @@ func reportOnMemory() { // Utility function for producing a hex encoded sha1 hash for a string. func hashFor(name string) string { h := sha1.New() - h.Write([]byte(name)) //nolint + h.Write([]byte(name)) //nolint return hex.EncodeToString(h.Sum(nil)) } @@ -365,6 +399,15 @@ func updateAndReindex( if newRev == rev { return rev, false } + if writeable, _ := vcs.IsWriteable(dbpath); !writeable { + // not writeable, let's update our ref list and look for it. + log.Printf( + "dbpath (%s) not writeable for update to (%s), updating existing indices", + dbpath, + name, + ) + return findNewExistingIndex(s, dbpath, name, repo.Url, rev, newRev) + } log.Printf("Rebuilding %s for %s", name, newRev) idx, err := buildAndOpenIndex( @@ -407,7 +450,6 @@ func newSearcher( return nil, err } - rev, err := wd.PullOrClone(vcsDir, repo.Url) if err != nil { return nil, err @@ -441,7 +483,8 @@ func newSearcher( vcsDir, idxDir, repo.Url, - rev) + rev, + ) if err != nil { return nil, err } diff --git a/vcs/vcs.go b/vcs/vcs.go index 26f0d4d8..04394025 100644 --- a/vcs/vcs.go +++ b/vcs/vcs.go @@ -3,6 +3,7 @@ package vcs import ( "fmt" "log" + "math/rand" "os" ) @@ -29,7 +30,6 @@ type Driver interface { // Return a list of filenames that are marked as auto-generated. AutoGeneratedFiles(dir string) []string - } // An API to interact with a vcs working directory. This is @@ -71,11 +71,36 @@ func exists(path string) bool { return true } +// Tests if a path is writeable by creating and then removing a +// temporary file in the directory +func IsWriteable(path string) (bool, error) { + + r := uint64(rand.Uint32())<<32 | uint64(rand.Uint32()) + tmpFile := fmt.Sprintf("tmp-%08x", r) + + file, err := os.CreateTemp(path, tmpFile) + if err != nil { + return false, err + } + + defer os.Remove(file.Name()) + defer file.Close() + + return true, nil +} + // A utility method that carries out the common operation of cloning // if the working directory is absent and pulling otherwise. func (w *WorkDir) PullOrClone(dir, url string) (string, error) { if exists(dir) { - return w.Pull(dir) + writeable, _ := IsWriteable(dir) + // if we can write to the directory, pull + if writeable { + return w.Pull(dir) + } + + // not writeable, just get current revision + return w.HeadRev(dir) } return w.Clone(dir, url) } diff --git a/vcs/vcs_test.go b/vcs/vcs_test.go index 5ef41e6b..ce782486 100644 --- a/vcs/vcs_test.go +++ b/vcs/vcs_test.go @@ -1,6 +1,8 @@ package vcs import ( + "os" + "runtime" "testing" ) @@ -8,7 +10,7 @@ import ( // Just make sure all drivers are tolerant of nil func TestNilConfigs(t *testing.T) { - for name, _ := range drivers { //nolint + for name, _ := range drivers { //nolint d, err := New(name, nil) if err != nil { t.Fatal(err) @@ -19,3 +21,20 @@ func TestNilConfigs(t *testing.T) { } } } + +func TestIsWriteable(t *testing.T) { + dir := t.TempDir() + if writeable, err := IsWriteable(dir); !writeable { + t.Fatalf("%s is not writeable but should be: %s", dir, err) + } + if runtime.GOOS == "windows" { + t.Skip("Skipping testing RO directory in Windows environment") + } + if err := os.Chmod(dir, 0555); err != nil { + t.Fatal(err) + } + + if writeable, err := IsWriteable(dir); writeable { + t.Fatalf("%s is writeable but should not be: %s", dir, err) + } +}