<commit_before>package herd

import (
	"flag"
	"fmt"
	"github.com/Symantec/Dominator/lib/constants"
	"github.com/Symantec/Dominator/lib/filesystem"
	"github.com/Symantec/Dominator/lib/hash"
	"github.com/Symantec/Dominator/lib/srpc"
	subproto "github.com/Symantec/Dominator/proto/sub"
	"github.com/Symantec/Dominator/sub/client"
	"net"
	"runtime"
	"strings"
	"time"
)

var (
	subConnectTimeout = flag.Uint("subConnectTimeout", 15,
		"Timeout in seconds for sub connections. If zero, OS timeout is used")
)

func (sub *Sub) tryMakeBusy() bool {
	sub.busyMutex.Lock()
	defer sub.busyMutex.Unlock()
	if sub.busy {
		return false
	}
	sub.busy = true
	return true
}

func (sub *Sub) makeUnbusy() {
	sub.busyMutex.Lock()
	defer sub.busyMutex.Unlock()
	sub.busy = false
}

func (sub *Sub) connectAndPoll() {
	previousStatus := sub.status
	sub.status = statusConnecting
	hostname := strings.SplitN(sub.hostname, "*", 2)[0]
	address := fmt.Sprintf("%s:%d", hostname, constants.SubPortNumber)
	sub.lastConnectionStartTime = time.Now()
	srpcClient, err := srpc.DialHTTP("tcp", address,
		time.Second*time.Duration(*subConnectTimeout))
	if err != nil {
		if err, ok := err.(*net.OpError); ok {
			if _, ok := err.Err.(*net.DNSError); ok {
				sub.status = statusDNSError
				return
			}
			if err.Timeout() {
				sub.status = statusConnectTimeout
				return
			}
		}
		sub.status = statusFailedToConnect
		return
	}
	defer srpcClient.Close()
	sub.status = statusWaitingToPoll
	sub.lastConnectionSucceededTime = time.Now()
	sub.lastConnectDuration =
		sub.lastConnectionSucceededTime.Sub(sub.lastConnectionStartTime)
	connectDistribution.Add(sub.lastConnectDuration)
	if sub.herd.getImage(sub.requiredImage) == nil {
		sub.status = statusImageNotReady
		return
	}
	sub.herd.pollSemaphore <- true
	sub.status = statusPolling
	sub.poll(srpcClient, previousStatus)
	<-sub.herd.pollSemaphore
}

func (sub *Sub) poll(srpcClient *srpc.Client, previousStatus subStatus) {
	// If the planned image has just become available, force a full poll.
	if previousStatus == statusSynced &&
		!sub.havePlannedImage &&
		sub.herd.getImage(sub.plannedImage) != nil {
		sub.havePlannedImage = true
		sub.generationCount = 0 // Force a full poll.
	}
	var request subproto.PollRequest
	request.HaveGeneration = sub.generationCount
	var reply subproto.PollResponse
	sub.lastPollStartTime = time.Now()
	logger := sub.herd.logger
	if err := client.CallPoll(srpcClient, request, &reply); err != nil {
		sub.status = statusFailedToPoll
		logger.Printf("Error calling %s.Poll()\t%s\n", sub.hostname, err)
		return
	}
	sub.lastPollSucceededTime = time.Now()
	if reply.GenerationCount == 0 {
		sub.reclaim()
		sub.generationCount = 0
	}
	if fs := reply.FileSystem; fs == nil {
		sub.lastShortPollDuration =
			sub.lastPollSucceededTime.Sub(sub.lastPollStartTime)
		shortPollDistribution.Add(sub.lastShortPollDuration)
	} else {
		if err := fs.RebuildInodePointers(); err != nil {
			sub.status = statusFailedToPoll
			logger.Printf("Error building pointers for: %s %s\n",
				sub.hostname, err)
			return
		}
		fs.BuildInodeToFilenamesTable()
		fs.BuildEntryMap()
		sub.fileSystem = fs
		sub.objectCache = reply.ObjectCache
		sub.generationCount = reply.GenerationCount
		sub.lastFullPollDuration =
			sub.lastPollSucceededTime.Sub(sub.lastPollStartTime)
		fullPollDistribution.Add(sub.lastFullPollDuration)
		logger.Printf("Polled: %s, GenerationCount=%d\n",
			sub.hostname, reply.GenerationCount)
	}
	if reply.FetchInProgress {
		sub.status = statusFetching
		return
	}
	if reply.UpdateInProgress {
		sub.status = statusUpdating
		return
	}
	if sub.generationCount < 1 {
		sub.status = statusSubNotReady
		return
	}
	if sub.generationCountAtChangeStart == sub.generationCount {
		sub.status = statusWaitingForNextFullPoll
		return
	}
	if sub.fileSystem == nil {
		sub.status = previousStatus
		if previousStatus != statusSynced {
			// Force a full poll next cycle so that we can see the state of the
			// sub and recompute/retry what needs to be done.
			sub.generationCount = 0
		}
		return
	}
	if idle, status := sub.fetchMissingObjects(srpcClient,
		sub.requiredImage); !idle {
		sub.status = status
		sub.reclaim()
		return
	}
	sub.status = statusComputingUpdate
	if idle, status := sub.sendUpdate(srpcClient); !idle {
		sub.status = status
		sub.reclaim()
		return
	}
	if idle, status := sub.fetchMissingObjects(srpcClient,
		sub.plannedImage); !idle {
		if status != statusImageNotReady {
			sub.status = status
			sub.reclaim()
			return
		}
	}
	sub.status = statusSynced
	sub.cleanup(srpcClient, sub.plannedImage)
	sub.reclaim()
}

func (sub *Sub) reclaim() {
	sub.fileSystem = nil  // Mark memory for reclaim.
	sub.objectCache = nil // Mark memory for reclaim.
	runtime.GC()          // Reclaim now.
}

// Returns true if all required objects are available.
func (sub *Sub) fetchMissingObjects(srpcClient *srpc.Client, imageName string) (
	bool, subStatus) {
	image := sub.herd.getImage(imageName)
	if image == nil {
		return false, statusImageNotReady
	}
	missingObjects := make(map[hash.Hash]struct{})
	for _, inode := range image.FileSystem.InodeTable {
		if inode, ok := inode.(*filesystem.RegularInode); ok {
			if inode.Size > 0 {
				missingObjects[inode.Hash] = struct{}{}
			}
		}
	}
	for _, hash := range sub.objectCache {
		delete(missingObjects, hash)
	}
	for _, inode := range sub.fileSystem.InodeTable {
		if inode, ok := inode.(*filesystem.RegularInode); ok {
			if inode.Size > 0 {
				delete(missingObjects, inode.Hash)
			}
		}
	}
	if len(missingObjects) < 1 {
		return true, statusSynced
	}
	logger := sub.herd.logger
	logger.Printf("Calling %s.Fetch() for: %d objects\n",
		sub.hostname, len(missingObjects))
	var request subproto.FetchRequest
	var reply subproto.FetchResponse
	request.ServerAddress = sub.herd.imageServerAddress
	for hash := range missingObjects {
		request.Hashes = append(request.Hashes, hash)
	}
	if err := client.CallFetch(srpcClient, request, &reply); err != nil {
		logger.Printf("Error calling %s.Fetch()\t%s\n", sub.hostname, err)
		return false, statusFailedToFetch
	}
	sub.generationCountAtChangeStart = sub.generationCount
	return false, statusFetching
}

// Returns true if no update needs to be performed.
func (sub *Sub) sendUpdate(srpcClient *srpc.Client) (bool, subStatus) {
	logger := sub.herd.logger
	var request subproto.UpdateRequest
	var reply subproto.UpdateResponse
	if sub.buildUpdateRequest(&request) {
		return true, statusSynced
	}
	if err := client.CallUpdate(srpcClient, request, &reply); err != nil {
		logger.Printf("Error calling %s:Subd.Update()\t%s\n", sub.hostname, err)
		return false, statusFailedToUpdate
	}
	sub.generationCountAtChangeStart = sub.generationCount
	return false, statusUpdating
}

func (sub *Sub) cleanup(srpcClient *srpc.Client, plannedImageName string) {
	logger := sub.herd.logger
	unusedObjects := make(map[hash.Hash]bool)
	for _, hash := range sub.objectCache {
		unusedObjects[hash] = false // Potential cleanup candidate.
	}
	for _, inode := range sub.fileSystem.InodeTable {
		if inode, ok := inode.(*filesystem.RegularInode); ok {
			if inode.Size > 0 {
				if _, ok := unusedObjects[inode.Hash]; ok {
					unusedObjects[inode.Hash] = true // Must clean this one up.
				}
			}
		}
	}
	image := sub.herd.getImage(plannedImageName)
	if image != nil {
		for _, inode := range image.FileSystem.InodeTable {
			if inode, ok := inode.(*filesystem.RegularInode); ok {
				if inode.Size > 0 {
					if clean, ok := unusedObjects[inode.Hash]; !clean && ok {
						delete(unusedObjects, inode.Hash)
					}
				}
			}
		}
	}
	if len(unusedObjects) < 1 {
		return
	}
	var request subproto.CleanupRequest
	var reply subproto.CleanupResponse
	request.Hashes = make([]hash.Hash, 0, len(unusedObjects))
	for hash := range unusedObjects {
		request.Hashes = append(request.Hashes, hash)
	}
	if err := client.CallCleanup(srpcClient, request, &reply); err != nil {
		logger.Printf("Error calling %s:Subd.Cleanup()\t%s\n",
			sub.hostname, err)
	} else {
		sub.generationCountAtChangeStart = sub.generationCount
	}
}
<commit_msg>Add -logUnknownSubConnectErrors option to dominator.<commit_after>package herd

import (
	"flag"
	"fmt"
	"github.com/Symantec/Dominator/lib/constants"
	"github.com/Symantec/Dominator/lib/filesystem"
	"github.com/Symantec/Dominator/lib/hash"
	"github.com/Symantec/Dominator/lib/srpc"
	subproto "github.com/Symantec/Dominator/proto/sub"
	"github.com/Symantec/Dominator/sub/client"
	"net"
	"runtime"
	"strings"
	"time"
)

var (
	subConnectTimeout = flag.Uint("subConnectTimeout", 15,
		"Timeout in seconds for sub connections. If zero, OS timeout is used")
	logUnknownSubConnectErrors = flag.Bool("logUnknownSubConnectErrors", false,
		"If true, log unknown sub connection errors")
)

func (sub *Sub) tryMakeBusy() bool {
	sub.busyMutex.Lock()
	defer sub.busyMutex.Unlock()
	if sub.busy {
		return false
	}
	sub.busy = true
	return true
}

func (sub *Sub) makeUnbusy() {
	sub.busyMutex.Lock()
	defer sub.busyMutex.Unlock()
	sub.busy = false
}

func (sub *Sub) connectAndPoll() {
	previousStatus := sub.status
	sub.status = statusConnecting
	hostname := strings.SplitN(sub.hostname, "*", 2)[0]
	address := fmt.Sprintf("%s:%d", hostname, constants.SubPortNumber)
	sub.lastConnectionStartTime = time.Now()
	srpcClient, err := srpc.DialHTTP("tcp", address,
		time.Second*time.Duration(*subConnectTimeout))
	if err != nil {
		if err, ok := err.(*net.OpError); ok {
			if _, ok := err.Err.(*net.DNSError); ok {
				sub.status = statusDNSError
				return
			}
			if err.Timeout() {
				sub.status = statusConnectTimeout
				return
			}
		}
		sub.status = statusFailedToConnect
		if *logUnknownSubConnectErrors {
			sub.herd.logger.Println(err)
		}
		return
	}
	defer srpcClient.Close()
	sub.status = statusWaitingToPoll
	sub.lastConnectionSucceededTime = time.Now()
	sub.lastConnectDuration =
		sub.lastConnectionSucceededTime.Sub(sub.lastConnectionStartTime)
	connectDistribution.Add(sub.lastConnectDuration)
	if sub.herd.getImage(sub.requiredImage) == nil {
		sub.status = statusImageNotReady
		return
	}
	sub.herd.pollSemaphore <- true
	sub.status = statusPolling
	sub.poll(srpcClient, previousStatus)
	<-sub.herd.pollSemaphore
}

