<commit_before>package core

import (
	"bytes"
	"crypto/rand"
	"fmt"
	"github.com/fire/go-ogre3d"
	"github.com/jackyb/go-sdl2/sdl"
	"github.com/jmckaskill/go-capnproto"
	"github.com/op/go-nanomsg"
	"math"
	"math/big"
	"time"
)

const ORIENTATIONLOG int = 10

type OrientationHistory struct {
	t uint64
	o ogre.Quaternion
}

type GameState struct {
	bounce        float32      // Limits of the bounce area:
	speed         float32      // Picked a speed to bounce around at startup.
	mousePressed  bool         // Go from mouse is pressed to click each time to change the control scheme.
	direction     ogre.Vector2 // Direction the head is moving on the plane:
	rotation      ogre.Vector3 // Rotation axis of the head:
	rotationSpeed Degree       // Rotation speed of the head in degrees:

	// use the last few frames of mouse input to build a smoothed angular velocity
	orientationIndex        int
	orientationHistory      [ORIENTATIONLOG]OrientationHistory
	smoothedAngular         ogre.Vector3
	smoothedAngularVelocity Degree // Degree
}

func gameInit(gsockets *GameThreadSockets, gs *GameState, rs *SharedRenderState) {
	fmt.Printf("Game Init.\n")
	gs.speed = randFloat32(59) + 40
	fmt.Printf("Random speed: %f\n", gs.speed)
	gs.bounce = 25.0
	dAngle := CreateDegree(randFloat32(359))
	gs.direction = ogre.CreateVector2FromValues(float32(math.Cos(float64(dAngle.ValueRadianFloat()))), float32(math.Sin(float64(dAngle.ValueRadianFloat()))))
	unitZ := ogre.CreateVector3()
	unitZ.UnitZ()
	rs.orientation = ogre.CreateQuaternion()
	rs.orientation.FromAngleAxis(0.0, unitZ)
	rs.position = ogre.CreateVector3()
	rs.position.Zero()
	gs.mousePressed = false
	gs.rotation = ogre.CreateVector3()
	gs.rotation.UnitX()
	gs.rotationSpeed = CreateDegree(0.0)
	gs.orientationIndex = 0
	for i := 0; i < ORIENTATIONLOG; i++ {
		gs.orientationHistory[i].o = ogre.CreateQuaternion()
	}
	gs.smoothedAngular = ogre.CreateVector3()
	fmt.Printf("Random angle: %f\n", dAngle.ValueDegreesFloat())
	// Set the input code to manipulate an object rather than look around.
	s := capn.NewBuffer(nil)
	lookAround := NewRootState(s)
	lookAround.SetConfigLookAround(true)
	lookAround.LookAround().SetManipulateObject(true)
	buf := bytes.Buffer{}
	s.WriteTo(&buf)
	gsockets.inputPush.Send(buf.Bytes(), 0)
}

func gameTick(gsockets *GameThreadSockets, gs *GameState, srs *SharedRenderState, now time.Duration) {
	fmt.Printf("Game Tick.\n")

	// Get the latest mouse buttons state and orientation.
	s := capn.NewBuffer(nil)
	state := NewRootState(s)
	state.SetMouse(true)
	buf := bytes.Buffer{}
	s.WriteTo(&buf)
	gsockets.inputPush.Send(buf.Bytes(), 0)

	b, err := gsockets.inputMouseSub.Recv(0)
	if err != nil {
		fmt.Printf("%s\n", err)
	}
	b = bytes.TrimPrefix(b, []byte("input.mouse:"))
	// var by bytes.Buffer
	// by.Write(b)
	// fmt.Printf("Bytestring START%sEND\n", by.String())
	s, _, err = capn.ReadFromMemoryZeroCopy(b)
	if err != nil {
		fmt.Printf("Read error %v\n", err)
		return
	}
	input := ReadRootInputMouse(s)
	orientation := ogre.CreateQuaternionFromValues(input.W(), input.X(), input.Y(), input.Z())
	buttons := input.Buttons()

	// At 16 ms tick and the last 10 orientations buffered, that's 150ms worth of orientation history.
	gs.orientationHistory[gs.orientationIndex].t = uint64(now)
	gs.orientationHistory[gs.orientationIndex].o = orientation
	gs.orientationIndex = (gs.orientationIndex + 1) % ORIENTATIONLOG

	// Oldest Orientation
	q1Index := gs.orientationIndex
	// NOTE: the problem with using the successive orientations to infer an angular speed,
	// is that if the orientation is changing fast enough, this code will 'flip' the speed around
	// e.g. this doesn't work, need to use the XY mouse data to track angular speed
	// NOTE: uncomment the following line to use the full history, notice the 'flip' happens at much lower speed
	q1Index = (q1Index + ORIENTATIONLOG - 2) % ORIENTATIONLOG
	q1 := gs.orientationHistory[q1Index].o
	q1T := gs.orientationHistory[q1Index].t
	omega := orientation.SubtractQuaternion(q1)
	omega = omega.MultiplyScalar(2.0)
	omega = omega.UnitInverse()
	omega = omega.MultiplyScalar(float32(float64(time.Second) / float64(now-time.Duration(q1T))))
	omega.Normalise()
	var tempDeg float32
	omega.ToAngleAxisDegree(&tempDeg, gs.smoothedAngular)
	gs.smoothedAngularVelocity = CreateDegree(tempDeg)
	fmt.Printf("%f %f %f - %f\n", gs.smoothedAngular.X(), gs.smoothedAngular.Y(), gs.smoothedAngular.Z(), gs.smoothedAngularVelocity.ValueDegreesFloat())
	srs.smoothedAngular = gs.smoothedAngular

	fmt.Printf("srs.smoothedAngular = gs.smoothedAngular\n")
	if (buttons & sdl.Button(sdl.BUTTON_LEFT)) != 0 {
		if !gs.mousePressed {
			gs.mousePressed = true
			// changing the control scheme: the player is now driving the orientation of the head directly with the mouse
			// tell the input logic to reset the orientation to match the current orientation of the head
			s := capn.NewBuffer(nil)
			state := NewRootState(s)
			state.SetMouseReset(true)
			state.Orientation().SetW(srs.orientation.W())
			state.Orientation().SetX(srs.orientation.X())
			state.Orientation().SetY(srs.orientation.Y())
			state.Orientation().SetZ(srs.orientation.Z())
			buf := bytes.Buffer{}
			s.WriteTo(&buf)
			gsockets.inputPush.Send(buf.Bytes(), 0)

			// IF RENDER TICK HAPPENS HERE: render will not know that it should grab the orientation directly from the mouse,
			// but the orientation coming from game should still be ok?

			s = capn.NewBuffer(nil)
			renderState := NewRootRenderStateMsg(s)
			renderState.SetHeadTrigger(true)
			renderState.SetFreeSpin(true)
			buf = bytes.Buffer{}
			s.WriteTo(&buf)
			gsockets.renderSocket.Send(buf.Bytes(), 0)
			// IF RENDER TICK HAPPENS HERE (before a new gamestate):
			// the now reset input orientation will combine with the old game state, that's bad
		}
	} else {
		if gs.mousePressed {
			gs.mousePressed = false
			// Changing the control scheme: the head will free spin and slow down for a bit, then it will resume bouncing around
			// the player looses mouse control, the game grabs latest orientation and angular velocity
			// the input thread was authoritative on orientation until now, so accept that as our starting orientation
			srs.orientation = orientation
			gs.rotationSpeed = gs.smoothedAngularVelocity
			gs.rotation = gs.smoothedAngular

			s := capn.NewBuffer(nil)
			renderState := NewRootRenderStateMsg(s)
			renderState.SetHeadTrigger(true)
			renderState.SetFreeSpin(false)
			buf := bytes.Buffer{}
			s.WriteTo(&buf)
			gsockets.renderSocket.Send(buf.Bytes(), 0)
			// IF RENDER TICK HAPPENS HERE (before a new gamestate): render will pull the head orientation from the game state rather than input, but game state won't have the fixed orientation yet
		}
	}

	if srs.position.X() > gs.bounce || srs.position.X() < -gs.bounce {
		gs.direction.SetX(gs.direction.X() * -1.0)
	}
	if srs.position.Y() > gs.bounce || srs.position.Y() < -gs.bounce {
		gs.direction.SetY(gs.direction.Y() * -1.0)
	}
	delta := gs.direction.MultiplyScalar(gs.speed * float32(float64(GAMEDELAY)/float64(time.Second)))
	if !gs.mousePressed {
		if gs.rotationSpeed.ValueDegreesFloat() == 0.9 {
			srs.position.SetX(srs.position.X() + delta.X())
			srs.position.SetY(srs.position.Y() + delta.Y())
		}
		//    fmt.Printf("game tick position: %f %f\n", rs.position.X(), rs.position.Y());

		// update the orientation of the head on a free roll
		// gs.rotation is unit length
		// gs.rotationSpeed is in degrees/seconds
		// NOTE: sinf/cosf really needed there?
		gs.rotationSpeed.Mul(0.97)
		if gs.rotationSpeed.ValueDegreesFloat() < 20.0 {
			gs.rotationSpeed = CreateDegree(0.0)
		}
		tempDegree := CreateDegree(gs.rotationSpeed.ValueDegreesFloat() * float32(GAMETICKFLOAT))
		var factor float32 = float32(math.Sin(float64(0.5 * tempDegree.ValueRadianFloat())))
		rotationTick := ogre.CreateQuaternionFromValues(
			float32(math.Cos(float64(0.5*tempDegree.ValueRadianFloat()))),
			factor*gs.rotation.X(),
			factor*gs.rotation.Y(),
			factor*gs.rotation.Z())
		rotationTick.Normalise()
		srs.orientation = rotationTick.MultiplyQuaternion(srs.orientation)
	} else {
		// Keep updating the orientation in the render state, even while the render thread is ignoring it:
		// when the game thread resumes control of the head orientation, it will interpolate from one of these states,
		// so we keep updating the orientation to avoid a short glitch at the discontinuity
		srs.orientation = orientation
	}
}

// Create a random 32bit float from [1,max+1).
func randFloat32(max uint64) float32 {
	i := big.NewInt(0)
	r, err := rand.Int(rand.Reader, i.SetUint64(uint64(1)<<63))
	if err != nil {
		fmt.Printf("%s\n", err)
	}
	return float32(float64(r.Uint64()) / float64(1<<63) * float64(max))
}

func emitRenderState(socket *nanomsg.BusSocket, time uint64, srs *SharedRenderState) {
	s := capn.NewBuffer(nil)
	emitted := NewRootEmittedRenderState(s)
	emitted.SetTime(time)
	emitted.Position().SetX(srs.position.X())
	emitted.Position().SetY(srs.position.Y())
	emitted.Orientation().SetW(srs.orientation.W())
	emitted.Orientation().SetX(srs.orientation.X())
	emitted.Orientation().SetY(srs.orientation.Y())
	emitted.Orientation().SetZ(srs.orientation.Z())
	emitted.SmoothedAngular().SetX(srs.smoothedAngular.X())
	emitted.SmoothedAngular().SetY(srs.smoothedAngular.Y())
	emitted.SmoothedAngular().SetZ(srs.smoothedAngular.Z())
	buf := bytes.Buffer{}
	s.WriteToPacked(&buf)
	socket.Send(buf.Bytes(), 0)
	fmt.Printf("Emitted render state.\n")
}
<commit_msg>Revert "Write a packed message."<commit_after>package core

import (
	"bytes"
	"crypto/rand"
	"fmt"
	"github.com/fire/go-ogre3d"
	"github.com/jackyb/go-sdl2/sdl"
	"github.com/jmckaskill/go-capnproto"
	"github.com/op/go-nanomsg"
	"math"
	"math/big"
	"time"
)

const ORIENTATIONLOG int = 10

type OrientationHistory struct {
	t uint64
	o ogre.Quaternion
}

type GameState struct {
	bounce        float32      // Limits of the bounce area:
	speed         float32      // Picked a speed to bounce around at startup.
	mousePressed  bool         // Go from mouse is pressed to click each time to change the control scheme.
	direction     ogre.Vector2 // Direction the head is moving on the plane:
	rotation      ogre.Vector3 // Rotation axis of the head:
	rotationSpeed Degree       // Rotation speed of the head in degrees:

	// use the last few frames of mouse input to build a smoothed angular velocity
	orientationIndex        int
	orientationHistory      [ORIENTATIONLOG]OrientationHistory
	smoothedAngular         ogre.Vector3
	smoothedAngularVelocity Degree // Degree
}

