elvish/eval/envPathList.go
Qi Xiao f001c783d5 Lock EnvPathList correctly. This fixes #146.
Although there are no explicit writes to $paths in the test cases, there is
one hidden write: when the first search on external commands is done. Since
the internal cache of EnvPathList starts empty, EnvPathList.get() notices that
it needs to fetch the environment to update itself. However, the update first
modifies .cachedValue and then .cachedPaths. If another goroutine uses .get()
in the middle, it would think that the cache is actually up to date and uses
the outdated .cachedPaths, which is empty.

EnvPathList.get() is now correctly guarded by a write lock. The offending test
case has been uncommented as well.
2016-02-20 22:25:26 +01:00

136 lines
2.7 KiB
Go

package eval
import (
"errors"
"os"
"strings"
"sync"
)
// Errors
var (
ErrCanOnlyAssignList = errors.New("can only assign compatible values")
ErrPathMustBeString = errors.New("path must be string")
ErrPathCannotContainColonZero = errors.New(`path cannot contain colon or \0`)
)
// EnvPathList is a variable whose value is constructed from an environment
// variable by splitting at colons. Changes to it are also propagated to the
// corresponding environment variable. Its elements cannot contain colons or
// \0; attempting to put colon or \0 in its elements will result in an error.
//
// EnvPathList implements both Value and Variable interfaces. It also satisfied
// ListLike.
type EnvPathList struct {
sync.RWMutex
envName string
cachedValue string
cachedPaths []string
}
var (
_ Variable = (*EnvPathList)(nil)
_ Value = (*EnvPathList)(nil)
_ ListLike = (*EnvPathList)(nil)
)
func (epl *EnvPathList) Get() Value {
return epl
}
func (epl *EnvPathList) Set(v Value) {
elemser, ok := v.(Elemser)
if !ok {
throw(ErrCanOnlyAssignList)
}
var paths []string
for v := range elemser.Elems() {
s, ok := v.(String)
if !ok {
throw(ErrPathMustBeString)
}
path := string(s)
if strings.ContainsAny(path, ":\x00") {
throw(ErrPathCannotContainColonZero)
}
paths = append(paths, string(s))
}
epl.set(paths)
}
func (epl *EnvPathList) Kind() string {
return "list"
}
func (epl *EnvPathList) Repr(indent int) string {
var b ListReprBuilder
b.Indent = indent
for _, path := range epl.get() {
b.WriteElem(quote(path))
}
return b.String()
}
func (epl *EnvPathList) Len() int {
return len(epl.get())
}
func (epl *EnvPathList) Elems() <-chan Value {
ch := make(chan Value)
go func() {
for _, p := range epl.get() {
ch <- String(p)
}
close(ch)
}()
return ch
}
func (epl *EnvPathList) IndexOne(idx Value) Value {
paths := epl.get()
i := intIndexWithin(idx, len(paths))
return String(paths[i])
}
func (epl *EnvPathList) IndexSet(idx, v Value) {
s, ok := v.(String)
if !ok {
throw(ErrPathMustBeString)
}
paths := epl.get()
i := intIndexWithin(idx, len(paths))
epl.Lock()
defer epl.Unlock()
paths[i] = string(s)
epl.syncFromPaths()
}
func (epl *EnvPathList) get() []string {
epl.Lock()
defer epl.Unlock()
value := os.Getenv(epl.envName)
if value == epl.cachedValue {
return epl.cachedPaths
}
epl.cachedValue = value
epl.cachedPaths = strings.Split(value, ":")
return epl.cachedPaths
}
func (epl *EnvPathList) set(paths []string) {
epl.Lock()
defer epl.Unlock()
epl.cachedPaths = paths
epl.syncFromPaths()
}
func (epl *EnvPathList) syncFromPaths() {
epl.cachedValue = strings.Join(epl.cachedPaths, ":")
err := os.Setenv(epl.envName, epl.cachedValue)
maybeThrow(err)
}