func (sub *Sub) poll(srpcClient *srpc.Client, previousStatus subStatus) {
	// If the planned image has just become available, force a full poll.
	if previousStatus == statusSynced &&
		!sub.havePlannedImage &&
		sub.herd.getImage(sub.plannedImage) != nil {
		sub.havePlannedImage = true
		sub.generationCount = 0 // Force a full poll.
	}
	var request subproto.PollRequest
	request.HaveGeneration = sub.generationCount
	var reply subproto.PollResponse
	sub.lastPollStartTime = time.Now()
	logger := sub.herd.logger
	if err := client.CallPoll(srpcClient, request, &reply); err != nil {
		sub.status = statusFailedToPoll
		logger.Printf("Error calling %s.Poll()\t%s\n", sub.hostname, err)
		return
	}
	sub.lastPollSucceededTime = time.Now()
	if reply.GenerationCount == 0 {
		sub.reclaim()
		sub.generationCount = 0
	}
	if fs := reply.FileSystem; fs == nil {
		sub.lastShortPollDuration =
			sub.lastPollSucceededTime.Sub(sub.lastPollStartTime)
		shortPollDistribution.Add(sub.lastShortPollDuration)
	} else {
		if err := fs.RebuildInodePointers(); err != nil {
			sub.status = statusFailedToPoll
			logger.Printf("Error building pointers for: %s %s\n",
				sub.hostname, err)
			return
		}
		fs.BuildInodeToFilenamesTable()
		fs.BuildEntryMap()
		sub.fileSystem = fs
		sub.objectCache = reply.ObjectCache
		sub.generationCount = reply.GenerationCount
		sub.lastFullPollDuration =
			sub.lastPollSucceededTime.Sub(sub.lastPollStartTime)
		fullPollDistribution.Add(sub.lastFullPollDuration)
		logger.Printf("Polled: %s, GenerationCount=%d\n",
			sub.hostname, reply.GenerationCount)
	}
	if reply.FetchInProgress {
		sub.status = statusFetching
		return
	}
	if reply.UpdateInProgress {
		sub.status = statusUpdating
		return
	}
	if sub.generationCount < 1 {
		sub.status = statusSubNotReady
		return
	}
	if sub.generationCountAtChangeStart == sub.generationCount {
		sub.status = statusWaitingForNextFullPoll
		return
	}
	if sub.fileSystem == nil {
		sub.status = previousStatus
		if previousStatus != statusSynced {
			// Force a full poll next cycle so that we can see the state of the
			// sub and recompute/retry what needs to be done.
			sub.generationCount = 0
		}
		return
	}
	if idle, status := sub.fetchMissingObjects(srpcClient,
		sub.requiredImage); !idle {
		sub.status = status
		sub.reclaim()
		return
	}
	sub.status = statusComputingUpdate
	if idle, status := sub.sendUpdate(srpcClient); !idle {
		sub.status = status
		sub.reclaim()
		return
	}
	if idle, status := sub.fetchMissingObjects(srpcClient,
		sub.plannedImage); !idle {
		if status != statusImageNotReady {
			sub.status = status
			sub.reclaim()
			return
		}
	}
	sub.status = statusSynced
	sub.cleanup(srpcClient, sub.plannedImage)
	sub.reclaim()
}

func (sub *Sub) reclaim() {
	sub.fileSystem = nil  // Mark memory for reclaim.
	sub.objectCache = nil // Mark memory for reclaim.
	runtime.GC()          // Reclaim now.
}

// Returns true if all required objects are available.
func (sub *Sub) fetchMissingObjects(srpcClient *srpc.Client, imageName string) (
	bool, subStatus) {
	image := sub.herd.getImage(imageName)
	if image == nil {
		return false, statusImageNotReady
	}
	missingObjects := make(map[hash.Hash]struct{})
	for _, inode := range image.FileSystem.InodeTable {
		if inode, ok := inode.(*filesystem.RegularInode); ok {
			if inode.Size > 0 {
				missingObjects[inode.Hash] = struct{}{}
			}
		}
	}
	for _, hash := range sub.objectCache {
		delete(missingObjects, hash)
	}
	for _, inode := range sub.fileSystem.InodeTable {
		if inode, ok := inode.(*filesystem.RegularInode); ok {
			if inode.Size > 0 {
				delete(missingObjects, inode.Hash)
			}
		}
	}
	if len(missingObjects) < 1 {
		return true, statusSynced
	}
	logger := sub.herd.logger
	logger.Printf("Calling %s.Fetch() for: %d objects\n",
		sub.hostname, len(missingObjects))
	var request subproto.FetchRequest
	var reply subproto.FetchResponse
	request.ServerAddress = sub.herd.imageServerAddress
	for hash := range missingObjects {
		request.Hashes = append(request.Hashes, hash)
	}
	if err := client.CallFetch(srpcClient, request, &reply); err != nil {
		logger.Printf("Error calling %s.Fetch()\t%s\n", sub.hostname, err)
		return false, statusFailedToFetch
	}
	sub.generationCountAtChangeStart = sub.generationCount
	return false, statusFetching
}

// Returns true if no update needs to be performed.
func (sub *Sub) sendUpdate(srpcClient *srpc.Client) (bool, subStatus) {
	logger := sub.herd.logger
	var request subproto.UpdateRequest
	var reply subproto.UpdateResponse
	if sub.buildUpdateRequest(&request) {
		return true, statusSynced
	}
	if err := client.CallUpdate(srpcClient, request, &reply); err != nil {
		logger.Printf("Error calling %s:Subd.Update()\t%s\n", sub.hostname, err)
		return false, statusFailedToUpdate
	}
	sub.generationCountAtChangeStart = sub.generationCount
	return false, statusUpdating
}

func (sub *Sub) cleanup(srpcClient *srpc.Client, plannedImageName string) {
	logger := sub.herd.logger
	unusedObjects := make(map[hash.Hash]bool)
	for _, hash := range sub.objectCache {
		unusedObjects[hash] = false // Potential cleanup candidate.
	}
	for _, inode := range sub.fileSystem.InodeTable {
		if inode, ok := inode.(*filesystem.RegularInode); ok {
			if inode.Size > 0 {
				if _, ok := unusedObjects[inode.Hash]; ok {
					unusedObjects[inode.Hash] = true // Must clean this one up.
				}
			}
		}
	}
	image := sub.herd.getImage(plannedImageName)
	if image != nil {
		for _, inode := range image.FileSystem.InodeTable {
			if inode, ok := inode.(*filesystem.RegularInode); ok {
				if inode.Size > 0 {
					if clean, ok := unusedObjects[inode.Hash]; !clean && ok {
						delete(unusedObjects, inode.Hash)
					}
				}
			}
		}
	}
	if len(unusedObjects) < 1 {
		return
	}
	var request subproto.CleanupRequest
	var reply subproto.CleanupResponse
	request.Hashes = make([]hash.Hash, 0, len(unusedObjects))
	for hash := range unusedObjects {
		request.Hashes = append(request.Hashes, hash)
	}
	if err := client.CallCleanup(srpcClient, request, &reply); err != nil {
		logger.Printf("Error calling %s:Subd.Cleanup()\t%s\n",
			sub.hostname, err)
	} else {
		sub.generationCountAtChangeStart = sub.generationCount
	}
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"text/template"
	"time"

	"github.com/samuel/go-zookeeper/zk"
)

var (
	templateFile   string
	nginxRoot      string
	zookeeperNodes string
	serviceRoot    string
)

func check(err error) {
	if err != nil {
		panic(err)
	}
}

// Config is a simple mapping of service name and upstream endpoints
// so that we can generate nginx service configuration
type Config struct {
	Service           string
	UpstreamEndpoints []string
}

func main() {
	flag.StringVar(&templateFile, "template", "default.tmpl", "nginx template to use")
	flag.StringVar(&nginxRoot, "nginx-root", "/etc/nginx/", "The root of the nginx installation")
	flag.StringVar(&zookeeperNodes, "zookeeper-nodes", "127.0.0.1:2181", "The zookeeper instance to connect to")
	flag.StringVar(&serviceRoot, "service-root", "/", "The root path with your service metadata")
	flag.Parse()

	sitesAvailable := fmt.Sprintf("%s/sites-available/", nginxRoot)
	sitesEnabled := fmt.Sprintf("%s/sites-enabled/", nginxRoot)
	reloadCommand := exec.Command("service", "nginx reload")

	c, _, err := zk.Connect([]string{zookeeperNodes}, time.Second)
	check(err)

	children, _, _, err := c.ChildrenW(serviceRoot)
	check(err)

	t, err := template.ParseFiles(templateFile)
	check(err)

	for _, child := range children {
		serviceInstance := strings.Join([]string{serviceRoot, child, "instances"}, "/")
		info, _, _, err := c.ChildrenW(serviceInstance)
		if err != nil {
			panic(err)
		}

		var upstreamEndpoints []string

		for _, instanceInfo := range info {
			i := strings.Join(strings.Split(instanceInfo, "_"), ":")
			upstreamEndpoints = append(upstreamEndpoints, i)
		}

		fmt.Printf("%+v\n", upstreamEndpoints)

		f, err := os.Create(fmt.Sprintf("%s/%s.service", sitesAvailable, child))
		check(err)
		defer f.Close()

		data := Config{
			Service:           child,
			UpstreamEndpoints: upstreamEndpoints,
		}

		t.Execute(f, data)

		os.Symlink(fmt.Sprintf("%s/%s.service", sitesAvailable, child),
			fmt.Sprintf("%s/%s.service", sitesEnabled, child))
	}

	err = reloadCommand.Run()
	check(err)

}
<commit_msg>Updated generator to reference packaged template as the default.<commit_after>package main

import (
	"flag"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"text/template"
	"time"

	"github.com/samuel/go-zookeeper/zk"
)

//go:generate go run scripts/package-templates.go

var (
	templateFile   string
	nginxRoot      string
	zookeeperNodes string
	serviceRoot    string
)

func check(err error) {
	if err != nil {
		panic(err)
	}
}

// Config is a simple mapping of service name and upstream endpoints
// so that we can generate nginx service configuration
type Config struct {
	Service           string
	UpstreamEndpoints []string
}

func main() {
	flag.StringVar(&templateFile, "template", nil, "nginx template to use")
	flag.StringVar(&nginxRoot, "nginx-root", "/etc/nginx/", "The root of the nginx installation")
	flag.StringVar(&zookeeperNodes, "zookeeper-nodes", "127.0.0.1:2181", "The zookeeper instance to connect to")
	flag.StringVar(&serviceRoot, "service-root", "/", "The root path with your service metadata")
	flag.Parse()

	sitesAvailable := fmt.Sprintf("%s/sites-available/", nginxRoot)
	sitesEnabled := fmt.Sprintf("%s/sites-enabled/", nginxRoot)
	reloadCommand := exec.Command("service", "nginx reload")

	if templateFile == nil {
		t, err := template.New(defaultService)
	} else {
		t, err := template.ParseFiles(templateFile)
		check(err)
	}

	c, _, err := zk.Connect([]string{zookeeperNodes}, time.Second)
	check(err)

	children, _, _, err := c.ChildrenW(serviceRoot)
	check(err)

	for _, child := range children {
		serviceInstance := strings.Join([]string{serviceRoot, child, "instances"}, "/")
		info, _, _, err := c.ChildrenW(serviceInstance)
		if err != nil {
			panic(err)
		}

		var upstreamEndpoints []string

		for _, instanceInfo := range info {
			i := strings.Join(strings.Split(instanceInfo, "_"), ":")
			upstreamEndpoints = append(upstreamEndpoints, i)
		}

		fmt.Printf("%+v\n", upstreamEndpoints)

		f, err := os.Create(fmt.Sprintf("%s/%s.service", sitesAvailable, child))
		check(err)
		defer f.Close()

		data := Config{
			Service:           child,
			UpstreamEndpoints: upstreamEndpoints,
		}

		t.Execute(f, data)

		os.Symlink(fmt.Sprintf("%s/%s.service", sitesAvailable, child),
			fmt.Sprintf("%s/%s.service", sitesEnabled, child))
	}

	err = reloadCommand.Run()
	check(err)

}
<|endoftext|>


<commit_before>package wallet

import (
	"math"

	"github.com/NebulousLabs/Sia/build"
	"github.com/NebulousLabs/Sia/modules"
	"github.com/NebulousLabs/Sia/types"
)

// updateConfirmedSet uses a consensus change to update the confirmed set of
// outputs as understood by the wallet.
func (w *Wallet) updateConfirmedSet(cc modules.ConsensusChange) {
	for _, diff := range cc.SiacoinOutputDiffs {
		// Verify that the diff is relevant to the wallet.
		_, exists := w.keys[diff.SiacoinOutput.UnlockHash]
		if !exists {
			continue
		}

		_, exists = w.siacoinOutputs[diff.ID]
		if diff.Direction == modules.DiffApply {
			if build.DEBUG && exists {
				panic("adding an existing output to wallet")
			}
			w.siacoinOutputs[diff.ID] = diff.SiacoinOutput
		} else {
			if build.DEBUG && !exists {
				panic("deleting nonexisting output from wallet")
			}
			delete(w.siacoinOutputs, diff.ID)
		}
	}
	for _, diff := range cc.SiafundOutputDiffs {
		// Verify that the diff is relevant to the wallet.
		_, exists := w.keys[diff.SiafundOutput.UnlockHash]
		if !exists {
			continue
		}

		_, exists = w.siafundOutputs[diff.ID]
		if diff.Direction == modules.DiffApply {
			if build.DEBUG && exists {
				panic("adding an existing output to wallet")
			}
			w.siafundOutputs[diff.ID] = diff.SiafundOutput
		} else {
			if build.DEBUG && !exists {
				panic("deleting nonexisting output from wallet")
			}
			delete(w.siafundOutputs, diff.ID)
		}
	}
	for _, diff := range cc.SiafundPoolDiffs {
		if diff.Direction == modules.DiffApply {
			w.siafundPool = diff.Adjusted
		} else {
			w.siafundPool = diff.Previous
		}
	}
}