func gameInit(gsockets *GameThreadSockets, gs *GameState, rs *SharedRenderState) {
	fmt.Printf("Game Init.\n")
	gs.speed = randFloat32(59) + 40
	fmt.Printf("Random speed: %f\n", gs.speed)
	gs.bounce = 25.0
	dAngle := CreateDegree(randFloat32(359))
	gs.direction = ogre.CreateVector2FromValues(float32(math.Cos(float64(dAngle.ValueRadianFloat()))), float32(math.Sin(float64(dAngle.ValueRadianFloat()))))
	unitZ := ogre.CreateVector3()
	unitZ.UnitZ()
	rs.orientation = ogre.CreateQuaternion()
	rs.orientation.FromAngleAxis(0.0, unitZ)
	rs.position = ogre.CreateVector3()
	rs.position.Zero()
	gs.mousePressed = false
	gs.rotation = ogre.CreateVector3()
	gs.rotation.UnitX()
	gs.rotationSpeed = CreateDegree(0.0)
	gs.orientationIndex = 0
	for i := 0; i < ORIENTATIONLOG; i++ {
		gs.orientationHistory[i].o = ogre.CreateQuaternion()
	}
	gs.smoothedAngular = ogre.CreateVector3()
	fmt.Printf("Random angle: %f\n", dAngle.ValueDegreesFloat())
	// Set the input code to manipulate an object rather than look around.
	s := capn.NewBuffer(nil)
	lookAround := NewRootState(s)
	lookAround.SetConfigLookAround(true)
	lookAround.LookAround().SetManipulateObject(true)
	buf := bytes.Buffer{}
	s.WriteTo(&buf)
	gsockets.inputPush.Send(buf.Bytes(), 0)
}

func gameTick(gsockets *GameThreadSockets, gs *GameState, srs *SharedRenderState, now time.Duration) {
	fmt.Printf("Game Tick.\n")

	// Get the latest mouse buttons state and orientation.
	s := capn.NewBuffer(nil)
	state := NewRootState(s)
	state.SetMouse(true)
	buf := bytes.Buffer{}
	s.WriteTo(&buf)
	gsockets.inputPush.Send(buf.Bytes(), 0)

	b, err := gsockets.inputMouseSub.Recv(0)
	if err != nil {
		fmt.Printf("%s\n", err)
	}
	b = bytes.TrimPrefix(b, []byte("input.mouse:"))
	// var by bytes.Buffer
	// by.Write(b)
	// fmt.Printf("Bytestring START%sEND\n", by.String())
	s, _, err = capn.ReadFromMemoryZeroCopy(b)
	if err != nil {
		fmt.Printf("Read error %v\n", err)
		return
	}
	input := ReadRootInputMouse(s)
	orientation := ogre.CreateQuaternionFromValues(input.W(), input.X(), input.Y(), input.Z())
	buttons := input.Buttons()

	// At 16 ms tick and the last 10 orientations buffered, that's 150ms worth of orientation history.
	gs.orientationHistory[gs.orientationIndex].t = uint64(now)
	gs.orientationHistory[gs.orientationIndex].o = orientation
	gs.orientationIndex = (gs.orientationIndex + 1) % ORIENTATIONLOG

	// Oldest Orientation
	q1Index := gs.orientationIndex
	// NOTE: the problem with using the successive orientations to infer an angular speed,
	// is that if the orientation is changing fast enough, this code will 'flip' the speed around
	// e.g. this doesn't work, need to use the XY mouse data to track angular speed
	// NOTE: uncomment the following line to use the full history, notice the 'flip' happens at much lower speed
	q1Index = (q1Index + ORIENTATIONLOG - 2) % ORIENTATIONLOG
	q1 := gs.orientationHistory[q1Index].o
	q1T := gs.orientationHistory[q1Index].t
	omega := orientation.SubtractQuaternion(q1)
	omega = omega.MultiplyScalar(2.0)
	omega = omega.UnitInverse()
	omega = omega.MultiplyScalar(float32(float64(time.Second) / float64(now-time.Duration(q1T))))
	omega.Normalise()
	var tempDeg float32
	omega.ToAngleAxisDegree(&tempDeg, gs.smoothedAngular)
	gs.smoothedAngularVelocity = CreateDegree(tempDeg)
	fmt.Printf("%f %f %f - %f\n", gs.smoothedAngular.X(), gs.smoothedAngular.Y(), gs.smoothedAngular.Z(), gs.smoothedAngularVelocity.ValueDegreesFloat())
	srs.smoothedAngular = gs.smoothedAngular

	fmt.Printf("srs.smoothedAngular = gs.smoothedAngular\n")
	if (buttons & sdl.Button(sdl.BUTTON_LEFT)) != 0 {
		if !gs.mousePressed {
			gs.mousePressed = true
			// changing the control scheme: the player is now driving the orientation of the head directly with the mouse
			// tell the input logic to reset the orientation to match the current orientation of the head
			s := capn.NewBuffer(nil)
			state := NewRootState(s)
			state.SetMouseReset(true)
			state.Orientation().SetW(srs.orientation.W())
			state.Orientation().SetX(srs.orientation.X())
			state.Orientation().SetY(srs.orientation.Y())
			state.Orientation().SetZ(srs.orientation.Z())
			buf := bytes.Buffer{}
			s.WriteTo(&buf)
			gsockets.inputPush.Send(buf.Bytes(), 0)

			// IF RENDER TICK HAPPENS HERE: render will not know that it should grab the orientation directly from the mouse,
			// but the orientation coming from game should still be ok?

			s = capn.NewBuffer(nil)
			renderState := NewRootRenderStateMsg(s)
			renderState.SetHeadTrigger(true)
			renderState.SetFreeSpin(true)
			buf = bytes.Buffer{}
			s.WriteTo(&buf)
			gsockets.renderSocket.Send(buf.Bytes(), 0)
			// IF RENDER TICK HAPPENS HERE (before a new gamestate):
			// the now reset input orientation will combine with the old game state, that's bad
		}
	} else {
		if gs.mousePressed {
			gs.mousePressed = false
			// Changing the control scheme: the head will free spin and slow down for a bit, then it will resume bouncing around
			// the player looses mouse control, the game grabs latest orientation and angular velocity
			// the input thread was authoritative on orientation until now, so accept that as our starting orientation
			srs.orientation = orientation
			gs.rotationSpeed = gs.smoothedAngularVelocity
			gs.rotation = gs.smoothedAngular

			s := capn.NewBuffer(nil)
			renderState := NewRootRenderStateMsg(s)
			renderState.SetHeadTrigger(true)
			renderState.SetFreeSpin(false)
			buf := bytes.Buffer{}
			s.WriteTo(&buf)
			gsockets.renderSocket.Send(buf.Bytes(), 0)
			// IF RENDER TICK HAPPENS HERE (before a new gamestate): render will pull the head orientation from the game state rather than input, but game state won't have the fixed orientation yet
		}
	}

	if srs.position.X() > gs.bounce || srs.position.X() < -gs.bounce {
		gs.direction.SetX(gs.direction.X() * -1.0)
	}
	if srs.position.Y() > gs.bounce || srs.position.Y() < -gs.bounce {
		gs.direction.SetY(gs.direction.Y() * -1.0)
	}
	delta := gs.direction.MultiplyScalar(gs.speed * float32(float64(GAMEDELAY)/float64(time.Second)))
	if !gs.mousePressed {
		if gs.rotationSpeed.ValueDegreesFloat() == 0.9 {
			srs.position.SetX(srs.position.X() + delta.X())
			srs.position.SetY(srs.position.Y() + delta.Y())
		}
		//    fmt.Printf("game tick position: %f %f\n", rs.position.X(), rs.position.Y());

		// update the orientation of the head on a free roll
		// gs.rotation is unit length
		// gs.rotationSpeed is in degrees/seconds
		// NOTE: sinf/cosf really needed there?
		gs.rotationSpeed.Mul(0.97)
		if gs.rotationSpeed.ValueDegreesFloat() < 20.0 {
			gs.rotationSpeed = CreateDegree(0.0)
		}
		tempDegree := CreateDegree(gs.rotationSpeed.ValueDegreesFloat() * float32(GAMETICKFLOAT))
		var factor float32 = float32(math.Sin(float64(0.5 * tempDegree.ValueRadianFloat())))
		rotationTick := ogre.CreateQuaternionFromValues(
			float32(math.Cos(float64(0.5*tempDegree.ValueRadianFloat()))),
			factor*gs.rotation.X(),
			factor*gs.rotation.Y(),
			factor*gs.rotation.Z())
		rotationTick.Normalise()
		srs.orientation = rotationTick.MultiplyQuaternion(srs.orientation)
	} else {
		// Keep updating the orientation in the render state, even while the render thread is ignoring it:
		// when the game thread resumes control of the head orientation, it will interpolate from one of these states,
		// so we keep updating the orientation to avoid a short glitch at the discontinuity
		srs.orientation = orientation
	}
}

// Create a random 32bit float from [1,max+1).
func randFloat32(max uint64) float32 {
	i := big.NewInt(0)
	r, err := rand.Int(rand.Reader, i.SetUint64(uint64(1)<<63))
	if err != nil {
		fmt.Printf("%s\n", err)
	}
	return float32(float64(r.Uint64()) / float64(1<<63) * float64(max))
}

func emitRenderState(socket *nanomsg.BusSocket, time uint64, srs *SharedRenderState) {
	s := capn.NewBuffer(nil)
	emitted := NewRootEmittedRenderState(s)
	emitted.SetTime(time)
	emitted.Position().SetX(srs.position.X())
	emitted.Position().SetY(srs.position.Y())
	emitted.Orientation().SetW(srs.orientation.W())
	emitted.Orientation().SetX(srs.orientation.X())
	emitted.Orientation().SetY(srs.orientation.Y())
	emitted.Orientation().SetZ(srs.orientation.Z())
	emitted.SmoothedAngular().SetX(srs.smoothedAngular.X())
	emitted.SmoothedAngular().SetY(srs.smoothedAngular.Y())
	emitted.SmoothedAngular().SetZ(srs.smoothedAngular.Z())
	buf := bytes.Buffer{}
	s.WriteTo(&buf)
	socket.Send(buf.Bytes(), 0)
	fmt.Printf("Emitted render state.\n")
}
<|endoftext|>


<commit_before>// package ipnsfs implements an in memory model of a mutable ipns filesystem,
// to be used by the fuse filesystem.
//
// It consists of four main structs:
// 1) The Filesystem
//        The filesystem serves as a container and entry point for the ipns filesystem
// 2) KeyRoots
//        KeyRoots represent the root of the keyspace controlled by a given keypair
// 3) Directories
// 4) Files
package ipnsfs

import (
	"errors"
	"fmt"
	"os"
	"sync"
	"time"

	key "github.com/ipfs/go-ipfs/blocks/key"
	dag "github.com/ipfs/go-ipfs/merkledag"
	namesys "github.com/ipfs/go-ipfs/namesys"
	ci "github.com/ipfs/go-ipfs/p2p/crypto"
	path "github.com/ipfs/go-ipfs/path"
	pin "github.com/ipfs/go-ipfs/pin"
	ft "github.com/ipfs/go-ipfs/unixfs"

	context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
	eventlog "github.com/ipfs/go-ipfs/thirdparty/eventlog"
)

var log = eventlog.Logger("ipnsfs")

var ErrIsDirectory = errors.New("error: is a directory")

// Filesystem is the writeable fuse filesystem structure
type Filesystem struct {
	dserv dag.DAGService

	nsys namesys.NameSystem

	resolver *path.Resolver

	pins pin.Pinner

	roots map[string]*KeyRoot
}

// NewFilesystem instantiates an ipns filesystem using the given parameters and locally owned keys
func NewFilesystem(ctx context.Context, ds dag.DAGService, nsys namesys.NameSystem, pins pin.Pinner, keys ...ci.PrivKey) (*Filesystem, error) {
	roots := make(map[string]*KeyRoot)
	fs := &Filesystem{
		roots:    roots,
		nsys:     nsys,
		dserv:    ds,
		pins:     pins,
		resolver: &path.Resolver{DAG: ds},
	}
	for _, k := range keys {
		pkh, err := k.GetPublic().Hash()
		if err != nil {
			return nil, err
		}

		root, err := fs.newKeyRoot(ctx, k)
		if err != nil {
			return nil, err
		}
		roots[key.Key(pkh).Pretty()] = root
	}

	return fs, nil
}

func (fs *Filesystem) Close() error {
	wg := sync.WaitGroup{}
	for _, r := range fs.roots {
		wg.Add(1)
		go func(r *KeyRoot) {
			defer wg.Done()
			err := r.Publish(context.TODO())
			if err != nil {
				log.Info(err)
				return
			}
		}(r)
	}
	wg.Wait()
	return nil
}

// GetRoot returns the KeyRoot of the given name
func (fs *Filesystem) GetRoot(name string) (*KeyRoot, error) {
	r, ok := fs.roots[name]
	if ok {
		return r, nil
	}
	return nil, os.ErrNotExist
}

type childCloser interface {
	closeChild(string, *dag.Node) error
}

type NodeType int

const (
	TFile NodeType = iota
	TDir
)

// FSNode represents any node (directory, root, or file) in the ipns filesystem
type FSNode interface {
	GetNode() (*dag.Node, error)
	Type() NodeType
	Lock()
	Unlock()
}

// KeyRoot represents the root of a filesystem tree pointed to by a given keypair
type KeyRoot struct {
	key ci.PrivKey

	// node is the merkledag node pointed to by this keypair
	node *dag.Node

	// A pointer to the filesystem to access components
	fs *Filesystem

	// val represents the node pointed to by this key. It can either be a File or a Directory
	val FSNode

	repub *Republisher
}

