diff --git a/lock.json b/lock.json index 3f65e44fb7..e5411840e2 100644 --- a/lock.json +++ b/lock.json @@ -1,5 +1,5 @@ { - "memo": "56093ed07896d0d5c47ee3472d6ad16e9a7e6826598364caf01ac30dc9c277c5", + "memo": "0c77f1de5f325906db6daf5260193d7de97af45498bc7155689395696cfd7c1b", "projects": [ { "name": "github.com/Masterminds/semver", @@ -36,7 +36,7 @@ { "name": "github.com/sdboyer/gps", "branch": "master", - "revision": "b27173f3bf78a69a70dba35122ce24eb1679a53a", + "revision": "88027dbe5e8fb2a2764036d1858f498a9aa6bf1c", "packages": [ "." ] diff --git a/vendor/github.com/sdboyer/gps/hash.go b/vendor/github.com/sdboyer/gps/hash.go index c8bd4642ba..5dc51665e9 100644 --- a/vendor/github.com/sdboyer/gps/hash.go +++ b/vendor/github.com/sdboyer/gps/hash.go @@ -16,14 +16,21 @@ import ( // unnecessary. // // (Basically, this is for memoization.) -func (s *solver) HashInputs() []byte { +func (s *solver) HashInputs() (digest []byte) { + buf := new(bytes.Buffer) + s.writeHashingInputs(buf) + + hd := sha256.Sum256(buf.Bytes()) + digest = hd[:] + return +} + +func (s *solver) writeHashingInputs(buf *bytes.Buffer) { // Apply overrides to the constraints from the root. Otherwise, the hash // would be computed on the basis of a constraint from root that doesn't // actually affect solving. p := s.ovr.overrideAll(s.rm.DependencyConstraints().merge(s.rm.TestDependencyConstraints())) - // Build up a buffer of all the inputs. - buf := new(bytes.Buffer) for _, pd := range p { buf.WriteString(string(pd.Ident.ProjectRoot)) buf.WriteString(pd.Ident.Source) @@ -103,9 +110,18 @@ func (s *solver) HashInputs() []byte { an, av := s.b.AnalyzerInfo() buf.WriteString(an) buf.WriteString(av.String()) +} - hd := sha256.Sum256(buf.Bytes()) - return hd[:] +// HashingInputsAsString returns the raw input data used by Solver.HashInputs() +// as a string. +// +// This is primarily intended for debugging purposes. +func HashingInputsAsString(s Solver) string { + ts := s.(*solver) + buf := new(bytes.Buffer) + ts.writeHashingInputs(buf) + + return buf.String() } type sortPackageOrErr []PackageOrErr diff --git a/vendor/github.com/sdboyer/gps/hash_test.go b/vendor/github.com/sdboyer/gps/hash_test.go index 2aa8fb9d8b..d55f2de431 100644 --- a/vendor/github.com/sdboyer/gps/hash_test.go +++ b/vendor/github.com/sdboyer/gps/hash_test.go @@ -3,6 +3,7 @@ package gps import ( "bytes" "crypto/sha256" + "strings" "testing" ) @@ -42,7 +43,12 @@ func TestHashInputs(t *testing.T) { correct := h.Sum(nil) if !bytes.Equal(dig, correct) { - t.Errorf("Hashes are not equal") + t.Error("Hashes are not equal") + } + + fixstr, hisstr := strings.Join(elems, ""), HashingInputsAsString(s) + if fixstr != hisstr { + t.Errorf("Hashing inputs not equal:\n\t(GOT) %s\n\t(WNT) %s", hisstr, fixstr) } } @@ -94,6 +100,11 @@ func TestHashInputsReqsIgs(t *testing.T) { t.Errorf("Hashes are not equal") } + fixstr, hisstr := strings.Join(elems, ""), HashingInputsAsString(s) + if fixstr != hisstr { + t.Errorf("Hashing inputs not equal:\n\t(GOT) %s\n\t(WNT) %s", hisstr, fixstr) + } + // Add requires rm.req = map[string]bool{ "baz": true, @@ -137,6 +148,11 @@ func TestHashInputsReqsIgs(t *testing.T) { t.Errorf("Hashes are not equal") } + fixstr, hisstr = strings.Join(elems, ""), HashingInputsAsString(s) + if fixstr != hisstr { + t.Errorf("Hashing inputs not equal:\n\t(GOT) %s\n\t(WNT) %s", hisstr, fixstr) + } + // remove ignores, just test requires alone rm.ig = nil params.Manifest = rm @@ -173,6 +189,11 @@ func TestHashInputsReqsIgs(t *testing.T) { if !bytes.Equal(dig, correct) { t.Errorf("Hashes are not equal") } + + fixstr, hisstr = strings.Join(elems, ""), HashingInputsAsString(s) + if fixstr != hisstr { + t.Errorf("Hashing inputs not equal:\n\t(GOT) %s\n\t(WNT) %s", hisstr, fixstr) + } } func TestHashInputsOverrides(t *testing.T) { @@ -224,6 +245,11 @@ func TestHashInputsOverrides(t *testing.T) { t.Errorf("Hashes are not equal") } + fixstr, hisstr := strings.Join(elems, ""), HashingInputsAsString(s) + if fixstr != hisstr { + t.Errorf("Hashing inputs not equal:\n\t(GOT) %s\n\t(WNT) %s", hisstr, fixstr) + } + // Override not in root, just with constraint rm.ovr["d"] = ProjectProperties{ Constraint: NewBranch("foobranch"), @@ -257,6 +283,11 @@ func TestHashInputsOverrides(t *testing.T) { t.Errorf("Hashes are not equal") } + fixstr, hisstr = strings.Join(elems, ""), HashingInputsAsString(s) + if fixstr != hisstr { + t.Errorf("Hashing inputs not equal:\n\t(GOT) %s\n\t(WNT) %s", hisstr, fixstr) + } + // Override not in root, both constraint and network name rm.ovr["e"] = ProjectProperties{ Source: "groucho", @@ -294,6 +325,11 @@ func TestHashInputsOverrides(t *testing.T) { t.Errorf("Hashes are not equal") } + fixstr, hisstr = strings.Join(elems, ""), HashingInputsAsString(s) + if fixstr != hisstr { + t.Errorf("Hashing inputs not equal:\n\t(GOT) %s\n\t(WNT) %s", hisstr, fixstr) + } + // Override in root, just constraint rm.ovr["a"] = ProjectProperties{ Constraint: NewVersion("fluglehorn"), @@ -332,6 +368,11 @@ func TestHashInputsOverrides(t *testing.T) { t.Errorf("Hashes are not equal") } + fixstr, hisstr = strings.Join(elems, ""), HashingInputsAsString(s) + if fixstr != hisstr { + t.Errorf("Hashing inputs not equal:\n\t(GOT) %s\n\t(WNT) %s", hisstr, fixstr) + } + // Override in root, only network name rm.ovr["a"] = ProjectProperties{ Source: "nota", @@ -371,6 +412,11 @@ func TestHashInputsOverrides(t *testing.T) { t.Errorf("Hashes are not equal") } + fixstr, hisstr = strings.Join(elems, ""), HashingInputsAsString(s) + if fixstr != hisstr { + t.Errorf("Hashing inputs not equal:\n\t(GOT) %s\n\t(WNT) %s", hisstr, fixstr) + } + // Override in root, network name and constraint rm.ovr["a"] = ProjectProperties{ Source: "nota", @@ -411,4 +457,9 @@ func TestHashInputsOverrides(t *testing.T) { if !bytes.Equal(dig, correct) { t.Errorf("Hashes are not equal") } + + fixstr, hisstr = strings.Join(elems, ""), HashingInputsAsString(s) + if fixstr != hisstr { + t.Errorf("Hashing inputs not equal:\n\t(GOT) %s\n\t(WNT) %s", hisstr, fixstr) + } } diff --git a/vendor/github.com/sdboyer/gps/lock.go b/vendor/github.com/sdboyer/gps/lock.go index a349761c74..bbcdbf5708 100644 --- a/vendor/github.com/sdboyer/gps/lock.go +++ b/vendor/github.com/sdboyer/gps/lock.go @@ -167,7 +167,14 @@ func (lp LockedProject) Eq(lp2 LockedProject) bool { } } - if !lp.v.Matches(lp2.v) { + v1n := lp.v == nil + v2n := lp2.v == nil + + if v1n != v2n { + return false + } + + if !v1n && !lp.v.Matches(lp2.v) { return false } diff --git a/vendor/github.com/sdboyer/gps/lock_test.go b/vendor/github.com/sdboyer/gps/lock_test.go index f462b224e9..a65179be89 100644 --- a/vendor/github.com/sdboyer/gps/lock_test.go +++ b/vendor/github.com/sdboyer/gps/lock_test.go @@ -34,6 +34,7 @@ func TestLockedProjectsEq(t *testing.T) { NewLockedProject(mkPI("github.com/sdboyer/gps"), NewVersion("v0.10.0"), []string{"flugle", "gps"}), NewLockedProject(mkPI("github.com/sdboyer/gps"), NewVersion("v0.10.0").Is("278a227dfc3d595a33a77ff3f841fd8ca1bc8cd0"), []string{"gps"}), NewLockedProject(mkPI("github.com/sdboyer/gps"), NewVersion("v0.11.0"), []string{"gps"}), + NewLockedProject(mkPI("github.com/sdboyer/gps"), Revision("278a227dfc3d595a33a77ff3f841fd8ca1bc8cd0"), []string{"gps"}), } fix := []struct { @@ -49,6 +50,8 @@ func TestLockedProjectsEq(t *testing.T) { {0, 2, false, "should not eq when other pkg list is longer"}, {2, 4, false, "should not eq when pkg lists are out of order"}, {0, 3, false, "should not eq totally different lp"}, + {7, 7, true, "should eq with only rev"}, + {5, 7, false, "should not eq when only rev matches"}, } for _, f := range fix { diff --git a/vendor/github.com/sdboyer/gps/manager_test.go b/vendor/github.com/sdboyer/gps/manager_test.go index 4dbf75c6f6..da52df09fd 100644 --- a/vendor/github.com/sdboyer/gps/manager_test.go +++ b/vendor/github.com/sdboyer/gps/manager_test.go @@ -9,6 +9,7 @@ import ( "runtime" "sync" "testing" + "time" "github.com/Masterminds/semver" ) @@ -643,3 +644,157 @@ func TestMultiFetchThreadsafe(t *testing.T) { } wg.Wait() } + +func TestErrAfterRelease(t *testing.T) { + sm, clean := mkNaiveSM(t) + clean() + id := ProjectIdentifier{} + + _, err := sm.SourceExists(id) + if err == nil { + t.Errorf("SourceExists did not error after calling Release()") + } else if terr, ok := err.(smIsReleased); !ok { + t.Errorf("SourceExists errored after Release(), but with unexpected error: %T %s", terr, terr.Error()) + } + + err = sm.SyncSourceFor(id) + if err == nil { + t.Errorf("SyncSourceFor did not error after calling Release()") + } else if terr, ok := err.(smIsReleased); !ok { + t.Errorf("SyncSourceFor errored after Release(), but with unexpected error: %T %s", terr, terr.Error()) + } + + _, err = sm.ListVersions(id) + if err == nil { + t.Errorf("ListVersions did not error after calling Release()") + } else if terr, ok := err.(smIsReleased); !ok { + t.Errorf("ListVersions errored after Release(), but with unexpected error: %T %s", terr, terr.Error()) + } + + _, err = sm.RevisionPresentIn(id, "") + if err == nil { + t.Errorf("RevisionPresentIn did not error after calling Release()") + } else if terr, ok := err.(smIsReleased); !ok { + t.Errorf("RevisionPresentIn errored after Release(), but with unexpected error: %T %s", terr, terr.Error()) + } + + _, err = sm.ListPackages(id, nil) + if err == nil { + t.Errorf("ListPackages did not error after calling Release()") + } else if terr, ok := err.(smIsReleased); !ok { + t.Errorf("ListPackages errored after Release(), but with unexpected error: %T %s", terr, terr.Error()) + } + + _, _, err = sm.GetManifestAndLock(id, nil) + if err == nil { + t.Errorf("GetManifestAndLock did not error after calling Release()") + } else if terr, ok := err.(smIsReleased); !ok { + t.Errorf("GetManifestAndLock errored after Release(), but with unexpected error: %T %s", terr, terr.Error()) + } + + err = sm.ExportProject(id, nil, "") + if err == nil { + t.Errorf("ExportProject did not error after calling Release()") + } else if terr, ok := err.(smIsReleased); !ok { + t.Errorf("ExportProject errored after Release(), but with unexpected error: %T %s", terr, terr.Error()) + } + + _, err = sm.DeduceProjectRoot("") + if err == nil { + t.Errorf("DeduceProjectRoot did not error after calling Release()") + } else if terr, ok := err.(smIsReleased); !ok { + t.Errorf("DeduceProjectRoot errored after Release(), but with unexpected error: %T %s", terr, terr.Error()) + } +} + +func TestSignalHandling(t *testing.T) { + if testing.Short() { + t.Skip("Skipping slow test in short mode") + } + + sm, clean := mkNaiveSM(t) + //get self proc + proc, err := os.FindProcess(os.Getpid()) + if err != nil { + t.Fatal("cannot find self proc") + } + + sigch := make(chan os.Signal) + sm.HandleSignals(sigch) + + sigch <- os.Interrupt + <-time.After(10 * time.Millisecond) + + if sm.releasing != 1 { + t.Error("Releasing flag did not get set") + } + if sm.released != 1 { + t.Error("Released flag did not get set") + } + + lpath := filepath.Join(sm.cachedir, "sm.lock") + if _, err := os.Stat(lpath); err == nil { + t.Fatal("Expected error on statting what should be an absent lock file") + } + clean() + + sm, clean = mkNaiveSM(t) + SetUpSigHandling(sm) + go sm.DeduceProjectRoot("rsc.io/pdf") + runtime.Gosched() + + // signal the process and call release right afterward + now := time.Now() + proc.Signal(os.Interrupt) + sigdur := time.Since(now) + t.Logf("time to send signal: %v", sigdur) + sm.Release() + reldur := time.Since(now) - sigdur + t.Logf("time to return from Release(): %v", reldur) + + if reldur < 10*time.Millisecond { + t.Errorf("finished too fast (%v); the necessary network request could not have completed yet", reldur) + } + if sm.releasing != 1 { + t.Error("Releasing flag did not get set") + } + if sm.released != 1 { + t.Error("Released flag did not get set") + } + + lpath = filepath.Join(sm.cachedir, "sm.lock") + if _, err := os.Stat(lpath); err == nil { + t.Error("Expected error on statting what should be an absent lock file") + } + clean() + + sm, clean = mkNaiveSM(t) + SetUpSigHandling(sm) + sm.StopSignalHandling() + SetUpSigHandling(sm) + + go sm.DeduceProjectRoot("rsc.io/pdf") + //runtime.Gosched() + // Ensure that it all works after teardown and re-set up + proc.Signal(os.Interrupt) + // Wait for twice the time it took to do it last time; should be safe + <-time.After(reldur * 2) + + // proc.Signal doesn't send for windows, so just force it + if runtime.GOOS == "windows" { + sm.Release() + } + + if sm.releasing != 1 { + t.Error("Releasing flag did not get set") + } + if sm.released != 1 { + t.Error("Released flag did not get set") + } + + lpath = filepath.Join(sm.cachedir, "sm.lock") + if _, err := os.Stat(lpath); err == nil { + t.Fatal("Expected error on statting what should be an absent lock file") + } + clean() +} diff --git a/vendor/github.com/sdboyer/gps/solver.go b/vendor/github.com/sdboyer/gps/solver.go index e855f06374..c74e104c88 100644 --- a/vendor/github.com/sdboyer/gps/solver.go +++ b/vendor/github.com/sdboyer/gps/solver.go @@ -173,10 +173,10 @@ type solver struct { // a "lock file" - and/or use it to write out a directory tree of dependencies, // suitable to be a vendor directory, via CreateVendorTree. type Solver interface { - // HashInputs produces a hash digest representing the unique inputs to this - // solver. It is guaranteed that, if the hash digest is equal to the digest - // from a previous Solution.InputHash(), that that Solution is valid for - // this Solver's inputs. + // HashInputs hashes the unique inputs to this solver, returning the hash + // digest. It is guaranteed that, if the resulting digest is equal to the + // digest returned from a previous Solution.InputHash(), that that Solution + // is valid for this Solver's inputs. // // In such a case, it may not be necessary to run Solve() at all. HashInputs() []byte diff --git a/vendor/github.com/sdboyer/gps/source_manager.go b/vendor/github.com/sdboyer/gps/source_manager.go index ce86307624..c8634f78c6 100644 --- a/vendor/github.com/sdboyer/gps/source_manager.go +++ b/vendor/github.com/sdboyer/gps/source_manager.go @@ -3,9 +3,13 @@ package gps import ( "fmt" "os" + "os/signal" "path/filepath" + "runtime" "strings" "sync" + "sync/atomic" + "time" "github.com/Masterminds/semver" ) @@ -81,15 +85,27 @@ type ProjectAnalyzer interface { // There's no (planned) reason why it would need to be reimplemented by other // tools; control via dependency injection is intended to be sufficient. type SourceMgr struct { - cachedir string - lf *os.File - srcs map[string]source - srcmut sync.RWMutex - srcfuts map[string]*unifiedFuture - srcfmut sync.RWMutex - an ProjectAnalyzer - dxt deducerTrie - rootxt prTrie + cachedir string // path to root of cache dir + lf *os.File // handle for the sm lock file on disk + srcs map[string]source // map of path names to source obj + srcmut sync.RWMutex // mutex protecting srcs map + srcfuts map[string]*unifiedFuture // map of paths to source-handling futures + srcfmut sync.RWMutex // mutex protecting futures map + an ProjectAnalyzer // analyzer injected by the caller + dxt deducerTrie // static trie with baseline source type deduction info + rootxt prTrie // dynamic trie, updated as ProjectRoots are deduced + qch chan struct{} // quit chan for signal handler + sigmut sync.Mutex // mutex protecting signal handling setup/teardown + glock sync.RWMutex // global lock for all ops, sm validity + opcount int32 // number of ops in flight + releasing int32 // flag indicating release of sm has begun + released int32 // flag indicating release of sm has finished +} + +type smIsReleased struct{} + +func (smIsReleased) Error() string { + return "this SourceMgr has been released, its methods can no longer be called" } type unifiedFuture struct { @@ -141,7 +157,7 @@ func NewSourceManager(an ProjectAnalyzer, cachedir string) (*SourceMgr, error) { } } - return &SourceMgr{ + sm := &SourceMgr{ cachedir: cachedir, lf: fi, srcs: make(map[string]source), @@ -149,7 +165,107 @@ func NewSourceManager(an ProjectAnalyzer, cachedir string) (*SourceMgr, error) { an: an, dxt: pathDeducerTrie(), rootxt: newProjectRootTrie(), - }, nil + qch: make(chan struct{}), + } + + return sm, nil +} + +// SetUpSigHandling sets up typical os.Interrupt signal handling for a +// SourceMgr. +func SetUpSigHandling(sm *SourceMgr) { + sigch := make(chan os.Signal, 1) + signal.Notify(sigch, os.Interrupt) + sm.HandleSignals(sigch) +} + +// HandleSignals sets up logic to handle incoming signals with the goal of +// shutting down the SourceMgr safely. +// +// Calling code must provide the signal channel, and is responsible for calling +// signal.Notify() on that channel. +// +// Successive calls to HandleSignals() will deregister the previous handler and +// set up a new one. It is not recommended that the same channel be passed +// multiple times to this method. +// +// SetUpSigHandling() will set up a handler that is appropriate for most +// use cases. +func (sm *SourceMgr) HandleSignals(sigch chan os.Signal) { + sm.sigmut.Lock() + // always start by closing the qch, which will lead to any existing signal + // handler terminating, and deregistering its sigch. + if sm.qch != nil { + close(sm.qch) + } + sm.qch = make(chan struct{}) + + // Run a new goroutine with the input sigch and the fresh qch + go func(sch chan os.Signal, qch <-chan struct{}) { + defer signal.Stop(sch) + for { + select { + case <-sch: + // Set up a timer to uninstall the signal handler after three + // seconds, so that the user can easily force termination with a + // second ctrl-c + go func(c <-chan time.Time) { + <-c + signal.Stop(sch) + }(time.After(3 * time.Second)) + + if !atomic.CompareAndSwapInt32(&sm.releasing, 0, 1) { + // Something's already called Release() on this sm, so we + // don't have to do anything, as we'd just be redoing + // that work. Instead, deregister and return. + return + } + + // Keep track of whether we waited for output purposes + var waited bool + opc := sm.opcount + if opc > 0 { + waited = true + fmt.Printf("Waiting for %v ops to complete...", opc) + } + + // Mutex interaction in a signal handler is, as a general rule, + // unsafe. I'm not clear on whether the guarantees Go provides + // around signal handling, or having passed this through a + // channel in general, obviate those concerns, but it's a lot + // easier to just hit the mutex right now, so do that until it + // proves problematic or someone provides a clear explanation. + sm.glock.Lock() + if waited && sm.released != 1 { + fmt.Print("done.\n") + } + sm.doRelease() + sm.glock.Unlock() + return + case <-qch: + // quit channel triggered - deregister our sigch and return + return + } + } + }(sigch, sm.qch) + // Try to ensure handler is blocked in for-select before releasing the mutex + runtime.Gosched() + + sm.sigmut.Unlock() +} + +// StopSignalHandling deregisters any signal handler running on this SourceMgr. +// +// It's normally not necessary to call this directly; it will be called as +// needed by Release(). +func (sm *SourceMgr) StopSignalHandling() { + sm.sigmut.Lock() + if sm.qch != nil { + close(sm.qch) + sm.qch = nil + runtime.Gosched() + } + sm.sigmut.Unlock() } // CouldNotCreateLockError describe failure modes in which creating a SourceMgr @@ -164,10 +280,42 @@ func (e CouldNotCreateLockError) Error() string { return e.Err.Error() } -// Release lets go of any locks held by the SourceManager. +// Release lets go of any locks held by the SourceManager. Once called, it is no +// longer safe to call methods against it; all method calls will immediately +// result in errors. func (sm *SourceMgr) Release() { - sm.lf.Close() - os.Remove(filepath.Join(sm.cachedir, "sm.lock")) + // This ensures a signal handling can't interleave with a Release call - + // exit early if we're already marked as having initiated a release process. + // + // Setting it before we acquire the lock also guarantees that no _more_ + // method calls will stack up. + if !atomic.CompareAndSwapInt32(&sm.releasing, 0, 1) { + return + } + + // Grab the global sm lock so that we only release once we're sure all other + // calls have completed + // + // (This could deadlock, ofc) + sm.glock.Lock() + sm.doRelease() + sm.glock.Unlock() +} + +// doRelease actually releases physical resources (files on disk, etc.). +func (sm *SourceMgr) doRelease() { + // One last atomic marker ensures actual disk changes only happen once. + if atomic.CompareAndSwapInt32(&sm.released, 0, 1) { + // Close the file handle for the lock file + sm.lf.Close() + // Remove the lock file from disk + os.Remove(filepath.Join(sm.cachedir, "sm.lock")) + // Close the qch, if non-nil, so the signal handlers run out. This will + // also deregister the sig channel, if any has been set up. + if sm.qch != nil { + close(sm.qch) + } + } } // AnalyzerInfo reports the name and version of the injected ProjectAnalyzer. @@ -183,6 +331,16 @@ func (sm *SourceMgr) AnalyzerInfo() (name string, version *semver.Version) { // The work of producing the manifest and lock is delegated to the injected // ProjectAnalyzer's DeriveManifestAndLock() method. func (sm *SourceMgr) GetManifestAndLock(id ProjectIdentifier, v Version) (Manifest, Lock, error) { + if atomic.CompareAndSwapInt32(&sm.releasing, 1, 1) { + return nil, nil, smIsReleased{} + } + atomic.AddInt32(&sm.opcount, 1) + sm.glock.RLock() + defer func() { + sm.glock.RUnlock() + atomic.AddInt32(&sm.opcount, -1) + }() + src, err := sm.getSourceFor(id) if err != nil { return nil, nil, err @@ -194,6 +352,16 @@ func (sm *SourceMgr) GetManifestAndLock(id ProjectIdentifier, v Version) (Manife // ListPackages parses the tree of the Go packages at and below the ProjectRoot // of the given ProjectIdentifier, at the given version. func (sm *SourceMgr) ListPackages(id ProjectIdentifier, v Version) (PackageTree, error) { + if atomic.CompareAndSwapInt32(&sm.releasing, 1, 1) { + return PackageTree{}, smIsReleased{} + } + atomic.AddInt32(&sm.opcount, 1) + sm.glock.RLock() + defer func() { + sm.glock.RUnlock() + atomic.AddInt32(&sm.opcount, -1) + }() + src, err := sm.getSourceFor(id) if err != nil { return PackageTree{}, err @@ -215,6 +383,16 @@ func (sm *SourceMgr) ListPackages(id ProjectIdentifier, v Version) (PackageTree, // is not accessible (network outage, access issues, or the resource actually // went away), an error will be returned. func (sm *SourceMgr) ListVersions(id ProjectIdentifier) ([]Version, error) { + if atomic.CompareAndSwapInt32(&sm.releasing, 1, 1) { + return nil, smIsReleased{} + } + atomic.AddInt32(&sm.opcount, 1) + sm.glock.RLock() + defer func() { + sm.glock.RUnlock() + atomic.AddInt32(&sm.opcount, -1) + }() + src, err := sm.getSourceFor(id) if err != nil { // TODO(sdboyer) More-er proper-er errors @@ -227,6 +405,16 @@ func (sm *SourceMgr) ListVersions(id ProjectIdentifier) ([]Version, error) { // RevisionPresentIn indicates whether the provided Revision is present in the given // repository. func (sm *SourceMgr) RevisionPresentIn(id ProjectIdentifier, r Revision) (bool, error) { + if atomic.CompareAndSwapInt32(&sm.releasing, 1, 1) { + return false, smIsReleased{} + } + atomic.AddInt32(&sm.opcount, 1) + sm.glock.RLock() + defer func() { + sm.glock.RUnlock() + atomic.AddInt32(&sm.opcount, -1) + }() + src, err := sm.getSourceFor(id) if err != nil { // TODO(sdboyer) More-er proper-er errors @@ -239,6 +427,16 @@ func (sm *SourceMgr) RevisionPresentIn(id ProjectIdentifier, r Revision) (bool, // SourceExists checks if a repository exists, either upstream or in the cache, // for the provided ProjectIdentifier. func (sm *SourceMgr) SourceExists(id ProjectIdentifier) (bool, error) { + if atomic.CompareAndSwapInt32(&sm.releasing, 1, 1) { + return false, smIsReleased{} + } + atomic.AddInt32(&sm.opcount, 1) + sm.glock.RLock() + defer func() { + sm.glock.RUnlock() + atomic.AddInt32(&sm.opcount, -1) + }() + src, err := sm.getSourceFor(id) if err != nil { return false, err @@ -252,6 +450,16 @@ func (sm *SourceMgr) SourceExists(id ProjectIdentifier) (bool, error) { // // The primary use case for this is prefetching. func (sm *SourceMgr) SyncSourceFor(id ProjectIdentifier) error { + if atomic.CompareAndSwapInt32(&sm.releasing, 1, 1) { + return smIsReleased{} + } + atomic.AddInt32(&sm.opcount, 1) + sm.glock.RLock() + defer func() { + sm.glock.RUnlock() + atomic.AddInt32(&sm.opcount, -1) + }() + src, err := sm.getSourceFor(id) if err != nil { return err @@ -263,6 +471,16 @@ func (sm *SourceMgr) SyncSourceFor(id ProjectIdentifier) error { // ExportProject writes out the tree of the provided ProjectIdentifier's // ProjectRoot, at the provided version, to the provided directory. func (sm *SourceMgr) ExportProject(id ProjectIdentifier, v Version, to string) error { + if atomic.CompareAndSwapInt32(&sm.releasing, 1, 1) { + return smIsReleased{} + } + atomic.AddInt32(&sm.opcount, 1) + sm.glock.RLock() + defer func() { + sm.glock.RUnlock() + atomic.AddInt32(&sm.opcount, -1) + }() + src, err := sm.getSourceFor(id) if err != nil { return err @@ -279,6 +497,16 @@ func (sm *SourceMgr) ExportProject(id ProjectIdentifier, v Version, to string) e // paths. (A special exception is written for gopkg.in to minimize network // activity, as its behavior is well-structured) func (sm *SourceMgr) DeduceProjectRoot(ip string) (ProjectRoot, error) { + if atomic.CompareAndSwapInt32(&sm.releasing, 1, 1) { + return "", smIsReleased{} + } + atomic.AddInt32(&sm.opcount, 1) + sm.glock.RLock() + defer func() { + sm.glock.RUnlock() + atomic.AddInt32(&sm.opcount, -1) + }() + if prefix, root, has := sm.rootxt.LongestPrefix(ip); has { // The non-matching tail of the import path could still be malformed. // Validate just that part, if it exists