// revertHistory reverts any transaction history that was destroyed by reverted
// blocks in the consensus change.
func (w *Wallet) revertHistory(cc modules.ConsensusChange) {
	for _, block := range cc.RevertedBlocks {
		// Remove any transactions that have been reverted.
		for i := len(block.Transactions) - 1; i >= 0; i-- {
			// If the transaction is relevant to the wallet, it will be the
			// most recent transaction appended to w.processedTransactions.
			// Relevance can be determined just by looking at the last element
			// of w.processedTransactions.
			txn := block.Transactions[i]
			txid := txn.ID()
			if len(w.processedTransactions) > 0 && txid == w.processedTransactions[len(w.processedTransactions)-1].TransactionID {
				w.processedTransactions = w.processedTransactions[:len(w.processedTransactions)-1]
				delete(w.processedTransactionMap, txid)
			}
		}

		// Remove the miner payout transaction if applicable.
		for _, mp := range block.MinerPayouts {
			_, exists := w.keys[mp.UnlockHash]
			if exists {
				w.processedTransactions = w.processedTransactions[:len(w.processedTransactions)-1]
				delete(w.processedTransactionMap, types.TransactionID(block.ID()))
				break
			}
		}
		w.consensusSetHeight--
	}
}

// applyHistory applies any transaction history that was introduced by the
// applied blocks.
func (w *Wallet) applyHistory(cc modules.ConsensusChange) {
	for _, block := range cc.AppliedBlocks {
		w.consensusSetHeight++
		// Apply the miner payout transaction if applicable.
		minerPT := modules.ProcessedTransaction{
			Transaction:           types.Transaction{},
			TransactionID:         types.TransactionID(block.ID()),
			ConfirmationHeight:    w.consensusSetHeight,
			ConfirmationTimestamp: block.Timestamp,
		}
		relevant := false
		for i, mp := range block.MinerPayouts {
			_, exists := w.keys[mp.UnlockHash]
			if exists {
				relevant = true
			}
			minerPT.Outputs = append(minerPT.Outputs, modules.ProcessedOutput{
				FundType:       types.SpecifierMinerPayout,
				MaturityHeight: w.consensusSetHeight + types.MaturityDelay,
				WalletAddress:  exists,
				RelatedAddress: mp.UnlockHash,
				Value:          mp.Value,
			})
			w.historicOutputs[types.OutputID(block.MinerPayoutID(uint64(i)))] = mp.Value
		}
		if relevant {
			w.processedTransactions = append(w.processedTransactions, minerPT)
			w.processedTransactionMap[minerPT.TransactionID] = &w.processedTransactions[len(w.processedTransactions)-1]
		}
		for _, txn := range block.Transactions {
			relevant := false
			pt := modules.ProcessedTransaction{
				Transaction:           txn,
				TransactionID:         txn.ID(),
				ConfirmationHeight:    w.consensusSetHeight,
				ConfirmationTimestamp: block.Timestamp,
			}
			for _, sci := range txn.SiacoinInputs {
				_, exists := w.keys[sci.UnlockConditions.UnlockHash()]
				if exists {
					relevant = true
				}
				pt.Inputs = append(pt.Inputs, modules.ProcessedInput{
					FundType:       types.SpecifierSiacoinInput,
					WalletAddress:  exists,
					RelatedAddress: sci.UnlockConditions.UnlockHash(),
					Value:          w.historicOutputs[types.OutputID(sci.ParentID)],
				})
			}
			for i, sco := range txn.SiacoinOutputs {
				_, exists := w.keys[sco.UnlockHash]
				if exists {
					relevant = true
				}
				pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
					FundType:       types.SpecifierSiacoinOutput,
					MaturityHeight: w.consensusSetHeight,
					WalletAddress:  exists,
					RelatedAddress: sco.UnlockHash,
					Value:          sco.Value,
				})
				w.historicOutputs[types.OutputID(txn.SiacoinOutputID(uint64(i)))] = sco.Value
			}
			for _, sfi := range txn.SiafundInputs {
				_, exists := w.keys[sfi.UnlockConditions.UnlockHash()]
				if exists {
					relevant = true
				}
				sfiValue := w.historicOutputs[types.OutputID(sfi.ParentID)]
				pt.Inputs = append(pt.Inputs, modules.ProcessedInput{
					FundType:       types.SpecifierSiafundInput,
					WalletAddress:  exists,
					RelatedAddress: sfi.UnlockConditions.UnlockHash(),
					Value:          sfiValue,
				})
				claimValue := w.siafundPool.Sub(w.historicClaimStarts[sfi.ParentID]).Mul(sfiValue)
				pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
					FundType:       types.SpecifierClaimOutput,
					MaturityHeight: w.consensusSetHeight + types.MaturityDelay,
					WalletAddress:  exists,
					RelatedAddress: sfi.ClaimUnlockHash,
					Value:          claimValue,
				})
			}
			for i, sfo := range txn.SiafundOutputs {
				_, exists := w.keys[sfo.UnlockHash]
				if exists {
					relevant = true
				}
				pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
					FundType:       types.SpecifierSiafundOutput,
					MaturityHeight: w.consensusSetHeight,
					WalletAddress:  exists,
					RelatedAddress: sfo.UnlockHash,
					Value:          sfo.Value,
				})
				w.historicOutputs[types.OutputID(txn.SiafundOutputID(uint64(i)))] = sfo.Value
				w.historicClaimStarts[txn.SiafundOutputID(uint64(i))] = sfo.ClaimStart
			}
			for _, fee := range txn.MinerFees {
				pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
					FundType: types.SpecifierMinerFee,
					Value:    fee,
				})
			}
			if relevant {
				w.processedTransactions = append(w.processedTransactions, pt)
				w.processedTransactionMap[pt.TransactionID] = &w.processedTransactions[len(w.processedTransactions)-1]
			}
		}
	}
}

// ProcessConsensusChange parses a consensus change to update the set of
// confirmed outputs known to the wallet.
func (w *Wallet) ProcessConsensusChange(cc modules.ConsensusChange) {
	if err := w.tg.Add(); err != nil {
		// The wallet should gracefully reject updates from the consensus set
		// or transaction pool that are sent after the wallet's Close method
		// has closed the wallet's ThreadGroup.
		return
	}
	defer w.tg.Done()
	w.mu.Lock()
	defer w.mu.Unlock()
	w.updateConfirmedSet(cc)
	w.revertHistory(cc)
	w.applyHistory(cc)
	w.defragWallet()
}

// ReceiveUpdatedUnconfirmedTransactions updates the wallet's unconfirmed
// transaction set.
func (w *Wallet) ReceiveUpdatedUnconfirmedTransactions(txns []types.Transaction, _ modules.ConsensusChange) {
	if err := w.tg.Add(); err != nil {
		// Gracefully reject transactions if the wallet's Close method has
		// closed the wallet's ThreadGroup already.
		return
	}
	defer w.tg.Done()
	w.mu.Lock()
	defer w.mu.Unlock()

	w.unconfirmedProcessedTransactions = nil
	for _, txn := range txns {
		// To save on code complexity, relevancy is determined while building
		// up the wallet transaction.
		relevant := false
		pt := modules.ProcessedTransaction{
			Transaction:           txn,
			TransactionID:         txn.ID(),
			ConfirmationHeight:    types.BlockHeight(math.MaxUint64),
			ConfirmationTimestamp: types.Timestamp(math.MaxUint64),
		}
		for _, sci := range txn.SiacoinInputs {
			_, exists := w.keys[sci.UnlockConditions.UnlockHash()]
			if exists {
				relevant = true
			}
			pt.Inputs = append(pt.Inputs, modules.ProcessedInput{
				FundType:       types.SpecifierSiacoinInput,
				WalletAddress:  exists,
				RelatedAddress: sci.UnlockConditions.UnlockHash(),
				Value:          w.historicOutputs[types.OutputID(sci.ParentID)],
			})
		}
		for i, sco := range txn.SiacoinOutputs {
			_, exists := w.keys[sco.UnlockHash]
			if exists {
				relevant = true
			}
			pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
				FundType:       types.SpecifierSiacoinOutput,
				MaturityHeight: types.BlockHeight(math.MaxUint64),
				WalletAddress:  exists,
				RelatedAddress: sco.UnlockHash,
				Value:          sco.Value,
			})
			w.historicOutputs[types.OutputID(txn.SiacoinOutputID(uint64(i)))] = sco.Value
		}
		for _, fee := range txn.MinerFees {
			pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
				FundType: types.SpecifierMinerFee,
				Value:    fee,
			})
		}
		if relevant {
			w.unconfirmedProcessedTransactions = append(w.unconfirmedProcessedTransactions, pt)
		}
	}
}
<commit_msg>only defrag if the wallet is synced and unlocked<commit_after>package wallet

import (
	"math"

	"github.com/NebulousLabs/Sia/build"
	"github.com/NebulousLabs/Sia/modules"
	"github.com/NebulousLabs/Sia/types"
)

// updateConfirmedSet uses a consensus change to update the confirmed set of
// outputs as understood by the wallet.
func (w *Wallet) updateConfirmedSet(cc modules.ConsensusChange) {
	for _, diff := range cc.SiacoinOutputDiffs {
		// Verify that the diff is relevant to the wallet.
		_, exists := w.keys[diff.SiacoinOutput.UnlockHash]
		if !exists {
			continue
		}

		_, exists = w.siacoinOutputs[diff.ID]
		if diff.Direction == modules.DiffApply {
			if build.DEBUG && exists {
				panic("adding an existing output to wallet")
			}
			w.siacoinOutputs[diff.ID] = diff.SiacoinOutput
		} else {
			if build.DEBUG && !exists {
				panic("deleting nonexisting output from wallet")
			}
			delete(w.siacoinOutputs, diff.ID)
		}
	}
	for _, diff := range cc.SiafundOutputDiffs {
		// Verify that the diff is relevant to the wallet.
		_, exists := w.keys[diff.SiafundOutput.UnlockHash]
		if !exists {
			continue
		}

		_, exists = w.siafundOutputs[diff.ID]
		if diff.Direction == modules.DiffApply {
			if build.DEBUG && exists {
				panic("adding an existing output to wallet")
			}
			w.siafundOutputs[diff.ID] = diff.SiafundOutput
		} else {
			if build.DEBUG && !exists {
				panic("deleting nonexisting output from wallet")
			}
			delete(w.siafundOutputs, diff.ID)
		}
	}
	for _, diff := range cc.SiafundPoolDiffs {
		if diff.Direction == modules.DiffApply {
			w.siafundPool = diff.Adjusted
		} else {
			w.siafundPool = diff.Previous
		}
	}
}

// revertHistory reverts any transaction history that was destroyed by reverted
// blocks in the consensus change.
func (w *Wallet) revertHistory(cc modules.ConsensusChange) {
	for _, block := range cc.RevertedBlocks {
		// Remove any transactions that have been reverted.
		for i := len(block.Transactions) - 1; i >= 0; i-- {
			// If the transaction is relevant to the wallet, it will be the
			// most recent transaction appended to w.processedTransactions.
			// Relevance can be determined just by looking at the last element
			// of w.processedTransactions.
			txn := block.Transactions[i]
			txid := txn.ID()
			if len(w.processedTransactions) > 0 && txid == w.processedTransactions[len(w.processedTransactions)-1].TransactionID {
				w.processedTransactions = w.processedTransactions[:len(w.processedTransactions)-1]
				delete(w.processedTransactionMap, txid)
			}
		}

		// Remove the miner payout transaction if applicable.
		for _, mp := range block.MinerPayouts {
			_, exists := w.keys[mp.UnlockHash]
			if exists {
				w.processedTransactions = w.processedTransactions[:len(w.processedTransactions)-1]
				delete(w.processedTransactionMap, types.TransactionID(block.ID()))
				break
			}
		}
		w.consensusSetHeight--
	}
}