// newKeyRoot creates a new KeyRoot for the given key, and starts up a republisher routine
// for it
func (fs *Filesystem) newKeyRoot(parent context.Context, k ci.PrivKey) (*KeyRoot, error) {
	hash, err := k.GetPublic().Hash()
	if err != nil {
		return nil, err
	}

	name := "/ipns/" + key.Key(hash).String()

	root := new(KeyRoot)
	root.key = k
	root.fs = fs

	ctx, cancel := context.WithCancel(parent)
	defer cancel()

	pointsTo, err := fs.nsys.Resolve(ctx, name)
	if err != nil {
		err = namesys.InitializeKeyspace(ctx, fs.dserv, fs.nsys, fs.pins, k)
		if err != nil {
			return nil, err
		}

		pointsTo, err = fs.nsys.Resolve(ctx, name)
		if err != nil {
			return nil, err
		}
	}

	mnode, err := fs.resolver.ResolvePath(ctx, pointsTo)
	if err != nil {
		log.Errorf("Failed to retrieve value '%s' for ipns entry: %s\n", pointsTo, err)
		return nil, err
	}

	root.node = mnode

	root.repub = NewRepublisher(root, time.Millisecond*300, time.Second*3)
	go root.repub.Run(parent)

	pbn, err := ft.FromBytes(mnode.Data)
	if err != nil {
		log.Error("IPNS pointer was not unixfs node")
		return nil, err
	}

	switch pbn.GetType() {
	case ft.TDirectory:
		root.val = NewDirectory(pointsTo.String(), mnode, root, fs)
	case ft.TFile, ft.TMetadata, ft.TRaw:
		fi, err := NewFile(pointsTo.String(), mnode, root, fs)
		if err != nil {
			return nil, err
		}
		root.val = fi
	default:
		panic("unrecognized! (NYI)")
	}
	return root, nil
}

func (kr *KeyRoot) GetValue() FSNode {
	return kr.val
}

// closeChild implements the childCloser interface, and signals to the publisher that
// there are changes ready to be published
func (kr *KeyRoot) closeChild(name string, nd *dag.Node) error {
	kr.repub.Touch()
	return nil
}

// Publish publishes the ipns entry associated with this key
func (kr *KeyRoot) Publish(ctx context.Context) error {
	child, ok := kr.val.(FSNode)
	if !ok {
		return errors.New("child of key root not valid type")
	}

	nd, err := child.GetNode()
	if err != nil {
		return err
	}

	// Holding this lock so our child doesnt change out from under us
	child.Lock()
	k, err := kr.fs.dserv.Add(nd)
	if err != nil {
		child.Unlock()
		return err
	}
	child.Unlock()
	// Dont want to hold the lock while we publish
	// otherwise we are holding the lock through a costly
	// network operation

	fmt.Println("Publishing!")
	return kr.fs.nsys.Publish(ctx, kr.key, path.FromKey(k))
}

// Republisher manages when to publish the ipns entry associated with a given key
type Republisher struct {
	TimeoutLong  time.Duration
	TimeoutShort time.Duration
	Publish      chan struct{}
	root         *KeyRoot
}

// NewRepublisher creates a new Republisher object to republish the given keyroot
// using the given short and long time intervals
func NewRepublisher(root *KeyRoot, tshort, tlong time.Duration) *Republisher {
	return &Republisher{
		TimeoutShort: tshort,
		TimeoutLong:  tlong,
		Publish:      make(chan struct{}, 1),
		root:         root,
	}
}

// Touch signals that an update has occurred since the last publish.
// Multiple consecutive touches may extend the time period before
// the next Publish occurs in order to more efficiently batch updates
func (np *Republisher) Touch() {
	select {
	case np.Publish <- struct{}{}:
	default:
	}
}

// Run is the main republisher loop
func (np *Republisher) Run(ctx context.Context) {
	for {
		select {
		case <-np.Publish:
			quick := time.After(np.TimeoutShort)
			longer := time.After(np.TimeoutLong)

		wait:
			select {
			case <-ctx.Done():
				return
			case <-np.Publish:
				quick = time.After(np.TimeoutShort)
				goto wait
			case <-quick:
			case <-longer:
			}

			log.Info("Publishing Changes!")
			err := np.root.Publish(ctx)
			if err != nil {
				log.Error("republishRoot error: %s", err)
			}

		case <-ctx.Done():
			return
		}
	}
}
<commit_msg>ipnsfs: remove "Publishing!" println<commit_after>// package ipnsfs implements an in memory model of a mutable ipns filesystem,
// to be used by the fuse filesystem.
//
// It consists of four main structs:
// 1) The Filesystem
//        The filesystem serves as a container and entry point for the ipns filesystem
// 2) KeyRoots
//        KeyRoots represent the root of the keyspace controlled by a given keypair
// 3) Directories
// 4) Files
package ipnsfs

import (
	"errors"
	"os"
	"sync"
	"time"

	key "github.com/ipfs/go-ipfs/blocks/key"
	dag "github.com/ipfs/go-ipfs/merkledag"
	namesys "github.com/ipfs/go-ipfs/namesys"
	ci "github.com/ipfs/go-ipfs/p2p/crypto"
	path "github.com/ipfs/go-ipfs/path"
	pin "github.com/ipfs/go-ipfs/pin"
	ft "github.com/ipfs/go-ipfs/unixfs"

	context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
	eventlog "github.com/ipfs/go-ipfs/thirdparty/eventlog"
)

var log = eventlog.Logger("ipnsfs")

var ErrIsDirectory = errors.New("error: is a directory")

// Filesystem is the writeable fuse filesystem structure
type Filesystem struct {
	dserv dag.DAGService

	nsys namesys.NameSystem

	resolver *path.Resolver

	pins pin.Pinner

	roots map[string]*KeyRoot
}

// NewFilesystem instantiates an ipns filesystem using the given parameters and locally owned keys
func NewFilesystem(ctx context.Context, ds dag.DAGService, nsys namesys.NameSystem, pins pin.Pinner, keys ...ci.PrivKey) (*Filesystem, error) {
	roots := make(map[string]*KeyRoot)
	fs := &Filesystem{
		roots:    roots,
		nsys:     nsys,
		dserv:    ds,
		pins:     pins,
		resolver: &path.Resolver{DAG: ds},
	}
	for _, k := range keys {
		pkh, err := k.GetPublic().Hash()
		if err != nil {
			return nil, err
		}

		root, err := fs.newKeyRoot(ctx, k)
		if err != nil {
			return nil, err
		}
		roots[key.Key(pkh).Pretty()] = root
	}

	return fs, nil
}

func (fs *Filesystem) Close() error {
	wg := sync.WaitGroup{}
	for _, r := range fs.roots {
		wg.Add(1)
		go func(r *KeyRoot) {
			defer wg.Done()
			err := r.Publish(context.TODO())
			if err != nil {
				log.Info(err)
				return
			}
		}(r)
	}
	wg.Wait()
	return nil
}

// GetRoot returns the KeyRoot of the given name
func (fs *Filesystem) GetRoot(name string) (*KeyRoot, error) {
	r, ok := fs.roots[name]
	if ok {
		return r, nil
	}
	return nil, os.ErrNotExist
}

type childCloser interface {
	closeChild(string, *dag.Node) error
}

type NodeType int

const (
	TFile NodeType = iota
	TDir
)

// FSNode represents any node (directory, root, or file) in the ipns filesystem
type FSNode interface {
	GetNode() (*dag.Node, error)
	Type() NodeType
	Lock()
	Unlock()
}

// KeyRoot represents the root of a filesystem tree pointed to by a given keypair
type KeyRoot struct {
	key  ci.PrivKey
	name string

	// node is the merkledag node pointed to by this keypair
	node *dag.Node

	// A pointer to the filesystem to access components
	fs *Filesystem

	// val represents the node pointed to by this key. It can either be a File or a Directory
	val FSNode

	repub *Republisher
}

// newKeyRoot creates a new KeyRoot for the given key, and starts up a republisher routine
// for it
func (fs *Filesystem) newKeyRoot(parent context.Context, k ci.PrivKey) (*KeyRoot, error) {
	hash, err := k.GetPublic().Hash()
	if err != nil {
		return nil, err
	}

	name := "/ipns/" + key.Key(hash).String()

	root := new(KeyRoot)
	root.key = k
	root.fs = fs
	root.name = name

	ctx, cancel := context.WithCancel(parent)
	defer cancel()

	pointsTo, err := fs.nsys.Resolve(ctx, name)
	if err != nil {
		err = namesys.InitializeKeyspace(ctx, fs.dserv, fs.nsys, fs.pins, k)
		if err != nil {
			return nil, err
		}

		pointsTo, err = fs.nsys.Resolve(ctx, name)
		if err != nil {
			return nil, err
		}
	}

	mnode, err := fs.resolver.ResolvePath(ctx, pointsTo)
	if err != nil {
		log.Errorf("Failed to retrieve value '%s' for ipns entry: %s\n", pointsTo, err)
		return nil, err
	}

	root.node = mnode

	root.repub = NewRepublisher(root, time.Millisecond*300, time.Second*3)
	go root.repub.Run(parent)

	pbn, err := ft.FromBytes(mnode.Data)
	if err != nil {
		log.Error("IPNS pointer was not unixfs node")
		return nil, err
	}

	switch pbn.GetType() {
	case ft.TDirectory:
		root.val = NewDirectory(pointsTo.String(), mnode, root, fs)
	case ft.TFile, ft.TMetadata, ft.TRaw:
		fi, err := NewFile(pointsTo.String(), mnode, root, fs)
		if err != nil {
			return nil, err
		}
		root.val = fi
	default:
		panic("unrecognized! (NYI)")
	}
	return root, nil
}

func (kr *KeyRoot) GetValue() FSNode {
	return kr.val
}

// closeChild implements the childCloser interface, and signals to the publisher that
// there are changes ready to be published
func (kr *KeyRoot) closeChild(name string, nd *dag.Node) error {
	kr.repub.Touch()
	return nil
}

// Publish publishes the ipns entry associated with this key
func (kr *KeyRoot) Publish(ctx context.Context) error {
	child, ok := kr.val.(FSNode)
	if !ok {
		return errors.New("child of key root not valid type")
	}

	nd, err := child.GetNode()
	if err != nil {
		return err
	}

	// Holding this lock so our child doesnt change out from under us
	child.Lock()
	k, err := kr.fs.dserv.Add(nd)
	if err != nil {
		child.Unlock()
		return err
	}
	child.Unlock()
	// Dont want to hold the lock while we publish
	// otherwise we are holding the lock through a costly
	// network operation

	kp := path.FromKey(k)

	ev := &eventlog.Metadata{"name": kr.name, "key": kp}
	defer log.EventBegin(ctx, "ipnsfsPublishing", ev).Done()
	log.Info("ipnsfs publishing %s -> %s", kr.name, kp)

	return kr.fs.nsys.Publish(ctx, kr.key, kp)
}

// Republisher manages when to publish the ipns entry associated with a given key
type Republisher struct {
	TimeoutLong  time.Duration
	TimeoutShort time.Duration
	Publish      chan struct{}
	root         *KeyRoot
}

// NewRepublisher creates a new Republisher object to republish the given keyroot
// using the given short and long time intervals
func NewRepublisher(root *KeyRoot, tshort, tlong time.Duration) *Republisher {
	return &Republisher{
		TimeoutShort: tshort,
		TimeoutLong:  tlong,
		Publish:      make(chan struct{}, 1),
		root:         root,
	}
}

// Touch signals that an update has occurred since the last publish.
// Multiple consecutive touches may extend the time period before
// the next Publish occurs in order to more efficiently batch updates
func (np *Republisher) Touch() {
	select {
	case np.Publish <- struct{}{}:
	default:
	}
}

// Run is the main republisher loop
func (np *Republisher) Run(ctx context.Context) {
	for {
		select {
		case <-np.Publish:
			quick := time.After(np.TimeoutShort)
			longer := time.After(np.TimeoutLong)

		wait:
			select {
			case <-ctx.Done():
				return
			case <-np.Publish:
				quick = time.After(np.TimeoutShort)
				goto wait
			case <-quick:
			case <-longer:
			}

			log.Info("Publishing Changes!")
			err := np.root.Publish(ctx)
			if err != nil {
				log.Error("republishRoot error: %s", err)
			}

		case <-ctx.Done():
			return
		}
	}
}
<|endoftext|>


<commit_before>package waveguide

import (
	"net/http"
	"net/url"

	"golang.org/x/net/context"
	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/log"
	"google.golang.org/appengine/taskqueue"
	"fmt"
	"io/ioutil"
	"regexp"
	"html"
	"strings"
	"time"
	"google.golang.org/appengine/urlfetch"
	"strconv"
	"html/template"
	"sort"
)

func init() {
	http.HandleFunc("/", handle(root))
	http.HandleFunc("/update_all", handle(updateAll))
	http.HandleFunc("/update_one", handle(updateOne))
	http.HandleFunc("/show", handle(show))
}