// applyHistory applies any transaction history that was introduced by the
// applied blocks.
func (w *Wallet) applyHistory(cc modules.ConsensusChange) {
	for _, block := range cc.AppliedBlocks {
		w.consensusSetHeight++
		// Apply the miner payout transaction if applicable.
		minerPT := modules.ProcessedTransaction{
			Transaction:           types.Transaction{},
			TransactionID:         types.TransactionID(block.ID()),
			ConfirmationHeight:    w.consensusSetHeight,
			ConfirmationTimestamp: block.Timestamp,
		}
		relevant := false
		for i, mp := range block.MinerPayouts {
			_, exists := w.keys[mp.UnlockHash]
			if exists {
				relevant = true
			}
			minerPT.Outputs = append(minerPT.Outputs, modules.ProcessedOutput{
				FundType:       types.SpecifierMinerPayout,
				MaturityHeight: w.consensusSetHeight + types.MaturityDelay,
				WalletAddress:  exists,
				RelatedAddress: mp.UnlockHash,
				Value:          mp.Value,
			})
			w.historicOutputs[types.OutputID(block.MinerPayoutID(uint64(i)))] = mp.Value
		}
		if relevant {
			w.processedTransactions = append(w.processedTransactions, minerPT)
			w.processedTransactionMap[minerPT.TransactionID] = &w.processedTransactions[len(w.processedTransactions)-1]
		}
		for _, txn := range block.Transactions {
			relevant := false
			pt := modules.ProcessedTransaction{
				Transaction:           txn,
				TransactionID:         txn.ID(),
				ConfirmationHeight:    w.consensusSetHeight,
				ConfirmationTimestamp: block.Timestamp,
			}
			for _, sci := range txn.SiacoinInputs {
				_, exists := w.keys[sci.UnlockConditions.UnlockHash()]
				if exists {
					relevant = true
				}
				pt.Inputs = append(pt.Inputs, modules.ProcessedInput{
					FundType:       types.SpecifierSiacoinInput,
					WalletAddress:  exists,
					RelatedAddress: sci.UnlockConditions.UnlockHash(),
					Value:          w.historicOutputs[types.OutputID(sci.ParentID)],
				})
			}
			for i, sco := range txn.SiacoinOutputs {
				_, exists := w.keys[sco.UnlockHash]
				if exists {
					relevant = true
				}
				pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
					FundType:       types.SpecifierSiacoinOutput,
					MaturityHeight: w.consensusSetHeight,
					WalletAddress:  exists,
					RelatedAddress: sco.UnlockHash,
					Value:          sco.Value,
				})
				w.historicOutputs[types.OutputID(txn.SiacoinOutputID(uint64(i)))] = sco.Value
			}
			for _, sfi := range txn.SiafundInputs {
				_, exists := w.keys[sfi.UnlockConditions.UnlockHash()]
				if exists {
					relevant = true
				}
				sfiValue := w.historicOutputs[types.OutputID(sfi.ParentID)]
				pt.Inputs = append(pt.Inputs, modules.ProcessedInput{
					FundType:       types.SpecifierSiafundInput,
					WalletAddress:  exists,
					RelatedAddress: sfi.UnlockConditions.UnlockHash(),
					Value:          sfiValue,
				})
				claimValue := w.siafundPool.Sub(w.historicClaimStarts[sfi.ParentID]).Mul(sfiValue)
				pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
					FundType:       types.SpecifierClaimOutput,
					MaturityHeight: w.consensusSetHeight + types.MaturityDelay,
					WalletAddress:  exists,
					RelatedAddress: sfi.ClaimUnlockHash,
					Value:          claimValue,
				})
			}
			for i, sfo := range txn.SiafundOutputs {
				_, exists := w.keys[sfo.UnlockHash]
				if exists {
					relevant = true
				}
				pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
					FundType:       types.SpecifierSiafundOutput,
					MaturityHeight: w.consensusSetHeight,
					WalletAddress:  exists,
					RelatedAddress: sfo.UnlockHash,
					Value:          sfo.Value,
				})
				w.historicOutputs[types.OutputID(txn.SiafundOutputID(uint64(i)))] = sfo.Value
				w.historicClaimStarts[txn.SiafundOutputID(uint64(i))] = sfo.ClaimStart
			}
			for _, fee := range txn.MinerFees {
				pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
					FundType: types.SpecifierMinerFee,
					Value:    fee,
				})
			}
			if relevant {
				w.processedTransactions = append(w.processedTransactions, pt)
				w.processedTransactionMap[pt.TransactionID] = &w.processedTransactions[len(w.processedTransactions)-1]
			}
		}
	}
}

// ProcessConsensusChange parses a consensus change to update the set of
// confirmed outputs known to the wallet.
func (w *Wallet) ProcessConsensusChange(cc modules.ConsensusChange) {
	if err := w.tg.Add(); err != nil {
		// The wallet should gracefully reject updates from the consensus set
		// or transaction pool that are sent after the wallet's Close method
		// has closed the wallet's ThreadGroup.
		return
	}
	defer w.tg.Done()
	w.mu.Lock()
	defer w.mu.Unlock()
	w.updateConfirmedSet(cc)
	w.revertHistory(cc)
	w.applyHistory(cc)

	if cc.Synced && w.unlocked {
		w.defragWallet()
	}
}

// ReceiveUpdatedUnconfirmedTransactions updates the wallet's unconfirmed
// transaction set.
func (w *Wallet) ReceiveUpdatedUnconfirmedTransactions(txns []types.Transaction, _ modules.ConsensusChange) {
	if err := w.tg.Add(); err != nil {
		// Gracefully reject transactions if the wallet's Close method has
		// closed the wallet's ThreadGroup already.
		return
	}
	defer w.tg.Done()
	w.mu.Lock()
	defer w.mu.Unlock()

	w.unconfirmedProcessedTransactions = nil
	for _, txn := range txns {
		// To save on code complexity, relevancy is determined while building
		// up the wallet transaction.
		relevant := false
		pt := modules.ProcessedTransaction{
			Transaction:           txn,
			TransactionID:         txn.ID(),
			ConfirmationHeight:    types.BlockHeight(math.MaxUint64),
			ConfirmationTimestamp: types.Timestamp(math.MaxUint64),
		}
		for _, sci := range txn.SiacoinInputs {
			_, exists := w.keys[sci.UnlockConditions.UnlockHash()]
			if exists {
				relevant = true
			}
			pt.Inputs = append(pt.Inputs, modules.ProcessedInput{
				FundType:       types.SpecifierSiacoinInput,
				WalletAddress:  exists,
				RelatedAddress: sci.UnlockConditions.UnlockHash(),
				Value:          w.historicOutputs[types.OutputID(sci.ParentID)],
			})
		}
		for i, sco := range txn.SiacoinOutputs {
			_, exists := w.keys[sco.UnlockHash]
			if exists {
				relevant = true
			}
			pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
				FundType:       types.SpecifierSiacoinOutput,
				MaturityHeight: types.BlockHeight(math.MaxUint64),
				WalletAddress:  exists,
				RelatedAddress: sco.UnlockHash,
				Value:          sco.Value,
			})
			w.historicOutputs[types.OutputID(txn.SiacoinOutputID(uint64(i)))] = sco.Value
		}
		for _, fee := range txn.MinerFees {
			pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
				FundType: types.SpecifierMinerFee,
				Value:    fee,
			})
		}
		if relevant {
			w.unconfirmedProcessedTransactions = append(w.unconfirmedProcessedTransactions, pt)
		}
	}
}
<|endoftext|>


<commit_before>//go:build !darwin && !windows
// +build !darwin,!windows

package mount

import (
	"os"
	"path"
	"strings"
	"testing"

	"github.com/moby/sys/mountinfo"
)

func TestMountOptionsParsing(t *testing.T) {
	options := "noatime,ro,noexec,size=10k"

	flag, data := parseOptions(options)

	if data != "size=10k" {
		t.Fatalf("Expected size=10 got %s", data)
	}

	expected := NOATIME | RDONLY | NOEXEC

	if flag != expected {
		t.Fatalf("Expected %d got %d", expected, flag)
	}
}

func TestMounted(t *testing.T) {
	if os.Getuid() != 0 {
		t.Skip("root required")
	}

	var (
		tmp        = t.TempDir()
		sourceDir  = path.Join(tmp, "source")
		targetDir  = path.Join(tmp, "target")
		sourcePath = path.Join(sourceDir, "file.txt")
		targetPath = path.Join(targetDir, "file.txt")
	)

	if err := os.Mkdir(sourceDir, 0o777); err != nil {
		t.Fatal(err)
	}
	if err := os.Mkdir(targetDir, 0o777); err != nil {
		t.Fatal(err)
	}

	if err := os.WriteFile(sourcePath, []byte("hello"), 0o644); err != nil {
		t.Fatal(err)
	}

	if err := os.WriteFile(targetPath, nil, 0o644); err != nil {
		t.Fatal(err)
	}

	if err := Mount(sourceDir, targetDir, "none", "bind,rw"); err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := Unmount(targetDir); err != nil {
			t.Fatal(err)
		}
	}()

	mounted, err := mountinfo.Mounted(targetDir)
	if err != nil {
		t.Fatal(err)
	}
	if !mounted {
		t.Fatalf("Expected %s to be mounted", targetDir)
	}
	if _, err := os.Stat(targetDir); err != nil {
		t.Fatal(err)
	}
}

func TestMountTmpfsOptions(t *testing.T) {
	if os.Getuid() != 0 {
		t.Skip("root required")
	}

	testCases := []struct {
		opts       string
		expected   string
		unexpected string
	}{
		{
			opts:       "exec",
			unexpected: "noexec",
		},
		{
			opts:       "noexec",
			expected:   "noexec",
			unexpected: "exec",
		},
	}

	target := t.TempDir()
	for _, tc := range testCases {
		t.Run(tc.opts, func(t *testing.T) {
			if err := Mount("tmpfs", target, "tmpfs", tc.opts); err != nil {
				t.Fatal(err)
			}
			defer ensureUnmount(t, target)

			mounts, err := mountinfo.GetMounts(mountinfo.SingleEntryFilter(target))
			if err != nil {
				t.Fatal(err)
			}
			if len(mounts) != 1 {
				t.Fatal("Mount point ", target, " not found")
			}
			entry := mounts[0]
			opts := "," + entry.Options + ","
			if tc.expected != "" && !strings.Contains(opts, ","+tc.expected+",") {
				t.Fatal("Expected option ", tc.expected, " missing from ", entry.Options)
			}
			if tc.unexpected != "" && strings.Contains(opts, ","+tc.unexpected+",") {
				t.Fatal("Unexpected option ", tc.unexpected, " in ", entry.Options)
			}
		})
	}
}

func TestMountReadonly(t *testing.T) {
	if os.Getuid() != 0 {
		t.Skip("root required")
	}

	var (
		tmp        = t.TempDir()
		sourceDir  = path.Join(tmp, "source")
		targetDir  = path.Join(tmp, "target")
		sourcePath = path.Join(sourceDir, "file.txt")
		targetPath = path.Join(targetDir, "file.txt")
	)

	if err := os.Mkdir(sourceDir, 0o777); err != nil {
		t.Fatal(err)
	}
	if err := os.Mkdir(targetDir, 0o777); err != nil {
		t.Fatal(err)
	}

	if err := os.WriteFile(sourcePath, []byte("hello"), 0o644); err != nil {
		t.Fatal(err)
	}

	if err := os.WriteFile(targetPath, nil, 0o644); err != nil {
		t.Fatal(err)
	}

	if err := Mount(sourceDir, targetDir, "none", "bind,ro"); err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := Unmount(targetDir); err != nil {
			t.Fatal(err)
		}
	}()

	if err := os.WriteFile(targetPath, []byte("hello"), 0o644); err == nil {
		t.Fatal("Should not be able to open a ro file as rw")
	}
}

func TestMergeTmpfsOptions(t *testing.T) {
	options := []string{"noatime", "ro", "size=10k", "defaults", "noexec", "atime", "defaults", "rw", "rprivate", "size=1024k", "slave", "exec"}
	expected := []string{"atime", "rw", "size=1024k", "slave", "exec"}
	merged, err := MergeTmpfsOptions(options)
	if err != nil {
		t.Fatal(err)
	}
	if len(expected) != len(merged) {
		t.Fatalf("Expected %s got %s", expected, merged)
	}
	for index := range merged {
		if merged[index] != expected[index] {
			t.Fatalf("Expected %s for the %dth option, got %s", expected, index, merged)
		}
	}

	options = []string{"noatime", "ro", "size=10k", "atime", "rw", "rprivate", "size=1024k", "slave", "size", "exec"}
	_, err = MergeTmpfsOptions(options)
	if err == nil {
		t.Fatal("Expected error got nil")
	}
}

func TestRecursiveUnmountTooGreedy(t *testing.T) {
	if os.Getuid() != 0 {
		t.Skip("root required")
	}

	tmp := t.TempDir()
	// Create a bunch of tmpfs mounts. Make sure "dir" itself is not
	// a mount point, or we'll hit the fast path in RecursiveUnmount.
	dirs := []string{"dir-other", "dir/subdir1", "dir/subdir1/subsub", "dir/subdir2/subsub"}
	for _, d := range dirs {
		dir := path.Join(tmp, d)
		if err := os.MkdirAll(dir, 0o700); err != nil {
			t.Fatal(err)
		}
		if err := Mount("tmpfs", dir, "tmpfs", ""); err != nil {
			t.Fatal(err)
		}
		//nolint:errcheck
		defer Unmount(dir)
	}
	// sanity check
	mounted, err := mountinfo.Mounted(path.Join(tmp, "dir-other"))
	if err != nil {
		t.Fatalf("[pre-check] error from mountinfo.mounted: %v", err)
	}
	if !mounted {
		t.Fatal("[pre-check] expected dir-other to be mounted, but it's not")
	}
	// Unmount dir, make sure dir-other is still mounted.
	if err := RecursiveUnmount(path.Join(tmp, "dir")); err != nil {
		t.Fatal(err)
	}
	mounted, err = mountinfo.Mounted(path.Join(tmp, "dir-other"))
	if err != nil {
		t.Fatalf("error from mountinfo.mounted: %v", err)
	}
	if !mounted {
		t.Fatal("expected dir-other to be mounted, but it's not")
	}
}
<commit_msg>Add test for RecursiveUnmount when a submount fails to unmount.<commit_after>//go:build !darwin && !windows
// +build !darwin,!windows

package mount

import (
	"errors"
	"os"
	"path"
	"strings"
	"testing"

	"github.com/moby/sys/mountinfo"
	"golang.org/x/sys/unix"
)

func TestMountOptionsParsing(t *testing.T) {
	options := "noatime,ro,noexec,size=10k"

	flag, data := parseOptions(options)

	if data != "size=10k" {
		t.Fatalf("Expected size=10 got %s", data)
	}

	expected := NOATIME | RDONLY | NOEXEC

	if flag != expected {
		t.Fatalf("Expected %d got %d", expected, flag)
	}
}

func TestMounted(t *testing.T) {
	if os.Getuid() != 0 {
		t.Skip("root required")
	}

	var (
		tmp        = t.TempDir()
		sourceDir  = path.Join(tmp, "source")
		targetDir  = path.Join(tmp, "target")
		sourcePath = path.Join(sourceDir, "file.txt")
		targetPath = path.Join(targetDir, "file.txt")
	)

	if err := os.Mkdir(sourceDir, 0o777); err != nil {
		t.Fatal(err)
	}
	if err := os.Mkdir(targetDir, 0o777); err != nil {
		t.Fatal(err)
	}

	if err := os.WriteFile(sourcePath, []byte("hello"), 0o644); err != nil {
		t.Fatal(err)
	}

	if err := os.WriteFile(targetPath, nil, 0o644); err != nil {
		t.Fatal(err)
	}

	if err := Mount(sourceDir, targetDir, "none", "bind,rw"); err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := Unmount(targetDir); err != nil {
			t.Fatal(err)
		}
	}()

	mounted, err := mountinfo.Mounted(targetDir)
	if err != nil {
		t.Fatal(err)
	}
	if !mounted {
		t.Fatalf("Expected %s to be mounted", targetDir)
	}
	if _, err := os.Stat(targetDir); err != nil {
		t.Fatal(err)
	}
}

func TestMountTmpfsOptions(t *testing.T) {
	if os.Getuid() != 0 {
		t.Skip("root required")
	}

	testCases := []struct {
		opts       string
		expected   string
		unexpected string
	}{
		{
			opts:       "exec",
			unexpected: "noexec",
		},
		{
			opts:       "noexec",
			expected:   "noexec",
			unexpected: "exec",
		},
	}

	target := t.TempDir()
	for _, tc := range testCases {
		t.Run(tc.opts, func(t *testing.T) {
			if err := Mount("tmpfs", target, "tmpfs", tc.opts); err != nil {
				t.Fatal(err)
			}
			defer ensureUnmount(t, target)

			mounts, err := mountinfo.GetMounts(mountinfo.SingleEntryFilter(target))
			if err != nil {
				t.Fatal(err)
			}
			if len(mounts) != 1 {
				t.Fatal("Mount point ", target, " not found")
			}
			entry := mounts[0]
			opts := "," + entry.Options + ","
			if tc.expected != "" && !strings.Contains(opts, ","+tc.expected+",") {
				t.Fatal("Expected option ", tc.expected, " missing from ", entry.Options)
			}
			if tc.unexpected != "" && strings.Contains(opts, ","+tc.unexpected+",") {
				t.Fatal("Unexpected option ", tc.unexpected, " in ", entry.Options)
			}
		})
	}
}

func TestMountReadonly(t *testing.T) {
	if os.Getuid() != 0 {
		t.Skip("root required")
	}

	var (
		tmp        = t.TempDir()
		sourceDir  = path.Join(tmp, "source")
		targetDir  = path.Join(tmp, "target")
		sourcePath = path.Join(sourceDir, "file.txt")
		targetPath = path.Join(targetDir, "file.txt")
	)

	if err := os.Mkdir(sourceDir, 0o777); err != nil {
		t.Fatal(err)
	}
	if err := os.Mkdir(targetDir, 0o777); err != nil {
		t.Fatal(err)
	}

	if err := os.WriteFile(sourcePath, []byte("hello"), 0o644); err != nil {
		t.Fatal(err)
	}

	if err := os.WriteFile(targetPath, nil, 0o644); err != nil {
		t.Fatal(err)
	}

	if err := Mount(sourceDir, targetDir, "none", "bind,ro"); err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := Unmount(targetDir); err != nil {
			t.Fatal(err)
		}
	}()

	if err := os.WriteFile(targetPath, []byte("hello"), 0o644); err == nil {
		t.Fatal("Should not be able to open a ro file as rw")
	}
}

func TestMergeTmpfsOptions(t *testing.T) {
	options := []string{"noatime", "ro", "size=10k", "defaults", "noexec", "atime", "defaults", "rw", "rprivate", "size=1024k", "slave", "exec"}
	expected := []string{"atime", "rw", "size=1024k", "slave", "exec"}
	merged, err := MergeTmpfsOptions(options)
	if err != nil {
		t.Fatal(err)
	}
	if len(expected) != len(merged) {
		t.Fatalf("Expected %s got %s", expected, merged)
	}
	for index := range merged {
		if merged[index] != expected[index] {
			t.Fatalf("Expected %s for the %dth option, got %s", expected, index, merged)
		}
	}

	options = []string{"noatime", "ro", "size=10k", "atime", "rw", "rprivate", "size=1024k", "slave", "size", "exec"}
	_, err = MergeTmpfsOptions(options)
	if err == nil {
		t.Fatal("Expected error got nil")
	}
}

func TestRecursiveUnmountTooGreedy(t *testing.T) {
	if os.Getuid() != 0 {
		t.Skip("root required")
	}

	tmp := t.TempDir()
	// Create a bunch of tmpfs mounts. Make sure "dir" itself is not
	// a mount point, or we'll hit the fast path in RecursiveUnmount.
	dirs := []string{"dir-other", "dir/subdir1", "dir/subdir1/subsub", "dir/subdir2/subsub"}
	for _, d := range dirs {
		dir := path.Join(tmp, d)
		if err := os.MkdirAll(dir, 0o700); err != nil {
			t.Fatal(err)
		}
		if err := Mount("tmpfs", dir, "tmpfs", ""); err != nil {
			t.Fatal(err)
		}
		//nolint:errcheck
		defer Unmount(dir)
	}
	// sanity check
	mounted, err := mountinfo.Mounted(path.Join(tmp, "dir-other"))
	if err != nil {
		t.Fatalf("[pre-check] error from mountinfo.mounted: %v", err)
	}
	if !mounted {
		t.Fatal("[pre-check] expected dir-other to be mounted, but it's not")
	}
	// Unmount dir, make sure dir-other is still mounted.
	if err := RecursiveUnmount(path.Join(tmp, "dir")); err != nil {
		t.Fatal(err)
	}
	mounted, err = mountinfo.Mounted(path.Join(tmp, "dir-other"))
	if err != nil {
		t.Fatalf("error from mountinfo.mounted: %v", err)
	}
	if !mounted {
		t.Fatal("expected dir-other to be mounted, but it's not")
	}
}

func TestRecursiveUnmount_SubMountFailsToUnmount(t *testing.T) {
	if os.Getuid() != 0 {
		t.Skip("root required")
	}

	var (
		tmp        = t.TempDir()
		parent     = tmp + "/sub1"
		child      = tmp + "/sub1/sub2"
		grandChild = tmp + "/sub1/sub2/sub3"
	)

	err := os.MkdirAll(grandChild, 0o700)
	if err != nil {
		t.Fatal(err)
	}

	// Create a set of mounts that should result in RecursiveUnmount failure,
	// caused by the fact that the grandchild mount is shadowed by the child mount,
	// and the child mount is shadowed by the parent mount. So. these two mounts
	// are listed in mountinfo, but since they are unreachable, unmount will fail.
	toMount := []string{grandChild, child, parent}
	for _, dir := range toMount {
		dir := dir
		if err := Mount("tmpfs", dir, "tmpfs", ""); err != nil {
			t.Fatal(err)
		}
		defer Unmount(dir) //nolint:errcheck
	}

	// unmount shadowed mounts
	shadowedMounts := []string{child, grandChild}
	for _, shadowedMount := range shadowedMounts {
		t.Run(shadowedMount, func(t *testing.T) {
			err := RecursiveUnmount(shadowedMount)
			if !errors.Is(err, unix.ENOENT) {
				t.Fatalf("expected submount(shadowed) %s to return unix.ENOENT, got %v", shadowedMount, err)
			}
		})
	}
}
<|endoftext|>


<commit_before>package acl

import (
	"github.com/ligato/cn-infra/logging"
	"github.com/ligato/vpp-agent/clientv1/linux"

	"github.com/contiv/vpp/plugins/policy/renderer"
	"github.com/contiv/vpp/plugins/policy/renderer/cache"
)

// Renderer renders Contiv Rules into VPP ACLs.
// ACLs are installed into VPP by the aclplugin from vpp-agent.
// The configuration changes are transported into aclplugin via localclient.
type Renderer struct {
	Deps

	aclTxnFactory func() (dsl linux.DataChangeDSL)
	aclResyncTxnFactory func() (dsl linux.DataResyncDSL)
}

// Deps lists dependencies of Renderer.
type Deps struct {
	Log   logging.Logger
	Cache cache.ContivRuleCacheAPI
}

// RendererTxn represents a single transaction of Renderer.
type RendererTxn struct {
	cacheTxn cache.Txn
	resync   bool
}

func NewRenderer(
	aclTxnFactory func() (dsl linux.DataChangeDSL),
	aclResyncTxnFactory func() (dsl linux.DataResyncDSL),
) *Renderer {
	return &Renderer{aclTxnFactory: aclTxnFactory, aclResyncTxnFactory: aclResyncTxnFactory}
}

// NewTxn starts a new transaction. The rendering executes only after Commit()
// is called. Rollback is not yet supported however.
// If <resync> is enabled, the supplied configuration will completely
// replace the existing one. Otherwise, the change is performed incrementally,
// i.e. interfaces not mentioned in the transaction are left unaffected.
func (ar *Renderer) NewTxn(resync bool) *RendererTxn {
	return &RendererTxn{cacheTxn: ar.Cache.NewTxn(resync), resync: resync}
}

// Render applies the set of ingress & egress rules for a given VPP interface.
// The existing rules are replaced.
// Te actual change is performed only after the commit.
func (art *RendererTxn) Render(ifName string, ingress []renderer.ContivRule, egress []renderer.ContivRule) *RendererTxn {
	art.cacheTxn.Update(ifName, ingress, egress)
	return art
}

// Commit proceeds with the rendering. A minimalistic set of changes is
// calculated using ContivRuleCache and applied as one transaction via the
// localclient.
func (art *RendererTxn) Commit() error {
	// ingress,egress := art.cacheTxn.Changes()
	// TODO: process and apply changes via localclient as one transaction
	// art.cacheTxn.Commit()
	return nil
}
<commit_msg>Fix golint and gofmt issues.<commit_after>package acl

import (
	"github.com/ligato/cn-infra/logging"
	"github.com/ligato/vpp-agent/clientv1/linux"

	"github.com/contiv/vpp/plugins/policy/renderer"
	"github.com/contiv/vpp/plugins/policy/renderer/cache"
)

// Renderer renders Contiv Rules into VPP ACLs.
// ACLs are installed into VPP by the aclplugin from vpp-agent.
// The configuration changes are transported into aclplugin via localclient.
type Renderer struct {
	Deps

	aclTxnFactory       func() (dsl linux.DataChangeDSL)
	aclResyncTxnFactory func() (dsl linux.DataResyncDSL)
}

// Deps lists dependencies of Renderer.
type Deps struct {
	Log   logging.Logger
	Cache cache.ContivRuleCacheAPI
}

// RendererTxn represents a single transaction of Renderer.
type RendererTxn struct {
	cacheTxn cache.Txn
	resync   bool
}

// NewRenderer is a constructor for ACL Renderer.
func NewRenderer(
	aclTxnFactory func() (dsl linux.DataChangeDSL),
	aclResyncTxnFactory func() (dsl linux.DataResyncDSL),
) *Renderer {
	return &Renderer{aclTxnFactory: aclTxnFactory, aclResyncTxnFactory: aclResyncTxnFactory}
}

// NewTxn starts a new transaction. The rendering executes only after Commit()
// is called. Rollback is not yet supported however.
// If <resync> is enabled, the supplied configuration will completely
// replace the existing one. Otherwise, the change is performed incrementally,
// i.e. interfaces not mentioned in the transaction are left unaffected.
func (ar *Renderer) NewTxn(resync bool) *RendererTxn {
	return &RendererTxn{cacheTxn: ar.Cache.NewTxn(resync), resync: resync}
}

// Render applies the set of ingress & egress rules for a given VPP interface.
// The existing rules are replaced.
// Te actual change is performed only after the commit.
func (art *RendererTxn) Render(ifName string, ingress []renderer.ContivRule, egress []renderer.ContivRule) *RendererTxn {
	art.cacheTxn.Update(ifName, ingress, egress)
	return art
}