func handle(f func(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error)) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := appengine.NewContext(r)
		status, err := f(ctx, w, r)
		if err != nil {
			log.Errorf(ctx, "%v", err)
			w.WriteHeader(status)
			fmt.Fprintf(w, "%v", err)
		}
	}
}

// root shows the main page.
func root(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	if r.URL.Path != "/" {
		return http.StatusNotFound, fmt.Errorf("%s not found", r.URL.Path)
	}
	q := datastore.NewQuery("Spot")
	var spots []Spot
	log.Infof(ctx, "root: Before GetAll")
	_, err := q.GetAll(ctx, &spots)
	log.Infof(ctx, "root: After GetAll")
	if err != nil {
		return http.StatusInternalServerError, fmt.Errorf( "root: fetching spots: %v", err)
	}
	sort.Sort(ByRating(spots))
	data := struct {
		Head template.HTML
		Spots []Spot
	} {
		Head: head,
		Spots: spots,
	}
	log.Infof(ctx, "root: spots: %+v", spots)
	err = rootTmpl.Execute(w, data)
	if err != nil {
		return http.StatusInternalServerError, fmt.Errorf("root: template: %v", err)
	}
	return http.StatusOK, nil
}

func updateAll(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	if err := r.ParseForm(); err != nil {
		return http.StatusInternalServerError, err
	}
	sn := r.FormValue("n")
	if sn == "" {
		sn = "-1"
	}
	n, err := strconv.Atoi(sn)

	// Download magicseaweed.com/site-map.php
	client := urlfetch.Client(ctx)
	body, err := get(client,"http://magicseaweed.com/site-map.php")
	if err != nil {
		return http.StatusInternalServerError, err
	}
	reportPaths := reportRx.FindAll(body, n)

	// For each report url:
	for _, rp := range reportPaths {
		// TODO: See about using PutMulti if this needs to be more
		// efficient.
		err = saveNewSurfReportPath(ctx, rp)
		if err != nil {
			return http.StatusInternalServerError, err
		}

		// Add a task to the queue to download the report and store its stars and wave
		// height to the db.
		srp := string(rp)
		su := "http://magicseaweed.com" + srp
		t := taskqueue.NewPOSTTask("/update_one", map[string][]string{"url": {su}})
		if _, err := taskqueue.Add(ctx, t, ""); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return http.StatusInternalServerError, err
		}
	}

	fmt.Fprintf(w,"Processed %d paths.", len(reportPaths))

	return http.StatusOK, nil
}

// updateOne updates the conditions for a single surfing spot, given the
// path to the surf report for it.
func updateOne(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	if err := r.ParseForm(); err != nil {
		return http.StatusInternalServerError, err
	}
	su := r.FormValue("url")
	log.Infof(ctx, "/update_one: url=%q", su)
	u, err := url.Parse(su)
	if err != nil {
		return http.StatusBadRequest, err
	}
	path := u.Path
	qual, err := fetchSpotQuality(ctx, su)
	if err != nil {
		return http.StatusInternalServerError, err
	}
	AddQualityToSpot(ctx, SpotKey(ctx, path), qual)
	w.Write([]byte("ok"))
	return http.StatusOK, nil
}

func show(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	if err := r.ParseForm(); err != nil {
		return http.StatusInternalServerError, err
	}
	log.Infof(ctx, "/show. form: %v", r.Form)
	su := r.FormValue("url")
	u, err := url.Parse(su)
	if err != nil {
		return http.StatusBadRequest, err
	}
	path := u.Path
	key := SpotKey(ctx, path)
	var s Spot
	if err := datastore.Get(ctx, key, &s); err != nil {
		return http.StatusInternalServerError, err
	}
	fmt.Fprintf(w, "%+v", s)
	return http.StatusOK, nil
}

func get(client *http.Client, url string) ([]byte, error) {
	res, err := client.Get(url)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, fmt.Errorf("Failed to read body of %s", url)
	}
	return body, nil
}

var starSectionRx = regexp.MustCompile(`<ul class="rating rating-large clearfix">.*?</ul>`)
var starRx = regexp.MustCompile(`<li class="active"> *<i class="glyphicon glyphicon-star"></i> *</li>`)
var heightRx = regexp.MustCompile(`(\d+(?:-\d+)?)<small>ft`)
var reportRx = regexp.MustCompile(`/[^"/]+-Surf-Report/\d+/`)
var srpTailRx = regexp.MustCompile(`-Surf-Report/\d+/`)

func surfReportPathToName(srp string) (string, error) {
	s := srpTailRx.ReplaceAllString(srp, "")
	// TODO: Use PathUnescape once GAE supports Go 1.8
	s, err := url.QueryUnescape(s)
	if err != nil {
		return "", fmt.Errorf("Failed to unescape path in surfReportPathToName(%q). %v", srp, err)
	}
	s = html.UnescapeString(s)
	s = s[1:] // Remove leading /
	s = strings.Replace(s, "-", " ", -1)
	return s, nil
}

func saveNewSurfReportPath(ctx context.Context, path []byte) error {
	sp := string(path)
	name, err := surfReportPathToName(sp)
	if err != nil {
		return fmt.Errorf("saveNewSurfReportPaths: %v", err)
	}
	s := Spot{Name: name, MswPath: sp}
	key := SpotKey(ctx, sp)
	// TODO: First check whether there is already something at this key.
	_, err = datastore.Put(ctx, key, &s)
	if err != nil {
		return fmt.Errorf("saveNewSurfReportPaths: %v", err)
	}
	return nil
}

// fetchSpotQuality scrapes the surf report at the given url and returns a Quality struct
// summarizing the conditions.
func fetchSpotQuality(ctx context.Context, url string) (*Quality, error) {
	log.Infof(ctx, "Fetching %s", url)
	client := urlfetch.Client(ctx)
	body, err := get(client, url)
	if err != nil {
		return nil, err
	}
	rating := countTopStars(body)
	hMatch := heightRx.FindSubmatch(body)
	if len(hMatch) != 2 {
		return nil, fmt.Errorf("Wave height regex failed.")
	}
	height := fmt.Sprintf("%s ft", hMatch[1])
	q := &Quality {
		Rating: rating,
		WaveHeight: height,
		TimeUnix: time.Now().Unix(),
	}
	return q, nil
}

// countTopStars returns the number of stars in the first rating section on the page.
func countTopStars(body []byte) int {
	starSection := starSectionRx.Find(body)
	foundStars := starRx.FindAll(starSection, -1)
	return len(foundStars)
}
<commit_msg>Remove logging of all spots on one log line<commit_after>package waveguide

import (
	"net/http"
	"net/url"

	"golang.org/x/net/context"
	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/log"
	"google.golang.org/appengine/taskqueue"
	"fmt"
	"io/ioutil"
	"regexp"
	"html"
	"strings"
	"time"
	"google.golang.org/appengine/urlfetch"
	"strconv"
	"html/template"
	"sort"
)

func init() {
	http.HandleFunc("/", handle(root))
	http.HandleFunc("/update_all", handle(updateAll))
	http.HandleFunc("/update_one", handle(updateOne))
	http.HandleFunc("/show", handle(show))
}

func handle(f func(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error)) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := appengine.NewContext(r)
		status, err := f(ctx, w, r)
		if err != nil {
			log.Errorf(ctx, "%v", err)
			w.WriteHeader(status)
			fmt.Fprintf(w, "%v", err)
		}
	}
}

// root shows the main page.
func root(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	if r.URL.Path != "/" {
		return http.StatusNotFound, fmt.Errorf("%s not found", r.URL.Path)
	}
	q := datastore.NewQuery("Spot")
	var spots []Spot
	log.Infof(ctx, "root: Before GetAll")
	_, err := q.GetAll(ctx, &spots)
	log.Infof(ctx, "root: After GetAll")
	if err != nil {
		return http.StatusInternalServerError, fmt.Errorf( "root: fetching spots: %v", err)
	}
	sort.Sort(ByRating(spots))
	data := struct {
		Head template.HTML
		Spots []Spot
	} {
		Head: head,
		Spots: spots,
	}
	err = rootTmpl.Execute(w, data)
	if err != nil {
		return http.StatusInternalServerError, fmt.Errorf("root: template: %v", err)
	}
	return http.StatusOK, nil
}

func updateAll(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	if err := r.ParseForm(); err != nil {
		return http.StatusInternalServerError, err
	}
	sn := r.FormValue("n")
	if sn == "" {
		sn = "-1"
	}
	n, err := strconv.Atoi(sn)

	// Download magicseaweed.com/site-map.php
	client := urlfetch.Client(ctx)
	body, err := get(client,"http://magicseaweed.com/site-map.php")
	if err != nil {
		return http.StatusInternalServerError, err
	}
	reportPaths := reportRx.FindAll(body, n)

	// For each report url:
	for _, rp := range reportPaths {
		// TODO: See about using PutMulti if this needs to be more
		// efficient.
		err = saveNewSurfReportPath(ctx, rp)
		if err != nil {
			return http.StatusInternalServerError, err
		}

		// Add a task to the queue to download the report and store its stars and wave
		// height to the db.
		srp := string(rp)
		su := "http://magicseaweed.com" + srp
		t := taskqueue.NewPOSTTask("/update_one", map[string][]string{"url": {su}})
		if _, err := taskqueue.Add(ctx, t, ""); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return http.StatusInternalServerError, err
		}
	}

	fmt.Fprintf(w,"Processed %d paths.", len(reportPaths))

	return http.StatusOK, nil
}

// updateOne updates the conditions for a single surfing spot, given the
// path to the surf report for it.
func updateOne(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	if err := r.ParseForm(); err != nil {
		return http.StatusInternalServerError, err
	}
	su := r.FormValue("url")
	log.Infof(ctx, "/update_one: url=%q", su)
	u, err := url.Parse(su)
	if err != nil {
		return http.StatusBadRequest, err
	}
	path := u.Path
	qual, err := fetchSpotQuality(ctx, su)
	if err != nil {
		return http.StatusInternalServerError, err
	}
	AddQualityToSpot(ctx, SpotKey(ctx, path), qual)
	w.Write([]byte("ok"))
	return http.StatusOK, nil
}

func show(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	if err := r.ParseForm(); err != nil {
		return http.StatusInternalServerError, err
	}
	log.Infof(ctx, "/show. form: %v", r.Form)
	su := r.FormValue("url")
	u, err := url.Parse(su)
	if err != nil {
		return http.StatusBadRequest, err
	}
	path := u.Path
	key := SpotKey(ctx, path)
	var s Spot
	if err := datastore.Get(ctx, key, &s); err != nil {
		return http.StatusInternalServerError, err
	}
	fmt.Fprintf(w, "%+v", s)
	return http.StatusOK, nil
}

func get(client *http.Client, url string) ([]byte, error) {
	res, err := client.Get(url)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, fmt.Errorf("Failed to read body of %s", url)
	}
	return body, nil
}

var starSectionRx = regexp.MustCompile(`<ul class="rating rating-large clearfix">.*?</ul>`)
var starRx = regexp.MustCompile(`<li class="active"> *<i class="glyphicon glyphicon-star"></i> *</li>`)
var heightRx = regexp.MustCompile(`(\d+(?:-\d+)?)<small>ft`)
var reportRx = regexp.MustCompile(`/[^"/]+-Surf-Report/\d+/`)
var srpTailRx = regexp.MustCompile(`-Surf-Report/\d+/`)

func surfReportPathToName(srp string) (string, error) {
	s := srpTailRx.ReplaceAllString(srp, "")
	// TODO: Use PathUnescape once GAE supports Go 1.8
	s, err := url.QueryUnescape(s)
	if err != nil {
		return "", fmt.Errorf("Failed to unescape path in surfReportPathToName(%q). %v", srp, err)
	}
	s = html.UnescapeString(s)
	s = s[1:] // Remove leading /
	s = strings.Replace(s, "-", " ", -1)
	return s, nil
}

func saveNewSurfReportPath(ctx context.Context, path []byte) error {
	sp := string(path)
	name, err := surfReportPathToName(sp)
	if err != nil {
		return fmt.Errorf("saveNewSurfReportPaths: %v", err)
	}
	s := Spot{Name: name, MswPath: sp}
	key := SpotKey(ctx, sp)
	// TODO: First check whether there is already something at this key.
	_, err = datastore.Put(ctx, key, &s)
	if err != nil {
		return fmt.Errorf("saveNewSurfReportPaths: %v", err)
	}
	return nil
}

// fetchSpotQuality scrapes the surf report at the given url and returns a Quality struct
// summarizing the conditions.
func fetchSpotQuality(ctx context.Context, url string) (*Quality, error) {
	log.Infof(ctx, "Fetching %s", url)
	client := urlfetch.Client(ctx)
	body, err := get(client, url)
	if err != nil {
		return nil, err
	}
	rating := countTopStars(body)
	hMatch := heightRx.FindSubmatch(body)
	if len(hMatch) != 2 {
		return nil, fmt.Errorf("Wave height regex failed.")
	}
	height := fmt.Sprintf("%s ft", hMatch[1])
	q := &Quality {
		Rating: rating,
		WaveHeight: height,
		TimeUnix: time.Now().Unix(),
	}
	return q, nil
}