// Commit proceeds with the rendering. A minimalistic set of changes is
// calculated using ContivRuleCache and applied as one transaction via the
// localclient.
func (art *RendererTxn) Commit() error {
	// ingress,egress := art.cacheTxn.Changes()
	// TODO: process and apply changes via localclient as one transaction
	// art.cacheTxn.Commit()
	return nil
}
<|endoftext|>


<commit_before>package play

import (
	"time"

	"github.com/CyCoreSystems/ari"
	"github.com/pkg/errors"
	"golang.org/x/net/context"
)

// AllDTMF is a string which contains all possible
// DTMF digits.
const AllDTMF = "0123456789ABCD*#"

// playStages executes a staged playback, waiting for its completion
func playStaged(ctx context.Context, h *ari.PlaybackHandle, opts *Options) (Status, error) {
	if opts == nil {
		opts = NewDefaultOptions()
	}

	started := h.Subscribe(ari.Events.PlaybackStarted)
	defer started.Cancel()
	finished := h.Subscribe(ari.Events.PlaybackFinished)
	defer finished.Cancel()

	err := h.Exec()
	if err != nil {
		return Failed, errors.Wrap(err, "failed to start playback")
	}
	defer h.Stop() // nolint: errcheck

	select {
	case <-ctx.Done():
		return Cancelled, nil
	case <-time.After(opts.playbackStartTimeout):
		return Timeout, errors.New("timeout waiting for playback to start")
	case <-finished.Events():
		return Finished, nil
	case <-started.Events():
	}

	// Wait for playback to complete
	select {
	case <-ctx.Done():
		return Cancelled, nil
	case <-finished.Events():
		return Finished, nil
	}
}

// NewPlay creates a new audio Options suitable for general audio playback
func NewPlay(ctx context.Context, p ari.Player, opts ...OptionFunc) (*Options, error) {
	o := NewDefaultOptions()
	err := o.ApplyOptions(opts...)

	return o, err
}

// NewPrompt creates a new audio Options suitable for prompt-style playback-and-get-response situations
func NewPrompt(ctx context.Context, p ari.Player, opts ...OptionFunc) (*Options, error) {
	o := NewPromptOptions()
	err := o.ApplyOptions(opts...)

	return o, err
}

// Play plays a set of media URIs.  Pass these URIs in with the `URI` OptionFunc.
func Play(ctx context.Context, p ari.Player, opts ...OptionFunc) *Result {
	o, err := NewPlay(ctx, p, opts...)
	if err != nil && o.result.Error != nil {
		o.result.Error = err
		return o.result
	}

	o.result.Error = o.Play(ctx, p)
	return o.result
}

// Prompt plays the given media URIs and waits for a DTMF response.  The
// received DTMF is available as `DTMF` in the Result object.  Further
// customize the behaviour with Match type OptionFuncs.
func Prompt(ctx context.Context, p ari.Player, opts ...OptionFunc) *Result {
	o, err := NewPrompt(ctx, p, opts...)
	if err != nil && o.result.Error != nil {
		o.result.Error = err
		return o.result
	}

	o.result.Error = o.Play(ctx, p)
	return o.result
}

// PlayAsync executes a playback, returning a channel which will be closed when the playback is ended
func (o *Options) PlayAsync(ctx context.Context, p ari.Player) <-chan error {
	ch := make(chan error)

	go func() {
		ch <- o.Play(ctx, p)
		close(ch)
	}()

	return ch
}

// Play executes a playback with the existing options.
func (o *Options) Play(ctx context.Context, p ari.Player) error {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	if o.result == nil {
		o.result = new(Result)
	}

	if o.uriList == nil || o.uriList.Empty() {
		return errors.New("empty playback URI list")
	}

	// cancel if we go over the maximum time
	go o.watchMaxTime(ctx)

	// Listen for DTMF
	go o.listenDTMF(ctx, p)

	for i := 0; i < o.maxReplays+1; i++ {
		if ctx.Err() != nil {
			break
		}

		// Reset the digit cache
		o.result.mu.Lock()
		o.result.DTMF = ""
		o.result.mu.Unlock()

		// Play the sequence of audio URIs
		err := o.playSequence(ctx, p)
		if err != nil {
			return err
		}

		// Wait for digits in the silence after the playback sequence completes
		o.waitDigits(ctx)
	}

	return nil

}

// playSequence plays the complete audio sequence
func (o *Options) playSequence(ctx context.Context, p ari.Player) (err error) {
	s := newSequence(o)
	defer s.Stop()

	go s.Play(ctx, p)

	select {
	case <-ctx.Done():
	case <-o.digitChan:
	case err = <-s.Done():
	}

	return err
}

func (o *Options) waitDigits(ctx context.Context) {
	overallTimer := time.NewTimer(o.overallDigitTimeout)
	defer overallTimer.Stop()

	digitTimeout := o.firstDigitTimeout

	for {
		select {
		case <-ctx.Done():
			return
		case <-time.After(digitTimeout):
			return
		case <-overallTimer.C:
			return
		case <-o.digitChan:
			digitTimeout = o.interDigitTimeout

			// Determine if a match was found
			if o.matchFunc != nil {
				o.result.mu.Lock()
				o.result.DTMF, o.result.MatchResult = o.matchFunc(o.result.DTMF)
				o.result.mu.Unlock()

				switch o.result.MatchResult {
				case Complete:
					// If we have a complete response, close the entire playback
					// and return
					o.Stop()
					return
				case Invalid:
					// If invalid, return without waiting
					// for any more digits
					return
				default:
					// Incomplete means we should wait for more
				}
			}
		}
	}

}

// Stop terminates the execution of a playback
func (o *Options) Stop() {
	if o.result == nil {
		o.result = new(Result)
	}

	if o.result.Status == InProgress {
		o.result.Status = Cancelled
	}

	if o.cancel != nil {
		o.cancel()
	}
}

func (o *Options) watchMaxTime(ctx context.Context) {
	select {
	case <-ctx.Done():
		return
	case <-time.After(o.maxPlaybackTime):
		o.Stop()
	}
}
func (o *Options) listenDTMF(ctx context.Context, p ari.Player) {
	sub := p.Subscribe(ari.Events.ChannelDtmfReceived)
	defer sub.Cancel()

	for {
		select {
		case <-ctx.Done():
			return
		case e := <-sub.Events():
			if e == nil {
				return
			}
			v, ok := e.(*ari.ChannelDtmfReceived)
			if !ok {
				continue
			}
			o.result.mu.Lock()
			o.result.DTMF += v.Digit
			o.result.mu.Unlock()

			// Signal receipt of digit, but never block in doing so
			select {
			case o.digitChan <- v.Digit:
			default:
			}

		}
	}
}
<commit_msg>Proper stop cleanup and notes in playSequence<commit_after>package play

import (
	"time"

	"github.com/CyCoreSystems/ari"
	"github.com/pkg/errors"
	"golang.org/x/net/context"
)

// AllDTMF is a string which contains all possible
// DTMF digits.
const AllDTMF = "0123456789ABCD*#"

// playStages executes a staged playback, waiting for its completion
func playStaged(ctx context.Context, h *ari.PlaybackHandle, opts *Options) (Status, error) {
	if opts == nil {
		opts = NewDefaultOptions()
	}

	started := h.Subscribe(ari.Events.PlaybackStarted)
	defer started.Cancel()
	finished := h.Subscribe(ari.Events.PlaybackFinished)
	defer finished.Cancel()

	err := h.Exec()
	if err != nil {
		return Failed, errors.Wrap(err, "failed to start playback")
	}
	defer h.Stop() // nolint: errcheck

	select {
	case <-ctx.Done():
		return Cancelled, nil
	case <-time.After(opts.playbackStartTimeout):
		return Timeout, errors.New("timeout waiting for playback to start")
	case <-finished.Events():
		return Finished, nil
	case <-started.Events():
	}

	// Wait for playback to complete
	select {
	case <-ctx.Done():
		return Cancelled, nil
	case <-finished.Events():
		return Finished, nil
	}
}

// NewPlay creates a new audio Options suitable for general audio playback
func NewPlay(ctx context.Context, p ari.Player, opts ...OptionFunc) (*Options, error) {
	o := NewDefaultOptions()
	err := o.ApplyOptions(opts...)

	return o, err
}

// NewPrompt creates a new audio Options suitable for prompt-style playback-and-get-response situations
func NewPrompt(ctx context.Context, p ari.Player, opts ...OptionFunc) (*Options, error) {
	o := NewPromptOptions()
	err := o.ApplyOptions(opts...)

	return o, err
}

// Play plays a set of media URIs.  Pass these URIs in with the `URI` OptionFunc.
func Play(ctx context.Context, p ari.Player, opts ...OptionFunc) *Result {
	o, err := NewPlay(ctx, p, opts...)
	if err != nil && o.result.Error != nil {
		o.result.Error = err
		return o.result
	}

	o.result.Error = o.Play(ctx, p)
	return o.result
}

// Prompt plays the given media URIs and waits for a DTMF response.  The
// received DTMF is available as `DTMF` in the Result object.  Further
// customize the behaviour with Match type OptionFuncs.
func Prompt(ctx context.Context, p ari.Player, opts ...OptionFunc) *Result {
	o, err := NewPrompt(ctx, p, opts...)
	if err != nil && o.result.Error != nil {
		o.result.Error = err
		return o.result
	}

	o.result.Error = o.Play(ctx, p)
	return o.result
}

// PlayAsync executes a playback, returning a channel which will be closed when the playback is ended
func (o *Options) PlayAsync(ctx context.Context, p ari.Player) <-chan error {
	ch := make(chan error)

	go func() {
		ch <- o.Play(ctx, p)
		close(ch)
	}()

	return ch
}

// Play executes a playback with the existing options.
func (o *Options) Play(ctx context.Context, p ari.Player) error {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	if o.result == nil {
		o.result = new(Result)
	}

	if o.uriList == nil || o.uriList.Empty() {
		return errors.New("empty playback URI list")
	}

	// cancel if we go over the maximum time
	go o.watchMaxTime(ctx)

	// Listen for DTMF
	go o.listenDTMF(ctx, p)

	for i := 0; i < o.maxReplays+1; i++ {
		if ctx.Err() != nil {
			break
		}

		// Reset the digit cache
		o.result.mu.Lock()
		o.result.DTMF = ""
		o.result.mu.Unlock()

		// Play the sequence of audio URIs
		err := o.playSequence(ctx, p)
		if err != nil {
			return err
		}

		// Wait for digits in the silence after the playback sequence completes
		o.waitDigits(ctx)
	}

	return nil

}

// playSequence plays the complete audio sequence
func (o *Options) playSequence(ctx context.Context, p ari.Player) (err error) {
	s := newSequence(o)

	go s.Play(ctx, p)

	select {
	case <-ctx.Done():
	case <-o.digitChan: //NOTE: this interrupts all playback, no matter what
	case <-s.Done():
	}

	s.Stop()

	// wait for cleanup of sequence so we can get the proper error result
	select {
	case <-s.Done():
	case <-ctx.Done():
	}

	err = s.opts.result.Err()
	return err
}

func (o *Options) waitDigits(ctx context.Context) {
	overallTimer := time.NewTimer(o.overallDigitTimeout)
	defer overallTimer.Stop()

	digitTimeout := o.firstDigitTimeout

	for {
		select {
		case <-ctx.Done():
			return
		case <-time.After(digitTimeout):
			return
		case <-overallTimer.C:
			return
		case <-o.digitChan:
			digitTimeout = o.interDigitTimeout

			// Determine if a match was found
			if o.matchFunc != nil {
				o.result.mu.Lock()
				o.result.DTMF, o.result.MatchResult = o.matchFunc(o.result.DTMF)
				o.result.mu.Unlock()

				switch o.result.MatchResult {
				case Complete:
					// If we have a complete response, close the entire playback
					// and return
					o.Stop()
					return
				case Invalid:
					// If invalid, return without waiting
					// for any more digits
					return
				default:
					// Incomplete means we should wait for more
				}
			}
		}
	}

}

// Stop terminates the execution of a playback
func (o *Options) Stop() {
	if o.result == nil {
		o.result = new(Result)
	}

	if o.result.Status == InProgress {
		o.result.Status = Cancelled
	}

	if o.cancel != nil {
		o.cancel()
	}
}

func (o *Options) watchMaxTime(ctx context.Context) {
	select {
	case <-ctx.Done():
		return
	case <-time.After(o.maxPlaybackTime):
		o.Stop()
	}
}
func (o *Options) listenDTMF(ctx context.Context, p ari.Player) {
	sub := p.Subscribe(ari.Events.ChannelDtmfReceived)
	defer sub.Cancel()

	for {
		select {
		case <-ctx.Done():
			return
		case e := <-sub.Events():
			if e == nil {
				return
			}
			v, ok := e.(*ari.ChannelDtmfReceived)
			if !ok {
				continue
			}
			o.result.mu.Lock()
			o.result.DTMF += v.Digit
			o.result.mu.Unlock()

			// Signal receipt of digit, but never block in doing so
			select {
			case o.digitChan <- v.Digit:
			default:
			}

		}
	}
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"math/rand"
	"net/http"
	"net/url"
	"time"

	"github.com/gorilla/mux"

	"context"
)

// ThemeVersion is the version of the current CSS-JS theme.
// It is the name of the folder containing the theme files.
const ThemeVersion = "default"

// ThemeDate is the prefix used for "revving" the static files and enable long-term HTTP cache.
// It MUST end with underscore _ (see app.yaml)
const ThemeDate = "20200421b_"

var r = mux.NewRouter()

func init() {
	initEnv()
	initToggles()
	initRoutes()

	// We want the random results to be different even if we reboot the server. Thus, we use
	// the clock to seed the default generator.
	// See https://www.programming-idioms.org/idiom/70/use-clock-as-random-generator-seed/346/go
	rand.Seed(time.Now().UnixNano())
}

func initRoutes() {
	if !toggles["online"] {
		handle("/", makeWall(" Under maintenance."))
		//r.HandleFunc("/", makeWall(" Coming soon."))
	} else {
		//handle("/", makeWall(" Coming soon."))
		handle("/", home)
		handle("/home", home)
		handle("/wall", makeWall(" Coming soon."))
		handle("/about", about)
		handle("/idiom/{idiomId}", idiomDetail)
		handle("/idiom/{idiomId}/impl/{implId}", idiomDetail)
		handle("/idiom/{idiomId}/{idiomTitle}", idiomDetail)
		handle("/idiom/{idiomId}/diff/{v1}/{v2}", versionDiff)
		handle("/idiom/{idiomId}/{idiomTitle}/{implId}/{implLang}", idiomDetail)
		handle("/history/{idiomId}", idiomHistory)
		handle("/revert", revertIdiomVersion)
		handle("/history-restore", restoreIdiomVersion)
		handle("/all-idioms", allIdioms)
		handle("/random-idiom/having/{havingLang}", randomIdiomHaving)
		handle("/random-idiom/not-having/{notHavingLang}", randomIdiomNotHaving)
		handle("/random-idiom", randomIdiom)
		handle("/search", searchRedirect)
		handle("/search/{q}", search)
		handle("/list-by-language/{langs}", listByLanguage)
		handle("/missing-fields/{lang}", missingList)
		handle("/idiom-picture", idiomPicture)
		handle("/rss-recently-created", rssRecentlyCreated)
		handle("/rss-recently-updated", rssRecentlyUpdated)
		handle("/rss-recent-changes", rssRecentChanges)
		handle("/my/{nickname}/{langs}", bookmarkableUserURL)
		handle("/my/{langs}", bookmarkableUserURL)
		handle("/cheatsheet/{lang}", cheatsheet)
		handleAjax("/typeahead-languages", typeaheadLanguages)
		handleAjax("/supported-languages", supportedLanguages)
		handleAjax("/ajax-other-implementations", ajaxOtherImplementations)
		if toggles["writable"] {
			// When not in "read-only" mode
			handle("/idiom-save", idiomSave)
			handle("/idiom-edit/{idiomId}", idiomEdit)
			handle("/idiom-add-picture/{idiomId}", idiomAddPicture)
			handle("/idiom-save-picture", idiomSavePicture)
			handle("/impl-edit/{idiomId}/{implId}", implEdit)
			//handle("/fake-idiom-save", fakeIdiomSave)
			handle("/idiom-create", idiomCreate)
			handle("/impl-create/{idiomId}", implCreate)
			handle("/impl-create/{idiomId}/{lang}", implCreate)
			handle("/impl-save", implSave)
			// Ajax
			handleAjax("/ajax-idiom-vote", ajaxIdiomVote)
			handleAjax("/ajax-impl-vote", ajaxImplVote)
			handleAjax("/ajax-demo-site-suggest", ajaxDemoSiteSuggest)
			handleAjax("/ajax-user-message-box", userMessageBoxAjax)
			handleAjax("/ajax-dismiss-user-message", dismissUserMessage)
			handle("/about-block-project", ajaxAboutProject)
			handle("/about-block-all-idioms", ajaxAboutAllIdioms)
			handle("/about-block-language-coverage", ajaxAboutLanguageCoverage)
			handle("/about-block-rss", ajaxAboutRss)
			handle("/about-block-cheatsheets", ajaxAboutCheatsheets)
			handle("/about-block-see-also", ajaxAboutSeeAlso)
			handle("/about-block-contact", ajaxAboutContact)
			// Admin
			handle("/admin", admin)
			handle("/admin-data-export", adminExport)
			handle("/admin-data-import", adminImport)
			handle("/admin-resave-entities", adminResaveEntities)
			handleAjax("/admin-repair-history-versions", adminRepairHistoryVersions)
			handleAjax("/admin-data-import-ajax", adminImportAjax)
			handleAjax("/admin-reindex-ajax", adminReindexAjax)
			handleAjax("/admin-refresh-toggles-ajax", ajaxRefreshToggles)
			handleAjax("/admin-set-toggle-ajax", ajaxSetToggle)
			handleAjax("/admin-create-relation-ajax", ajaxCreateRelation)
			handleAjax("/admin-idiom-delete", idiomDelete)
			handleAjax("/admin-impl-delete", implDelete)
			handleAjax("/admin-send-message-for-user", sendMessageForUserAjax)
		}
		handleAjax("/api/idiom/{idiomId}", jsonIdiom)
		handleAjax("/api/idioms/all", jsonAllIdioms)
		handleAjax("/api/search/{q}", jsonSearch)

		handle("/auth", handleAuth)
		handle("/_ah/login_required", handleAuth)
	}
	http.Handle("/", r)
}

// Request will fail if path parameters are missing
var neededPathVariables = map[string][]string{
	"/idiom/{idiomId}":                                  {"idiomId"},
	"/idiom/{idiomId}/impl/{implId}":                    {"idiomId"},
	"/idiom/{idiomId}/{idiomTitle}":                     {"idiomId"},
	"/idiom/{idiomId}/{idiomTitle}/{implId}/{implLang}": {"idiomId"},
	"/search/{q}":                                       {"q"},
	"/my/{nickname}/{langs}":                            {"nickname", "langs"},
	"/idiom-edit/{idiomId}":                             {"idiomId"},
	"/idiom-add-picture/{idiomId}":                      {"idiomId"},
	"/impl-edit/{idiomId}/{implId}":                     {"idiomId", "implId"},
	"/impl-create/{idiomId}":                            {"idiomId"},
	"/impl-create/{idiomId}/{lang}":                     {"idiomId"},
	"/cheatsheet/{lang}":                                {"lang"},
}

// Request will fail if it doesn't provide the required GET or POST parameters
var neededParameters = map[string][]string{
	"/typeahead-languages":         { /*todo*/ },
	"/idiom-save":                  {"idiom_title"},
	"/idiom-save-picture":          { /*todo*/ },
	"/impl-save":                   {"idiom_id", "impl_code"},
	"/revert":                      {"idiomId", "version"},
	"/ajax-idiom-vote":             {"idiomId", "choice"},
	"/ajax-impl-vote":              {"implId", "choice"},
	"/ajax-demo-site-suggest":      { /*todo*/ },
	"/ajax-dismiss-user-message":   {"key"},
	"/admin-data-export":           { /*todo*/ },
	"/admin-data-import":           { /*todo*/ },
	"/admin-data-import-ajax":      { /*todo*/ },
	"/admin-set-toggle-ajax":       {"toggle", "value"},
	"/admin-create-relation-ajax":  {"idiomAId", "idiomBId"},
	"/admin-idiom-delete":          {"idiomId"},
	"/admin-impl-delete":           {"idiomId", "implId"},
	"/admin-send-message-for-user": {"username", "message"},
	"/api/idiom":                   {"idiomId"},
}

// Request will fail if corresponding toggle is off
var neededToggles = map[string][]string{
	"/home":                         {"online"},
	"/search":                       {"searchable"},
	"/search/{q}":                   {"searchable"},
	"/idiom-save":                   {"writable"},
	"/idiom-edit/{idiomId}":         {"writable", "writable", "idiomEditing"},
	"/idiom-add-picture/{idiomId}":  {"writable", "idiomEditing"},
	"/idiom-save-picture":           {"writable", "idiomEditing"},
	"/impl-edit/{idiomId}/{implId}": {"writable", "implEditing"},
	"/idiom-create":                 {"writable"},
	"/impl-create/{idiomId}":        {"writable", "implAddition"},
	"/impl-create/{idiomId}/{lang}": {"writable", "implAddition"},
	"/impl-save":                    {"writable"},
	"/ajax-idiom-vote":              {"writable"},
	"/ajax-impl-vote":               {"writable"},
	"/admin":                        {"administrable"},
	"/admin-data-export":            {"administrable"},
	"/admin-data-import":            {"administrable"},
	"/admin-data-import-ajax":       {"administrable"},
	"/admin-set-toggle-ajax":        {"administrable"},
	"/admin-create-relation-ajax":   {"administrable"},
	"/admin-idiom-delete":           {"administrable"},
	"/admin-impl-delete":            {"administrable"},
}

type standardHandler func(w http.ResponseWriter, r *http.Request)
type betterHandler func(w http.ResponseWriter, r *http.Request) error

// Wrap HandleFunc with
// - error handling
// - mandatory path variables check
// - mandatory parameters check
// - toggles check
func handle(path string, h betterHandler) {
	r.HandleFunc(path,
		func(w http.ResponseWriter, r *http.Request) {
			if isSpam(w, r) {
				return
			}

			defer func() {
				if msg := recover(); msg != nil {
					msgStr := fmt.Sprintf("%v", msg)
					errorPage(w, r, PiError{msgStr, http.StatusInternalServerError})
					return
				}
			}()
			if configTime == "0" {
				ctx := r.Context()
				_ = refreshToggles(ctx)
				// If it fails... well, ignore for now and continue with non-fresh toggles.
			}

			if err := muxVarsMissing(w, r, neededPathVariables[path]...); err != nil {
				errorPage(w, r, err)
				return
			}
			if err := togglesMissing(w, r, neededToggles[path]...); err != nil {
				errorPage(w, r, err)
				return
			}
			if err := parametersMissing(w, r, neededParameters[path]...); err != nil {
				errorPage(w, r, err)
				return
			}
			err := h(w, r)
			if err != nil {
				errorPage(w, r, err)
			}
		})
}

func handleAjax(path string, h betterHandler) {
	r.HandleFunc(path,
		func(w http.ResponseWriter, r *http.Request) {
			if isSpam(w, r) {
				return
			}

			defer func() {
				if msg := recover(); msg != nil {
					msgStr := fmt.Sprintf("%v", msg)
					errorJSON(w, r, PiError{msgStr, http.StatusInternalServerError})
					return
				}
			}()
			if configTime == "0" {
				ctx := r.Context()
				_ = refreshToggles(ctx)
				// If it fails... well, ignore for now and continue with non-fresh toggles.
			}

			if err := muxVarsMissing(w, r, neededPathVariables[path]...); err != nil {
				errorJSON(w, r, err)
				return
			}
			if err := togglesMissing(w, r, neededToggles[path]...); err != nil {
				errorJSON(w, r, err)
				return
			}
			if err := parametersMissing(w, r, neededParameters[path]...); err != nil {
				errorJSON(w, r, err)
				return
			}
			err := h(w, r)
			if err != nil {
				errorJSON(w, r, err)
			}
		})
}

var datastoreDao = GaeDatastoreAccessor{}
var memcachedDao = MemcacheDatastoreAccessor{datastoreDao}
var dao = memcachedDao

var daoVotes = GaeVotesAccessor{}

func parametersMissing(w http.ResponseWriter, r *http.Request, params ...string) error {
	missing := []string{}
	for _, param := range params {
		if r.FormValue(param) == "" {
			missing = append(missing, param)
		}
	}
	if len(missing) > 0 {
		return PiError{fmt.Sprintf("Missing parameters : %s", missing), http.StatusBadRequest}
	}
	return nil
}

// Looks in gorilla mux populated variables
func muxVarsMissing(w http.ResponseWriter, r *http.Request, params ...string) error {
	missing := []string{}
	muxvars := mux.Vars(r)
	for _, param := range params {
		if muxvars[param] == "" {
			missing = append(missing, param)
		}
	}
	if len(missing) > 0 {
		return PiError{fmt.Sprintf("Missing parameters : %s", missing), http.StatusBadRequest}
	}
	return nil
}

func validateURLFormat(urlStr string) error {
	u, err := url.Parse(urlStr)
	if err != nil {
		return err
	}
	if !u.IsAbs() {
		return fmt.Errorf("Requires an absolute URL")
	}
	return nil
}

func validateURLFormatOrEmpty(urlStr string) error {
	if urlStr == "" {
		return nil
	}
	return validateURLFormat(urlStr)
}

/*
func logIf(err error, logfunc func(format string, args ...interface{}), when string) {
	if err != nil {
		logfunc("Problem on %v: %v", when, err.Error())
	}
}
*/

func logIf(err error, logfunc func(ctx context.Context, format string, args ...interface{}), ctx context.Context, when string) {
	if err != nil {
		logfunc(ctx, "Problem on %v: %v", when, err.Error())
	}
}
<commit_msg>Bumped static assets.<commit_after>package main