// countTopStars returns the number of stars in the first rating section on the page.
func countTopStars(body []byte) int {
	starSection := starSectionRx.Find(body)
	foundStars := starRx.FindAll(starSection, -1)
	return len(foundStars)
}
<|endoftext|>


<commit_before>package main

import (
	"context"
	"flag"
	"fmt"
	"github.com/google/go-github/github"
	"github.com/mitchellh/go-homedir"
	"github.com/pkg/errors"
	"golang.org/x/oauth2"
	"golang.org/x/sync/errgroup"
	"io/ioutil"
	"log"
	"os"
	"path"
)

const config = ".gistrc"

func checkConf(path string) string {
	key, err := ioutil.ReadFile(path)
	if err != nil {
		log.Fatal("set key first, use 'gist -s <key>'")
	}
	if string(key) == "" {
		log.Fatal("set key first, use 'gist -s <key>'")
	}
	return string(key)
}

func stringAddress(v string) *string { return &v }

func checkError(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

func getFiles(ctx context.Context, files []string) (map[github.GistFilename]github.GistFile, error) {
	eg, ctx := errgroup.WithContext(ctx)
	semaphore := make(chan struct{}, len(files))
	results := map[github.GistFilename]github.GistFile{}
	for _, file := range files {
		file := file
		eg.Go(func() error {
			semaphore <- struct{}{}
			defer func() {
				<-semaphore
			}()
			content, err := ioutil.ReadFile(file)
			fmt.Fprintf(os.Stdout, "--> Parsing file: %15s\n", file)
			if err != nil {
				return errors.Wrapf(err,
					"failed to get file content: %s", file)
			}
			results[github.GistFilename(path.Base(file))] = github.GistFile{Filename: stringAddress(string(path.Base(file))), Content: stringAddress(string(content))}
			return nil
		})
	}
	if err := eg.Wait(); err != nil {
		return nil, errors.Wrap(err, "one of the goroutines failed")
	}
	return results, nil
}

func main() {
	home, err := homedir.Dir()
	checkError(err)
	configFile := path.Join(home, config)
	help := flag.Bool("h", false, "show help")
	setKey := flag.Bool("s", false, "set token for auth")
	delKey := flag.Bool("r", false, "remove token")
	isPublic := flag.Bool("p", false, "create public gist?")
	description := flag.String("d", "published by 'zcong1993/gist' with golang", "add custom description")
	flag.Parse()
	if *help {
		fmt.Println("\nUsage :\n\tgist [flag] [files...]")
		fmt.Println("\nFlags :")
		fmt.Println()
		fmt.Println("\t -s, \t set token for auth")
		fmt.Println("\t -r, \t remove token")
		fmt.Println("\t -p, \t create public gist?")
		fmt.Println("\t -d, \t add custom description, default is `published by 'zcong1993/gist' with golang`")
		fmt.Println("\t -h, \t show help")
		os.Exit(0)
	}
	if *setKey {
		if len(flag.Args()) == 0 {
			log.Fatal("token is required")
		}
		err = ioutil.WriteFile(configFile, []byte(flag.Args()[0]), 0644)
		checkError(err)
		println("token set success")
		os.Exit(0)
	}
	if *delKey {
		err = ioutil.WriteFile(configFile, []byte(""), 0644)
		checkError(err)
		println("token delete success")
		os.Exit(0)
	}
	key := checkConf(configFile)
	files := flag.Args()
	ctx := context.Background()
	ts := oauth2.StaticTokenSource(
		&oauth2.Token{AccessToken: key},
	)
	tc := oauth2.NewClient(ctx, ts)
	client := github.NewClient(tc)
	postFiles, err := getFiles(context.TODO(), files)
	checkError(err)
	gist := github.Gist{
		Public:      isPublic,
		Files:       postFiles,
		Description: description,
	}
	g, _, err := client.Gists.Create(context.Background(), &gist)
	checkError(err)
	fmt.Printf("Done! The gist url is %s", *g.HTMLURL)
}
<commit_msg>use errgroup.group without ctx<commit_after>package main

import (
	"context"
	"flag"
	"fmt"
	"github.com/google/go-github/github"
	"github.com/mitchellh/go-homedir"
	"github.com/pkg/errors"
	"golang.org/x/oauth2"
	"golang.org/x/sync/errgroup"
	"io/ioutil"
	"log"
	"os"
	"path"
)

const config = ".gistrc"

func checkConf(path string) string {
	key, err := ioutil.ReadFile(path)
	if err != nil {
		log.Fatal("set key first, use 'gist -s <key>'")
	}
	if string(key) == "" {
		log.Fatal("set key first, use 'gist -s <key>'")
	}
	return string(key)
}

func stringAddress(v string) *string { return &v }

func checkError(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

func getFiles(files []string) (map[github.GistFilename]github.GistFile, error) {
	eg := errgroup.Group{}
	semaphore := make(chan struct{}, len(files))
	results := map[github.GistFilename]github.GistFile{}
	for _, file := range files {
		file := file
		eg.Go(func() error {
			semaphore <- struct{}{}
			defer func() {
				<-semaphore
			}()
			content, err := ioutil.ReadFile(file)
			fmt.Fprintf(os.Stdout, "--> Parsing file: %15s\n", file)
			if err != nil {
				return errors.Wrapf(err,
					"failed to get file content: %s", file)
			}
			results[github.GistFilename(path.Base(file))] = github.GistFile{Filename: stringAddress(string(path.Base(file))), Content: stringAddress(string(content))}
			return nil
		})
	}
	if err := eg.Wait(); err != nil {
		return nil, errors.Wrap(err, "one of the goroutines failed")
	}
	return results, nil
}

func main() {
	home, err := homedir.Dir()
	checkError(err)
	configFile := path.Join(home, config)
	help := flag.Bool("h", false, "show help")
	setKey := flag.Bool("s", false, "set token for auth")
	delKey := flag.Bool("r", false, "remove token")
	isPublic := flag.Bool("p", false, "create public gist?")
	description := flag.String("d", "published by 'zcong1993/gist' with golang", "add custom description")
	flag.Parse()
	if *help {
		fmt.Println("\nUsage :\n\tgist [flag] [files...]")
		fmt.Println("\nFlags :")
		fmt.Println()
		fmt.Println("\t -s, \t set token for auth")
		fmt.Println("\t -r, \t remove token")
		fmt.Println("\t -p, \t create public gist?")
		fmt.Println("\t -d, \t add custom description, default is `published by 'zcong1993/gist' with golang`")
		fmt.Println("\t -h, \t show help")
		os.Exit(0)
	}
	if *setKey {
		if len(flag.Args()) == 0 {
			log.Fatal("token is required")
		}
		err = ioutil.WriteFile(configFile, []byte(flag.Args()[0]), 0644)
		checkError(err)
		println("token set success")
		os.Exit(0)
	}
	if *delKey {
		err = ioutil.WriteFile(configFile, []byte(""), 0644)
		checkError(err)
		println("token delete success")
		os.Exit(0)
	}
	key := checkConf(configFile)
	files := flag.Args()
	ctx := context.Background()
	ts := oauth2.StaticTokenSource(
		&oauth2.Token{AccessToken: key},
	)
	tc := oauth2.NewClient(ctx, ts)
	client := github.NewClient(tc)
	postFiles, err := getFiles(files)
	checkError(err)
	gist := github.Gist{
		Public:      isPublic,
		Files:       postFiles,
		Description: description,
	}
	g, _, err := client.Gists.Create(context.Background(), &gist)
	checkError(err)
	fmt.Printf("\nDone! The gist url is %s\n", *g.HTMLURL)
}
<|endoftext|>


<commit_before>package web

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/MiniProfiler/go/miniprofiler"
	"github.com/StackExchange/scollector/opentsdb"
	"github.com/StackExchange/tsaf/expr"
)

func Query(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) {
	var oreq opentsdb.Request
	err := json.Unmarshal([]byte(r.FormValue("json")), &oreq)
	if err != nil {
		serveError(w, err)
		return
	}
	err = Autods(&oreq, 200)
	if err != nil {
		serveError(w, err)
		return
	}
	for _, q := range oreq.Queries {
		if err := expr.ExpandSearch(q); err != nil {
			serveError(w, err)
			return
		}
	}
	var tr opentsdb.ResponseSet
	q, _ := url.QueryUnescape(oreq.String())
	t.StepCustomTiming("tsdb", "query", q, func() {
		tr, err = tsdbHost.Query(oreq)
	})
	if err != nil {
		serveError(w, err)
		return
	}
	qr, err := rickchart(tr)
	if err != nil {
		serveError(w, err)
		return
	}
	b, err := json.Marshal(qr)
	if err != nil {
		serveError(w, err)
		return
	}
	w.Write(b)
}

func ParseAbsTime(s string) (time.Time, error) {
	var t time.Time
	t_formats := [4]string{
		"2006/01/02-15:04:05",
		"2006/01/02-15:04",
		"2006/01/02-15",
		"2006/01/02",
	}
	for _, f := range t_formats {
		t, err := time.Parse(f, s)
		if err == nil {
			return t, nil
		}
	}
	i, err := strconv.ParseInt(s, 10, 64)
	if err != nil {
		return t, err
	}
	t = time.Unix(i, 0)
	return t, nil
}

func ParseTime(v interface{}) (time.Time, error) {
	switch i := v.(type) {
	case string:
		if i != "" {
			if strings.HasSuffix(i, "-ago") {
				s := strings.TrimSuffix(i, "-ago")
				d, err := expr.ParseDuration(s)
				if err != nil {
					return time.Now().UTC(), err
				}
				return time.Now().UTC().Add(-d), nil
			} else {
				a, err := ParseAbsTime(i)
				if err != nil {
					return time.Now().UTC(), err
				}
				return a, nil
			}
		} else {
			return time.Now().UTC(), nil
		}
	case int:
		return time.Unix(int64(i), 0), nil
	case int64:
		return time.Unix(i, 0), nil
	default:
		return time.Now().UTC(), errors.New("type must be string, int, or int64")
	}
}

func GetDuration(r *opentsdb.Request) (time.Duration, error) {
	var t time.Duration
	if v, ok := r.Start.(string); ok && v == "" {
		return t, errors.New("start time must be provided")
	}
	start, err := ParseTime(r.Start)
	if err != nil {
		return t, err
	}
	end := time.Now()
	if r.End != nil {
		end, err = ParseTime(r.End)
		if err != nil {
			return t, err
		}
	}
	return end.Sub(start), nil
}

func Autods(r *opentsdb.Request, l int64) error {
	if l == 0 {
		return errors.New("tsaf: target length must be > 0")
	}
	cd, err := GetDuration(r)
	if err != nil {
		return err
	}
	d := cd / time.Duration(l)
	if d < time.Second*15 {
		return nil
	}
	ds := fmt.Sprintf("%ds-avg", d/time.Second)
	for _, q := range r.Queries {
		q.Downsample = ds
	}
	return nil
}

func rickchart(r opentsdb.ResponseSet) ([]*RickSeries, error) {
	var series []*RickSeries
	for _, resp := range r {
		dps := make([]RickDP, 0)
		for k, v := range resp.DPS {
			ki, err := strconv.ParseInt(k, 10, 64)
			if err != nil {
				return nil, err
			}
			dps = append(dps, RickDP{
				X: ki,
				Y: v,
			})
		}
		if len(dps) > 0 {
			sort.Sort(ByX(dps))
			name := resp.Metric
			var id []string
			for k, v := range resp.Tags {
				id = append(id, fmt.Sprintf("%v=%v", k, v))
			}
			if len(id) > 0 {
				name = fmt.Sprintf("%s{%s}", name, strings.Join(id, ","))
			}
			series = append(series, &RickSeries{
				Name: name,
				Data: dps,
			})
		}
	}
	return series, nil
}

type RickSeries struct {
	Name string   `json:"name"`
	Data []RickDP `json:"data"`
}

type RickDP struct {
	X int64          `json:"x"`
	Y opentsdb.Point `json:"y"`
}

type ByX []RickDP

func (a ByX) Len() int           { return len(a) }
func (a ByX) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByX) Less(i, j int) bool { return a[i].X < a[j].X }
<commit_msg>Read the audods parameter from a query<commit_after>package web

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/MiniProfiler/go/miniprofiler"
	"github.com/StackExchange/scollector/opentsdb"
	"github.com/StackExchange/tsaf/expr"
)

func Query(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) {
	var oreq opentsdb.Request
	err := json.Unmarshal([]byte(r.FormValue("json")), &oreq)
	if err != nil {
		serveError(w, err)
		return
	}
	ads_v := r.FormValue("autods")
	if ads_v != "" {
		ads_i, err := strconv.ParseInt(ads_v, 10, 64)
		if err != nil {
			serveError(w, err)
			return
		}
		err = Autods(&oreq, ads_i)
	}
	if err != nil {
		serveError(w, err)
		return
	}
	for _, q := range oreq.Queries {
		if err := expr.ExpandSearch(q); err != nil {
			serveError(w, err)
			return
		}
	}
	var tr opentsdb.ResponseSet
	q, _ := url.QueryUnescape(oreq.String())
	t.StepCustomTiming("tsdb", "query", q, func() {
		tr, err = tsdbHost.Query(oreq)
	})
	if err != nil {
		serveError(w, err)
		return
	}
	qr, err := rickchart(tr)
	if err != nil {
		serveError(w, err)
		return
	}
	b, err := json.Marshal(qr)
	if err != nil {
		serveError(w, err)
		return
	}
	w.Write(b)
}

func ParseAbsTime(s string) (time.Time, error) {
	var t time.Time
	t_formats := [4]string{
		"2006/01/02-15:04:05",
		"2006/01/02-15:04",
		"2006/01/02-15",
		"2006/01/02",
	}
	for _, f := range t_formats {
		t, err := time.Parse(f, s)
		if err == nil {
			return t, nil
		}
	}
	i, err := strconv.ParseInt(s, 10, 64)
	if err != nil {
		return t, err
	}
	t = time.Unix(i, 0)
	return t, nil
}

func ParseTime(v interface{}) (time.Time, error) {
	switch i := v.(type) {
	case string:
		if i != "" {
			if strings.HasSuffix(i, "-ago") {
				s := strings.TrimSuffix(i, "-ago")
				d, err := expr.ParseDuration(s)
				if err != nil {
					return time.Now().UTC(), err
				}
				return time.Now().UTC().Add(-d), nil
			} else {
				a, err := ParseAbsTime(i)
				if err != nil {
					return time.Now().UTC(), err
				}
				return a, nil
			}
		} else {
			return time.Now().UTC(), nil
		}
	case int:
		return time.Unix(int64(i), 0), nil
	case int64:
		return time.Unix(i, 0), nil
	default:
		return time.Now().UTC(), errors.New("type must be string, int, or int64")
	}
}

func GetDuration(r *opentsdb.Request) (time.Duration, error) {
	var t time.Duration
	if v, ok := r.Start.(string); ok && v == "" {
		return t, errors.New("start time must be provided")
	}
	start, err := ParseTime(r.Start)
	if err != nil {
		return t, err
	}
	end := time.Now()
	if r.End != nil {
		end, err = ParseTime(r.End)
		if err != nil {
			return t, err
		}
	}
	return end.Sub(start), nil
}

func Autods(r *opentsdb.Request, l int64) error {
	if l == 0 {
		return errors.New("tsaf: target length must be > 0")
	}
	cd, err := GetDuration(r)
	if err != nil {
		return err
	}
	d := cd / time.Duration(l)
	if d < time.Second*15 {
		return nil
	}
	ds := fmt.Sprintf("%ds-avg", d/time.Second)
	for _, q := range r.Queries {
		q.Downsample = ds
	}
	return nil
}

func rickchart(r opentsdb.ResponseSet) ([]*RickSeries, error) {
	var series []*RickSeries
	for _, resp := range r {
		dps := make([]RickDP, 0)
		for k, v := range resp.DPS {
			ki, err := strconv.ParseInt(k, 10, 64)
			if err != nil {
				return nil, err
			}
			dps = append(dps, RickDP{
				X: ki,
				Y: v,
			})
		}
		if len(dps) > 0 {
			sort.Sort(ByX(dps))
			name := resp.Metric
			var id []string
			for k, v := range resp.Tags {
				id = append(id, fmt.Sprintf("%v=%v", k, v))
			}
			if len(id) > 0 {
				name = fmt.Sprintf("%s{%s}", name, strings.Join(id, ","))
			}
			series = append(series, &RickSeries{
				Name: name,
				Data: dps,
			})
		}
	}
	return series, nil
}

type RickSeries struct {
	Name string   `json:"name"`
	Data []RickDP `json:"data"`
}

type RickDP struct {
	X int64          `json:"x"`
	Y opentsdb.Point `json:"y"`
}

type ByX []RickDP

func (a ByX) Len() int           { return len(a) }
func (a ByX) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByX) Less(i, j int) bool { return a[i].X < a[j].X }
<|endoftext|>


<commit_before>package glue

import (
	"net/http"
	"os"

	"github.com/tmc/inj"
)

// Handler is a generic type that must be a callable function.
//
// It is invoked with the Call method of inj.Injector
// (http://godoc.org/github.com/tmc/inj#Injector.Call) which provides DI
// (Dependency Injection) based on the types of arguments it accepts.
//
// Accepting a glue.Context allows you to inspect the DI container and examine
// the currently registered types.
//
// The default registered ResponseHandler expects Handlers to return either one or two values.
//
// If one value, it should return a string or a byte slice.
// If two values, the first should be an int which will be used as the return code.
type Handler interface{}

// AfterHandler is a type that a glue Handler can return and have it invoked
// after the default handler. This allows middleware to execute logic after a
// response has started. See github.com/tmc/glue/loggers for an example.
type AfterHandler func(Context)

// Glue is the primary struct that exposes routing and Handler registration
type Glue struct {
	inj.Injector
	*router
	handlers       []Handler // the set of handlers invoked for every request
	defaultHandler Handler   // the handler that is handled last
}

// New prepares a new Glue instance and registers the default ResponseHandler
func New() *Glue {
	r := &router{}
	g := &Glue{inj.New(), r, []Handler{}, r.Handle}
	// register the default ResponseHandler
	g.Register(defaultResponseHandler())
	return g
}

// ServeHTTP satisfies the http.Handler interface
func (g *Glue) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	g.newContext(w, r).handle()
}

// Add adds a handler to the default set of handlers for a Glue instance
func (g *Glue) AddHandler(handler Handler) {
	//@todo verify is func
	g.handlers = append(g.handlers, handler)
}

// Listen attempts to ListenAndServe based on the environment variables HOST and PORT
func (g *Glue) Listen() {
	port, host := os.Getenv("PORT"), os.Getenv("HOST")
	if port == "" {
		port = "5000"
	}
	http.ListenAndServe(host+":"+port, g)
}
<commit_msg>Return error from ListenAndServe<commit_after>package glue

import (
	"net/http"
	"os"

	"github.com/tmc/inj"
)

// Handler is a generic type that must be a callable function.
//
// It is invoked with the Call method of inj.Injector
// (http://godoc.org/github.com/tmc/inj#Injector.Call) which provides DI
// (Dependency Injection) based on the types of arguments it accepts.
//
// Accepting a glue.Context allows you to inspect the DI container and examine
// the currently registered types.
//
// The default registered ResponseHandler expects Handlers to return either one or two values.
//
// If one value, it should return a string or a byte slice.
// If two values, the first should be an int which will be used as the return code.
type Handler interface{}

// AfterHandler is a type that a glue Handler can return and have it invoked
// after the default handler. This allows middleware to execute logic after a
// response has started. See github.com/tmc/glue/loggers for an example.
type AfterHandler func(Context)

// Glue is the primary struct that exposes routing and Handler registration
type Glue struct {
	inj.Injector
	*router
	handlers       []Handler // the set of handlers invoked for every request
	defaultHandler Handler   // the handler that is handled last
}

// New prepares a new Glue instance and registers the default ResponseHandler
func New() *Glue {
	r := &router{}
	g := &Glue{inj.New(), r, []Handler{}, r.Handle}
	// register the default ResponseHandler
	g.Register(defaultResponseHandler())
	return g
}

// ServeHTTP satisfies the http.Handler interface
func (g *Glue) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	g.newContext(w, r).handle()
}

// Add adds a handler to the default set of handlers for a Glue instance
func (g *Glue) AddHandler(handler Handler) {
	//@todo verify is func
	g.handlers = append(g.handlers, handler)
}

// Listen attempts to ListenAndServe based on the environment variables HOST and PORT
func (g *Glue) Listen() error {
	port, host := os.Getenv("PORT"), os.Getenv("HOST")
	if port == "" {
		port = "5000"
	}
	return http.ListenAndServe(host+":"+port, g)
}
<|endoftext|>


<commit_before>// Copyright (C) 2012, Johan P. P. Samyn <johan.samyn@gmail.com> All rights reserved.
// Use of this source code is governed by the Simplified BSD License
// that can be found in the LICENSE.txt file.

// Package gohg is a Go client library for using the Mercurial dvcs
// via it's Command Server.
//
// For Mercurial see: http://mercurial/selenic.com/wiki
// For the Hg Command Server see: http://mercurial.selenic.com/wiki/CommandServer
package gohg

import (
	"encoding/binary"
	"bytes"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
)

var repo string
var err error
var libdir string
var logfile string

var hgserver *exec.Cmd

// The in and out pipe ends are to be considered from the point of view
// of the Hg Command Server instance. So closing pin closes the Hg CS.
var pout io.ReadCloser
var pin io.WriteCloser

type hgMsg struct {
	Ch   string
	Ln   uint
	Data string
}

type hgCmd struct {
	Cmd  string
	Ln   uint
	Args string
}

func init() {
	var exedir string
	exedir = path.Dir(os.Args[0])
	exedir, err = filepath.Abs(exedir)
	if err != nil {
		log.Fatal("Could not determine path for the gohg.log logfile.")
	}
	logfile = exedir + string(os.PathSeparator) + "gohg.log"
} // init()

// Connect establishes the connection with the Mercurial CommandServer.
//
// If anything went wrong when trying to make the connection,
// an error is returned.
//
// Parameters:
//		hg			The command to run mercurial. Optional.
//		reponame	The folder of the Hg repository to work on. Optional.
//					When blanc the folder where the program is run is used
//					(see locateRepository).
//		config		Configuration settings that will be added to the necessary
//					default/fixed settings (see composeHgConfig() for more).
// Returns an error if the connection could not be established flawlessly.
func Connect(hg string, reponame string, config []string) error {

	// for example:
	// hgserver = exec.Command("M:\\DEV\\hg-stable\\hg",	// the Hg command
	// 		"-R", "C:\\DEV\\go\\src\\golout\\",				// the repo
	// 		"--config", "ui.interactive=True",				// mandatory settings
	// 		"--config", "extensions.color=!",				// more settings (for Windows)
	// 		"serve", "--cmdserver", "pipe")					// start the Command Server

	// Maybe accept a channel as an extra argument for sending the logging to ?
	// And if it's nil, log into a textfile in the folder of this lib.
	// Also do not override that logfile every launch.
	// Maybe even do this in the init() function ?

	if hgserver != nil {
		return errors.New("A Hg Command Server is already connected.")
	}

	if hg == "" {
		// Let the OS determine what Mercurial to run
		// for this machine/user combination.
		hg = "hg"
	}
	// fmt.Printf("hg: %s\n", hg)

	// The Hg Command Server needs a repository.
	repo, err = locateRepository(reponame)
	if err != nil {
		return err
	}
	if repo == "" {
		return errors.New("could not find a Hg repository at: " + reponame)
	}
	// fmt.Printf("repo: %s\n", repo)

	// Maybe we can also offer the possibility of a config file?
	// f.i.: a file gohg.cfg in the same folder as the gohg.exe,
	// and a section per repo, and one "general" section.
	// Or maybe just a [gohg] section in one of the 'normal' Hg config files ?

	var hgconfig []string
	hgconfig = composeHgConfig(hg, repo, config)
	// fmt.Printf("hgconfig: %v\n", hgconfig)

	hgserver = exec.Command(hg)
	hgserver.Args = hgconfig
	hgserver.Dir = repo
	// fmt.Printf("hgserver: %v\n", hgserver)

	pout, err = hgserver.StdoutPipe()
	if err != nil {
		return errors.New("could not connect StdoutPipe: " + err.Error())
	}
	pin, err = hgserver.StdinPipe()
	if err != nil {
		log.Fatal("could not connect StdinPipe: " + err.Error())
	}

	if err := hgserver.Start(); err != nil {
		return errors.New("could not start the Hg Command Server: " + err.Error())
	}

	err = readHelloMessage()
	if err != nil {
		return err
	}

	fmt.Println("Connected with Hg Command Server at: " + repo)

	return nil

} // Connect()

func locateRepository(reponame string) (string, error) {
	repo = reponame
	sep := string(os.PathSeparator)

	// first make a correct path from repo
	repo, err = filepath.Abs(repo)
	if err != nil {
		return "", errors.New(err.Error() +
			"\ncould not find absolute path for: " + repo)
	}
	repo = filepath.Clean(repo)

	// If we do not find a Hg repo in this dir, we search for one
	// up the path, in case we're deeper in it's working copy.
	for {
		_, err = os.Stat(repo + sep + ".hg")
		if err == nil {
			break
		}
		var dir, file string
		dir, file = filepath.Split(repo)
		if dir == "" || file == "" {
			repo = ""
			break
		}
		repo = dir
	}
	if err != nil || repo == "" {
		return "", nil
	}

	return repo, nil

} // locateRepository()

func composeHgConfig(hgcmd string, repo string, config []string) []string {
	var hgconfig []string

	// if len(config) > 0 {
	// 	var cfg string
	// 	for i := 0; i < range(config) {
	// 		cfg = cfg + "," + config[i]
	// 	}
	// 	cmd = cmd + "," + cfg
	// }

	hgconfig = append(hgconfig, hgcmd,
		"-R", repo,
		// These arguments are fixed.
		"--config", "ui.interactive=True",
		"--config", "extensions.color=!",
		"serve", "--cmdserver", "pipe")

	return hgconfig
}