import (
	"fmt"
	"math/rand"
	"net/http"
	"net/url"
	"time"

	"github.com/gorilla/mux"

	"context"
)

// ThemeVersion is the version of the current CSS-JS theme.
// It is the name of the folder containing the theme files.
const ThemeVersion = "default"

// ThemeDate is the prefix used for "revving" the static files and enable long-term HTTP cache.
// It MUST end with underscore _ (see app.yaml)
const ThemeDate = "20200426"

var r = mux.NewRouter()

func init() {
	initEnv()
	initToggles()
	initRoutes()

	// We want the random results to be different even if we reboot the server. Thus, we use
	// the clock to seed the default generator.
	// See https://www.programming-idioms.org/idiom/70/use-clock-as-random-generator-seed/346/go
	rand.Seed(time.Now().UnixNano())
}

func initRoutes() {
	if !toggles["online"] {
		handle("/", makeWall(" Under maintenance."))
		//r.HandleFunc("/", makeWall(" Coming soon."))
	} else {
		//handle("/", makeWall(" Coming soon."))
		handle("/", home)
		handle("/home", home)
		handle("/wall", makeWall(" Coming soon."))
		handle("/about", about)
		handle("/idiom/{idiomId}", idiomDetail)
		handle("/idiom/{idiomId}/impl/{implId}", idiomDetail)
		handle("/idiom/{idiomId}/{idiomTitle}", idiomDetail)
		handle("/idiom/{idiomId}/diff/{v1}/{v2}", versionDiff)
		handle("/idiom/{idiomId}/{idiomTitle}/{implId}/{implLang}", idiomDetail)
		handle("/history/{idiomId}", idiomHistory)
		handle("/revert", revertIdiomVersion)
		handle("/history-restore", restoreIdiomVersion)
		handle("/all-idioms", allIdioms)
		handle("/random-idiom/having/{havingLang}", randomIdiomHaving)
		handle("/random-idiom/not-having/{notHavingLang}", randomIdiomNotHaving)
		handle("/random-idiom", randomIdiom)
		handle("/search", searchRedirect)
		handle("/search/{q}", search)
		handle("/list-by-language/{langs}", listByLanguage)
		handle("/missing-fields/{lang}", missingList)
		handle("/idiom-picture", idiomPicture)
		handle("/rss-recently-created", rssRecentlyCreated)
		handle("/rss-recently-updated", rssRecentlyUpdated)
		handle("/rss-recent-changes", rssRecentChanges)
		handle("/my/{nickname}/{langs}", bookmarkableUserURL)
		handle("/my/{langs}", bookmarkableUserURL)
		handle("/cheatsheet/{lang}", cheatsheet)
		handleAjax("/typeahead-languages", typeaheadLanguages)
		handleAjax("/supported-languages", supportedLanguages)
		handleAjax("/ajax-other-implementations", ajaxOtherImplementations)
		if toggles["writable"] {
			// When not in "read-only" mode
			handle("/idiom-save", idiomSave)
			handle("/idiom-edit/{idiomId}", idiomEdit)
			handle("/idiom-add-picture/{idiomId}", idiomAddPicture)
			handle("/idiom-save-picture", idiomSavePicture)
			handle("/impl-edit/{idiomId}/{implId}", implEdit)
			//handle("/fake-idiom-save", fakeIdiomSave)
			handle("/idiom-create", idiomCreate)
			handle("/impl-create/{idiomId}", implCreate)
			handle("/impl-create/{idiomId}/{lang}", implCreate)
			handle("/impl-save", implSave)
			// Ajax
			handleAjax("/ajax-idiom-vote", ajaxIdiomVote)
			handleAjax("/ajax-impl-vote", ajaxImplVote)
			handleAjax("/ajax-demo-site-suggest", ajaxDemoSiteSuggest)
			handleAjax("/ajax-user-message-box", userMessageBoxAjax)
			handleAjax("/ajax-dismiss-user-message", dismissUserMessage)
			handle("/about-block-project", ajaxAboutProject)
			handle("/about-block-all-idioms", ajaxAboutAllIdioms)
			handle("/about-block-language-coverage", ajaxAboutLanguageCoverage)
			handle("/about-block-rss", ajaxAboutRss)
			handle("/about-block-cheatsheets", ajaxAboutCheatsheets)
			handle("/about-block-see-also", ajaxAboutSeeAlso)
			handle("/about-block-contact", ajaxAboutContact)
			// Admin
			handle("/admin", admin)
			handle("/admin-data-export", adminExport)
			handle("/admin-data-import", adminImport)
			handle("/admin-resave-entities", adminResaveEntities)
			handleAjax("/admin-repair-history-versions", adminRepairHistoryVersions)
			handleAjax("/admin-data-import-ajax", adminImportAjax)
			handleAjax("/admin-reindex-ajax", adminReindexAjax)
			handleAjax("/admin-refresh-toggles-ajax", ajaxRefreshToggles)
			handleAjax("/admin-set-toggle-ajax", ajaxSetToggle)
			handleAjax("/admin-create-relation-ajax", ajaxCreateRelation)
			handleAjax("/admin-idiom-delete", idiomDelete)
			handleAjax("/admin-impl-delete", implDelete)
			handleAjax("/admin-send-message-for-user", sendMessageForUserAjax)
		}
		handleAjax("/api/idiom/{idiomId}", jsonIdiom)
		handleAjax("/api/idioms/all", jsonAllIdioms)
		handleAjax("/api/search/{q}", jsonSearch)

		handle("/auth", handleAuth)
		handle("/_ah/login_required", handleAuth)
	}
	http.Handle("/", r)
}

// Request will fail if path parameters are missing
var neededPathVariables = map[string][]string{
	"/idiom/{idiomId}":                                  {"idiomId"},
	"/idiom/{idiomId}/impl/{implId}":                    {"idiomId"},
	"/idiom/{idiomId}/{idiomTitle}":                     {"idiomId"},
	"/idiom/{idiomId}/{idiomTitle}/{implId}/{implLang}": {"idiomId"},
	"/search/{q}":                                       {"q"},
	"/my/{nickname}/{langs}":                            {"nickname", "langs"},
	"/idiom-edit/{idiomId}":                             {"idiomId"},
	"/idiom-add-picture/{idiomId}":                      {"idiomId"},
	"/impl-edit/{idiomId}/{implId}":                     {"idiomId", "implId"},
	"/impl-create/{idiomId}":                            {"idiomId"},
	"/impl-create/{idiomId}/{lang}":                     {"idiomId"},
	"/cheatsheet/{lang}":                                {"lang"},
}

// Request will fail if it doesn't provide the required GET or POST parameters
var neededParameters = map[string][]string{
	"/typeahead-languages":         { /*todo*/ },
	"/idiom-save":                  {"idiom_title"},
	"/idiom-save-picture":          { /*todo*/ },
	"/impl-save":                   {"idiom_id", "impl_code"},
	"/revert":                      {"idiomId", "version"},
	"/ajax-idiom-vote":             {"idiomId", "choice"},
	"/ajax-impl-vote":              {"implId", "choice"},
	"/ajax-demo-site-suggest":      { /*todo*/ },
	"/ajax-dismiss-user-message":   {"key"},
	"/admin-data-export":           { /*todo*/ },
	"/admin-data-import":           { /*todo*/ },
	"/admin-data-import-ajax":      { /*todo*/ },
	"/admin-set-toggle-ajax":       {"toggle", "value"},
	"/admin-create-relation-ajax":  {"idiomAId", "idiomBId"},
	"/admin-idiom-delete":          {"idiomId"},
	"/admin-impl-delete":           {"idiomId", "implId"},
	"/admin-send-message-for-user": {"username", "message"},
	"/api/idiom":                   {"idiomId"},
}

// Request will fail if corresponding toggle is off
var neededToggles = map[string][]string{
	"/home":                         {"online"},
	"/search":                       {"searchable"},
	"/search/{q}":                   {"searchable"},
	"/idiom-save":                   {"writable"},
	"/idiom-edit/{idiomId}":         {"writable", "writable", "idiomEditing"},
	"/idiom-add-picture/{idiomId}":  {"writable", "idiomEditing"},
	"/idiom-save-picture":           {"writable", "idiomEditing"},
	"/impl-edit/{idiomId}/{implId}": {"writable", "implEditing"},
	"/idiom-create":                 {"writable"},
	"/impl-create/{idiomId}":        {"writable", "implAddition"},
	"/impl-create/{idiomId}/{lang}": {"writable", "implAddition"},
	"/impl-save":                    {"writable"},
	"/ajax-idiom-vote":              {"writable"},
	"/ajax-impl-vote":               {"writable"},
	"/admin":                        {"administrable"},
	"/admin-data-export":            {"administrable"},
	"/admin-data-import":            {"administrable"},
	"/admin-data-import-ajax":       {"administrable"},
	"/admin-set-toggle-ajax":        {"administrable"},
	"/admin-create-relation-ajax":   {"administrable"},
	"/admin-idiom-delete":           {"administrable"},
	"/admin-impl-delete":            {"administrable"},
}

type standardHandler func(w http.ResponseWriter, r *http.Request)
type betterHandler func(w http.ResponseWriter, r *http.Request) error

// Wrap HandleFunc with
// - error handling
// - mandatory path variables check
// - mandatory parameters check
// - toggles check
func handle(path string, h betterHandler) {
	r.HandleFunc(path,
		func(w http.ResponseWriter, r *http.Request) {
			if isSpam(w, r) {
				return
			}

			defer func() {
				if msg := recover(); msg != nil {
					msgStr := fmt.Sprintf("%v", msg)
					errorPage(w, r, PiError{msgStr, http.StatusInternalServerError})
					return
				}
			}()
			if configTime == "0" {
				ctx := r.Context()
				_ = refreshToggles(ctx)
				// If it fails... well, ignore for now and continue with non-fresh toggles.
			}

			if err := muxVarsMissing(w, r, neededPathVariables[path]...); err != nil {
				errorPage(w, r, err)
				return
			}
			if err := togglesMissing(w, r, neededToggles[path]...); err != nil {
				errorPage(w, r, err)
				return
			}
			if err := parametersMissing(w, r, neededParameters[path]...); err != nil {
				errorPage(w, r, err)
				return
			}
			err := h(w, r)
			if err != nil {
				errorPage(w, r, err)
			}
		})
}

func handleAjax(path string, h betterHandler) {
	r.HandleFunc(path,
		func(w http.ResponseWriter, r *http.Request) {
			if isSpam(w, r) {
				return
			}

			defer func() {
				if msg := recover(); msg != nil {
					msgStr := fmt.Sprintf("%v", msg)
					errorJSON(w, r, PiError{msgStr, http.StatusInternalServerError})
					return
				}
			}()
			if configTime == "0" {
				ctx := r.Context()
				_ = refreshToggles(ctx)
				// If it fails... well, ignore for now and continue with non-fresh toggles.
			}

			if err := muxVarsMissing(w, r, neededPathVariables[path]...); err != nil {
				errorJSON(w, r, err)
				return
			}
			if err := togglesMissing(w, r, neededToggles[path]...); err != nil {
				errorJSON(w, r, err)
				return
			}
			if err := parametersMissing(w, r, neededParameters[path]...); err != nil {
				errorJSON(w, r, err)
				return
			}
			err := h(w, r)
			if err != nil {
				errorJSON(w, r, err)
			}
		})
}

var datastoreDao = GaeDatastoreAccessor{}
var memcachedDao = MemcacheDatastoreAccessor{datastoreDao}
var dao = memcachedDao

var daoVotes = GaeVotesAccessor{}

func parametersMissing(w http.ResponseWriter, r *http.Request, params ...string) error {
	missing := []string{}
	for _, param := range params {
		if r.FormValue(param) == "" {
			missing = append(missing, param)
		}
	}
	if len(missing) > 0 {
		return PiError{fmt.Sprintf("Missing parameters : %s", missing), http.StatusBadRequest}
	}
	return nil
}

// Looks in gorilla mux populated variables
func muxVarsMissing(w http.ResponseWriter, r *http.Request, params ...string) error {
	missing := []string{}
	muxvars := mux.Vars(r)
	for _, param := range params {
		if muxvars[param] == "" {
			missing = append(missing, param)
		}
	}
	if len(missing) > 0 {
		return PiError{fmt.Sprintf("Missing parameters : %s", missing), http.StatusBadRequest}
	}
	return nil
}

func validateURLFormat(urlStr string) error {
	u, err := url.Parse(urlStr)
	if err != nil {
		return err
	}
	if !u.IsAbs() {
		return fmt.Errorf("Requires an absolute URL")
	}
	return nil
}

func validateURLFormatOrEmpty(urlStr string) error {
	if urlStr == "" {
		return nil
	}
	return validateURLFormat(urlStr)
}

/*
func logIf(err error, logfunc func(format string, args ...interface{}), when string) {
	if err != nil {
		logfunc("Problem on %v: %v", when, err.Error())
	}
}
*/

func logIf(err error, logfunc func(ctx context.Context, format string, args ...interface{}), ctx context.Context, when string) {
	if err != nil {
		logfunc(ctx, "Problem on %v: %v", when, err.Error())
	}
}
<|endoftext|>