func readHelloMessage() error {
	const t1 = "capabilities:"
	const t2 = "hg serve [OPTION]"

	s := make([]byte, 1+4+1024)
	_, err = pout.Read(s)
	if err != io.EOF && err != nil {
		return err
	}
	// fmt.Printf("s: %s\n", s)
	if len(s) == 0 {
		return errors.New("no data received from Hg Command Server")
	}
	var ln uint32
	buf := bytes.NewBuffer(s[1:5])
	err = binary.Read(buf, binary.BigEndian, &ln)
	if err != nil {
		fmt.Println("binary.Read failed:", err)
	}
	l := len(t2)
	if string(s[0:l]) == t2 {
		return errors.New("this version of Mercurial does not support the Command Server")
	}
	l = len(t1)
	if string(s[5:5+l]) != t1 {
		return errors.New("could not connect a Hg Command Server")
	}
	return nil
}

func Close() error {
	pout.Close()
	// Closing it's stdin is what really closes the Hg Command Server.
	pin.Close()
	err = hgserver.Wait()
	if err != nil {
		return err
	}
	fmt.Println("Disconnected from Hg Command Server at: " + repo)
	return nil
} // Close()
<commit_msg>added some more function comments<commit_after>// Copyright (C) 2012, Johan P. P. Samyn <johan.samyn@gmail.com> All rights reserved.
// Use of this source code is governed by the Simplified BSD License
// that can be found in the LICENSE.txt file.

// Package gohg is a Go client library for using the Mercurial dvcs
// via it's Command Server.
//
// For Mercurial see: http://mercurial/selenic.com/wiki
// For the Hg Command Server see: http://mercurial.selenic.com/wiki/CommandServer
package gohg

import (
	"encoding/binary"
	"bytes"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
)

var repo string
var err error
var libdir string
var logfile string

var hgserver *exec.Cmd

// The in and out pipe ends are to be considered from the point of view
// of the Hg Command Server instance. So closing pin closes the Hg CS.
var pout io.ReadCloser
var pin io.WriteCloser

type hgMsg struct {
	Ch   string
	Ln   uint
	Data string
}

type hgCmd struct {
	Cmd  string
	Ln   uint
	Args string
}

// init takes care of some householding, namely preparing a logfile where
// all communication between this lib and the Hg CS can be logged.
func init() {
	var exedir string
	exedir = path.Dir(os.Args[0])
	exedir, err = filepath.Abs(exedir)
	if err != nil {
		log.Fatal("Could not determine path for the gohg.log logfile.")
	}
	logfile = exedir + string(os.PathSeparator) + "gohg.log"
} // init()

// Connect establishes the connection with the Mercurial CommandServer.
//
// If anything went wrong when trying to make the connection,
// an error is returned.
//
// Parameters:
//		hg			The command to run mercurial. Optional.
//		reponame	The folder of the Hg repository to work on. Optional.
//					When blanc the folder where the program is run is used
//					(see locateRepository).
//		config		Configuration settings that will be added to the necessary
//					default/fixed settings (see composeHgConfig() for more).
// Returns an error if the connection could not be established flawlessly.
func Connect(hg string, reponame string, config []string) error {

	// for example:
	// hgserver = exec.Command("M:\\DEV\\hg-stable\\hg",	// the Hg command
	// 		"-R", "C:\\DEV\\go\\src\\golout\\",				// the repo
	// 		"--config", "ui.interactive=True",				// mandatory settings
	// 		"--config", "extensions.color=!",				// more settings (for Windows)
	// 		"serve", "--cmdserver", "pipe")					// start the Command Server

	// Maybe accept a channel as an extra argument for sending the logging to ?
	// And if it's nil, log into a textfile in the folder of this lib.
	// Also do not override that logfile every launch.
	// Maybe even do this in the init() function ?

	if hgserver != nil {
		return errors.New("A Hg Command Server is already connected.")
	}

	if hg == "" {
		// Let the OS determine what Mercurial to run
		// for this machine/user combination.
		hg = "hg"
	}
	// fmt.Printf("hg: %s\n", hg)

	// The Hg Command Server needs a repository.
	repo, err = locateRepository(reponame)
	if err != nil {
		return err
	}
	if repo == "" {
		return errors.New("could not find a Hg repository at: " + reponame)
	}
	// fmt.Printf("repo: %s\n", repo)

	// Maybe we can also offer the possibility of a config file?
	// f.i.: a file gohg.cfg in the same folder as the gohg.exe,
	// and a section per repo, and one "general" section.
	// Or maybe just a [gohg] section in one of the 'normal' Hg config files ?

	var hgconfig []string
	hgconfig = composeHgConfig(hg, repo, config)
	// fmt.Printf("hgconfig: %v\n", hgconfig)

	hgserver = exec.Command(hg)
	hgserver.Args = hgconfig
	hgserver.Dir = repo
	// fmt.Printf("hgserver: %v\n", hgserver)

	pout, err = hgserver.StdoutPipe()
	if err != nil {
		return errors.New("could not connect StdoutPipe: " + err.Error())
	}
	pin, err = hgserver.StdinPipe()
	if err != nil {
		log.Fatal("could not connect StdinPipe: " + err.Error())
	}

	if err := hgserver.Start(); err != nil {
		return errors.New("could not start the Hg Command Server: " + err.Error())
	}

	err = readHelloMessage()
	if err != nil {
		return err
	}

	fmt.Println("Connected with Hg Command Server at: " + repo)

	return nil

} // Connect()

// locateRepository takes care of assuring we have a Merurial repository
// available for working on via the Hg CommandServer, as that is necessary
// to be able to use the Hg CS.
func locateRepository(reponame string) (string, error) {
	repo = reponame
	sep := string(os.PathSeparator)

	// first make a correct path from repo
	repo, err = filepath.Abs(repo)
	if err != nil {
		return "", errors.New(err.Error() +
			"\ncould not find absolute path for: " + repo)
	}
	repo = filepath.Clean(repo)

	// If we do not find a Hg repo in this dir, we search for one
	// up the path, in case we're deeper in it's working copy.
	for {
		_, err = os.Stat(repo + sep + ".hg")
		if err == nil {
			break
		}
		var dir, file string
		dir, file = filepath.Split(repo)
		if dir == "" || file == "" {
			repo = ""
			break
		}
		repo = dir
	}
	if err != nil || repo == "" {
		return "", nil
	}

	return repo, nil

} // locateRepository()

// composeHgConfig handles the different config settings that will be used
// to make the connection with the Hg CS. It concerns settings specially for Hg.
func composeHgConfig(hgcmd string, repo string, config []string) []string {
	var hgconfig []string

	// if len(config) > 0 {
	// 	var cfg string
	// 	for i := 0; i < range(config) {
	// 		cfg = cfg + "," + config[i]
	// 	}
	// 	cmd = cmd + "," + cfg
	// }

	hgconfig = append(hgconfig, hgcmd,
		"-R", repo,
		// These arguments are fixed.
		"--config", "ui.interactive=True",
		"--config", "extensions.color=!",
		"serve", "--cmdserver", "pipe")

	return hgconfig
}

// readHelloMessage reads the special hello message send by the Hg CS.
//
// It has a fixed format, and contains info about the possibilities
// of the Hg CS at hand. It's also a first proof of a working connection.
func readHelloMessage() error {
	const t1 = "capabilities:"
	const t2 = "hg serve [OPTION]"

	s := make([]byte, 1+4+1024)
	_, err = pout.Read(s)
	if err != io.EOF && err != nil {
		return err
	}
	// fmt.Printf("s: %s\n", s)
	if len(s) == 0 {
		return errors.New("no data received from Hg Command Server")
	}
	var ln uint32
	buf := bytes.NewBuffer(s[1:5])
	err = binary.Read(buf, binary.BigEndian, &ln)
	if err != nil {
		fmt.Println("binary.Read failed:", err)
	}
	l := len(t2)
	if string(s[0:l]) == t2 {
		return errors.New("this version of Mercurial does not support the Command Server")
	}
	l = len(t1)
	if string(s[5:5+l]) != t1 {
		return errors.New("could not connect a Hg Command Server")
	}
	return nil
}

// Close ends the conection with the Mercurial CommandServer.
//
// In fact it's closing the stdin of the Hg CS that closes connection,
// as per the Hg CS documentation.
func Close() error {
	pout.Close()
	// Closing it's stdin is what really closes the Hg Command Server.
	pin.Close()
	err = hgserver.Wait()
	if err != nil {
		return err
	}
	fmt.Println("Disconnected from Hg Command Server at: " + repo)
	return nil
} // Close()
<|endoftext|>


<commit_before>package main

import (
	"bytes"
	"encoding/gob"
	"encoding/json"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"os"
	"sort"
	"sync"
	"time"

	"github.com/BurntSushi/xgb"
	"github.com/BurntSushi/xgb/screensaver"
	"github.com/BurntSushi/xgb/xproto"
)

const (
	port    = ":8001"
	dump    = "gone.gob"
	logf    = "gone.log"
	unknown = "unknown"
)

var (
	tracks = make(Tracker)
	tmpl   = template.Must(template.ParseFiles("index.html"))
	zzz    bool
	m      sync.Mutex
)

type Tracker map[Window]*Track

type Track struct {
	Seen  time.Time
	Spent time.Duration
}

type Window struct {
	Class string
	Name  string
}

type Xorg struct {
	X           *xgb.Conn
	root        xproto.Window
	activeAtom  *xproto.InternAtomReply
	netNameAtom *xproto.InternAtomReply
	nameAtom    *xproto.InternAtomReply
	classAtom   *xproto.InternAtomReply
}

func (t Track) String() string {
	return fmt.Sprint(t.Spent)
}

func (w Window) String() string {
	return fmt.Sprintf("%s %s", w.Class, w.Name)
}

func (x Xorg) atom(aname string) *xproto.InternAtomReply {
	a, err := xproto.InternAtom(x.X, true, uint16(len(aname)), aname).Reply()
	if err != nil {
		log.Fatal("atom: ", err)
	}
	return a
}

func (x Xorg) property(w xproto.Window, a *xproto.InternAtomReply) (*xproto.GetPropertyReply, error) {
	return xproto.GetProperty(x.X, false, w, a.Atom,
		xproto.GetPropertyTypeAny, 0, (1<<32)-1).Reply()
}

func (x Xorg) active() xproto.Window {
	p, err := x.property(x.root, x.activeAtom)
	if err != nil {
		return x.root
	}
	return xproto.Window(xgb.Get32(p.Value))
}

func (x Xorg) name(w xproto.Window) string {
	name, err := x.property(w, x.netNameAtom)
	if err != nil {
		return unknown
	}
	if string(name.Value) == "" {
		name, err = x.property(w, x.nameAtom)
		if err != nil || string(name.Value) == "" {
			return unknown
		}
	}
	return string(name.Value)
}

func (x Xorg) class(w xproto.Window) string {
	class, err := x.property(w, x.classAtom)
	if err != nil {
		return unknown
	}
	i := bytes.IndexByte(class.Value, 0)
	if i == -1 || string(class.Value[:i]) == "" {
		return unknown
	}
	return string(class.Value[:i])
}

func (x Xorg) winName() (Window, bool) {
	windowId := x.active()
	/* skip invalid window id */
	if windowId == 0 {
		return Window{}, false
	}
	x.spy(windowId)
	return Window{
		Class: x.class(windowId),
		Name:  x.name(windowId),
	}, true
}

func (x Xorg) spy(w xproto.Window) {
	xproto.ChangeWindowAttributes(x.X, w, xproto.CwEventMask,
		[]uint32{xproto.EventMaskPropertyChange})
}

func (x Xorg) update(t Tracker) (prev *Track) {
	if win, ok := x.winName(); ok {
		m.Lock()
		if _, ok := t[win]; !ok {
			t[win] = new(Track)
		}
		t[win].Seen = time.Now()
		prev = t[win]
		m.Unlock()
	}
	return
}

func connect() Xorg {
	var x Xorg
	var err error

	x.X, err = xgb.NewConn()
	if err != nil {
		log.Fatal("xgb: ", err)
	}

	err = screensaver.Init(x.X)
	if err != nil {
		log.Fatal("screensaver: ", err)
	}

	setup := xproto.Setup(x.X)
	x.root = setup.DefaultScreen(x.X).Root

	drw := xproto.Drawable(x.root)
	screensaver.SelectInput(x.X, drw, screensaver.EventNotifyMask)

	x.activeAtom = x.atom("_NET_ACTIVE_WINDOW")
	x.netNameAtom = x.atom("_NET_WM_NAME")
	x.nameAtom = x.atom("WM_NAME")
	x.classAtom = x.atom("WM_CLASS")

	x.spy(x.root)

	return x
}

func (t Tracker) collect() {
	x := connect()
	defer x.X.Close()

	prev := x.update(t)
	for {
		ev, everr := x.X.WaitForEvent()
		if everr != nil {
			log.Println("wait for event:", everr)
			continue
		}
		switch event := ev.(type) {
		case xproto.PropertyNotifyEvent:
			if prev != nil {
				m.Lock()
				prev.Spent += time.Since(prev.Seen)
				m.Unlock()
			}
			prev = x.update(t)
		case screensaver.NotifyEvent:
			switch event.State {
			case screensaver.StateOn:
				fmt.Println("away from keyboard")
				prev = nil
				zzz = true
			default:
				fmt.Println("back to keyboard")
				zzz = false
			}
		}
	}
}

func (t Tracker) cleanup(d time.Duration) {
	w, err := os.OpenFile(logf, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		log.Fatal(err)
	}
	defer w.Close()
	m.Lock()
	for k, v := range t {
		if time.Since(v.Seen) > d {
			log.Println("removing", k)
			log.SetOutput(w)
			log.Println("%s %s %s %s",
				v.Seen, v.Spent, k.Class, k.Name)
			log.SetOutput(os.Stderr)
			delete(t, k)
		}
	}
	m.Unlock()
}

func (t Tracker) load(fname string) {
	dump, err := os.Open(fname)
	if err != nil {
		log.Println(err)
		return
	}
	defer dump.Close()
	dec := gob.NewDecoder(dump)
	m.Lock()
	err = dec.Decode(&t)
	m.Unlock()
	if err != nil {
		log.Println(err)
	}
}

func (t Tracker) store(fname string) {
	dump, err := os.Create(fname + ".tmp")
	if err != nil {
		log.Println(err)
		return
	}
	defer dump.Close()
	enc := gob.NewEncoder(dump)
	m.Lock()
	err = enc.Encode(t)
	m.Unlock()
	if err != nil {
		log.Println(err)
	}
	os.Rename(fname+".tmp", fname)
}

func (t Tracker) reset() {
	m.Lock()
	for k := range t {
		delete(t, k)
	}
	m.Unlock()
}

type Index struct {
	Title   string
	Records Records
	Classes Records
	Total   time.Duration
	Zzz     bool
}

type Records []Record

type Record struct {
	Class string
	Name  string
	Spent time.Duration
	Seen  time.Time
	Odd   bool `json:"-"`
}

func (r Records) Len() int           { return len(r) }
func (r Records) Swap(i, j int)      { r[i], r[j] = r[j], r[i] }
func (r Records) Less(i, j int) bool { return r[i].Spent < r[j].Spent }

func indexHandler(w http.ResponseWriter, r *http.Request) {
	var idx Index
	idx.Title = "Gone Time Tracker"
	idx.Zzz = zzz
	class := r.URL.Path[1:]

	classtotal := make(map[string]time.Duration)

	m.Lock()
	for k, v := range tracks {
		classtotal[k.Class] += v.Spent
		idx.Total += v.Spent
		if class != "" && class != k.Class {
			continue
		}
		idx.Records = append(idx.Records, Record{
			Class: k.Class,
			Name:  k.Name,
			Spent: v.Spent})
	}
	m.Unlock()
	for k, v := range classtotal {
		idx.Classes = append(idx.Classes, Record{Class: k, Spent: v})
	}
	sort.Sort(sort.Reverse(idx.Classes))
	sort.Sort(sort.Reverse(idx.Records))
	for j := range idx.Records {
		idx.Records[j].Odd = j%2 == 0
	}
	err := tmpl.Execute(w, idx)
	if err != nil {
		log.Println(err)
	}
}

func dumpHandler(w http.ResponseWriter, r *http.Request) {
	var rec Records

	m.Lock()
	for k, v := range tracks {
		rec = append(rec, Record{
			Class: k.Class,
			Name:  k.Name,
			Spent: v.Spent,
			Seen:  v.Seen})
	}
	m.Unlock()

	data, err := json.MarshalIndent(rec, "", "\t")
	if err != nil {
		log.Println("dump:", err)
	}
	w.Write(data)
}

func resetHandler(w http.ResponseWriter, r *http.Request) {
	tracks.reset()
	http.Redirect(w, r, "/", http.StatusFound)
}

func main() {
	tracks.load(dump)
	go tracks.collect()
	go func() {
		for {
			tracks.cleanup(8 * time.Hour)
			tracks.store(dump)
			time.Sleep(time.Minute)
		}
	}()
	log.Println("listen on", port)
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/gone.json", dumpHandler)
	http.HandleFunc("/reset", resetHandler)
	http.ListenAndServe(port, nil)
}
<commit_msg>fix log<commit_after>package main

import (
	"bytes"
	"encoding/gob"
	"encoding/json"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"os"
	"sort"
	"sync"
	"time"

	"github.com/BurntSushi/xgb"
	"github.com/BurntSushi/xgb/screensaver"
	"github.com/BurntSushi/xgb/xproto"
)

const (
	port    = ":8001"
	dump    = "gone.gob"
	logf    = "gone.log"
	unknown = "unknown"
)

var (
	tracks = make(Tracker)
	tmpl   = template.Must(template.ParseFiles("index.html"))
	zzz    bool
	m      sync.Mutex
)

type Tracker map[Window]*Track

type Track struct {
	Seen  time.Time
	Spent time.Duration
}

type Window struct {
	Class string
	Name  string
}

type Xorg struct {
	X           *xgb.Conn
	root        xproto.Window
	activeAtom  *xproto.InternAtomReply
	netNameAtom *xproto.InternAtomReply
	nameAtom    *xproto.InternAtomReply
	classAtom   *xproto.InternAtomReply
}

func (t Track) String() string {
	return fmt.Sprint(t.Spent)
}

func (w Window) String() string {
	return fmt.Sprintf("%s %s", w.Class, w.Name)
}

func (x Xorg) atom(aname string) *xproto.InternAtomReply {
	a, err := xproto.InternAtom(x.X, true, uint16(len(aname)), aname).Reply()
	if err != nil {
		log.Fatal("atom: ", err)
	}
	return a
}

func (x Xorg) property(w xproto.Window, a *xproto.InternAtomReply) (*xproto.GetPropertyReply, error) {
	return xproto.GetProperty(x.X, false, w, a.Atom,
		xproto.GetPropertyTypeAny, 0, (1<<32)-1).Reply()
}

func (x Xorg) active() xproto.Window {
	p, err := x.property(x.root, x.activeAtom)
	if err != nil {
		return x.root
	}
	return xproto.Window(xgb.Get32(p.Value))
}

func (x Xorg) name(w xproto.Window) string {
	name, err := x.property(w, x.netNameAtom)
	if err != nil {
		return unknown
	}
	if string(name.Value) == "" {
		name, err = x.property(w, x.nameAtom)
		if err != nil || string(name.Value) == "" {
			return unknown
		}
	}
	return string(name.Value)
}

func (x Xorg) class(w xproto.Window) string {
	class, err := x.property(w, x.classAtom)
	if err != nil {
		return unknown
	}
	i := bytes.IndexByte(class.Value, 0)
	if i == -1 || string(class.Value[:i]) == "" {
		return unknown
	}
	return string(class.Value[:i])
}

func (x Xorg) winName() (Window, bool) {
	windowId := x.active()
	/* skip invalid window id */
	if windowId == 0 {
		return Window{}, false
	}
	x.spy(windowId)
	return Window{
		Class: x.class(windowId),
		Name:  x.name(windowId),
	}, true
}

func (x Xorg) spy(w xproto.Window) {
	xproto.ChangeWindowAttributes(x.X, w, xproto.CwEventMask,
		[]uint32{xproto.EventMaskPropertyChange})
}

func (x Xorg) update(t Tracker) (prev *Track) {
	if win, ok := x.winName(); ok {
		m.Lock()
		if _, ok := t[win]; !ok {
			t[win] = new(Track)
		}
		t[win].Seen = time.Now()
		prev = t[win]
		m.Unlock()
	}
	return
}

func connect() Xorg {
	var x Xorg
	var err error

	x.X, err = xgb.NewConn()
	if err != nil {
		log.Fatal("xgb: ", err)
	}

	err = screensaver.Init(x.X)
	if err != nil {
		log.Fatal("screensaver: ", err)
	}

	setup := xproto.Setup(x.X)
	x.root = setup.DefaultScreen(x.X).Root

	drw := xproto.Drawable(x.root)
	screensaver.SelectInput(x.X, drw, screensaver.EventNotifyMask)

	x.activeAtom = x.atom("_NET_ACTIVE_WINDOW")
	x.netNameAtom = x.atom("_NET_WM_NAME")
	x.nameAtom = x.atom("WM_NAME")
	x.classAtom = x.atom("WM_CLASS")

	x.spy(x.root)

	return x
}

func (t Tracker) collect() {
	x := connect()
	defer x.X.Close()

	prev := x.update(t)
	for {
		ev, everr := x.X.WaitForEvent()
		if everr != nil {
			log.Println("wait for event:", everr)
			continue
		}
		switch event := ev.(type) {
		case xproto.PropertyNotifyEvent:
			if prev != nil {
				m.Lock()
				prev.Spent += time.Since(prev.Seen)
				m.Unlock()
			}
			prev = x.update(t)
		case screensaver.NotifyEvent:
			switch event.State {
			case screensaver.StateOn:
				fmt.Println("away from keyboard")
				prev = nil
				zzz = true
			default:
				fmt.Println("back to keyboard")
				zzz = false
			}
		}
	}
}

func (t Tracker) cleanup(d time.Duration) {
	w, err := os.OpenFile(logf, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		log.Fatal(err)
	}
	defer w.Close()
	m.Lock()
	for k, v := range t {
		if time.Since(v.Seen) > d {
			log.Println("removing", k)
			log.SetOutput(w)
			log.Println(v.Seen.Format("2006/01/02 15:05:05"),
				v.Spent, k.Class, k.Name)
			log.SetOutput(os.Stderr)
			delete(t, k)
		}
	}
	m.Unlock()
}

func (t Tracker) load(fname string) {
	dump, err := os.Open(fname)
	if err != nil {
		log.Println(err)
		return
	}
	defer dump.Close()
	dec := gob.NewDecoder(dump)
	m.Lock()
	err = dec.Decode(&t)
	m.Unlock()
	if err != nil {
		log.Println(err)
	}
}

func (t Tracker) store(fname string) {
	dump, err := os.Create(fname + ".tmp")
	if err != nil {
		log.Println(err)
		return
	}
	defer dump.Close()
	enc := gob.NewEncoder(dump)
	m.Lock()
	err = enc.Encode(t)
	m.Unlock()
	if err != nil {
		log.Println(err)
	}
	os.Rename(fname+".tmp", fname)
}

func (t Tracker) reset() {
	m.Lock()
	for k := range t {
		delete(t, k)
	}
	m.Unlock()
}

type Index struct {
	Title   string
	Records Records
	Classes Records
	Total   time.Duration
	Zzz     bool
}

type Records []Record

type Record struct {
	Class string
	Name  string
	Spent time.Duration
	Seen  time.Time
	Odd   bool `json:"-"`
}

func (r Records) Len() int           { return len(r) }
func (r Records) Swap(i, j int)      { r[i], r[j] = r[j], r[i] }
func (r Records) Less(i, j int) bool { return r[i].Spent < r[j].Spent }

func indexHandler(w http.ResponseWriter, r *http.Request) {
	var idx Index
	idx.Title = "Gone Time Tracker"
	idx.Zzz = zzz
	class := r.URL.Path[1:]

	classtotal := make(map[string]time.Duration)

	m.Lock()
	for k, v := range tracks {
		classtotal[k.Class] += v.Spent
		idx.Total += v.Spent
		if class != "" && class != k.Class {
			continue
		}
		idx.Records = append(idx.Records, Record{
			Class: k.Class,
			Name:  k.Name,
			Spent: v.Spent})
	}
	m.Unlock()
	for k, v := range classtotal {
		idx.Classes = append(idx.Classes, Record{Class: k, Spent: v})
	}
	sort.Sort(sort.Reverse(idx.Classes))
	sort.Sort(sort.Reverse(idx.Records))
	for j := range idx.Records {
		idx.Records[j].Odd = j%2 == 0
	}
	err := tmpl.Execute(w, idx)
	if err != nil {
		log.Println(err)
	}
}

func dumpHandler(w http.ResponseWriter, r *http.Request) {
	var rec Records

	m.Lock()
	for k, v := range tracks {
		rec = append(rec, Record{
			Class: k.Class,
			Name:  k.Name,
			Spent: v.Spent,
			Seen:  v.Seen})
	}
	m.Unlock()

	data, err := json.MarshalIndent(rec, "", "\t")
	if err != nil {
		log.Println("dump:", err)
	}
	w.Write(data)
}

func resetHandler(w http.ResponseWriter, r *http.Request) {
	tracks.reset()
	http.Redirect(w, r, "/", http.StatusFound)
}

func main() {
	tracks.load(dump)
	go tracks.collect()
	go func() {
		for {
			tracks.cleanup(8 * time.Hour)
			tracks.store(dump)
			time.Sleep(time.Minute)
		}
	}()
	log.Println("listen on", port)
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/gone.json", dumpHandler)
	http.HandleFunc("/reset", resetHandler)
	http.ListenAndServe(port, nil)
}
<|endoftext|>